From d23e20843080e570ca82a804f11d11fcb69cc2ae Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 18 Jun 2026 09:34:36 +0300 Subject: [PATCH] =?UTF-8?q?comptime=20VM:=20Phase=203=20=E2=80=94=20field-?= =?UTF-8?q?level=20reflection=20readers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three more read-only compiler-API readers on the TypeId-handle shape, each backed by a new TypeTable query that both the legacy handler and the VM call (no drift): type_nominal_name(t: TypeId) -> StringId (nominalName; loud-bail for unnamed types) type_field_name(t: TypeId, idx: i64) -> StringId (memberName) type_field_type(t: TypeId, idx: i64) -> TypeId (memberType) All loud-bail on out-of-range idx / no-member — no silent default. First multi-arg compiler fns (callCompilerFn now reads arg 1 = idx); added Vm.argHandle/argTypeId range-checked arg readers and moved find_type/type_field_count onto them. Names use the type_* family to avoid colliding with the std metatype builtins (field_name / type_name in core.sx); the new TypeTable.nominalName is distinct from the existing typeName(id) display-string renderer. Example 0629 reflects Pair { lo: Point; hi: Point } — each field name + the nominal name of a field's type, #run-folded, VM-HANDLED natively. VM unit test added. Parity 690/690 (gate OFF and -Dcomptime-flat). --- current/CHECKPOINT-COMPILER-API.md | 59 ++++++++++----- current/PLAN-COMPILER-VM.md | 28 +++++-- .../0629-comptime-compiler-field-reflect.sx | 45 +++++++++++ .../0629-comptime-compiler-field-reflect.exit | 1 + ...629-comptime-compiler-field-reflect.stderr | 1 + ...629-comptime-compiler-field-reflect.stdout | 3 + src/ir/compiler_lib.zig | 40 ++++++++++ src/ir/comptime_vm.test.zig | 74 +++++++++++++++++++ src/ir/comptime_vm.zig | 44 +++++++++-- src/ir/types.zig | 54 ++++++++++++++ 10 files changed, 316 insertions(+), 33 deletions(-) create mode 100644 examples/0629-comptime-compiler-field-reflect.sx create mode 100644 examples/expected/0629-comptime-compiler-field-reflect.exit create mode 100644 examples/expected/0629-comptime-compiler-field-reflect.stderr create mode 100644 examples/expected/0629-comptime-compiler-field-reflect.stdout diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index 91ac8171..7487be34 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -26,26 +26,30 @@ 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):** **Phase 3 is UNDER WAY.** The VM now hosts the first -> read-only reflection readers — `find_type(name: StringId) -> TypeId` and -> `type_field_count(t: TypeId) -> i64` — bound exactly like the `intern`/`text_of` seed -> (a type handle is a plain `u32` `TypeId`, so the calls stay clean scalar host-calls). -> Example `0628` chains `intern → find_type → type_field_count`, VM-HANDLED natively. -> Parity **689/689** (gate ON and OFF), VM unit test added. Phase 1.final op-porting was -> already complete (the VM covers scalars/control-flow/aggregates/strings/optionals/enums, -> calls+recursion, the implicit context + full allocator protocol, globals, failables + -> return traces); both comptime call sites route through the VM with legacy fallback. -> **Forward (P3.2):** more read-only readers on the same `TypeId`-handle shape -> (`type_name`, `field_name`, `field_type`, kind queries), then `register_struct` (the -> first MUTATING fn — mints a `TypeId`; resolve the mutable-table / host-ABI-vs-target-ABI -> boundary deliberately). Re-expressing `declare`/`define`/`type_info` as sx (the metatype, -> which runs at LOWERING time) needs the VM hardened against malformed lowering-time IR -> first — keep it on the legacy path until then. Phase 2 (bytecode) is the orthogonal -> speed work. **Decision recorded:** `find_type` returns a non-optional `TypeId` using the -> `unresolved` (0) sentinel, NOT `?Type` (a `Type` value is `.any`-typed, which the VM -> doesn't represent, and an optional can't cross the eval bridge) — see `PLAN-COMPILER-VM.md` -> Phase 3 progress note. -> Build/verify: `zig build && zig build test` (689, gate OFF). Run the corpus ON the VM: +> **Next action (2026-06-18):** **Phase 3 is UNDER WAY.** The VM now hosts five read-only +> reflection readers, all on the same plain-`u32`-handle shape (a `TypeId` is a `u32`, like +> `StringId`), so the calls stay clean scalar host-calls — handle in, scalar out, no +> marshaling: `find_type(name) -> TypeId`, `type_field_count(t) -> i64`, +> `type_nominal_name(t) -> StringId`, `type_field_name(t, idx) -> StringId`, +> `type_field_type(t, idx) -> TypeId`. Each is backed by a `TypeTable` query +> (`findByName`/`memberCount`/`nominalName`/`memberName`/`memberType`) that BOTH the legacy +> handler and the VM call, so the two paths can't drift. Examples `0628` (`find_type` + +> `type_field_count`) and `0629` (field reflection over `Pair { lo: Point; hi: Point }`), +> both VM-HANDLED natively. Parity **690/690** (gate ON and OFF), VM unit tests added. +> Phase 1.final op-porting was already complete (the VM covers +> scalars/control-flow/aggregates/strings/optionals/enums, calls+recursion, the implicit +> context + full allocator protocol, globals, failables + return traces); both comptime +> call sites route through the VM with legacy fallback. +> **Forward (P3.3):** `register_struct` (the first MUTATING fn — mints a `TypeId`; resolve +> the mutable-table / host-ABI-vs-target-ABI boundary deliberately), plus any remaining +> read-only readers the metatype needs (kind query, `field_value_int`). Re-expressing +> `declare`/`define`/`type_info` as sx (the metatype, which runs at LOWERING time) needs the +> VM hardened against malformed lowering-time IR first — keep it on the legacy path until +> then. Phase 2 (bytecode) is the orthogonal speed work. **Decisions recorded:** `find_type` +> returns a non-optional `TypeId` using the `unresolved` (0) sentinel, NOT `?Type`; reader +> names use the `type_*` family to avoid colliding with the std metatype builtins +> (`field_name`/`type_name` in core.sx) — see `PLAN-COMPILER-VM.md` Phase 3 progress note. +> Build/verify: `zig build && zig build test` (690, gate OFF). Run the corpus ON the VM: > `zig build test -Dcomptime-flat` (the build flag) OR env `SX_COMPTIME_FLAT=1`. Coverage > trace: `SX_COMPTIME_FLAT_TRACE=1`. @@ -318,6 +322,21 @@ when reached (sentinels or accessor fns; see the design doc Risks). `List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.) ## Log +- **Phase 3 P3.2 (VM plan) — field-level reflection readers: `type_nominal_name` + `type_field_name` + `type_field_type` (2026-06-18).** + Three more `compiler`-library readers on the same `TypeId`-handle shape (added to + `compiler_lib.bound_fns` AND `Vm.callCompilerFn`), each backed by a new `TypeTable` query + BOTH paths call (no drift): `nominalName` (a named type's own name handle; loud-bail for + unnamed types like `i64`/pointers), `memberName` (struct/union/tagged-union field, enum + variant, named-tuple element), `memberType` (struct/tuple/array/vector member type). All + loud-bail on out-of-range idx / no-member (no silent default). First MULTI-ARG compiler + fns — `callCompilerFn` reads arg 1 = idx; added `Vm.argHandle`/`argTypeId` (range-checked + u32/TypeId arg reads) and refactored `find_type`/`type_field_count` onto them. Named + `type_*` to avoid clashing with the std metatype builtins (`field_name`/`type_name` exist + in core.sx); `nominalName` (the TypeTable method) is distinct from the existing + `typeName(id) []const u8` display-string renderer. Example `0629-comptime-compiler-field-reflect` + reflects `Pair { lo: Point; hi: Point }` — each field name + the nominal name of a field's + type, all `#run`-folded, all VM-HANDLED natively. VM unit test added (type_field_name → "hi"; + type_nominal_name(type_field_type(Pair,0)) → "Point"). **Parity 690/690** (gate ON and OFF). - **Phase 3 P3.1 (VM plan) — first read-only reflection readers: `find_type` + `type_field_count` (2026-06-18).** Two more `compiler`-library fns, bound the same way as the `intern`/`text_of` seed (added to `compiler_lib.bound_fns` for the legacy handler + the welded-decl export check, AND to diff --git a/current/PLAN-COMPILER-VM.md b/current/PLAN-COMPILER-VM.md index 3a80b47c..275c50e3 100644 --- a/current/PLAN-COMPILER-VM.md +++ b/current/PLAN-COMPILER-VM.md @@ -275,13 +275,27 @@ host through it: marker (see CLAUDE.md REJECTED PATTERNS — a dedicated sentinel is the required shape), so the caller checks the handle against 0. This keeps the reader a clean scalar mirror of `intern`/`text_of` and defers `.any`/optional plumbing to when it's actually needed. -- **Next (P3.2):** more read-only readers on the same `TypeId`-handle shape (`type_name(t) - -> StringId`, `field_name(t, i) -> StringId`, `field_type(t, i) -> TypeId`, kind queries), - then `register_struct` (the first MUTATING fn — mints a `TypeId`; resolve the mutable-table - / host-ABI-vs-target-ABI boundary deliberately, per the open questions). Re-expressing - `declare`/`define`/`type_info` as sx (the metatype, which runs at LOWERING time) still - needs the VM hardened against malformed lowering-time IR first — keep that on the legacy - path until then (see the resume note in CHECKPOINT-COMPILER-API.md). +- **(P3.2) Field-level reflection readers — `type_nominal_name` + `type_field_name` + + `type_field_type` (DONE).** Three more readers on the same `TypeId`-handle shape (each + backed by a new `TypeTable` query that BOTH the legacy handler and the VM call, so no + drift): `type_nominal_name(t: TypeId) -> StringId` (`nominalName` — a named type's own + name; loud-bail for unnamed types), `type_field_name(t: TypeId, idx: i64) -> StringId` + (`memberName` — struct/union/tagged-union field, enum variant, named-tuple element), and + `type_field_type(t: TypeId, idx: i64) -> TypeId` (`memberType` — struct/tuple/array/vector + member type). All loud-bail on out-of-range idx / no-member (no silent default). These are + the first MULTI-ARG compiler fns (the VM's `callCompilerFn` now reads arg 1 = idx); added + `Vm.argHandle`/`argTypeId` helpers (range-checked u32/TypeId arg reads). Naming uses the + `type_*` family so nothing collides with the std metatype builtins (`field_name`/`type_name` + exist in `core.sx`). Example `0629` reflects `Pair { lo: Point; hi: Point }` — reads each + field name and the nominal name of a field's type, all folded at `#run`, all VM-HANDLED + natively. Parity **690/690** (gate ON and OFF); VM unit test added. +- **Next (P3.3):** `register_struct` (the first MUTATING fn — mints a `TypeId`; resolve the + mutable-table / host-ABI-vs-target-ABI boundary deliberately, per the open questions), + plus any remaining read-only readers needed to re-express the metatype (kind query, + `field_value_int` for enums). Re-expressing `declare`/`define`/`type_info` as sx (the + metatype, which runs at LOWERING time) still needs the VM hardened against malformed + lowering-time IR first — keep that on the legacy path until then (see the resume note in + CHECKPOINT-COMPILER-API.md). ### Phase 3 — Compiler-API on flat memory (resume the stream — no weld) With native-byte comptime values, re-home the compiler-API: diff --git a/examples/0629-comptime-compiler-field-reflect.sx b/examples/0629-comptime-compiler-field-reflect.sx new file mode 100644 index 00000000..9f3006af --- /dev/null +++ b/examples/0629-comptime-compiler-field-reflect.sx @@ -0,0 +1,45 @@ +// Comptime compiler API — field-level reflection readers (Phase 3). +// +// Builds on the `find_type` / `type_field_count` readers (example 0628) with the +// per-member readers, all on the same plain-`u32`-handle shape (scalar in, +// handle out — no marshaling): +// +// type_field_name(t, i) → the i-th member's name handle (StringId) +// type_field_type(t, i) → the i-th member's type handle (TypeId) +// type_nominal_name(t) → a named type's own name handle (StringId) +// +// Reflecting `Pair { lo: Point; hi: Point; }`: read each field's name, and the +// nominal name of each field's type. Chains +// intern → find_type → type_field_{name,type} → (type_nominal_name) → text_of, +// all folded at comptime, all serviced natively by the flat-memory VM. + +#import "modules/std.sx"; + +compiler :: #library "compiler"; + +StringId :: u32; +TypeId :: u32; + +intern :: (s: string) -> StringId abi(.zig) extern compiler; +text_of :: (id: StringId) -> string abi(.zig) extern compiler; +find_type :: (name: StringId) -> TypeId abi(.zig) extern compiler; +type_field_count :: (t: TypeId) -> i64 abi(.zig) extern compiler; +type_nominal_name :: (t: TypeId) -> StringId abi(.zig) extern compiler; +type_field_name :: (t: TypeId, idx: i64) -> StringId abi(.zig) extern compiler; +type_field_type :: (t: TypeId, idx: i64) -> TypeId abi(.zig) extern compiler; + +Point :: struct { x: i64; y: i64; } +Pair :: struct { lo: Point; hi: Point; } + +pair :: #run find_type(intern("Pair")); +n :: #run type_field_count(find_type(intern("Pair"))); +f0_name :: #run text_of(type_field_name(find_type(intern("Pair")), 0)); +f1_name :: #run text_of(type_field_name(find_type(intern("Pair")), 1)); +// field 0's type is `Point` — read its nominal name through the type handle. +f0_type :: #run text_of(type_nominal_name(type_field_type(find_type(intern("Pair")), 0))); + +main :: () { + print("Pair has {} fields\n", n); + print("field 0 = {} : {}\n", f0_name, f0_type); + print("field 1 = {} : {}\n", f1_name, f0_type); +} diff --git a/examples/expected/0629-comptime-compiler-field-reflect.exit b/examples/expected/0629-comptime-compiler-field-reflect.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0629-comptime-compiler-field-reflect.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0629-comptime-compiler-field-reflect.stderr b/examples/expected/0629-comptime-compiler-field-reflect.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0629-comptime-compiler-field-reflect.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0629-comptime-compiler-field-reflect.stdout b/examples/expected/0629-comptime-compiler-field-reflect.stdout new file mode 100644 index 00000000..cd988e45 --- /dev/null +++ b/examples/expected/0629-comptime-compiler-field-reflect.stdout @@ -0,0 +1,3 @@ +Pair has 2 fields +field 0 = lo : Point +field 1 = hi : Point diff --git a/src/ir/compiler_lib.zig b/src/ir/compiler_lib.zig index 09e52024..01ae8b89 100644 --- a/src/ir/compiler_lib.zig +++ b/src/ir/compiler_lib.zig @@ -49,6 +49,9 @@ pub const bound_fns = [_]BoundFn{ .{ .sx_name = "text_of", .handler = handleTextOf }, .{ .sx_name = "find_type", .handler = handleFindType }, .{ .sx_name = "type_field_count", .handler = handleTypeFieldCount }, + .{ .sx_name = "type_nominal_name", .handler = handleTypeNominalName }, + .{ .sx_name = "type_field_name", .handler = handleTypeFieldName }, + .{ .sx_name = "type_field_type", .handler = handleTypeFieldType }, }; /// Look up a compiler function by its sx name. Returns null when the name is not @@ -111,3 +114,40 @@ fn handleTypeFieldCount(interp: *Interpreter, args: []const Value) InterpError!V const count = interp.module.types.memberCount(tid) orelse return error.TypeError; return Value{ .int = count }; } + +/// Read an integer `Value` arg as a `u32` handle (StringId / TypeId). Errors on a +/// non-int or out-of-u32-range value — never a silent clamp. +fn handleArg(args: []const Value, i: usize) InterpError!u32 { + if (args[i] != .int) return error.TypeError; + if (args[i].int < 0 or args[i].int > std.math.maxInt(u32)) return error.TypeError; + return @intCast(args[i].int); +} + +/// `type_nominal_name(t: TypeId) -> StringId` — the nominal name handle of a named +/// type (struct/enum/union/…). Loud error for an unnamed type (no silent default). +fn handleTypeNominalName(interp: *Interpreter, args: []const Value) InterpError!Value { + if (args.len != 1) return error.TypeError; + const tid: types.TypeId = @enumFromInt(try handleArg(args, 0)); + const sid = interp.module.types.nominalName(tid) orelse return error.TypeError; + return Value{ .int = @intFromEnum(sid) }; +} + +/// `type_field_name(t: TypeId, idx: i64) -> StringId` — name handle of member `idx` +/// (struct/union/tagged-union field, enum variant, named-tuple element). Loud +/// error for an out-of-range idx or a type with no named members. +fn handleTypeFieldName(interp: *Interpreter, args: []const Value) InterpError!Value { + if (args.len != 2 or args[1] != .int) return error.TypeError; + const tid: types.TypeId = @enumFromInt(try handleArg(args, 0)); + const sid = interp.module.types.memberName(tid, args[1].int) orelse return error.TypeError; + return Value{ .int = @intFromEnum(sid) }; +} + +/// `type_field_type(t: TypeId, idx: i64) -> TypeId` — type handle of member `idx` +/// (struct/union/tagged-union field, tuple/array/vector element). Loud error for +/// an out-of-range idx or a type with no member types (e.g. a payloadless enum). +fn handleTypeFieldType(interp: *Interpreter, args: []const Value) InterpError!Value { + if (args.len != 2 or args[1] != .int) return error.TypeError; + const tid: types.TypeId = @enumFromInt(try handleArg(args, 0)); + const mty = interp.module.types.memberType(tid, args[1].int) orelse return error.TypeError; + return Value{ .int = mty.index() }; +} diff --git a/src/ir/comptime_vm.test.zig b/src/ir/comptime_vm.test.zig index a21bd4b7..6e919af3 100644 --- a/src/ir/comptime_vm.test.zig +++ b/src/ir/comptime_vm.test.zig @@ -833,6 +833,80 @@ test "comptime_vm exec: compiler-fn find_type + type_field_count (native reflect ); } +test "comptime_vm exec: compiler-fn type_field_name/type/nominal_name (native reflection)" { + const alloc = std.testing.allocator; + var module = Module.init(alloc); + defer module.deinit(); + + // Point { x, y } and Pair { lo: Point; hi: Point } in the type table. + const point_name = module.types.internString("Point"); + const pfields = [_]types.TypeInfo.StructInfo.Field{ + .{ .name = module.types.internString("x"), .ty = .i64 }, + .{ .name = module.types.internString("y"), .ty = .i64 }, + }; + const point = module.types.intern(.{ .@"struct" = .{ .name = point_name, .fields = &pfields } }); + const lo_name = module.types.internString("lo"); + const hi_name = module.types.internString("hi"); + const rfields = [_]types.TypeInfo.StructInfo.Field{ + .{ .name = lo_name, .ty = point }, + .{ .name = hi_name, .ty = point }, + }; + const pair = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Pair"), .fields = &rfields } }); + + // extern type_field_name(t: u32, idx: i64) -> u32 [compiler] (FuncId 0) + const fnp = [_]Function.Param{ param(.u32), param(.i64) }; + var fnb = Fb.init(alloc, &fnp, .u32); + fnb.func.is_extern = true; + fnb.func.compiler_welded = true; + fnb.func.name = module.types.internString("type_field_name"); + const fname_id = module.addFunction(fnb.func); + + // extern type_field_type(t: u32, idx: i64) -> u32 [compiler] (FuncId 1) + const ftp = [_]Function.Param{ param(.u32), param(.i64) }; + var ftb = Fb.init(alloc, &ftp, .u32); + ftb.func.is_extern = true; + ftb.func.compiler_welded = true; + ftb.func.name = module.types.internString("type_field_type"); + const ftype_id = module.addFunction(ftb.func); + + // extern type_nominal_name(t: u32) -> u32 [compiler] (FuncId 2) + const nnp = [_]Function.Param{param(.u32)}; + var nnb = Fb.init(alloc, &nnp, .u32); + nnb.func.is_extern = true; + nnb.func.compiler_welded = true; + nnb.func.name = module.types.internString("type_nominal_name"); + const nname_id = module.addFunction(nnb.func); + + // main(): return type_field_name(Pair, 1) → StringId("hi") + var fb = Fb.init(alloc, &.{}, .u32); + const b0 = fb.block(&.{}); + const t = fb.add(b0, inst(.{ .const_int = @intFromEnum(pair) }, .u32)); + const one = fb.add(b0, inst(.{ .const_int = 1 }, .i64)); + const nargs = [_]Ref{ ref(t), ref(one) }; + const fn1 = fb.add(b0, inst(.{ .call = .{ .callee = fname_id, .args = &nargs } }, .u32)); + _ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(fn1) } }, .void)); + const main_id = module.addFunction(fb.func); + + var v = vm.Vm.init(alloc); + v.table = &module.types; + v.module = &module; + defer v.deinit(); + try std.testing.expectEqual(@as(i64, @intFromEnum(hi_name)), toI64(try v.run(module.getFunction(main_id), &.{}))); + + // type_nominal_name(type_field_type(Pair, 0)) → StringId("Point") + var fb2 = Fb.init(alloc, &.{}, .u32); + const c0 = fb2.block(&.{}); + const t2 = fb2.add(c0, inst(.{ .const_int = @intFromEnum(pair) }, .u32)); + const zero = fb2.add(c0, inst(.{ .const_int = 0 }, .i64)); + const targs = [_]Ref{ ref(t2), ref(zero) }; + const fty = fb2.add(c0, inst(.{ .call = .{ .callee = ftype_id, .args = &targs } }, .u32)); + const nnargs = [_]Ref{ref(fty)}; + const nn = fb2.add(c0, inst(.{ .call = .{ .callee = nname_id, .args = &nnargs } }, .u32)); + _ = fb2.add(c0, inst(.{ .ret = .{ .operand = ref(nn) } }, .void)); + const main2 = module.addFunction(fb2.func); + try std.testing.expectEqual(@as(i64, @intFromEnum(point_name)), toI64(try v.run(module.getFunction(main2), &.{}))); +} + test "comptime_vm exec: func_ref + call_indirect dispatch" { const alloc = std.testing.allocator; var module = Module.init(alloc); diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index 7b1858d9..26ce5631 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -988,6 +988,19 @@ pub const Vm = struct { /// legacy `compiler_lib` handlers, but reads/writes flat memory directly instead /// of marshaling `Value`s. The seed pair is the string-pool round-trip: /// `intern(s: string) -> StringId` and `text_of(id: StringId) -> string`. + /// Read compiler-call arg `i` as a u32 handle (a `StringId` / `TypeId` word), + /// range-checked — never a silent truncation. + fn argHandle(self: *Vm, args: []const Ref, frame: *Frame, i: usize) Error!u32 { + const raw = frame.get(args[i].index()); + if (raw > std.math.maxInt(u32)) return self.failMsg("comptime compiler call: handle arg out of u32 range"); + return @intCast(raw); + } + + /// Read compiler-call arg `i` as a `TypeId` handle. + fn argTypeId(self: *Vm, args: []const Ref, frame: *Frame, i: usize) Error!TypeId { + return @enumFromInt(try self.argHandle(args, frame, i)); + } + fn callCompilerFn(self: *Vm, name: []const u8, args: []const Ref, frame: *Frame) Error!?Reg { const table = try self.requireTable(); if (std.mem.eql(u8, name, "intern")) { @@ -1016,9 +1029,7 @@ pub const Vm = struct { // these mirror intern/text_of's shape: word in, word out, no marshaling. if (std.mem.eql(u8, name, "find_type")) { if (args.len != 1) return self.failMsg("comptime find_type: expected one StringId arg"); - const raw = frame.get(args[0].index()); - if (raw > std.math.maxInt(u32)) return self.failMsg("comptime find_type: StringId out of range"); - const sid: types.StringId = @enumFromInt(@as(u32, @intCast(raw))); + const sid: types.StringId = @enumFromInt(try self.argHandle(args, frame, 0)); // Not found → the dedicated `unresolved` (0) sentinel, never a real // type id (mirrors `compiler_lib.handleFindType`). const tid = table.findByName(sid) orelse TypeId.unresolved; @@ -1026,15 +1037,36 @@ pub const Vm = struct { } if (std.mem.eql(u8, name, "type_field_count")) { if (args.len != 1) return self.failMsg("comptime type_field_count: expected one TypeId arg"); - const raw = frame.get(args[0].index()); - if (raw > std.math.maxInt(u32)) return self.failMsg("comptime type_field_count: TypeId out of range"); - const tid: TypeId = @enumFromInt(@as(u32, @intCast(raw))); + const tid = try self.argTypeId(args, frame, 0); // Same `TypeTable.memberCount` the legacy handler reads → no drift; a // type with no member count bails loudly (no silent 0). const count = table.memberCount(tid) orelse return self.failMsg("comptime type_field_count: type has no field/variant count"); return @as(Reg, @bitCast(count)); } + if (std.mem.eql(u8, name, "type_nominal_name")) { + if (args.len != 1) return self.failMsg("comptime type_nominal_name: expected one TypeId arg"); + const tid = try self.argTypeId(args, frame, 0); + const sid = table.nominalName(tid) orelse + return self.failMsg("comptime type_nominal_name: type has no nominal name"); + return @as(Reg, @intFromEnum(sid)); + } + if (std.mem.eql(u8, name, "type_field_name")) { + if (args.len != 2) return self.failMsg("comptime type_field_name: expected (TypeId, idx)"); + const tid = try self.argTypeId(args, frame, 0); + const idx: i64 = @bitCast(frame.get(args[1].index())); + const sid = table.memberName(tid, idx) orelse + return self.failMsg("comptime type_field_name: out-of-range idx or unnamed member"); + return @as(Reg, @intFromEnum(sid)); + } + if (std.mem.eql(u8, name, "type_field_type")) { + if (args.len != 2) return self.failMsg("comptime type_field_type: expected (TypeId, idx)"); + const tid = try self.argTypeId(args, frame, 0); + const idx: i64 = @bitCast(frame.get(args[1].index())); + const mty = table.memberType(tid, idx) orelse + return self.failMsg("comptime type_field_type: out-of-range idx or member has no type"); + return @as(Reg, mty.index()); + } return null; // not a known compiler function → caller bails to legacy } diff --git a/src/ir/types.zig b/src/ir/types.zig index f929e7d2..65d68578 100644 --- a/src/ir/types.zig +++ b/src/ir/types.zig @@ -500,6 +500,60 @@ pub const TypeTable = struct { }; } + /// Nominal name of a named type (struct / union / tagged-union / enum / + /// error-set / protocol), or null for an unnamed type (scalar, pointer, + /// slice, …) or an out-of-range id. Backs the `type_nominal_name` comptime + /// compiler-API reader (legacy handler + VM both call it — no drift). + /// (Distinct from `typeName` below, which renders a display string for any + /// type; this returns the interned nominal-name handle for NAMED types only.) + pub fn nominalName(self: *const TypeTable, id: TypeId) ?StringId { + if (id.index() >= self.infos.items.len) return null; + return switch (self.get(id)) { + .@"struct" => |s| s.name, + .@"union" => |u| u.name, + .tagged_union => |u| u.name, + .@"enum" => |e| e.name, + .error_set => |e| e.name, + .protocol => |p| p.name, + else => null, + }; + } + + /// Name of member `idx` of an aggregate: a struct/union/tagged-union field + /// name, an enum variant name, or a named-tuple element name. Null for a + /// negative / out-of-range `idx`, an unnamed tuple element, or a type with no + /// named members. Backs the `type_field_name` reader. + pub fn memberName(self: *const TypeTable, id: TypeId, idx: i64) ?StringId { + if (idx < 0 or id.index() >= self.infos.items.len) return null; + const i: usize = @intCast(idx); + return switch (self.get(id)) { + .@"struct" => |s| if (i < s.fields.len) s.fields[i].name else null, + .@"union" => |u| if (i < u.fields.len) u.fields[i].name else null, + .tagged_union => |u| if (i < u.fields.len) u.fields[i].name else null, + .@"enum" => |e| if (i < e.variants.len) e.variants[i] else null, + .tuple => |t| if (t.names) |ns| (if (i < ns.len) ns[i] else null) else null, + else => null, + }; + } + + /// Type of member `idx` of an aggregate: a struct/union/tagged-union field + /// type, a tuple element type, or an array/vector element type. Null for a + /// negative / out-of-range `idx` or a type with no member types (e.g. a + /// payloadless enum). Backs the `type_field_type` reader. + pub fn memberType(self: *const TypeTable, id: TypeId, idx: i64) ?TypeId { + if (idx < 0 or id.index() >= self.infos.items.len) return null; + const i: usize = @intCast(idx); + return switch (self.get(id)) { + .@"struct" => |s| if (i < s.fields.len) s.fields[i].ty else null, + .@"union" => |u| if (i < u.fields.len) u.fields[i].ty else null, + .tagged_union => |u| if (i < u.fields.len) u.fields[i].ty else null, + .tuple => |t| if (i < t.fields.len) t.fields[i] else null, + .array => |a| if (i < a.length) a.element else null, + .vector => |v| if (i < v.length) v.element else null, + else => null, + }; + } + /// Source-sensitive variant of `findByName`: asserts at most one named type /// matches, then returns it (or null). Quarantines the global first-match /// scan — new resolver code that must not silently pick a first-of-many