From b44a5d05ef98cfe49c1f4098f7639f7a53e0a7d8 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 1 Jun 2026 12:52:14 +0300 Subject: [PATCH] ERR/E3.0 (slice 1): thread source spans into IR instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/ir/lower.test.zig | 60 +++++++++++++++++++++++++++++++++++++++++++ src/ir/lower.zig | 13 ++++++++++ src/ir/module.zig | 10 ++++++-- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index a09c264..5d5e8be 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -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); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index c33e3d7..84d3f15 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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 `$` in expression position → an `[]Type` slice // value where each element is a `const_type(arg_types[i])`. diff --git a/src/ir/module.zig b/src/ir/module.zig index 49a9701..77b030f 100644 --- a/src/ir/module.zig +++ b/src/ir/module.zig @@ -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 ───────────────────────────────────────────────────