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).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.?);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user