ERR/E3.0 (slice 2): emit DWARF line-info

Attach LLVM debug metadata so a captured return-address PC resolves to
file:line:col (the runtime half E3.3 needs) and sx binaries become
debuggable in lldb/gdb.

- llvm_api.zig: bind llvm-c/DebugInfo.h (DIBuilder C API was unbound).
- emit_llvm.zig: DIBuilder + one DICompileUnit/DIFile on the main file,
  a DISubprogram per function (LLVMSetSubprogram), and a DILocation per
  instruction from Inst.span (errors.SourceLoc.compute, scoped to the
  subprogram). Plus the "Debug Info Version"/"Dwarf Version" module
  flags and LLVMDIBuilderFinalize.
- Gated on opt none/less + a wired source map (setDebugContext from
  core.zig), mirroring lower.zig's tracesEnabled; release strips it.

Verified: sx ir/sx asm --opt none show correct DILocations + .loc
directives; the 290-example JIT suite (-O0 -> debug on) verifies and
runs unchanged. +2 DWARF unit tests.
This commit is contained in:
agra
2026-06-01 13:14:00 +03:00
parent b44a5d05ef
commit c32d694d57
4 changed files with 273 additions and 0 deletions

View File

@@ -951,3 +951,69 @@ test "emit: box_any and unbox_any" {
try std.testing.expect(std.mem.indexOf(u8, ir_str, "insertvalue") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "extractvalue") != null);
}
test "emit: ERR E3.0 — DWARF debug info (compile unit + subprogram + per-inst location)" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func main() -> s64 { return 42; } — with the `return` instruction
// carrying a span that lands on line 3 of the source map below.
_ = b.beginFunction(str(&module, "main"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
// "a\nb\nXYZ" — byte offset 4 ('X') is line 3, col 1.
b.current_span = .{ .start = 4, .end = 5 };
const c42 = b.constInt(42, .s64);
b.ret(c42, .s64);
b.finalize();
// Source map keyed on the main file. setDebugContext + opt none
// turns DWARF emission on (release opt levels skip it entirely).
var sources = std.StringHashMap([:0]const u8).init(alloc);
defer sources.deinit();
try sources.put("probe.sx", "a\nb\nXYZ");
var emitter = LLVMEmitter.init(alloc, &module, "test_dwarf", .{ .opt_level = .none });
defer emitter.deinit();
emitter.setDebugContext(&sources, "probe.sx");
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
// Module flags, compile unit on the main file, a subprogram for main,
// and the return instruction's location resolved to line 3.
try std.testing.expect(std.mem.indexOf(u8, ir_str, "\"Debug Info Version\"") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "\"Dwarf Version\"") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DICompileUnit") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DIFile(filename: \"probe.sx\"") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DISubprogram(name: \"main\"") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DILocation(line: 3") != null);
}
test "emit: ERR E3.0 — no DWARF without a debug context (unit-test default)" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "main"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
b.ret(b.constInt(42, .s64), .s64);
b.finalize();
// No setDebugContext call → no source map → debug info off even at
// opt none. Confirms the gate keeps the metadata out by default.
var emitter = LLVMEmitter.init(alloc, &module, "test_no_dwarf", .{ .opt_level = .none });
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DICompileUnit") == null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "!dbg") == null);
}