comptime VM: real lowering-time Context — allocating + List-building type-fns run on the VM (issue 0141)

The VM can now evaluate a comptime type-fn that allocates at lowering time (the
0141 family) — the legacy interp cannot. Four changes:

- runComptimeTypeFunc (lower/comptime.zig): force the CAllocator->Allocator thunks
  to exist (getOrCreateThunks, idempotent, guarded) BEFORE eval. A type-fn const
  runs at scanDecls (Pass 1), before Pass 1c builds the default-context global +
  thunks, so the comptime allocator was otherwise null.
- materializeDefaultContext: build a REAL context at lowering time when the global
  is absent — find the two thunks by name and lay their func-refs into the inline
  Allocator value at the head of Context, so context.allocator.alloc_bytes
  dispatches call_indirect -> thunk -> native VM malloc.
- aggType: deref a pointer base_type (the List write path emits struct_gep with
  base_type = *Struct; fieldOffset panicked on the pointer — now derefs, no panic).
- subslice: handle a [*]T many-pointer / *T base (a List's items field — the base
  IS the data pointer).

Verified end-to-end (manual probe): a compiler-API type-fn building its []Member in
a List(Member) runs HANDLED on the VM and mints (green=7) — the 0141 List-growth
pattern. Can't be a corpus test yet (gate-OFF/legacy can't allocate at lowering
time — the dual-path bind), so locked in via VM unit tests (many-pointer subslice;
struct_gep with a pointer base_type). 697/0 both gates + all unit tests.
This commit is contained in:
agra
2026-06-18 15:04:55 +03:00
parent 3c0e0852a8
commit eb68d9ed94
5 changed files with 165 additions and 17 deletions

View File

@@ -348,19 +348,35 @@ pub const Vm = struct {
if (g.init_val) |iv| try self.layoutConst(table, iv, g.ty, addr);
return addr;
}
// No `__sx_default_context` global yet — this is the LOWERING-time path
// (the global is emitted later, at codegen). Materialize a ZEROED `Context`
// of the right size instead: a type-fn that never touches the allocator
// ignores it; one that DOES allocate reads a null `alloc_fn` (zeroed) and
// `call_indirect` on the null func-ref bails → legacy fallback (which has a
// real default context). A real lowering-time context (with the CAllocator
// thunk func-refs, so allocating type-fns also run on the VM) is a follow-up.
// `internString` of an existing name is idempotent (pool-only, no layout
// change) — the same `@constCast` the reader handlers use on the table.
// No `__sx_default_context` global yet — the LOWERING-time path (a type-fn
// const runs at scanDecls, before Pass 1c emits that global). Build the
// REAL default context directly from the CAllocator→Allocator thunks
// (forced to exist by `runComptimeTypeFunc` before this runs), mirroring
// the legacy `defaultContextValue` / `emitDefaultContextGlobal`: the inline
// `Allocator` value is `{ ctx: *void = null, alloc_fn, dealloc_fn }` (three
// pointer-sized words) at the head of `Context`, so `context.allocator`
// dispatches `alloc_bytes` → `call_indirect` → the thunk → native `malloc`,
// all on the VM. If a thunk is absent (std not imported), the field stays
// null (zeroed) and an allocating body bails — same as a non-std program.
const ctx_name = @constCast(table).internString("Context");
const ctx_ty = table.findByName(ctx_name) orelse
return self.failMsg("comptime VM: no Context type to materialize the implicit context");
return self.machine.allocBytes(table.typeSizeBytes(ctx_ty), table.typeAlignBytes(ctx_ty)); // zeroed
const addr = self.machine.allocBytes(table.typeSizeBytes(ctx_ty), table.typeAlignBytes(ctx_ty)); // zeroed
const ps: Addr = table.pointer_size;
if (self.findFuncByName(module, "__thunk_CAllocator_Allocator_alloc_bytes")) |fid|
try self.machine.writeWord(addr + ps, ps, funcRefWord(fid)); // allocator.alloc_fn @ +ptr_size
if (self.findFuncByName(module, "__thunk_CAllocator_Allocator_dealloc_bytes")) |fid|
try self.machine.writeWord(addr + 2 * ps, ps, funcRefWord(fid)); // allocator.dealloc_fn @ +2*ptr_size
return addr;
}
/// Find a module function by its exact name → its `FuncId`, or null. Used to
/// resolve the CAllocator thunk func-refs for the lowering-time default context.
fn findFuncByName(_: *Vm, module: *const Module, name: []const u8) ?inst_mod.FuncId {
for (module.functions.items, 0..) |*f, i| {
if (std.mem.eql(u8, module.types.getString(f.name), name)) return inst_mod.FuncId.fromIndex(@intCast(i));
}
return null;
}
/// Lay a static `ConstantValue` of type `ty` into flat memory at `addr` (the
@@ -689,6 +705,10 @@ pub const Vm = struct {
} else if (!bty.isBuiltin()) {
switch (table.get(bty)) {
.array => |a| elem = a.element,
// `[*]T` (a List's `items` field) / `*T`: the base IS the
// data pointer; subslicing yields `{ base + lo, hi - lo }`.
.many_pointer => |mp| elem = mp.element,
.pointer => |p| elem = p.pointee,
.slice => |sl| {
elem = sl.element;
data = try self.sliceData(table, base);
@@ -1496,13 +1516,15 @@ pub const Vm = struct {
/// lowering set it, else the base operand's Ref type — dereferenced when the
/// base is a POINTER (`struct_gep` on an `alloca` result is `*S` → `S`).
fn aggType(self: *Vm, table: *const types.TypeTable, fa: inst_mod.FieldAccess, ref_types: []const TypeId) Error!TypeId {
if (fa.base_type) |bt| return bt;
const rt = try self.refTy(ref_types, fa.base);
if (!rt.isBuiltin()) {
const info = table.get(rt);
if (info == .pointer) return info.pointer.pointee;
}
return rt;
// The explicit `base_type` when lowering set it, else the base operand's
// Ref type. Either way, deref ONE pointer level when the result is a
// pointer-to-struct: a `struct_gep`/`struct_get` on a `*Struct` receiver
// (e.g. `list.field` where `list: *List`) computes the field offset on the
// POINTEE struct, with the base register already holding the pointer
// address. Lowering sets `base_type = *Struct` on the write/lvalue path.
const raw = fa.base_type orelse (try self.refTy(ref_types, fa.base));
if (!raw.isBuiltin() and table.get(raw) == .pointer) return table.get(raw).pointer.pointee;
return raw;
}
/// The byte offset of tuple element `idx` — the positional analogue of