From 736f64e66470875b9297225138dde1ee342bb077 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 18 Jun 2026 15:57:11 +0300 Subject: [PATCH] =?UTF-8?q?comptime=20VM:=20VM-native=20type=5Finfo=20REFL?= =?UTF-8?q?ECTION=20=E2=80=94=20whole=20metatype=20surface=20HANDLED=20(P3?= =?UTF-8?q?.4=20step=208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- current/CHECKPOINT-COMPILER-API.md | 51 ++++++++----- src/ir/comptime_vm.zig | 118 +++++++++++++++++++++++++++-- 2 files changed, 146 insertions(+), 23 deletions(-) diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index 8dac2f78..c636e770 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -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` diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index b4a6adfd..a9dcf24c 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -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).