comptime VM: model .type_value natively (word); harden struct_init vs arrays
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).
This commit is contained in:
@@ -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 <Type value u32> → 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);
|
||||
|
||||
@@ -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 => {},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user