mem: Steps 5-7 — context-identifier rebind + interp ctx bootstrap
Step 5 — `context` resolves through `current_ctx_ref`. The compile-time
emit of the default GPA into the `context` global is gone; entry points
already bind `current_ctx_ref` to `&__sx_default_context` and every
sx-to-sx call forwards it. `allocViaContext` sources from
`current_ctx_ref` too. `matchContextAllocCall` is kept as a comptime
escape hatch: the ct_module spun up by `evalComptimeString` doesn't get
the full Allocator/CAllocator/Context type registration so the protocol-
dispatch chain wouldn't run in the interp; codegen also wins from the
direct libc malloc/free.
Step 6 — `push Context.{...}` stack-discipline rewrite. Allocates a
fresh `Context` slot, binds `current_ctx_ref` to it for the body's
lexical scope, restores on exit. No global, no walk.
Step 7 — interp parity. `defaultContextValue()` builds the Context
aggregate (CAllocator thunks for alloc/dealloc, null data) on demand.
`interp.call` bootstraps slot_ptr(0) when an entry function with
implicit ctx is called sans args; `materializeCtxArg` dereferences the
caller's slot_ptr into the aggregate at every sx-to-sx call boundary so
the callee's `load(ref_0)` lands on the value; `load` of an aggregate
is a passthrough. `.global_addr` of `__sx_default_context` returns the
aggregate directly so exported entries' first-line `global_addr(...)`
runs cleanly in `#run`.
`ct_lowering` inherits `implicit_ctx_enabled` + `has_implicit_ctx` so
functions lowered into the ct module carry ctx like their main-module
twins.
152/152 example tests pass. Snapshots regen.
This commit is contained in:
@@ -164,6 +164,37 @@ pub const Interpreter = struct {
|
||||
self.hooks.deinit();
|
||||
}
|
||||
|
||||
// ── Implicit Context ──────────────────────────────────────────
|
||||
|
||||
/// Build the default Context aggregate for top-level interp calls.
|
||||
/// Mirrors the static `__sx_default_context` LLVM global: a Context
|
||||
/// whose `allocator` field is the stateless CAllocator inline-protocol
|
||||
/// value (alloc/dealloc thunks bottom out at libc malloc/free).
|
||||
fn defaultContextValue(self: *Interpreter) Value {
|
||||
const tbl_ptr: *const @import("types.zig").TypeTable = &self.module.types;
|
||||
const tbl = @constCast(tbl_ptr);
|
||||
const alloc_thunk_name = tbl.internString("__thunk_CAllocator_Allocator_alloc");
|
||||
const dealloc_thunk_name = tbl.internString("__thunk_CAllocator_Allocator_dealloc");
|
||||
|
||||
var alloc_fn: Value = .null_val;
|
||||
var dealloc_fn: Value = .null_val;
|
||||
for (self.module.functions.items, 0..) |func, i| {
|
||||
if (func.name == alloc_thunk_name) alloc_fn = .{ .func_ref = FuncId.fromIndex(@intCast(i)) };
|
||||
if (func.name == dealloc_thunk_name) dealloc_fn = .{ .func_ref = FuncId.fromIndex(@intCast(i)) };
|
||||
}
|
||||
|
||||
const allocator_fields = self.alloc.alloc(Value, 3) catch unreachable;
|
||||
allocator_fields[0] = .null_val; // CAllocator receiver — stateless
|
||||
allocator_fields[1] = alloc_fn;
|
||||
allocator_fields[2] = dealloc_fn;
|
||||
const allocator_val: Value = .{ .aggregate = allocator_fields };
|
||||
|
||||
const ctx_fields = self.alloc.alloc(Value, 2) catch unreachable;
|
||||
ctx_fields[0] = allocator_val;
|
||||
ctx_fields[1] = .null_val;
|
||||
return .{ .aggregate = ctx_fields };
|
||||
}
|
||||
|
||||
// ── Heap operations ────────────────────────────────────────────
|
||||
|
||||
fn heapAlloc(self: *Interpreter, size: usize) Value.HeapPtr {
|
||||
@@ -367,9 +398,23 @@ pub const Interpreter = struct {
|
||||
var frame = Frame.initSized(self.alloc, total_refs);
|
||||
defer frame.deinit();
|
||||
|
||||
// Bind parameters as initial refs (indices 0..N-1)
|
||||
// Implicit-context bootstrap: when an entry point with implicit
|
||||
// ctx is called without an explicit ctx arg, materialise the
|
||||
// default context in a fresh slot and bind slot_ptr(0) to ref 0.
|
||||
// This is the interp-side equivalent of FFI-inbound wrappers
|
||||
// installing `&__sx_default_context` at function entry.
|
||||
var skip_first: u32 = 0;
|
||||
if (func.has_implicit_ctx and args.len + 1 == func.params.len) {
|
||||
const ctx_val = self.defaultContextValue();
|
||||
const slot = frame.allocSlot(self.alloc);
|
||||
frame.storeSlot(slot, ctx_val);
|
||||
frame.setRef(0, .{ .slot_ptr = slot });
|
||||
skip_first = 1;
|
||||
}
|
||||
|
||||
// Bind parameters as initial refs (indices skip_first..N-1)
|
||||
for (args, 0..) |arg, i| {
|
||||
frame.setRef(@intCast(i), arg);
|
||||
frame.setRef(@intCast(i + skip_first), arg);
|
||||
}
|
||||
|
||||
// Start at the entry block (index 0)
|
||||
@@ -536,6 +581,10 @@ pub const Interpreter = struct {
|
||||
}
|
||||
return .{ .value = slot_val };
|
||||
},
|
||||
// The implicit __sx_ctx arrives as an aggregate after
|
||||
// materializeCtxArg dereferences the caller's slot_ptr.
|
||||
// `load(ref_0)` then naturally yields the Context value.
|
||||
.aggregate => return .{ .value = ptr },
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
@@ -667,6 +716,14 @@ pub const Interpreter = struct {
|
||||
// its own slot table and read garbage.
|
||||
args[i] = self.materializeForCall(frame, frame.getRef(ref));
|
||||
}
|
||||
// The implicit __sx_ctx is logically a `*Context` but the
|
||||
// interp can't dereference cross-frame slot_ptrs. Materialise
|
||||
// args[0] to the loaded Context aggregate so the callee can
|
||||
// treat its slot 0 as the value directly.
|
||||
const callee_func = self.module.getFunction(c.callee);
|
||||
if (callee_func.has_implicit_ctx and args.len >= 1) {
|
||||
args[0] = self.materializeCtxArg(frame, args[0]);
|
||||
}
|
||||
const result = try self.call(c.callee, args);
|
||||
return .{ .value = result };
|
||||
},
|
||||
@@ -1020,8 +1077,18 @@ pub const Interpreter = struct {
|
||||
const val = try self.getGlobal(gid);
|
||||
return .{ .value = val };
|
||||
},
|
||||
.global_addr => {
|
||||
// Address-of-global not meaningful in interpreter
|
||||
.global_addr => |gid| {
|
||||
// The implicit-context default global is the only global
|
||||
// whose address sees runtime use. Return the Context
|
||||
// aggregate directly so `load(args[0])` yields it via the
|
||||
// aggregate-passthrough branch of the `.load` handler.
|
||||
if (gid.index() < self.module.globals.items.len) {
|
||||
const global = &self.module.globals.items[gid.index()];
|
||||
const name = self.module.types.getString(global.name);
|
||||
if (std.mem.eql(u8, name, "__sx_default_context")) {
|
||||
return .{ .value = self.defaultContextValue() };
|
||||
}
|
||||
}
|
||||
return error.CannotEvalComptime;
|
||||
},
|
||||
.func_ref => |fid| {
|
||||
@@ -1114,7 +1181,11 @@ pub const Interpreter = struct {
|
||||
const args = self.alloc.alloc(Value, ci.args.len) catch return error.CannotEvalComptime;
|
||||
defer self.alloc.free(args);
|
||||
for (ci.args, 0..) |ref, i| {
|
||||
args[i] = frame.getRef(ref);
|
||||
args[i] = self.materializeForCall(frame, frame.getRef(ref));
|
||||
}
|
||||
const target = self.module.getFunction(fid);
|
||||
if (target.has_implicit_ctx and args.len >= 1) {
|
||||
args[0] = self.materializeCtxArg(frame, args[0]);
|
||||
}
|
||||
const result = try self.call(fid, args);
|
||||
return .{ .value = result };
|
||||
@@ -1232,6 +1303,17 @@ pub const Interpreter = struct {
|
||||
/// emitted by `struct_gep` / `index_gep`) into the resolved parent value.
|
||||
/// Slot indices are frame-local; a slice passed across a call would otherwise
|
||||
/// read its data_ptr out of the callee's slot table.
|
||||
/// Resolve the implicit __sx_ctx arg to its loaded Context value so
|
||||
/// callees can treat their own slot 0 as the aggregate directly
|
||||
/// (no cross-frame slot_ptr indirection).
|
||||
fn materializeCtxArg(self: *Interpreter, frame: *Frame, val: Value) Value {
|
||||
_ = self;
|
||||
return switch (val) {
|
||||
.slot_ptr => |slot| frame.loadSlot(slot),
|
||||
else => val,
|
||||
};
|
||||
}
|
||||
|
||||
fn materializeForCall(self: *Interpreter, frame: *Frame, val: Value) Value {
|
||||
switch (val) {
|
||||
.aggregate => |fields| {
|
||||
|
||||
Reference in New Issue
Block a user