refactor(backend): extract DWARF debug info into src/backend/llvm/debug.zig (A7.2 debug)

Move the DWARF debug-info emission out of emit_llvm.zig into a DebugInfo backend
*LLVMEmitter facade (field `e`). Behavior-preserving relocation — self.* ->
self.e.* only.

- src/backend/llvm/debug.zig (DebugInfo): debugEnabled + diFileFor (private) +
  initDebugInfo / beginFunctionDebug / endFunctionDebug / setInstDebugLocation /
  finalizeDebugInfo (pub). The mutable DI state (di_builder/di_cu/di_files/
  di_scope/current_func_file) + the shared source map (import_sources/main_file)
  stay on LLVMEmitter; the facade reads/writes them via self.e.*.
- Routed the 5 pass-order call sites in LLVMEmitter.emit (init/finalize/
  begin/end/setInstDebugLocation) through a new debugInfo() accessor.
- setDebugContext stays on LLVMEmitter (shared-state setter; callers in main.zig/
  core.zig/test). sourceForFile stays on LLVMEmitter and is widened to pub — it is
  shared with reflection's trace-frame emission (emitTraceFrame), not debug-only.
- No DI logic / module-flag / DWARF-version / scope-line change.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (no churn).
This commit is contained in:
agra
2026-06-03 09:22:40 +03:00
parent 71f1cb2fb0
commit f92a743c85
2 changed files with 171 additions and 144 deletions

160
src/backend/llvm/debug.zig Normal file
View File

@@ -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);
}
};

View File

@@ -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 {