comptime VM: port the WRITE side (declare_type/pointer_to/register_type) -> first HANDLED lowering-time type-fns
declare_type / pointer_to / register_type are now serviced natively in
Vm.callCompilerFn, mirroring the legacy compiler_lib handlers (mint via
@constCast(table) — the lowering-time mint target is &module.types). register_type
reads the []Member slice from flat memory: ref_types is threaded through invoke ->
callCompilerFn so the slice element type (Member = {name: string, ty: Type}) gives
the field offsets + stride; each {name, ty} is decoded and minted with the same
kind branching + dup/payload rejections + idempotent re-fill as legacy.
Key unblock: the synthesized comptime type-fn wrapper was built with return type
.any, so regToValue bailed at the VM<->legacy boundary; changed to .type_value
(the legacy path reads via asTypeId regardless). The compiler-API write type-fns
(0631 register-graph, 0635 multi-edge import) now run HANDLED end-to-end on the VM
at lowering time — parity-correct, on the zeroed lowering-time context (fixed
member arrays, no allocation). The metatype make_enum/define examples still fall
back cleanly through call_builtin(define).
697/0 both gates + EXIT=0.
This commit is contained in:
@@ -31,11 +31,16 @@ with ONE welded mechanism. Branch: `reify` (off `master`). Update after every st
|
||||
> wired end-to-end: foundation (`6844fb9`), resolver flip + full `.any`-ref migration (`94f60c5`),
|
||||
> and the VM models `.type_value` natively (`554871b`). **697/0 BOTH gates + 494 unit tests.**
|
||||
> `first_user` is now **100** (slots 20–99 reserved builtin headroom so future builtins don't
|
||||
> renumber user TypeIds / churn snapshots). What remains is the PAYOFF, no longer gated on the WALL:
|
||||
> port the **WRITE side** (declare_type / register_type / pointer_to) into `Vm.callCompilerFn` +
|
||||
> give the lowering-time path a **REAL Context** (CAllocator thunk func-refs, not zeroed) → the
|
||||
> first **HANDLED** lowering-time type-fn end-to-end on the VM. Today those type-fns run partway on
|
||||
> the VM then bail cleanly at the `define`/`register_type` `call_builtin` → legacy mints (parity).
|
||||
> renumber user TypeIds / churn snapshots). The PAYOFF is now LANDED (`<this commit>`): the
|
||||
> **WRITE side** (declare_type / register_type / pointer_to) is VM-native in `Vm.callCompilerFn`
|
||||
> — the compiler-API type-fns (`0631`/`0635`) run **HANDLED end-to-end on the VM at LOWERING
|
||||
> time** (parity-correct), the first lowering-time comptime to do so; they run on the zeroed
|
||||
> lowering-time context (no allocation). **697/0 both gates + EXIT=0.** What's LEFT toward the
|
||||
> end-state ONE evaluator: (1) re-express the metatype `define`/`make_enum` over the compiler-API
|
||||
> + delete the bespoke interp arms (the `make_enum` examples still fall back cleanly through
|
||||
> `call_builtin(define)`); (2) a REAL lowering-time Context (CAllocator thunk func-refs) for
|
||||
> List-growing type-fns — deferred (no HANDLED type-fn allocates); (3) eventually flip the VM to
|
||||
> default + delete `interp.zig`.
|
||||
>
|
||||
> Done so far in Phase 3:
|
||||
> - **READ side (7 readers, dual-path):** `find_type`/`type_kind`/`type_field_count`/
|
||||
@@ -341,6 +346,27 @@ when reached (sentinels or accessor fns; see the design doc Risks).
|
||||
`List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.)
|
||||
|
||||
## Log
|
||||
- **Phase 3 P3.4 step 5 (VM plan) — WRITE side ported to the VM → FIRST HANDLED lowering-time type-fns (2026-06-18).**
|
||||
Ported `declare_type` / `pointer_to` / `register_type` into `Vm.callCompilerFn`, mirroring the
|
||||
legacy `compiler_lib` handlers (mint via `@constCast(table)` — the same mutable access the
|
||||
read-side `intern` uses; the lowering-time mint target IS `&module.types`). `register_type`
|
||||
reads the `[]Member` slice from FLAT MEMORY: threaded `ref_types` through `invoke` →
|
||||
`callCompilerFn` so the slice's element type (`Member = {name: string, ty: Type}`) gives the
|
||||
field offsets + stride; decodes each `{name, ty}` and branches on `kind` (1 struct · 2 enum ·
|
||||
3 tagged_union · 4 tuple) exactly as legacy (dup-name / payload-on-enum rejections, idempotent
|
||||
re-fill via `nominalIdentOf`). **Key unblock:** the synthesized comptime type-fn wrapper
|
||||
(`createComptimeFunction`/`…WithPrelude`) was built with return type `.any` → `regToValue`
|
||||
bailed at the VM↔legacy boundary; changed to `.type_value` (the legacy path reads via `asTypeId`
|
||||
regardless, so no legacy change). **Result: the compiler-API write type-fns now run HANDLED
|
||||
end-to-end on the VM at LOWERING time** — `0631` (register-graph: 2 HANDLED, A↔B cycle via
|
||||
forward handles + `pointer_to`) and `0635` (multi-edge import: 2 HANDLED), parity-correct. They
|
||||
run on the ZEROED lowering-time context (fixed `.[…]` member arrays, no allocation). The
|
||||
metatype `make_enum`/`define` examples (`0632`) still fall back CLEANLY through
|
||||
`call_builtin(define)` (the separate metatype path — re-expressing it onto the compiler-API is
|
||||
the other half of P3.4). **697/0 BOTH gates + EXIT=0.** On `reify`. **Next:** (optional, deferred)
|
||||
a REAL lowering-time Context (CAllocator thunk func-refs) for List-growing type-fns; and
|
||||
re-express the metatype `define`/`make_enum` over the compiler-API to delete the bespoke interp
|
||||
arms (the end-state: ONE evaluator).
|
||||
- **Phase 3 P3.4 step 4 (VM plan) — model `.type_value` natively in the comptime VM (2026-06-18).**
|
||||
The VM now HANDLES Type values instead of bailing: `kindOf(.type_value)` → `.word`; a new
|
||||
`const_type` exec arm → the word `TypeId.index()`; `regToValue` maps a `.type_value` word back
|
||||
|
||||
@@ -242,6 +242,20 @@ fn isFloat(ty: TypeId) bool {
|
||||
return ty == .f32 or ty == .f64;
|
||||
}
|
||||
|
||||
/// The nominal identity (`name` + stable `nominal_id`) of a `declare_type`'d slot —
|
||||
/// from the forward `tagged_union` OR an already-completed nominal (so a re-fill
|
||||
/// preserves identity). Mirrors `compiler_lib.nominalIdent`. Null for a non-nominal
|
||||
/// handle (not a `declare_type` result).
|
||||
fn nominalIdentOf(info: types.TypeInfo) ?struct { name: types.StringId, nominal_id: u32 } {
|
||||
return switch (info) {
|
||||
.tagged_union => |u| .{ .name = u.name, .nominal_id = u.nominal_id },
|
||||
.@"enum" => |e| .{ .name = e.name, .nominal_id = e.nominal_id },
|
||||
.@"struct" => |s| .{ .name = s.name, .nominal_id = s.nominal_id },
|
||||
.tuple => .{ .name = types.StringId.empty, .nominal_id = 0 }, // structural; name vestigial
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// A signed integer type narrower-or-equal to 64 bits — its loaded bytes must be
|
||||
/// SIGN-extended into the register (the legacy `.int` model is i64).
|
||||
fn isSignedInt(ty: TypeId) bool {
|
||||
@@ -772,7 +786,7 @@ pub const Vm = struct {
|
||||
|
||||
// ── Calls ───────────────────────────────────────────
|
||||
// Direct call: resolve the static callee `FuncId` and dispatch.
|
||||
.call => |c| return .{ .value = try self.invoke(c.callee, c.args, frame) },
|
||||
.call => |c| return .{ .value = try self.invoke(c.callee, c.args, frame, ref_types) },
|
||||
// Indirect call: the callee is a `func_ref` value (its `FuncId.index()`
|
||||
// as a word) in a register — e.g. an allocator protocol's `alloc_fn`.
|
||||
// A null (0) function pointer can't be dispatched → bail.
|
||||
@@ -782,7 +796,7 @@ pub const Vm = struct {
|
||||
self.detail = "comptime VM: call_indirect through a null function pointer";
|
||||
return error.Unsupported;
|
||||
};
|
||||
return .{ .value = try self.invoke(fid, ci.args, frame) };
|
||||
return .{ .value = try self.invoke(fid, ci.args, frame, ref_types) };
|
||||
},
|
||||
|
||||
// ── Globals / function values ───────────────────────
|
||||
@@ -923,7 +937,7 @@ pub const Vm = struct {
|
||||
/// extern/bodyless callee routes to the native libc memory builtins (else
|
||||
/// bails); a normal callee runs on the VM. Aggregate args pass as their Addr
|
||||
/// over the shared flat memory (no copy).
|
||||
fn invoke(self: *Vm, fid: inst_mod.FuncId, args: []const Ref, frame: *Frame) Error!Reg {
|
||||
fn invoke(self: *Vm, fid: inst_mod.FuncId, args: []const Ref, frame: *Frame, ref_types: []const TypeId) Error!Reg {
|
||||
const module = self.module orelse return self.failMsg("comptime VM: call needs a module (not provided)");
|
||||
if (fid.index() >= module.functions.items.len) return self.failMsg("comptime VM: call to an out-of-range function id");
|
||||
const callee = module.getFunction(fid);
|
||||
@@ -937,7 +951,7 @@ pub const Vm = struct {
|
||||
// the comptime compiler-API, serviced natively on flat memory (Phase 3
|
||||
// seed). The `compiler_welded` flag is the safety boundary.
|
||||
if (callee.compiler_welded) {
|
||||
if (try self.callCompilerFn(name, args, frame)) |r| return r;
|
||||
if (try self.callCompilerFn(name, args, frame, ref_types)) |r| return r;
|
||||
}
|
||||
// Any other extern bails → the legacy interpreter's dlsym path.
|
||||
self.detail = "comptime VM: call to an extern/builtin function not yet ported";
|
||||
@@ -1043,7 +1057,7 @@ pub const Vm = struct {
|
||||
return @enumFromInt(try self.argHandle(args, frame, i));
|
||||
}
|
||||
|
||||
fn callCompilerFn(self: *Vm, name: []const u8, args: []const Ref, frame: *Frame) Error!?Reg {
|
||||
fn callCompilerFn(self: *Vm, name: []const u8, args: []const Ref, frame: *Frame, ref_types: []const TypeId) Error!?Reg {
|
||||
const table = try self.requireTable();
|
||||
if (std.mem.eql(u8, name, "intern")) {
|
||||
if (args.len != 1) return self.failMsg("comptime intern: expected one string arg");
|
||||
@@ -1122,9 +1136,113 @@ pub const Vm = struct {
|
||||
return self.failMsg("comptime type_field_value: non-enum or out-of-range idx");
|
||||
return @as(Reg, @bitCast(v));
|
||||
}
|
||||
// ── write side (lowering-time, mints into the type table) ───────────
|
||||
// These MINT into the type table via `@constCast(table)` — the same
|
||||
// mutable access the read-side `intern` uses (the table is genuinely
|
||||
// mutable; the VM merely holds it `const`). They take/return real `Type`
|
||||
// values (`.type_value` words = `TypeId.index()`). Mirror the legacy
|
||||
// `compiler_lib` handlers exactly so the dual paths can't drift.
|
||||
if (std.mem.eql(u8, name, "declare_type")) {
|
||||
if (args.len != 1) return self.failMsg("comptime declare_type: expected (name)");
|
||||
const s = frame.get(args[0].index()); // string fat-pointer Addr
|
||||
const text = try self.machine.bytes(try self.sliceData(table, s), @intCast(try self.sliceLen(s)));
|
||||
const tbl = @constCast(table);
|
||||
const name_id = tbl.internString(text);
|
||||
if (tbl.findByName(name_id)) |existing| return @as(Reg, existing.index());
|
||||
const tid = tbl.internNominal(.{ .tagged_union = .{ .name = name_id, .fields = &.{}, .tag_type = .i64 } }, 0);
|
||||
return @as(Reg, tid.index());
|
||||
}
|
||||
if (std.mem.eql(u8, name, "pointer_to")) {
|
||||
if (args.len != 1) return self.failMsg("comptime pointer_to: expected (Type)");
|
||||
const t = try self.argTypeId(args, frame, 0);
|
||||
return @as(Reg, @constCast(table).intern(.{ .pointer = .{ .pointee = t } }).index());
|
||||
}
|
||||
if (std.mem.eql(u8, name, "register_type")) {
|
||||
return self.registerTypeVm(args, frame, ref_types);
|
||||
}
|
||||
return null; // not a known compiler function → caller bails to legacy
|
||||
}
|
||||
|
||||
/// VM-native `register_type(handle: Type, kind: i64, members: []Member) -> Type`
|
||||
/// — fill a `declare_type`'d forward slot, branching on `kind` in the compiler
|
||||
/// (mirrors `compiler_lib.handleRegisterType`, but reads `[]Member` from flat
|
||||
/// memory instead of decoding a `Value`). `Member` is `{ name: string, ty: Type }`.
|
||||
fn registerTypeVm(self: *Vm, args: []const Ref, frame: *Frame, ref_types: []const TypeId) Error!?Reg {
|
||||
const table = try self.requireTable();
|
||||
if (args.len != 3) return self.failMsg("comptime register_type: expected (handle, kind, members)");
|
||||
const handle = try self.argTypeId(args, frame, 0);
|
||||
const kind: i64 = @bitCast(frame.get(args[1].index()));
|
||||
|
||||
// Decode the `[]Member` slice: element layout comes from the slice's IR
|
||||
// element type (`Member` — `{ name: string @0, ty: Type @1 }`).
|
||||
const slice_ty = try self.refTy(ref_types, args[2]);
|
||||
if (slice_ty.isBuiltin() or table.get(slice_ty) != .slice)
|
||||
return self.failMsg("comptime register_type: members arg is not a slice");
|
||||
const member_ty = table.get(slice_ty).slice.element;
|
||||
if (member_ty.isBuiltin() or table.get(member_ty) != .@"struct" or table.get(member_ty).@"struct".fields.len != 2)
|
||||
return self.failMsg("comptime register_type: Member element must be a {name, ty} struct");
|
||||
const mfields = table.get(member_ty).@"struct".fields;
|
||||
const name_off = fieldOffset(table, member_ty, 0);
|
||||
const ty_off = fieldOffset(table, member_ty, 1);
|
||||
const name_fty = mfields[0].ty; // string
|
||||
const members_word = frame.get(args[2].index());
|
||||
const len = try self.sliceLen(members_word);
|
||||
const base = try self.sliceData(table, members_word);
|
||||
const stride: Addr = @intCast(table.typeSizeBytes(member_ty));
|
||||
if (len == 0) return self.failMsg("comptime register_type: a type with no members is never valid");
|
||||
|
||||
const tbl = @constCast(table);
|
||||
// The slot's nominal identity — accept the forward `tagged_union` from
|
||||
// `declare_type` AND an already-completed nominal of the same name (so a
|
||||
// re-fill via two import edges is idempotent). A non-nominal handle (not a
|
||||
// `declare_type`'d slot) is rejected.
|
||||
const ident = nominalIdentOf(table.get(handle)) orelse
|
||||
return self.failMsg("comptime register_type: handle is not a declare_type'd nominal slot");
|
||||
|
||||
const M = struct { name: types.StringId, ty: TypeId };
|
||||
// Read each `Member { name, ty }` out of flat memory.
|
||||
var members = std.ArrayList(M).empty;
|
||||
defer members.deinit(self.gpa);
|
||||
for (0..@intCast(len)) |i| {
|
||||
const elem = base + @as(Addr, @intCast(i)) * stride;
|
||||
const name_fp = try self.readField(table, elem + name_off, name_fty); // string fat-pointer Addr
|
||||
const mname = try self.machine.bytes(try self.sliceData(table, name_fp), @intCast(try self.sliceLen(name_fp)));
|
||||
const mty: TypeId = @enumFromInt(@as(u32, @intCast(try self.readField(table, elem + ty_off, .type_value))));
|
||||
members.append(self.gpa, .{ .name = tbl.internString(mname), .ty = mty }) catch return self.failMsg("comptime register_type: out of memory");
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
4 => { // tuple — positional element types (names ignored)
|
||||
const tys = self.gpa.alloc(TypeId, members.items.len) catch return self.failMsg("comptime register_type: out of memory");
|
||||
for (members.items, 0..) |m, i| tys[i] = m.ty;
|
||||
tbl.replaceKeyedInfo(handle, .{ .tuple = .{ .fields = tys, .names = null } });
|
||||
},
|
||||
2 => { // actual (payloadless) enum — members are variant NAMES; payload must be void
|
||||
const names = self.gpa.alloc(types.StringId, members.items.len) catch return self.failMsg("comptime register_type: out of memory");
|
||||
for (members.items, 0..) |m, i| {
|
||||
if (m.ty != .void) return self.failMsg("comptime register_type: payload variant — use kind 3 (tagged_union)");
|
||||
for (names[0..i]) |prev| if (prev == m.name) return self.failMsg("comptime register_type: duplicate enum variant");
|
||||
names[i] = m.name;
|
||||
}
|
||||
tbl.replaceKeyedInfo(handle, .{ .@"enum" = .{ .name = ident.name, .variants = names, .nominal_id = ident.nominal_id } });
|
||||
},
|
||||
1, 3 => { // struct / tagged_union — `{ name, ty }` fields (dup names rejected)
|
||||
const flds = self.gpa.alloc(types.TypeInfo.StructInfo.Field, members.items.len) catch return self.failMsg("comptime register_type: out of memory");
|
||||
for (members.items, 0..) |m, i| {
|
||||
for (flds[0..i]) |prev| if (prev.name == m.name) return self.failMsg("comptime register_type: duplicate member name");
|
||||
flds[i] = .{ .name = m.name, .ty = m.ty };
|
||||
}
|
||||
const full: types.TypeInfo = if (kind == 1)
|
||||
.{ .@"struct" = .{ .name = ident.name, .fields = flds, .nominal_id = ident.nominal_id } }
|
||||
else
|
||||
.{ .tagged_union = .{ .name = ident.name, .fields = flds, .tag_type = .i64, .nominal_id = ident.nominal_id } };
|
||||
tbl.replaceKeyedInfo(handle, full);
|
||||
},
|
||||
else => return self.failMsg("comptime register_type: unknown kind code"),
|
||||
}
|
||||
return @as(Reg, handle.index());
|
||||
}
|
||||
|
||||
// ── Reg ↔ Value bridge (legacy-interop boundary) ────────────────────────
|
||||
//
|
||||
// The wiring step routes a comptime eval through the VM, falling back to the
|
||||
|
||||
@@ -449,7 +449,11 @@ pub fn evalComptimeType(self: *Lowering, expr: *const Node) ?TypeId {
|
||||
// lowering because a `*Name` can lower before its `declare` within the same
|
||||
// body. The interp's `declare` returns this same slot; `define` completes it.
|
||||
preregisterForwardTypes(self, expr);
|
||||
const func_id = self.createComptimeFunction("__ctype", expr, .any);
|
||||
// The wrapper returns a `Type` value → `.type_value` (the dedicated 8-byte
|
||||
// handle). The legacy path reads the result via `asTypeId` regardless, but the
|
||||
// VM path converts `func.ret` — `.type_value` → `.type_tag` (an `.any` return
|
||||
// would box the result and bail at the VM↔legacy boundary).
|
||||
const func_id = self.createComptimeFunction("__ctype", expr, .type_value);
|
||||
return self.runComptimeTypeFunc(func_id, expr.span);
|
||||
}
|
||||
|
||||
@@ -463,7 +467,8 @@ pub fn evalComptimeTypeBody(self: *Lowering, body: *const Node, ret_expr: *const
|
||||
// the prelude, not the return) so forward types register before lowering.
|
||||
preregisterForwardTypes(self, body);
|
||||
const prelude = preludeBeforeReturn(body);
|
||||
const func_id = self.createComptimeFunctionWithPrelude("__ctype", prelude, ret_expr, .any);
|
||||
// Return type `.type_value` (a `Type` value) — see `evalComptimeType`.
|
||||
const func_id = self.createComptimeFunctionWithPrelude("__ctype", prelude, ret_expr, .type_value);
|
||||
return self.runComptimeTypeFunc(func_id, ret_expr.span);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user