ERR/E4.1: trace.print_interpreter_frames() — Phase E4 complete

The last E4 item: a comptime call-frame dump.

- New nullary `interp_print_frames` IR op (inst/print). The interpreter
  maintains a `call_chain` side-stack (push/pop a FuncId around each sx-bodied
  `call`, freed in deinit) and `printInterpFrames` appends the chain to its
  output — most-recent-last, with the dump frame itself skipped. emit_llvm
  makes the op a no-op: compiled code has no interpreter stack, and the only
  caller is `process.exit`'s dead `is_comptime()` branch.
- Lowered from a name-recognized `__interp_print_frames()` builtin
  (tryLowerReflectionCall + inferExprType → void).
- `trace.print_interpreter_frames()` wraps the builtin; wired into
  `process.exit`'s comptime branch (process.sx now imports trace.sx).
- Frame source locations await IR-offset resolution (the comptime analog of
  DWARF), so only function names print today.

examples/252-interp-frames.sx (top-level `#run` drives the dump; exit 0).
Phase E4 (entry-point + stdlib error story) is now 100% complete.
This commit is contained in:
agra
2026-06-01 12:22:23 +03:00
parent e04bec488b
commit d67fb7b9b3
10 changed files with 88 additions and 0 deletions

View File

@@ -143,6 +143,10 @@ pub const Interpreter = struct {
output: std.ArrayList(u8),
call_depth: u32 = 0,
max_call_depth: u32 = 256,
/// Active sx call-frame chain (oldest→newest), maintained across `call` for
/// `trace.print_interpreter_frames()` (ERR E4.1). Only sx-bodied frames are
/// tracked — foreign calls return before the frame is pushed.
call_chain: std.ArrayList(FuncId) = .empty,
// Heap: dynamically allocated memory blocks
heap: std.ArrayList([]u8),
@@ -206,6 +210,7 @@ pub const Interpreter = struct {
}
self.heap.deinit(self.alloc);
self.output.deinit(self.alloc);
self.call_chain.deinit(self.alloc);
self.global_values.deinit();
self.hooks.deinit();
}
@@ -424,6 +429,24 @@ pub const Interpreter = struct {
};
}
/// Append the current sx call-frame chain to the interp output, most-recent
/// last (ERR E4.1). The topmost frame is `print_interpreter_frames` itself
/// (the dump site), so it's skipped. Frame source locations await IR-offset
/// resolution (the comptime analog of DWARF), so only function names print.
fn printInterpFrames(self: *Interpreter) void {
const n = self.call_chain.items.len;
if (n <= 1) return;
self.output.appendSlice(self.alloc, "comptime call frames (most recent call last):\n") catch {};
var i: usize = 0;
while (i < n - 1) : (i += 1) {
const fid = self.call_chain.items[i];
const fname = self.module.types.getString(self.module.getFunction(fid).name);
const line = std.fmt.allocPrint(self.alloc, " at {s}\n", .{fname}) catch continue;
defer self.alloc.free(line);
self.output.appendSlice(self.alloc, line) catch {};
}
}
fn callForeign(self: *Interpreter, func: *const inst_mod.Function, args: []const Value) InterpError!Value {
const name = self.module.types.getString(func.name);
@@ -508,6 +531,10 @@ pub const Interpreter = struct {
return self.callForeign(func, args);
}
// Track the sx call chain for `trace.print_interpreter_frames()`.
self.call_chain.append(self.alloc, func_id) catch {};
defer _ = self.call_chain.pop();
// Compute total refs: params + all instructions across all blocks
var total_refs: u32 = @intCast(func.params.len);
for (func.blocks.items) |blk| {
@@ -609,6 +636,10 @@ pub const Interpreter = struct {
.const_null => return .{ .value = .null_val },
.const_undef => return .{ .value = .undef },
.is_comptime => return .{ .value = .{ .boolean = true } },
.interp_print_frames => {
self.printInterpFrames();
return .{ .value = .void_val };
},
.const_type => |tid| return .{ .value = .{ .type_tag = tid } },
// ── Arithmetic ──────────────────────────────────────