diff --git a/src/backend/llvm/debug.zig b/src/backend/llvm/debug.zig new file mode 100644 index 0000000..637c008 --- /dev/null +++ b/src/backend/llvm/debug.zig @@ -0,0 +1,160 @@ +const std = @import("std"); +const llvm = @import("../../llvm_api.zig"); +const c = llvm.c; +const errors = @import("../../errors.zig"); +const emit = @import("../../ir/emit_llvm.zig"); +const ir_inst = @import("../../ir/inst.zig"); + +const LLVMEmitter = emit.LLVMEmitter; +const Function = ir_inst.Function; +const Span = ir_inst.Span; + +/// DWARF debug-info emission (architecture phase A7.2), extracted from +/// `LLVMEmitter`. A backend `*LLVMEmitter` facade (field `e`): it owns the +/// `DIBuilder` lifecycle, the compile unit, per-function `DISubprogram` scopes, +/// and per-instruction `DILocation`s. The mutable DI state (`di_builder`/ +/// `di_cu`/`di_files`/`di_scope`/`current_func_file`) + the shared source map +/// (`import_sources`/`main_file`, also read by `#caller_location`) stay on +/// `LLVMEmitter`; this reads/writes them via `self.e.*`. `LLVMEmitter.emit` +/// drives the pass order and calls in via `self.debugInfo()`. +pub const DebugInfo = struct { + e: *LLVMEmitter, + + /// Debug info is emitted only when error traces are kept (opt_level + /// none/less, matching `tracesEnabled` in lower.zig) and a source + /// map is available. Release builds (default/aggressive) skip it, so + /// the DWARF is strippable cost-free. + fn debugEnabled(self: DebugInfo) bool { + if (self.e.import_sources == null) return false; + return self.e.target_config.opt_level == .none or self.e.target_config.opt_level == .less; + } + + /// The `DIFile` for `path`, created once and cached. Splits the path + /// into basename + directory as DWARF expects. The directory MUST be + /// non-empty: an empty `DW_AT_comp_dir` makes Apple's `ld` silently drop + /// the whole object's debug map (no `N_OSO`), so a binary built from a + /// bare filename (e.g. `sx build main.sx`) becomes undebuggable. Fall back + /// to "." when the path has no directory component. + fn diFileFor(self: DebugInfo, path: []const u8) c.LLVMMetadataRef { + if (self.e.di_files.get(path)) |f| return f; + const slash = std.mem.lastIndexOfScalar(u8, path, '/'); + const dir = if (slash) |s| (if (s == 0) "/" else path[0..s]) else "."; + const base = if (slash) |s| path[s + 1 ..] else path; + const f = c.LLVMDIBuilderCreateFile(self.e.di_builder, base.ptr, base.len, dir.ptr, dir.len); + self.e.di_files.put(path, f) catch {}; + return f; + } + + /// Create the DIBuilder, the module flags ("Debug Info Version" / + /// "Dwarf Version"), and the single compile unit on the main file. + pub fn initDebugInfo(self: DebugInfo) void { + if (!self.debugEnabled()) return; + self.e.di_builder = c.LLVMCreateDIBuilder(self.e.llvm_module); + + c.LLVMAddModuleFlag( + self.e.llvm_module, + c.LLVMModuleFlagBehaviorWarning, + "Debug Info Version", + "Debug Info Version".len, + c.LLVMValueAsMetadata(c.LLVMConstInt(self.e.cached_i32, c.LLVMDebugMetadataVersion(), 0)), + ); + c.LLVMAddModuleFlag( + self.e.llvm_module, + c.LLVMModuleFlagBehaviorWarning, + "Dwarf Version", + "Dwarf Version".len, + c.LLVMValueAsMetadata(c.LLVMConstInt(self.e.cached_i32, 4, 0)), + ); + + const cu_file = self.diFileFor(if (self.e.main_file.len > 0) self.e.main_file else "sx"); + self.e.di_cu = c.LLVMDIBuilderCreateCompileUnit( + self.e.di_builder, + c.LLVMDWARFSourceLanguageC, + cu_file, + "sx", + "sx".len, + 0, // isOptimized + "", + 0, // flags + 0, // runtime version + "", + 0, // split name + c.LLVMDWARFEmissionFull, + 0, // DWOId + 0, // split debug inlining + 0, // debug info for profiling + "", + 0, // sysroot + "", + 0, // sdk + ); + } + + /// Create a `DISubprogram` for `func` and attach it to `llvm_func`, + /// making it the scope (`di_scope`) for the function's instruction + /// locations. Clears any stale builder location first so synthetic + /// functions emitted between sx functions carry none. + pub fn beginFunctionDebug(self: DebugInfo, func: *const Function, llvm_func: c.LLVMValueRef, name: []const u8) void { + self.e.di_scope = null; + c.LLVMSetCurrentDebugLocation2(self.e.builder, null); + if (self.e.di_builder == null) return; + + const file = func.source_file orelse self.e.main_file; + self.e.current_func_file = file; + const di_file = self.diFileFor(file); + const subroutine_ty = c.LLVMDIBuilderCreateSubroutineType(self.e.di_builder, di_file, null, 0, c.LLVMDIFlagZero); + + // Line = the first instruction's line (the function body's start), + // else 1 when the body is empty / span-less. + var line: c_uint = 1; + if (func.blocks.items.len > 0 and func.blocks.items[0].insts.items.len > 0) { + const sp = func.blocks.items[0].insts.items[0].span; + const src = self.e.sourceForFile(file); + line = errors.SourceLoc.compute(src, sp.start).line; + } + + const is_local: c.LLVMBool = if (func.linkage == .external) 0 else 1; + const subprogram = c.LLVMDIBuilderCreateFunction( + self.e.di_builder, + di_file, // scope + name.ptr, + name.len, + name.ptr, + name.len, // linkage name + di_file, + line, + subroutine_ty, + is_local, + 1, // is definition + line, // scope line + c.LLVMDIFlagZero, + 0, // isOptimized + ); + c.LLVMSetSubprogram(llvm_func, subprogram); + self.e.di_scope = subprogram; + } + + /// End the current function's debug scope and clear the builder's + /// location, so the next (possibly synthetic) function doesn't + /// inherit a DILocation pointing into this function's subprogram. + pub fn endFunctionDebug(self: DebugInfo) void { + self.e.di_scope = null; + c.LLVMSetCurrentDebugLocation2(self.e.builder, null); + } + + /// Set the builder's current debug location from an instruction span, + /// scoped to the current function's subprogram. No-op when debug info + /// is off (`di_scope == null`). + pub fn setInstDebugLocation(self: DebugInfo, span: Span) void { + const scope = self.e.di_scope orelse return; + const src = self.e.sourceForFile(self.e.current_func_file); + const loc = errors.SourceLoc.compute(src, span.start); + const di_loc = c.LLVMDIBuilderCreateDebugLocation(self.e.context, loc.line, loc.col, scope, null); + c.LLVMSetCurrentDebugLocation2(self.e.builder, di_loc); + } + + pub fn finalizeDebugInfo(self: DebugInfo) void { + if (self.e.di_builder == null) return; + c.LLVMDIBuilderFinalize(self.e.di_builder); + } +}; diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 9f60884..5593048 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -12,6 +12,7 @@ const StringId = ir_types.StringId; const errors = @import("../errors.zig"); const llvm_types = @import("../backend/llvm/types.zig"); const llvm_abi = @import("../backend/llvm/abi.zig"); +const llvm_debug = @import("../backend/llvm/debug.zig"); const ir_inst = @import("inst.zig"); const Ref = ir_inst.Ref; const Span = ir_inst.Span; @@ -323,7 +324,7 @@ pub const LLVMEmitter = struct { pub fn emit(self: *LLVMEmitter) void { // Pass -1: Set up DWARF debug info (compile unit + module flags). // Must precede any DISubprogram (created per function below). - self.initDebugInfo(); + self.debugInfo().initDebugInfo(); // Pass 0: Declare and initialize globals self.emitGlobals(); @@ -373,7 +374,7 @@ pub const LLVMEmitter = struct { // Pass 4: Resolve DWARF temporary metadata. Must come after all // DISubprograms / DILocations are created and before the module // is verified or emitted. - self.finalizeDebugInfo(); + self.debugInfo().finalizeDebugInfo(); } // ── DWARF debug info (ERR E3.0) ────────────────────────────────── @@ -386,19 +387,10 @@ pub const LLVMEmitter = struct { self.main_file = main_file; } - /// Debug info is emitted only when error traces are kept (opt_level - /// none/less, matching `tracesEnabled` in lower.zig) and a source - /// map is available. Release builds (default/aggressive) skip it, so - /// the DWARF is strippable cost-free. - fn debugEnabled(self: *const LLVMEmitter) bool { - if (self.import_sources == null) return false; - return self.target_config.opt_level == .none or self.target_config.opt_level == .less; - } - /// Source text for `file` via the diagnostics' file→source map (the /// same map `#caller_location` uses). Empty when unavailable — /// line:col then degrades to 1:1 rather than crash. - fn sourceForFile(self: *LLVMEmitter, file: []const u8) []const u8 { + pub fn sourceForFile(self: *LLVMEmitter, file: []const u8) []const u8 { const is = self.import_sources orelse return ""; if (is.get(file)) |s| return s; if (self.main_file.len > 0) { @@ -407,135 +399,6 @@ pub const LLVMEmitter = struct { return ""; } - /// The `DIFile` for `path`, created once and cached. Splits the path - /// into basename + directory as DWARF expects. The directory MUST be - /// non-empty: an empty `DW_AT_comp_dir` makes Apple's `ld` silently drop - /// the whole object's debug map (no `N_OSO`), so a binary built from a - /// bare filename (e.g. `sx build main.sx`) becomes undebuggable. Fall back - /// to "." when the path has no directory component. - fn diFileFor(self: *LLVMEmitter, path: []const u8) c.LLVMMetadataRef { - if (self.di_files.get(path)) |f| return f; - const slash = std.mem.lastIndexOfScalar(u8, path, '/'); - const dir = if (slash) |s| (if (s == 0) "/" else path[0..s]) else "."; - const base = if (slash) |s| path[s + 1 ..] else path; - const f = c.LLVMDIBuilderCreateFile(self.di_builder, base.ptr, base.len, dir.ptr, dir.len); - self.di_files.put(path, f) catch {}; - return f; - } - - /// Create the DIBuilder, the module flags ("Debug Info Version" / - /// "Dwarf Version"), and the single compile unit on the main file. - fn initDebugInfo(self: *LLVMEmitter) void { - if (!self.debugEnabled()) return; - self.di_builder = c.LLVMCreateDIBuilder(self.llvm_module); - - c.LLVMAddModuleFlag( - self.llvm_module, - c.LLVMModuleFlagBehaviorWarning, - "Debug Info Version", - "Debug Info Version".len, - c.LLVMValueAsMetadata(c.LLVMConstInt(self.cached_i32, c.LLVMDebugMetadataVersion(), 0)), - ); - c.LLVMAddModuleFlag( - self.llvm_module, - c.LLVMModuleFlagBehaviorWarning, - "Dwarf Version", - "Dwarf Version".len, - c.LLVMValueAsMetadata(c.LLVMConstInt(self.cached_i32, 4, 0)), - ); - - const cu_file = self.diFileFor(if (self.main_file.len > 0) self.main_file else "sx"); - self.di_cu = c.LLVMDIBuilderCreateCompileUnit( - self.di_builder, - c.LLVMDWARFSourceLanguageC, - cu_file, - "sx", - "sx".len, - 0, // isOptimized - "", - 0, // flags - 0, // runtime version - "", - 0, // split name - c.LLVMDWARFEmissionFull, - 0, // DWOId - 0, // split debug inlining - 0, // debug info for profiling - "", - 0, // sysroot - "", - 0, // sdk - ); - } - - /// Create a `DISubprogram` for `func` and attach it to `llvm_func`, - /// making it the scope (`di_scope`) for the function's instruction - /// locations. Clears any stale builder location first so synthetic - /// functions emitted between sx functions carry none. - fn beginFunctionDebug(self: *LLVMEmitter, func: *const Function, llvm_func: c.LLVMValueRef, name: []const u8) void { - self.di_scope = null; - c.LLVMSetCurrentDebugLocation2(self.builder, null); - if (self.di_builder == null) return; - - const file = func.source_file orelse self.main_file; - self.current_func_file = file; - const di_file = self.diFileFor(file); - const subroutine_ty = c.LLVMDIBuilderCreateSubroutineType(self.di_builder, di_file, null, 0, c.LLVMDIFlagZero); - - // Line = the first instruction's line (the function body's start), - // else 1 when the body is empty / span-less. - var line: c_uint = 1; - if (func.blocks.items.len > 0 and func.blocks.items[0].insts.items.len > 0) { - const sp = func.blocks.items[0].insts.items[0].span; - const src = self.sourceForFile(file); - line = errors.SourceLoc.compute(src, sp.start).line; - } - - const is_local: c.LLVMBool = if (func.linkage == .external) 0 else 1; - const subprogram = c.LLVMDIBuilderCreateFunction( - self.di_builder, - di_file, // scope - name.ptr, - name.len, - name.ptr, - name.len, // linkage name - di_file, - line, - subroutine_ty, - is_local, - 1, // is definition - line, // scope line - c.LLVMDIFlagZero, - 0, // isOptimized - ); - c.LLVMSetSubprogram(llvm_func, subprogram); - self.di_scope = subprogram; - } - - /// End the current function's debug scope and clear the builder's - /// location, so the next (possibly synthetic) function doesn't - /// inherit a DILocation pointing into this function's subprogram. - fn endFunctionDebug(self: *LLVMEmitter) void { - self.di_scope = null; - c.LLVMSetCurrentDebugLocation2(self.builder, null); - } - - /// Set the builder's current debug location from an instruction span, - /// scoped to the current function's subprogram. No-op when debug info - /// is off (`di_scope == null`). - fn setInstDebugLocation(self: *LLVMEmitter, span: Span) void { - const scope = self.di_scope orelse return; - const src = self.sourceForFile(self.current_func_file); - const loc = errors.SourceLoc.compute(src, span.start); - const di_loc = c.LLVMDIBuilderCreateDebugLocation(self.context, loc.line, loc.col, scope, null); - c.LLVMSetCurrentDebugLocation2(self.builder, di_loc); - } - - fn finalizeDebugInfo(self: *LLVMEmitter) void { - if (self.di_builder == null) return; - c.LLVMDIBuilderFinalize(self.di_builder); - } - /// Synthesize a module constructor that populates each interned /// Obj-C selector slot via `sel_registerName`, once at module /// load. Registered in `@llvm.global_ctors` so dyld / ld.so / the @@ -1868,7 +1731,7 @@ pub const LLVMEmitter = struct { // DWARF: describe this function and make it the scope for the // per-instruction locations set in emitInst (no-op if off). - self.beginFunctionDebug(func, llvm_func, name); + self.debugInfo().beginFunctionDebug(func, llvm_func, name); // Clear ref_map and pre-map parameter refs self.ref_map.clearRetainingCapacity(); @@ -1930,7 +1793,7 @@ pub const LLVMEmitter = struct { self.fixupPhiNodes(func, func_idx); // DWARF: leave no stale location for the next function. - self.endFunctionDebug(); + self.debugInfo().endFunctionDebug(); } /// After emitting all blocks, fill in PHI incoming values from branch args. @@ -1982,7 +1845,7 @@ pub const LLVMEmitter = struct { fn emitInst(self: *LLVMEmitter, instruction: *const Inst, func_idx: u32) void { // DWARF: stamp every LLVM instruction this op emits with the sx // source location (no-op when debug info is off). - self.setInstDebugLocation(instruction.span); + self.debugInfo().setInstDebugLocation(instruction.span); switch (instruction.op) { // ── Constants ─────────────────────────────────────────── .const_int => |val| { @@ -4561,6 +4424,10 @@ pub const LLVMEmitter = struct { return .{ .e = self }; } + fn debugInfo(self: *LLVMEmitter) llvm_debug.DebugInfo { + return .{ .e = self }; + } + /// IR-type → LLVM-type lowering lives in `backend/llvm/types.zig` /// (`TypeLowering`). This stays the facade entry point (~97 callers). pub fn toLLVMType(self: *LLVMEmitter, ty: TypeId) c.LLVMTypeRef {