Add DWARF debug info to an existing LLVM 22 IR frontend. Covers DIBuilder setup, compile unit, function subprograms, local variable declarations, source locations on instructions, and module flags.
62
72%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./tiles/tessl-llvm/skills/add-debug-info/SKILL.mdUse this skill when the user wants to add debuggable output (source-line mapping, variable inspection in gdb/lldb) to an existing LLVM IR code generator.
Module, IRBuilder<>, and functions already emitting IR.DIBuilder is in LLVMCore.In your CodeGen context, add:
#include "llvm/IR/DIBuilder.h"
#include "llvm/IR/DebugInfoMetadata.h"
// Add to CodeGenCtx or equivalent:
std::unique_ptr<llvm::DIBuilder> DBuilder;
llvm::DICompileUnit *CU = nullptr;
llvm::DIFile *MainFile = nullptr;Initialize before emitting any IR:
DBuilder = std::make_unique<llvm::DIBuilder>(*M);
MainFile = DBuilder->createFile(
"myfile.my", // filename (basename)
"/path/to/source" // directory
);
CU = DBuilder->createCompileUnit(
llvm::dwarf::DW_LANG_C, // closest standard language code
MainFile,
"MyLang Compiler 1.0", // producer
/*isOptimized=*/false,
/*Flags=*/"",
/*RuntimeVersion=*/0
);
// Required module flags — add immediately after creating CU
M->addModuleFlag(llvm::Module::Warning, "Debug Info Version",
llvm::DEBUG_METADATA_VERSION);
M->addModuleFlag(llvm::Module::Warning, "Dwarf Version", 5);Create a DIType * for each source-language type. Do this once and cache:
std::map<std::string, llvm::DIType *> DITypeCache;
llvm::DIType *getOrCreateDIType(const std::string &Name) {
auto It = DITypeCache.find(Name);
if (It != DITypeCache.end()) return It->second;
llvm::DIType *T = nullptr;
if (Name == "int") T = DBuilder->createBasicType("int", 32, llvm::dwarf::DW_ATE_signed);
else if (Name == "bool") T = DBuilder->createBasicType("bool", 1, llvm::dwarf::DW_ATE_boolean);
else if (Name == "float") T = DBuilder->createBasicType("float",64, llvm::dwarf::DW_ATE_float);
else if (Name == "byte") T = DBuilder->createBasicType("byte", 8, llvm::dwarf::DW_ATE_unsigned);
// Add struct/array/pointer types similarly
DITypeCache[Name] = T;
return T;
}Do this at the start of emitting each function, before the entry block:
llvm::DISubprogram *beginFunction(llvm::Function *F,
const FuncDecl &Decl,
unsigned Line) {
// Build subroutine type: [retType, param1Type, param2Type, ...]
llvm::SmallVector<llvm::Metadata *, 8> EltTys;
EltTys.push_back(getOrCreateDIType(Decl.retType));
for (auto &P : Decl.params)
EltTys.push_back(getOrCreateDIType(P.typeName));
auto *FnDITy = DBuilder->createSubroutineType(
DBuilder->getOrCreateTypeArray(EltTys));
auto *SP = DBuilder->createFunction(
CU, // scope
Decl.name, // name
F->getName(), // linkage name
MainFile,
Line, // declaration line
FnDITy,
Line, // scope line (opening brace)
llvm::DINode::FlagPrototyped,
llvm::DISubprogram::SPFlagDefinition
);
F->setSubprogram(SP);
return SP;
}After creating allocas for parameters in the entry block:
void declareParam(llvm::AllocaInst *Alloca, const std::string &Name,
unsigned ArgNo, unsigned Line,
llvm::DISubprogram *SP) {
auto *DIVar = DBuilder->createParameterVariable(
SP, Name, ArgNo, MainFile, Line,
getOrCreateDIType(lookupParamTypeName(Name)),
/*AlwaysPreserve=*/true // keep even if optimized away
);
// insertDeclare returns DbgInstPtr in LLVM 22 — usually ignored
DBuilder->insertDeclare(
Alloca, DIVar, DBuilder->createExpression(),
llvm::DILocation::get(M->getContext(), Line, /*Col=*/1, SP),
&*F->getEntryBlock().getFirstInsertionPt()
);
}When emitting a let x = ... declaration:
void declareLocal(llvm::AllocaInst *Alloca, const std::string &Name,
unsigned Line, llvm::DISubprogram *SP) {
auto *DIVar = DBuilder->createAutoVariable(
SP, Name, MainFile, Line,
getOrCreateDIType(lookupLocalTypeName(Name)),
/*AlwaysPreserve=*/false
);
DBuilder->insertDeclare(
Alloca, DIVar, DBuilder->createExpression(),
llvm::DILocation::get(M->getContext(), Line, 1, SP),
B.GetInsertBlock()
);
}The most impactful step for debugger stepping — every statement should carry a !dbg location.
// Set location on IRBuilder — all subsequent instructions inherit it
void setLocation(unsigned Line, unsigned Col, llvm::DISubprogram *SP) {
B.SetCurrentDebugLocation(
llvm::DILocation::get(M->getContext(), Line, Col, SP));
}
// Clear for compiler-synthesized code (alloca prologue, mem2reg artifacts)
void clearLocation() {
B.SetCurrentDebugLocation(llvm::DebugLoc());
}Pattern — surround each statement emission:
// Before emitting each statement:
setLocation(Stmt.line, Stmt.col, SP);
// Emit alloca prologue without location (avoid mis-stepping):
clearLocation();
AllocaInst *Slot = createEntryAlloca(F, Ty, Name);
// ... store initial value ...
// Restore location for the actual statement:
setLocation(Stmt.line, Stmt.col, SP);Critical: call DBuilder->finalize() after all functions are emitted, before printing/compiling the module:
// After emitting all functions and global variables:
DBuilder->finalize();
// Then verify
if (llvm::verifyModule(*M, &llvm::errs()))
llvm::report_fatal_error("Module verification failed after debug info");# Compile to object
clang -c output.ll -o output.o
# Inspect DWARF
llvm-dwarfdump output.o
# Check for errors
llvm-dwarfdump --verify output.o
# Check line table
llvm-dwarfdump --debug-line output.oExpected: each source line you marked with setLocation appears in the .debug_line section.
# Compile with debug info preserved
clang -g output.ll -o my_program
# Debug
lldb my_program
# (lldb) b main
# (lldb) run
# (lldb) n ← step over (should show source lines)
# (lldb) p x ← print variable x (requires Step 4/5)DBuilder->finalize() — most common mistake; causes silent truncation of debug metadata.DILocation object across all instructions — create a new one per source location.AlwaysPreserve=false for parameters — optimizers may eliminate them from debug info; use true for all parameters.Debug Info Version and Dwarf Version module flags before finalize().clearLocation()) for compiler-synthesized instructions not present in source.a251bde
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.