comptime VM: VM-native type_info REFLECTION — whole metatype surface HANDLED (P3.4 step 8)

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 (VM-native mirror of legacy reflectTypeInfo).

- Decodes the source type into a tag + members (tagged-union/struct field &
  enum variant -> {name, ty}, payloadless variant -> void; tuple -> bare
  positional Types), then lays the nested value out bottom-up using layouts
  derived from the TypeInfo RESULT type (ins.ty, now threaded into
  callBuiltinVm): element array -> {ptr,len} slice -> info struct
  (EnumInfo/StructInfo/TupleInfo) -> TypeInfo {tag, payload} tagged union
  (reusing step 7's tagged-union write).
- Variant/field names materialize via makeStringValue, extracted from text_of.
- Same backing_type guard as step 7 (bail rather than mis-read the tag).

The ENTIRE metatype surface now runs HANDLED on the VM with ZERO fallback:
0614-0624 + 0632 (0616 field_type folds at lower time). The
define(declare, type_info(T)) round-trips (0619/0622/0623) mint byte-identical
copies on the VM; VM output byte-matches legacy for all. 697/0 both gates + all
unit tests. Remaining VM fallbacks in the comptime corpus are now genuinely
non-metatype emit-time side effects (print/global_addr/compiler_call/inline-asm).
This commit is contained in:
agra
2026-06-18 15:57:11 +03:00
parent d0ebc55f99
commit 736f64e664
2 changed files with 146 additions and 23 deletions

View File

@@ -876,7 +876,7 @@ pub const Vm = struct {
// mirror of the legacy `execBuiltin` arms; an unmodeled builtin returns
// null → bail with its name → legacy fallback (dual-path parity).
.call_builtin => |bi| {
if (try self.callBuiltinVm(bi, frame, ref_types)) |r| return .{ .value = r };
if (try self.callBuiltinVm(bi, ins.ty, frame, ref_types)) |r| return .{ .value = r };
self.detail = @tagName(bi.builtin);
return error.Unsupported;
},
@@ -1130,10 +1130,7 @@ pub const Vm = struct {
const raw = frame.get(args[0].index());
if (raw > std.math.maxInt(u32)) return self.failMsg("comptime text_of: StringId out of range");
const id: types.StringId = @enumFromInt(@as(u32, @intCast(raw)));
const text = table.getString(id);
const data = self.machine.allocBytes(text.len + 1, 1); // +1: NUL (zero-init)
if (text.len > 0) @memcpy(try self.machine.bytes(data, text.len), text);
return try self.makeSlice(table, data, text.len);
return try self.makeStringValue(table, table.getString(id));
}
// ── read-only reflection readers (Phase 3) ──────────────────────────
// Type handle = a u32 `TypeId` (a word), exactly like `StringId` — so
@@ -1334,7 +1331,7 @@ pub const Vm = struct {
/// `interp.execBuiltinInner` arms. Returns the result word, or `null` for a
/// builtin the VM doesn't model yet (caller bails → legacy fallback, so dual-path
/// parity holds). Keeps BOTH paths alive during the VM-default transition.
fn callBuiltinVm(self: *Vm, bi: inst_mod.BuiltinCall, frame: *Frame, ref_types: []const TypeId) Error!?Reg {
fn callBuiltinVm(self: *Vm, bi: inst_mod.BuiltinCall, ins_ty: TypeId, frame: *Frame, ref_types: []const TypeId) Error!?Reg {
switch (bi.builtin) {
// declare(name) → mint an EMPTY nominal slot, returned as a Type value.
.declare => {
@@ -1370,6 +1367,15 @@ pub const Vm = struct {
return self.failMsg("comptime define: TypeInfo payload is not a single-slice info struct");
return try self.defineFromInfo(table, handle, @intCast(tag), payload_ty, info_addr + tag_size);
},
// type_info($T) → reflect a type INTO a TypeInfo VALUE (the inverse of
// define's decode). The arg folded to a `const_type` (a `.type_value`
// word = the source TypeId); build the value in flat memory.
.type_info => {
const table = try self.requireTable();
if (bi.args.len != 1) return self.failMsg("comptime type_info: expected (Type)");
const tid = try self.argTypeId(bi.args, frame, 0);
return try self.buildTypeInfo(table, ins_ty, tid);
},
else => return null, // not modeled on the VM yet → caller bails to legacy
}
}
@@ -1453,6 +1459,97 @@ pub const Vm = struct {
return @as(Reg, handle.index());
}
/// Reflect type `tid` INTO a `TypeInfo` VALUE built in flat memory — the inverse
/// of `defineFromInfo` and the VM-native mirror of legacy `reflectTypeInfo`. The
/// element/struct layouts come from the `result_ty` (= the metatype `TypeInfo`
/// tagged union): variant tag `t` → payload struct `EnumInfo`/`StructInfo`/
/// `TupleInfo` (one slice field) → the slice element (`EnumVariant`/`StructField`/
/// `Type`). Mirrors the legacy member shapes: a tagged-union/struct field and an
/// enum variant reflect as `{ name, ty }` (a payloadless variant carries `void`);
/// tuple elements are bare positional `Type`s. `define(declare(n), type_info(T))`
/// round-trips to a byte-identical nominal copy.
fn buildTypeInfo(self: *Vm, table: *const types.TypeTable, result_ty: TypeId, tid: TypeId) Error!Reg {
if (result_ty.isBuiltin() or table.get(result_ty) != .tagged_union)
return self.failMsg("comptime type_info: result type is not the TypeInfo tagged union");
const ti = table.get(result_ty).tagged_union;
if (ti.backing_type != null)
return self.failMsg("comptime type_info: TypeInfo result is a backing_type tagged union (unexpected layout)");
if (tid.isBuiltin())
return self.failMsg("comptime type_info: only enum / tagged-union / struct / tuple types reflect");
// Decode the source type into a tag + members. enum/tagged-union/struct share
// the `{ name, ty }` element; tuple uses bare positional `Type`s.
const info = table.get(tid);
var pairs = std.ArrayList(NamedMember).empty;
defer pairs.deinit(self.gpa);
var tup = std.ArrayList(TypeId).empty;
defer tup.deinit(self.gpa);
const tag: u32 = switch (info) {
.tagged_union => |u| blk: {
for (u.fields) |f| pairs.append(self.gpa, .{ .name = f.name, .ty = f.ty }) catch return self.failMsg("comptime type_info: out of memory");
break :blk 0;
},
.@"enum" => |e| blk: {
for (e.variants) |v| pairs.append(self.gpa, .{ .name = v, .ty = .void }) catch return self.failMsg("comptime type_info: out of memory");
break :blk 0;
},
.@"struct" => |s| blk: {
for (s.fields) |f| pairs.append(self.gpa, .{ .name = f.name, .ty = f.ty }) catch return self.failMsg("comptime type_info: out of memory");
break :blk 1;
},
.tuple => |t| blk: {
for (t.fields) |ety| tup.append(self.gpa, ety) catch return self.failMsg("comptime type_info: out of memory");
break :blk 2;
},
else => return self.failMsg("comptime type_info: only enum / tagged-union / struct / tuple types reflect"),
};
const count = if (tag == 2) tup.items.len else pairs.items.len;
if (count == 0) return self.failMsg("comptime type_info: type has no members");
// Layout from the TypeInfo result: payload struct (one slice field) → element.
if (tag >= ti.fields.len) return self.failMsg("comptime type_info: TypeInfo has no variant for this kind");
const payload_ty = ti.fields[tag].ty;
if (payload_ty.isBuiltin() or table.get(payload_ty) != .@"struct" or table.get(payload_ty).@"struct".fields.len != 1)
return self.failMsg("comptime type_info: TypeInfo payload is not a single-slice info struct");
const slice_field_ty = table.get(payload_ty).@"struct".fields[0].ty;
if (slice_field_ty.isBuiltin() or table.get(slice_field_ty) != .slice)
return self.failMsg("comptime type_info: info struct field is not a slice");
const elem_ty = table.get(slice_field_ty).slice.element;
const elem_size: Addr = @intCast(table.typeSizeBytes(elem_ty));
// Build the element array: bare `Type` words for a tuple, else `{ name, ty }`.
const data = self.machine.allocBytes(@intCast(elem_size * @as(Addr, @intCast(count))), table.typeAlignBytes(elem_ty));
if (tag == 2) {
for (tup.items, 0..) |ety, i| try self.writeField(table, data + @as(Addr, @intCast(i)) * elem_size, elem_ty, @as(Reg, ety.index()));
} else {
if (elem_ty.isBuiltin() or table.get(elem_ty) != .@"struct" or table.get(elem_ty).@"struct".fields.len != 2)
return self.failMsg("comptime type_info: member element is not a {name, ty} struct");
const name_fty = table.get(elem_ty).@"struct".fields[0].ty; // string
const name_off = fieldOffset(table, elem_ty, 0);
const ty_off = fieldOffset(table, elem_ty, 1);
for (pairs.items, 0..) |m, i| {
const elem = data + @as(Addr, @intCast(i)) * elem_size;
const name_val = try self.makeStringValue(table, table.getString(m.name));
try self.writeField(table, elem + name_off, name_fty, name_val);
try self.writeField(table, elem + ty_off, .type_value, @as(Reg, m.ty.index()));
}
}
// members slice → info struct { slice } → TypeInfo { tag, info }.
const slice = try self.makeSlice(table, data, @intCast(count));
const pinfo = self.machine.allocBytes(table.typeSizeBytes(payload_ty), table.typeAlignBytes(payload_ty));
@memset(try self.machine.bytes(pinfo, table.typeSizeBytes(payload_ty)), 0);
try self.writeField(table, pinfo + fieldOffset(table, payload_ty, 0), slice_field_ty, slice);
const ti_size = table.typeSizeBytes(result_ty);
const ti_addr = self.machine.allocBytes(ti_size, table.typeAlignBytes(result_ty));
@memset(try self.machine.bytes(ti_addr, ti_size), 0);
try self.writeField(table, ti_addr, ti.tag_type, @as(Reg, tag));
const tag_size: Addr = @intCast(table.typeSizeBytes(ti.tag_type));
try self.writeField(table, ti_addr + tag_size, payload_ty, pinfo);
return @as(Reg, ti_addr);
}
// ── Reg ↔ Value bridge (legacy-interop boundary) ────────────────────────
//
// The wiring step routes a comptime eval through the VM, falling back to the
@@ -1766,6 +1863,15 @@ pub const Vm = struct {
return data +% idx *% @as(u64, @intCast(elem_size));
}
/// Materialize `text` into flat memory as a `string` VALUE — NUL-terminated
/// bytes + a `{ptr, len}` fat pointer (len excludes the NUL). Shared by
/// `text_of` and `type_info`'s variant/field-name construction.
fn makeStringValue(self: *Vm, table: *const types.TypeTable, text: []const u8) Error!Reg {
const data = self.machine.allocBytes(text.len + 1, 1); // +1: NUL (zero-init)
if (text.len > 0) @memcpy(try self.machine.bytes(data, text.len), text);
return try self.makeSlice(table, data, text.len);
}
/// Build a `{ptr, len}` fat pointer (slice/string value) in flat memory and
/// return its address. `ptr` is `pointer_size` bytes at offset 0; `len` is an
/// i64 at offset 8 (the layout `typeSizeBytes` uses for slice/string: 16B).