ERR/E3.0 (slice 3b): comptime trace resolution

#run failures now print the same `func at file:line:col` trace as
runtime, resolved in-process via the interpreter's IR/source tables.

- Read-side context-split op `.trace_resolve` (mirror of .trace_frame),
  lowered from a name-recognized `__trace_resolve_frame(u64) -> Frame`.
- emit_llvm: inttoptr the operand to *Frame + load (the value
  .trace_frame stamped in).
- interp: unpack (func_id << 32 | span.start); resolve func/file from
  module.functions and line/col via SourceLoc.compute over a new
  source_map (setSourceMap wired at every production interp site).
- trace.sx: frame_at -> u64; to_string routes each frame through
  __trace_resolve_frame, so one source works in both machines.

Compiled path behavior unchanged (243/244/247 identical; it now loads
via the op). New examples/253-comptime-trace.sx exercises the comptime
path. Gates: zig build, zig build test, run_examples.sh -> 291 passed.
This commit is contained in:
agra
2026-06-01 15:33:50 +03:00
parent 11f6377d9c
commit b5241243e6
11 changed files with 120 additions and 5 deletions

View File

@@ -180,6 +180,7 @@ pub const Compilation = struct {
const mod = self.ir_module orelse return error.NoIRModule;
var interp = ir.Interpreter.init(mod, self.allocator);
defer interp.deinit();
interp.setSourceMap(&self.import_sources);
if (self.ir_emitter) |*e| interp.build_config = &e.build_config;
ir.Interpreter.last_bail_op = null;
ir.Interpreter.last_bail_builtin = null;

View File

@@ -1347,6 +1347,7 @@ pub const LLVMEmitter = struct {
const func_id = ir_inst.FuncId.fromIndex(@intCast(i));
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
if (self.import_sources) |sm| interp_inst.setSourceMap(sm);
_ = interp_inst.call(func_id, &.{}) catch {};
// Route #run `print` output to fd 1 so it joins the
// JIT-executed runtime's stream. Same call site shape as
@@ -1385,6 +1386,7 @@ pub const LLVMEmitter = struct {
if (global.comptime_func) |func_id| {
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
if (self.import_sources) |sm| interp_inst.setSourceMap(sm);
Interpreter.last_bail_op = null;
Interpreter.last_bail_builtin = null;
Interpreter.last_bail_detail = null;
@@ -1896,6 +1898,14 @@ pub const LLVMEmitter = struct {
.trace_frame => {
self.mapRef(self.emitTraceFrame(instruction));
},
.trace_resolve => |u| {
// The operand is a `Frame*` stamped in by `.trace_frame` (as
// i64); reinterpret and load it.
const raw = self.resolveRef(u.operand);
const frame_ty = self.getFrameStructType();
const ptr = c.LLVMBuildIntToPtr(self.builder, raw, self.cached_ptr, "frame.ptr");
self.mapRef(c.LLVMBuildLoad2(self.builder, frame_ty, ptr, "frame.val"));
},
.const_string => |str_id| {
const str = self.ir_mod.types.getString(str_id);
const llvm_val = self.emitStringConstant(str);
@@ -2517,6 +2527,7 @@ pub const LLVMEmitter = struct {
if (callee_func.is_comptime and call_op.args.len == 0) {
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
if (self.import_sources) |sm| interp_inst.setSourceMap(sm);
defer interp_inst.deinit();
if (interp_inst.call(call_op.callee, &.{})) |result| {
if (result.asInt()) |v| {

View File

@@ -104,6 +104,13 @@ pub const Op = union(enum) {
/// `(func_id << 32 | span.start)` for the comptime resolver (slice 3b). The
/// result feeds the existing `sx_trace_push(u64)` call.
trace_frame,
/// ERR E3.0 slice 3b — the read-side resolver: a raw trace-buffer `u64` →
/// a `Frame` value. The mirror of `trace_frame`'s context split.
/// `emit_llvm` reinterprets the operand as `*Frame` and loads it (the value
/// `trace_frame` stamped in). `interp` unpacks `(func_id, span.start)` and
/// resolves it via the module's functions + the source map into a `Frame`
/// aggregate. Result type is the `Frame` `TypeId`.
trace_resolve: UnaryOp,
/// Comptime-only Type value. Carried as a `Value.type_tag(TypeId)`
/// in the interpreter. NEVER emitted to LLVM — types are erased
/// after lowering. `emit_llvm` bails loudly if it sees one,

View File

@@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator;
const types = @import("types.zig");
const inst_mod = @import("inst.zig");
const mod_mod = @import("module.zig");
const errors = @import("../errors.zig");
const TypeId = types.TypeId;
const TypeTable = types.TypeTable;
@@ -148,6 +149,12 @@ pub const Interpreter = struct {
/// tracked — foreign calls return before the frame is pushed.
call_chain: std.ArrayList(FuncId) = .empty,
/// File → source text (the diagnostics' import_sources). Set by the host
/// where available so `.trace_resolve` can turn a `(func_id, span.start)`
/// frame into `file:line:col` at comptime (ERR E3.0 slice 3b). Null → the
/// resolver degrades to line/col 1:1.
source_map: ?*const std.StringHashMap([:0]const u8) = null,
// Heap: dynamically allocated memory blocks
heap: std.ArrayList([]u8),
@@ -203,6 +210,12 @@ pub const Interpreter = struct {
};
}
/// Provide the file→source map so `.trace_resolve` can compute file:line:col
/// for comptime trace frames. Optional — absent in unit tests.
pub fn setSourceMap(self: *Interpreter, sm: *const std.StringHashMap([:0]const u8)) void {
self.source_map = sm;
}
pub fn deinit(self: *Interpreter) void {
// Free all heap allocations
for (self.heap.items) |block| {
@@ -652,6 +665,32 @@ pub const Interpreter = struct {
const packed_frame: u64 = (fid << 32) | @as(u64, instruction.span.start);
return .{ .value = .{ .int = @bitCast(packed_frame) } };
},
.trace_resolve => |u| {
// Unpack the comptime frame `(func_id << 32 | span.start)` and
// resolve it to a `Frame { file, line, col, func }` aggregate.
const raw: u64 = @bitCast(frame.getRef(u.operand).asInt() orelse 0);
const fid: u32 = @intCast(raw >> 32);
const offset: u32 = @truncate(raw);
const func = self.module.getFunction(FuncId.fromIndex(fid));
const func_name = self.module.types.getString(func.name);
const file_full = func.source_file orelse "";
const file = std.fs.path.basename(file_full);
var line: i64 = 1;
var col: i64 = 1;
if (self.source_map) |sm| {
if (sm.get(file_full)) |src| {
const loc = errors.SourceLoc.compute(src, offset);
line = @intCast(loc.line);
col = @intCast(loc.col);
}
}
const fields = self.alloc.alloc(Value, 4) catch return .{ .value = .undef };
fields[0] = .{ .string = file };
fields[1] = .{ .int = line };
fields[2] = .{ .int = col };
fields[3] = .{ .string = func_name };
return .{ .value = .{ .aggregate = fields } };
},
.const_type => |tid| return .{ .value = .{ .type_tag = tid } },
// ── Arithmetic ──────────────────────────────────────

View File

@@ -8554,6 +8554,7 @@ pub const Lowering = struct {
var interp = interp_mod.Interpreter.init(self.module, self.alloc);
defer interp.deinit();
if (self.diagnostics) |d| if (d.import_sources) |sm| interp.setSourceMap(sm);
const result = interp.call(ct_func_id, &.{}) catch return null;
@@ -10345,6 +10346,18 @@ pub const Lowering = struct {
// chain at comptime, no-op in compiled code (ERR E4.1).
return self.builder.emit(.{ .interp_print_frames = {} }, .void);
}
if (std.mem.eql(u8, name, "__trace_resolve_frame")) {
// Backs `trace.sx`'s formatter: a raw trace-buffer u64 → a `Frame`.
// Compiled code reinterprets the operand as `*Frame` and loads it;
// the interp unpacks (func_id, span.start) and resolves (ERR E3.0
// slice 3b). Result type is the `Frame` struct from trace.sx.
const frame_ty = self.module.types.findByName(self.module.types.internString("Frame")) orelse {
if (self.diagnostics) |d| d.addFmt(.err, null, "`__trace_resolve_frame` needs `Frame` (from trace.sx) in scope", .{});
return self.builder.constInt(0, .void);
};
const arg = self.lowerExpr(c.args[0]);
return self.builder.emit(.{ .trace_resolve = .{ .operand = arg } }, frame_ty);
}
if (std.mem.eql(u8, name, "error_tag_name")) {
// error_tag_name(e) → look the error-set value's runtime tag id up
// in the always-linked tag-name table. The value IS its u32 tag id.
@@ -13981,6 +13994,8 @@ pub const Lowering = struct {
if (std.mem.eql(u8, bare_name, "error_tag_name")) return .string;
if (std.mem.eql(u8, bare_name, "is_comptime")) return .bool;
if (std.mem.eql(u8, bare_name, "__interp_print_frames")) return .void;
if (std.mem.eql(u8, bare_name, "__trace_resolve_frame"))
return self.module.types.findByName(self.module.types.internString("Frame")) orelse .unresolved;
if (std.mem.eql(u8, bare_name, "is_flags")) return .bool;
if (std.mem.eql(u8, bare_name, "type_of")) return .any;
if (std.mem.eql(u8, bare_name, "field_value")) return .any;

View File

@@ -146,6 +146,7 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write
.is_comptime => try writer.writeAll("is_comptime : "),
.interp_print_frames => try writer.writeAll("interp_print_frames : "),
.trace_frame => try writer.writeAll("trace_frame : "),
.trace_resolve => |u| try writer.print("trace_resolve %{d} : ", .{u.operand.index()}),
.const_type => |tid| try writer.print("const type({s}) : ", .{tt.typeName(tid)}),
// ── Arithmetic ──────────────────────────────────────────