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:
@@ -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;
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ──────────────────────────────────────
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ──────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user