From 554871ba0bbee72290cd55094ef8d3a58a0878fb Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 18 Jun 2026 14:05:16 +0300 Subject: [PATCH] comptime VM: model .type_value natively (word); harden struct_init vs arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kindOf(.type_value) -> .word; new const_type exec arm -> word = TypeId.index(); regToValue maps a .type_value word back to a .type_tag Value at the legacy boundary. The VM now runs comptime evals involving Type values instead of bailing. This reached a latent VM panic: struct_init assumed a .@"struct" result type and union-access-panicked on an array literal (EnumVariant.[...]). It is the generic aggregate-literal op, so it now dispatches on the result kind (struct/array/ tuple) and bails loudly on anything else — never panics (CLAUDE.md no-panic). 697/0 both gates (make_enum type-fns run further on the VM, then bail cleanly at the define call_builtin -> legacy mints; no mutation before bail). VM unit test added (const_type -> word -> regToValue -> .type_tag). --- current/CHECKPOINT-COMPILER-API.md | 14 ++++++++++++ src/ir/comptime_vm.test.zig | 23 ++++++++++++++++++++ src/ir/comptime_vm.zig | 34 +++++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index ee9acb4e..8f613fdf 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -335,6 +335,20 @@ 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 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 + to a `.type_tag` Value at the legacy boundary (`valueToReg` already mapped `.type_tag` → + index). Surfaced + fixed a VM PANIC (forbidden): `struct_init` assumed a `.@"struct"` result + type and union-access-panicked on an ARRAY literal (`EnumVariant.[ … ]`, reached now that Type + args no longer bail early) — it's the generic aggregate-literal op, so it now dispatches on the + result kind (struct / array / tuple) and BAILS loudly on anything else, never panics. **697/0 + both gates** (the make_enum type-fns now run further on the VM, then bail cleanly at the + `define`/`make_enum` `call_builtin` → legacy mints — no mutation before the bail, parity holds). + VM unit test added (const_type → word → regToValue → `.type_tag`). On `reify`. **Next (the + payoff):** 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. - **Phase 3 P3.4 step 3 (VM plan) — dedicated `Type` builtin TypeId: RESOLVER FLIPPED + `.any` migration (2026-06-18).** Flipped `type_resolver:64` (`"Type"` → `.type_value`), `module.zig` `constType` (result type → `.type_value`), and `emitConstType` (a bare i64 carrying `tid.index()`, NOT a 16-byte Any diff --git a/src/ir/comptime_vm.test.zig b/src/ir/comptime_vm.test.zig index eddfb5c5..b43cfbf7 100644 --- a/src/ir/comptime_vm.test.zig +++ b/src/ir/comptime_vm.test.zig @@ -589,6 +589,29 @@ test "comptime_vm exec: payloadless enum_init + enum_tag" { try std.testing.expectEqual(@as(i64, 11), toI64(try v.run(&fb.func, &.{}))); } +test "comptime_vm exec: const_type yields a Type-value word; regToValue bridges it to .type_tag" { + const alloc = std.testing.allocator; + var table = types.TypeTable.init(alloc); + defer table.deinit(); + + // return → a `.type_value`-typed entry whose word is u32.index() + var fb = Fb.init(alloc, &.{}, .type_value); + defer fb.deinit(); + const b0 = fb.block(&.{}); + const ct = fb.add(b0, inst(.{ .const_type = .u32 }, .type_value)); + _ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(ct) } }, .void)); + + var v = vm.Vm.init(alloc); + v.table = &table; + defer v.deinit(); + const word = try v.run(&fb.func, &.{}); + try std.testing.expectEqual(@as(u64, types.TypeId.u32.index()), word); + + // The legacy boundary maps the word back to a first-class `.type_tag` Value. + const val = try v.regToValue(alloc, &table, word, .type_value); + try std.testing.expectEqual(types.TypeId.u32, val.type_tag); +} + test "comptime_vm exec: deref a pointer; addr_of passes through a struct address" { const alloc = std.testing.allocator; var table = types.TypeTable.init(alloc); diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index 90959383..a8da5c28 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -499,6 +499,10 @@ pub const Vm = struct { .const_bool => |v| return .{ .value = @intFromBool(v) }, .const_float => |v| return .{ .value = @bitCast(v) }, .const_null, .const_undef => return .{ .value = null_addr }, + // A `Type` literal: the 8-byte handle is the `TypeId` index in a word + // (the `.type_value` representation). `regToValue` maps it back to a + // `.type_tag` Value at the legacy boundary. + .const_type => |tid| return .{ .value = @as(Reg, tid.index()) }, // ── Arithmetic ────────────────────────────────────── .add, .sub, .mul, .div, .mod => |b| return .{ @@ -546,12 +550,26 @@ pub const Vm = struct { .struct_init => |agg| { const table = try self.requireTable(); const sty = ins.ty; + if (sty.isBuiltin()) return self.failMsg("comptime VM: struct_init at a builtin result type"); const addr = self.machine.allocBytes(table.typeSizeBytes(sty), table.typeAlignBytes(sty)); - const fields = table.get(sty).@"struct".fields; - for (fields, 0..) |f, i| { - if (i >= agg.fields.len) break; - const off = fieldOffset(table, sty, @intCast(i)); - try self.writeField(table, addr + off, f.ty, frame.get(agg.fields[i].index())); + // `struct_init` is the generic aggregate-literal op — its result + // type may be a struct, an ARRAY (e.g. `EnumVariant.[ … ]`), or a + // tuple. Lay each operand out at the matching offset; bail loudly on + // any other shape (never a `.@"struct"`-union-access panic). + switch (table.get(sty)) { + .@"struct" => |s| for (s.fields, 0..) |f, i| { + if (i >= agg.fields.len) break; + try self.writeField(table, addr + fieldOffset(table, sty, @intCast(i)), f.ty, frame.get(agg.fields[i].index())); + }, + .array => |a| { + const esz: Addr = @intCast(table.typeSizeBytes(a.element)); + for (agg.fields, 0..) |fr, i| try self.writeField(table, addr + @as(Addr, @intCast(i)) * esz, a.element, frame.get(fr.index())); + }, + .tuple => |t| for (t.fields, 0..) |fty, i| { + if (i >= agg.fields.len) break; + try self.writeField(table, addr + tupleFieldOffset(table, sty, @intCast(i)), fty, frame.get(agg.fields[i].index())); + }, + else => return self.failMsg("comptime VM: struct_init at a non-aggregate result type"), } return .{ .value = addr }; }, @@ -1166,6 +1184,9 @@ pub const Vm = struct { .word => { if (isFloat(ty)) return .{ .float = @bitCast(reg) }; if (ty == .bool) return .{ .boolean = reg != 0 }; + // A `Type` value word is a `TypeId` index → the first-class + // `.type_tag` Value the legacy interp/host uses for Type values. + if (ty == .type_value) return .{ .type_tag = TypeId.fromIndex(@intCast(reg)) }; // A function-typed word is an encoded func-ref; map it back to // `.func_ref` (or `.null_val` for the null word) so the host // serializes it identically to the legacy (e.g. the comptime-global @@ -1214,6 +1235,9 @@ pub const Vm = struct { fn kindOf(table: *const types.TypeTable, ty: TypeId) Kind { switch (ty) { .bool, .i8, .u8, .i16, .u16, .i32, .u32, .f32, .i64, .u64, .f64, .usize, .isize, .cstring => return .word, + // A comptime `Type` value is an 8-byte handle (a `TypeId` in a word) — + // distinct from the 16-byte boxed `.any`. It rides as a word. + .type_value => return .word, .string => return .aggregate, // {ptr,len} fat pointer (16B), by-address else => {}, }