From 1526d198e2aa4c7bb82b97fd73f10b81e2d45317 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 18 Jun 2026 16:56:50 +0300 Subject: [PATCH] comptime VM: box_any/unbox_any + .any as a 16-byte flat-memory aggregate (Phase 4A.1) Ported the Any-boxing conversion pair: - box_any: alloc the 16-byte { type_tag@0, value@8 } box, tag = source TypeId index (matches the legacy comptime interp; runtime anyTag also normalizes arbitrary-width ints). Value slot holds a word source's scalar bytes (via writeField(source_type) so f32 round-trips) or an aggregate source's flat-memory ADDR (the runtime pointer-in-value-slot shape). - unbox_any: read the value slot back (word -> readField; aggregate -> the stored ADDR). Required promoting .any to a first-class flat-memory aggregate (was kindOf -> .unsupported): kindOf(.any) = .aggregate (16B, by-address) and fieldOffset special-cases .any to the {@0, @8} layout (shared with string/slice). Without the latter a struct_get on an Any panicked (union field 'struct' while 'any' is active) -- caught + fixed, no crash. Updated two unit tests that used unbox_any as the "unported op" example -> compiler_call; added a box->unbox round-trip test. 697/0 both gates + all unit tests. The 6 box_any examples no longer bail at box_any (output matches legacy) but fall back further at switch_br/type_name/out (later 4A steps). --- current/CHECKPOINT-COMPILER-API.md | 15 ++++++++++++ src/ir/comptime_vm.test.zig | 34 ++++++++++++++++++++------ src/ir/comptime_vm.zig | 39 ++++++++++++++++++++++++++++-- 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index 43115da9..8ec7f0a8 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -352,6 +352,21 @@ when reached (sentinels or accessor fns; see the design doc Risks). `List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.) ## Log +- **Phase 4A.1 (VM plan) — `box_any`/`unbox_any` on the VM + `.any` as a 16-byte aggregate (2026-06-18).** + Ported the Any-boxing conversion pair: `box_any` allocates the 16-byte `{ type_tag@0, value@8 }` + box (tag = source TypeId index, matching the legacy comptime interp), writing a word source's + scalar via `writeField(source_type)` (so f32 round-trips) or an aggregate source's flat-memory + ADDR (the runtime pointer-in-value-slot shape); `unbox_any` reads the value slot back (word → + `readField`, aggregate → the stored ADDR). **Required making `.any` a first-class flat-memory + aggregate** (it was `kindOf → .unsupported`): `kindOf(.any) = .aggregate` (16B, by-address) + + `fieldOffset` special-cases `.any` to the `{@0, @8}` layout (shared with string/slice) — without + the latter, a `struct_get` on an Any panicked (`union field 'struct' while 'any' is active`), + caught + fixed (no crash; "never crash" upheld). Updated two unit tests that used `unbox_any` as + the "unported op" example → now `compiler_call`; added a box→unbox round-trip test. **697/0 BOTH + gates + all unit tests.** On `reify`. The 6 box_any examples (0114/0520–0524/1035) no longer bail + at box_any and produce VM output byte-matching legacy, but are not YET fully HANDLED — they now + fall back further at `switch_br` (comptime Any-tag type-switch), `type_name`, and `out`/print + (4A.2+/later steps). **Next (4A.2):** comptime `out`/print (VM output buffer + flush). - **Phase 3 P3.4 step 8 (VM plan) — VM-native `type_info` REFLECTION → the whole metatype surface is HANDLED (2026-06-18).** Ported `type_info($T)` into the VM (`callBuiltinVm` `.type_info` arm → new `buildTypeInfo`), the inverse of step 7's `define`: reflect a type INTO a `TypeInfo` VALUE built in FLAT MEMORY (the diff --git a/src/ir/comptime_vm.test.zig b/src/ir/comptime_vm.test.zig index c797e54e..fb28eba8 100644 --- a/src/ir/comptime_vm.test.zig +++ b/src/ir/comptime_vm.test.zig @@ -689,6 +689,26 @@ test "comptime_vm exec: tagged-union enum_init with payload lays out {tag@0, pay try std.testing.expectEqual(@as(u64, 42), try v.machine.readWord(addr + 8, 8)); // payload } +test "comptime_vm exec: box_any/unbox_any round-trips a scalar through the {tag, value} box" { + const alloc = std.testing.allocator; + var table = types.TypeTable.init(alloc); + defer table.deinit(); + // a := box_any(42, i64); return unbox_any(a) → 42 (exercises both the 16-byte + // {tag@0, value@8} box write and the value-slot read-back). + var fb = Fb.init(alloc, &.{}, .i64); + defer fb.deinit(); + const b0 = fb.block(&.{}); + const c = fb.add(b0, inst(.{ .const_int = 42 }, .i64)); + const a = fb.add(b0, inst(.{ .box_any = .{ .operand = ref(c), .source_type = .i64 } }, .any)); + const u = fb.add(b0, inst(.{ .unbox_any = .{ .operand = ref(a) } }, .i64)); + _ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(u) } }, .void)); + + var v = vm.Vm.init(alloc); + v.table = &table; + defer v.deinit(); + try std.testing.expectEqual(@as(i64, 42), 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); @@ -1245,11 +1265,12 @@ test "comptime_vm tryEval: pure function → Value; unsupported → null" { const v = vm.tryEval(alloc, &module, ok_id) orelse return error.VmShouldHaveHandledIt; try std.testing.expectEqual(@as(i64, 42), v.int); - // fn bad() { unbox_any(1) } → tryEval yields null (caller falls back to legacy) + // fn bad() { compiler_call() } → an unported op → tryEval yields null (caller + // falls back to legacy). (box_any/unbox_any are now VM-native; compiler_call is + // still unported until Phase 4D.) var fb2 = Fb.init(alloc, &.{}, .void); const c0 = fb2.block(&.{}); - const c = fb2.add(c0, inst(.{ .const_int = 1 }, .i64)); - _ = fb2.add(c0, inst(.{ .unbox_any = .{ .operand = ref(c) } }, .i64)); + _ = fb2.add(c0, inst(.{ .compiler_call = .{ .name = 0, .args = &.{} } }, .void)); _ = fb2.add(c0, inst(.ret_void, .void)); const bad_id = module.addFunction(fb2.func); @@ -1271,19 +1292,18 @@ test "comptime_vm exec: division by zero and unsupported op bail loudly" { try std.testing.expectEqual(@as(i64, 4), toI64(try v.run(&fb.func, &.{ fromI64(12), fromI64(3) }))); try std.testing.expectError(error.DivisionByZero, v.run(&fb.func, &.{ fromI64(12), fromI64(0) })); } - // A not-yet-ported op (unbox_any) → Unsupported with the op name in `detail`. + // A not-yet-ported op (compiler_call) → Unsupported with the op name in `detail`. { var fb = Fb.init(std.testing.allocator, &.{}, .void); defer fb.deinit(); const b0 = fb.block(&.{}); - const c = fb.add(b0, inst(.{ .const_int = 1 }, .i64)); - _ = fb.add(b0, inst(.{ .unbox_any = .{ .operand = ref(c) } }, .i64)); + _ = fb.add(b0, inst(.{ .compiler_call = .{ .name = 0, .args = &.{} } }, .void)); _ = fb.add(b0, inst(.ret_void, .void)); var v = vm.Vm.init(std.testing.allocator); defer v.deinit(); try std.testing.expectError(error.Unsupported, v.run(&fb.func, &.{})); - try std.testing.expectEqualStrings("unbox_any", v.detail.?); + try std.testing.expectEqualStrings("compiler_call", v.detail.?); } } diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index a9dcf24c..ce3a299d 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -872,6 +872,38 @@ pub const Vm = struct { .ret => |u| return .{ .ret = frame.get(u.operand.index()) }, .ret_void => return .ret_void, + // T → Any: a 16-byte box `{ type_tag: i64 @0, value: i64 @8 }` (the LLVM + // layout). The tag is the source TypeId index (matches the legacy comptime + // interp; runtime `anyTag` additionally normalizes arbitrary-width ints — + // an existing legacy/runtime split). The value slot holds a word source's + // scalar bytes, or an aggregate source's flat-memory ADDR (the runtime + // "pointer in the value slot" shape — see emit_llvm.coerceToI64's struct path). + .box_any => |ba| { + const table = try self.requireTable(); + const sz = table.typeSizeBytes(.any); // 16 + const addr = self.machine.allocBytes(sz, table.typeAlignBytes(.any)); + @memset(try self.machine.bytes(addr, sz), 0); + try self.machine.writeWord(addr, 8, @as(Reg, ba.source_type.index())); + const v = frame.get(ba.operand.index()); + switch (kindOf(table, ba.source_type)) { + .word => try self.writeField(table, addr + 8, ba.source_type, v), + .aggregate => try self.machine.writeWord(addr + 8, 8, v), + .unsupported => return self.failMsg("comptime VM: box_any of an unsupported source type (any/void/noreturn)"), + } + return .{ .value = addr }; + }, + // Any → T: read the value slot (offset 8). A word target reads its scalar + // bytes back; an aggregate target reads the stored ADDR (the boxed pointer). + .unbox_any => |ua| { + const table = try self.requireTable(); + const base = frame.get(ua.operand.index()); // Addr of the {tag, value} box + switch (kindOf(table, ins.ty)) { + .word => return .{ .value = try self.readField(table, base + 8, ins.ty) }, + .aggregate => return .{ .value = try self.machine.readWord(base + 8, 8) }, + .unsupported => return self.failMsg("comptime VM: unbox_any to an unsupported target type"), + } + }, + // Comptime metatype `#builtin`s (`declare`/`define`). The VM-native // mirror of the legacy `execBuiltin` arms; an unmodeled builtin returns // null → bail with its name → legacy fallback (dual-path parity). @@ -1664,9 +1696,10 @@ pub const Vm = struct { // 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 + .any => return .aggregate, // boxed { type_tag, value } (16B), by-address else => {}, } - if (ty.isBuiltin()) return .unsupported; // any (16B, different shape), void, noreturn, unresolved + if (ty.isBuiltin()) return .unsupported; // void, noreturn, unresolved return switch (table.get(ty)) { .pointer, .many_pointer, .function => .word, .@"enum" => .word, // payloadless enum: i64 (or its backing) — a word @@ -1787,7 +1820,9 @@ pub const Vm = struct { /// matches the table's size computation. A string/slice is a `{ptr@0, len@8}` /// fat pointer (the `makeSlice` layout), accessed by field 0 (ptr) / 1 (len). fn fieldOffset(table: *const types.TypeTable, sty: TypeId, idx: u32) Addr { - if (sty == .string or (!sty.isBuiltin() and table.get(sty) == .slice)) + // string/slice `{ptr@0, len@8}` and the boxed Any `{type_tag@0, value@8}` + // share the same two-8-byte-field layout. + if (sty == .string or sty == .any or (!sty.isBuiltin() and table.get(sty) == .slice)) return if (idx == 0) 0 else 8; const fields = table.get(sty).@"struct".fields; var off: usize = 0;