ERR/E3.0 (slice 1): thread source spans into IR instructions
Foundation for DWARF line-info (E3.0). The `Inst.span` field existed but was
never populated — `emit()` always passed the empty `{0,0}` default, so every
instruction had no source location (the lone reader, the interp's comptime
bail-offset, was always 0).
- Builder gains a `current_span`; `emit`/`emitVoid` stamp it onto each
instruction.
- `lowerExpr` / `lowerStmt` set `current_span` from the AST node's span on
entry and restore it on exit (save/restore), so a parent's later emits keep
the parent's span after a child lowers; the empty default is skipped so
synthetic nodes don't reset a meaningful enclosing span.
Behavior-neutral: codegen never reads spans, and the only consumer (the interp
bail-offset) merely gains real offsets. 290 examples pass unchanged, no `.ir`
snapshot drift. New unit test asserts an emitted `add` carries its `a + b` span.
Next (slice 2): bind `llvm-c/DebugInfo.h`, emit DICompileUnit / DISubprogram /
DIFile / DILocation from these spans, gate on debug/trace mode.
This commit is contained in:
@@ -86,6 +86,66 @@ test "lower: simple function with arithmetic" {
|
||||
try std.testing.expect(std.mem.indexOf(u8, output, "add %") != null or std.mem.indexOf(u8, output, "ret %") != null);
|
||||
}
|
||||
|
||||
test "lower: instructions carry their AST node's source span (ERR E3.0)" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
|
||||
// probe :: (a: s64, b: s64) -> s64 { return a + b; } — the `a + b` node
|
||||
// gets a distinctive span so we can find the emitted `add` instruction and
|
||||
// assert it was stamped (not left at the empty {0,0} default).
|
||||
const a_type = alloc.create(Node) catch unreachable;
|
||||
a_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } };
|
||||
const b_type = alloc.create(Node) catch unreachable;
|
||||
b_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } };
|
||||
const ret_type = alloc.create(Node) catch unreachable;
|
||||
ret_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } };
|
||||
const a_ident = alloc.create(Node) catch unreachable;
|
||||
a_ident.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "a" } } };
|
||||
const b_ident = alloc.create(Node) catch unreachable;
|
||||
b_ident.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "b" } } };
|
||||
const add_expr = alloc.create(Node) catch unreachable;
|
||||
add_expr.* = .{ .span = .{ .start = 42, .end = 47 }, .data = .{ .binary_op = .{ .op = .add, .lhs = a_ident, .rhs = b_ident } } };
|
||||
const ret_stmt = alloc.create(Node) catch unreachable;
|
||||
ret_stmt.* = .{ .span = .{ .start = 30, .end = 50 }, .data = .{ .return_stmt = .{ .value = add_expr } } };
|
||||
const body = alloc.create(Node) catch unreachable;
|
||||
const stmts: []const *Node = &.{ret_stmt};
|
||||
body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = stmts } } };
|
||||
defer {
|
||||
alloc.destroy(a_type);
|
||||
alloc.destroy(b_type);
|
||||
alloc.destroy(ret_type);
|
||||
alloc.destroy(a_ident);
|
||||
alloc.destroy(b_ident);
|
||||
alloc.destroy(add_expr);
|
||||
alloc.destroy(ret_stmt);
|
||||
alloc.destroy(body);
|
||||
}
|
||||
|
||||
const params: []const ast.Param = &.{
|
||||
.{ .name = "a", .name_span = .{ .start = 0, .end = 0 }, .type_expr = a_type },
|
||||
.{ .name = "b", .name_span = .{ .start = 0, .end = 0 }, .type_expr = b_type },
|
||||
};
|
||||
const fn_decl = ast.FnDecl{ .name = "probe", .params = params, .return_type = ret_type, .body = body };
|
||||
|
||||
var lowering = Lowering.init(&module);
|
||||
lowering.lowerFunction(&fn_decl, "probe", false);
|
||||
|
||||
// Find the `add` instruction and assert it carries the `a + b` span.
|
||||
const func = module.getFunction(FuncId.fromIndex(0));
|
||||
var found = false;
|
||||
for (func.blocks.items) |blk| {
|
||||
for (blk.insts.items) |inst| {
|
||||
if (inst.op == .add) {
|
||||
try std.testing.expectEqual(@as(u32, 42), inst.span.start);
|
||||
try std.testing.expectEqual(@as(u32, 47), inst.span.end);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
try std.testing.expect(found);
|
||||
}
|
||||
|
||||
test "lower: if/else generates basic blocks" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
|
||||
@@ -1544,6 +1544,11 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
fn lowerStmt(self: *Lowering, node: *const Node) void {
|
||||
// Stamp this statement's span onto its instructions (ERR E3.0); see
|
||||
// `lowerExpr`.
|
||||
const saved_span = self.builder.current_span;
|
||||
defer self.builder.current_span = saved_span;
|
||||
if (node.span.start != 0 or node.span.end != 0) self.builder.current_span = .{ .start = node.span.start, .end = node.span.end };
|
||||
switch (node.data) {
|
||||
.var_decl => |vd| self.lowerVarDecl(&vd),
|
||||
.const_decl => |cd| self.lowerConstDecl(&cd),
|
||||
@@ -2253,6 +2258,14 @@ pub const Lowering = struct {
|
||||
// ── Expression lowering ─────────────────────────────────────────
|
||||
|
||||
fn lowerExpr(self: *Lowering, node: *const Node) Ref {
|
||||
// Stamp this node's source span onto the instructions it emits (ERR
|
||||
// E3.0 — feeds DWARF line-info + comptime frame resolution). Save/
|
||||
// restore so a parent's later emits keep the parent's span after a
|
||||
// child lowers. Skip the empty default so synthetic nodes don't reset
|
||||
// a meaningful enclosing span to offset 0.
|
||||
const saved_span = self.builder.current_span;
|
||||
defer self.builder.current_span = saved_span;
|
||||
if (node.span.start != 0 or node.span.end != 0) self.builder.current_span = .{ .start = node.span.start, .end = node.span.end };
|
||||
return switch (node.data) {
|
||||
// Bare `$<pack>` in expression position → an `[]Type` slice
|
||||
// value where each element is a `const_type(arg_types[i])`.
|
||||
|
||||
@@ -253,6 +253,12 @@ pub const Builder = struct {
|
||||
current_block: ?BlockId = null,
|
||||
/// Running instruction counter within the current function (for Ref assignment).
|
||||
inst_counter: u32 = 0,
|
||||
/// Source span stamped onto every instruction emitted via `emit`/`emitVoid`
|
||||
/// (ERR E3.0). Lowering sets it (save/restore) at each AST node so the IR
|
||||
/// carries per-instruction locations for DWARF `.debug_line` + comptime
|
||||
/// frame resolution. Defaults empty for instructions emitted outside a
|
||||
/// node context (synthetic prologue/epilogue, etc.).
|
||||
current_span: Span = .{},
|
||||
|
||||
pub fn init(module: *Module) Builder {
|
||||
return .{ .module = module };
|
||||
@@ -344,7 +350,7 @@ pub const Builder = struct {
|
||||
// ── Emit helpers ────────────────────────────────────────────────
|
||||
|
||||
pub fn emit(self: *Builder, op: Op, ty: TypeId) Ref {
|
||||
return self.emitSpan(op, ty, .{});
|
||||
return self.emitSpan(op, ty, self.current_span);
|
||||
}
|
||||
|
||||
fn emitSpan(self: *Builder, op: Op, ty: TypeId, span: Span) Ref {
|
||||
@@ -359,7 +365,7 @@ pub const Builder = struct {
|
||||
pub fn emitVoid(self: *Builder, op: Op, ty: TypeId) void {
|
||||
const block = self.currentBlock();
|
||||
self.inst_counter += 1;
|
||||
block.insts.append(self.module.alloc, .{ .op = op, .ty = ty }) catch unreachable;
|
||||
block.insts.append(self.module.alloc, .{ .op = op, .ty = ty, .span = self.current_span }) catch unreachable;
|
||||
}
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user