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:
@@ -26,23 +26,22 @@ with ONE welded mechanism. Branch: `reify` (off `master`). Update after every st
|
||||
> breaks cross-compilation — host vs target layout — and loses the sandbox. A
|
||||
> flat-memory VM keeps both while getting native bytes + speed.)
|
||||
>
|
||||
> **Next action (2026-06-18) — VM-native metatype CONSTRUCTION landed (step 7, uncommitted).** The
|
||||
> metatype `declare`/`define` builtins + tagged-union `enum_init`-with-payload now run NATIVELY on
|
||||
> the VM (new `.call_builtin` exec arm → `callBuiltinVm`/`defineFromInfo`, reading the `TypeInfo`
|
||||
> value from FLAT MEMORY; faithful port of legacy `defineEnum`/`Struct`/`Tuple`). So the metatype
|
||||
> CONSTRUCTION examples run **fully HANDLED** on the VM (no `call_builtin` fallback): `0614`/`0620`/
|
||||
> `0621`/`0624`/`0632`; `0622`/`0623` define-HANDLED then fall back at the still-unported `type_info`.
|
||||
> Both `enum_init`/`define` bail loudly on a `backing_type` tagged union (wrong layout) rather than
|
||||
> silent-clobber. **697/0 BOTH gates + all unit tests** (added: tagged-union `enum_init` payload).
|
||||
> NOT yet committed. **THE NEXT STEP: port `type_info`** (reflect a type → build the `TypeInfo`
|
||||
> value in flat memory, the inverse of `define` — reuses tagged-union `enum_init`) so `0619`/`0622`/
|
||||
> `0623` go fully HANDLED; then drive the SX_COMPTIME_FLAT_TRACE fallback list toward
|
||||
> genuinely-non-comptime cases. Earlier landed: dedicated `Type` builtin TypeId (`6844fb9`/`94f60c5`/
|
||||
> `554871b`); WRITE side declare_type/register_type/pointer_to VM-native (`66005af`); real
|
||||
> lowering-time Context for allocating type-fns (`eb68d9e`). What's LEFT toward the end-state ONE
|
||||
> evaluator: (1) finish porting the comptime corpus onto the VM (type_info next); (2) THEN flip the
|
||||
> VM to default + delete `interp.zig` (with user go-ahead); (3) re-express `define`/`make_enum` as
|
||||
> sx over the compiler-API once legacy is gone (allocation works only on the sole VM evaluator).
|
||||
> **Next action (2026-06-18) — the WHOLE metatype surface is VM-native (steps 7+8, committed through
|
||||
> `d0ebc55`; step 8 uncommitted).** `declare`/`define`/`type_info` + tagged-union `enum_init` all run
|
||||
> NATIVELY on the VM (`.call_builtin` exec arm → `callBuiltinVm`; `defineFromInfo` decodes a
|
||||
> `TypeInfo` from flat memory, `buildTypeInfo` reflects one INTO flat memory — faithful ports of
|
||||
> legacy `defineEnum`/`Struct`/`Tuple`/`reflectTypeInfo`). The ENTIRE metatype range `0614`–`0624` +
|
||||
> `0632` runs **HANDLED with ZERO fallback** (incl. the `define(declare, type_info(T))` round-trips
|
||||
> `0619`/`0622`/`0623`); VM output byte-matches legacy. `enum_init`/`define`/`type_info` bail loudly
|
||||
> on a `backing_type` tagged union rather than silent-clobber. **697/0 BOTH gates + all unit tests.**
|
||||
> **THE NEXT STEP:** drive the remaining VM fallbacks (now ALL genuinely-non-metatype emit-time side
|
||||
> effects — `print`/`out` 0613, `global_addr` 0600, `compiler_call` #compiler hooks 0602/0603,
|
||||
> inline-asm global 1654) to empty (port or confirm each is legitimately non-comptime), THEN — with
|
||||
> explicit user go-ahead — flip the VM to default + delete `interp.zig` (the end-state ONE evaluator),
|
||||
> and re-express `define`/`make_enum` as sx over the compiler-API (allocation works on the sole VM).
|
||||
> Earlier landed: dedicated `Type` builtin TypeId (`6844fb9`/`94f60c5`/`554871b`); WRITE side
|
||||
> declare_type/register_type/pointer_to VM-native (`66005af`); real lowering-time Context (`eb68d9e`);
|
||||
> metatype construction declare/define/enum_init (`d0ebc55`).
|
||||
>
|
||||
> Done so far in Phase 3:
|
||||
> - **READ side (7 readers, dual-path):** `find_type`/`type_kind`/`type_field_count`/
|
||||
@@ -348,6 +347,24 @@ 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 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
|
||||
VM-native mirror of legacy `reflectTypeInfo`). Decodes the source type into a tag + members
|
||||
(tagged-union/struct field & enum variant → `{ name, ty }`, a payloadless variant → `void`;
|
||||
tuple → bare positional `Type`s), then lays out the nested value 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 a
|
||||
`makeStringValue` helper extracted from `text_of`. Same `backing_type` guard as step 7. **Result:
|
||||
the ENTIRE metatype surface runs HANDLED on the VM with ZERO fallback** — `0614`–`0624` + `0632`
|
||||
(0616 `field_type` folds at lower time, no comptime eval); 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.** On `reify`. **Remaining VM fallbacks in the
|
||||
comptime corpus are now genuinely-non-metatype** emit-time side effects: `print`/`out` (0613),
|
||||
`global_addr` (0600), `compiler_call` #compiler hooks (0602/0603), and the inline-asm global
|
||||
(1654). **Next:** port those (or confirm each is a legitimately-non-comptime case) to drive the
|
||||
fallback list to empty, then — with user go-ahead — flip the VM to default + delete `interp.zig`.
|
||||
- **Phase 3 P3.4 step 7 (VM plan) — VM-native metatype CONSTRUCTION: `declare`/`define` + tagged-union `enum_init` (2026-06-18).**
|
||||
Ported the metatype type-CONSTRUCTION builtins into the VM so the construction examples run
|
||||
HANDLED end-to-end (no `call_builtin` fallback). Three pieces: (1) **tagged-union `enum_init`
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user