comptime VM: Phase 3 — field-level reflection readers
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).
This commit is contained in:
@@ -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
|
> breaks cross-compilation — host vs target layout — and loses the sandbox. A
|
||||||
> flat-memory VM keeps both while getting native bytes + speed.)
|
> 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
|
> **Next action (2026-06-18):** **Phase 3 is UNDER WAY.** The VM now hosts five read-only
|
||||||
> read-only reflection readers — `find_type(name: StringId) -> TypeId` and
|
> reflection readers, all on the same plain-`u32`-handle shape (a `TypeId` is a `u32`, like
|
||||||
> `type_field_count(t: TypeId) -> i64` — bound exactly like the `intern`/`text_of` seed
|
> `StringId`), so the calls stay clean scalar host-calls — handle in, scalar out, no
|
||||||
> (a type handle is a plain `u32` `TypeId`, so the calls stay clean scalar host-calls).
|
> marshaling: `find_type(name) -> TypeId`, `type_field_count(t) -> i64`,
|
||||||
> Example `0628` chains `intern → find_type → type_field_count`, VM-HANDLED natively.
|
> `type_nominal_name(t) -> StringId`, `type_field_name(t, idx) -> StringId`,
|
||||||
> Parity **689/689** (gate ON and OFF), VM unit test added. Phase 1.final op-porting was
|
> `type_field_type(t, idx) -> TypeId`. Each is backed by a `TypeTable` query
|
||||||
> already complete (the VM covers scalars/control-flow/aggregates/strings/optionals/enums,
|
> (`findByName`/`memberCount`/`nominalName`/`memberName`/`memberType`) that BOTH the legacy
|
||||||
> calls+recursion, the implicit context + full allocator protocol, globals, failables +
|
> handler and the VM call, so the two paths can't drift. Examples `0628` (`find_type` +
|
||||||
> return traces); both comptime call sites route through the VM with legacy fallback.
|
> `type_field_count`) and `0629` (field reflection over `Pair { lo: Point; hi: Point }`),
|
||||||
> **Forward (P3.2):** more read-only readers on the same `TypeId`-handle shape
|
> both VM-HANDLED natively. Parity **690/690** (gate ON and OFF), VM unit tests added.
|
||||||
> (`type_name`, `field_name`, `field_type`, kind queries), then `register_struct` (the
|
> Phase 1.final op-porting was already complete (the VM covers
|
||||||
> first MUTATING fn — mints a `TypeId`; resolve the mutable-table / host-ABI-vs-target-ABI
|
> scalars/control-flow/aggregates/strings/optionals/enums, calls+recursion, the implicit
|
||||||
> boundary deliberately). Re-expressing `declare`/`define`/`type_info` as sx (the metatype,
|
> context + full allocator protocol, globals, failables + return traces); both comptime
|
||||||
> which runs at LOWERING time) needs the VM hardened against malformed lowering-time IR
|
> call sites route through the VM with legacy fallback.
|
||||||
> first — keep it on the legacy path until then. Phase 2 (bytecode) is the orthogonal
|
> **Forward (P3.3):** `register_struct` (the first MUTATING fn — mints a `TypeId`; resolve
|
||||||
> speed work. **Decision recorded:** `find_type` returns a non-optional `TypeId` using the
|
> the mutable-table / host-ABI-vs-target-ABI boundary deliberately), plus any remaining
|
||||||
> `unresolved` (0) sentinel, NOT `?Type` (a `Type` value is `.any`-typed, which the VM
|
> read-only readers the metatype needs (kind query, `field_value_int`). Re-expressing
|
||||||
> doesn't represent, and an optional can't cross the eval bridge) — see `PLAN-COMPILER-VM.md`
|
> `declare`/`define`/`type_info` as sx (the metatype, which runs at LOWERING time) needs the
|
||||||
> Phase 3 progress note.
|
> VM hardened against malformed lowering-time IR first — keep it on the legacy path until
|
||||||
> Build/verify: `zig build && zig build test` (689, gate OFF). Run the corpus ON the VM:
|
> 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
|
> `zig build test -Dcomptime-flat` (the build flag) OR env `SX_COMPTIME_FLAT=1`. Coverage
|
||||||
> trace: `SX_COMPTIME_FLAT_TRACE=1`.
|
> 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`.)
|
`List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.)
|
||||||
|
|
||||||
## Log
|
## 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).**
|
- **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
|
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
|
to `compiler_lib.bound_fns` for the legacy handler + the welded-decl export check, AND to
|
||||||
|
|||||||
@@ -275,13 +275,27 @@ host through it:
|
|||||||
marker (see CLAUDE.md REJECTED PATTERNS — a dedicated sentinel is the required shape),
|
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
|
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.
|
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)
|
- **(P3.2) Field-level reflection readers — `type_nominal_name` + `type_field_name` +
|
||||||
-> StringId`, `field_name(t, i) -> StringId`, `field_type(t, i) -> TypeId`, kind queries),
|
`type_field_type` (DONE).** Three more readers on the same `TypeId`-handle shape (each
|
||||||
then `register_struct` (the first MUTATING fn — mints a `TypeId`; resolve the mutable-table
|
backed by a new `TypeTable` query that BOTH the legacy handler and the VM call, so no
|
||||||
/ host-ABI-vs-target-ABI boundary deliberately, per the open questions). Re-expressing
|
drift): `type_nominal_name(t: TypeId) -> StringId` (`nominalName` — a named type's own
|
||||||
`declare`/`define`/`type_info` as sx (the metatype, which runs at LOWERING time) still
|
name; loud-bail for unnamed types), `type_field_name(t: TypeId, idx: i64) -> StringId`
|
||||||
needs the VM hardened against malformed lowering-time IR first — keep that on the legacy
|
(`memberName` — struct/union/tagged-union field, enum variant, named-tuple element), and
|
||||||
path until then (see the resume note in CHECKPOINT-COMPILER-API.md).
|
`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)
|
### Phase 3 — Compiler-API on flat memory (resume the stream — no weld)
|
||||||
With native-byte comptime values, re-home the compiler-API:
|
With native-byte comptime values, re-home the compiler-API:
|
||||||
|
|||||||
45
examples/0629-comptime-compiler-field-reflect.sx
Normal file
45
examples/0629-comptime-compiler-field-reflect.sx
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Pair has 2 fields
|
||||||
|
field 0 = lo : Point
|
||||||
|
field 1 = hi : Point
|
||||||
@@ -49,6 +49,9 @@ pub const bound_fns = [_]BoundFn{
|
|||||||
.{ .sx_name = "text_of", .handler = handleTextOf },
|
.{ .sx_name = "text_of", .handler = handleTextOf },
|
||||||
.{ .sx_name = "find_type", .handler = handleFindType },
|
.{ .sx_name = "find_type", .handler = handleFindType },
|
||||||
.{ .sx_name = "type_field_count", .handler = handleTypeFieldCount },
|
.{ .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
|
/// 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;
|
const count = interp.module.types.memberCount(tid) orelse return error.TypeError;
|
||||||
return Value{ .int = count };
|
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() };
|
||||||
|
}
|
||||||
|
|||||||
@@ -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" {
|
test "comptime_vm exec: func_ref + call_indirect dispatch" {
|
||||||
const alloc = std.testing.allocator;
|
const alloc = std.testing.allocator;
|
||||||
var module = Module.init(alloc);
|
var module = Module.init(alloc);
|
||||||
|
|||||||
@@ -988,6 +988,19 @@ pub const Vm = struct {
|
|||||||
/// legacy `compiler_lib` handlers, but reads/writes flat memory directly instead
|
/// legacy `compiler_lib` handlers, but reads/writes flat memory directly instead
|
||||||
/// of marshaling `Value`s. The seed pair is the string-pool round-trip:
|
/// of marshaling `Value`s. The seed pair is the string-pool round-trip:
|
||||||
/// `intern(s: string) -> StringId` and `text_of(id: StringId) -> string`.
|
/// `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 {
|
fn callCompilerFn(self: *Vm, name: []const u8, args: []const Ref, frame: *Frame) Error!?Reg {
|
||||||
const table = try self.requireTable();
|
const table = try self.requireTable();
|
||||||
if (std.mem.eql(u8, name, "intern")) {
|
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.
|
// these mirror intern/text_of's shape: word in, word out, no marshaling.
|
||||||
if (std.mem.eql(u8, name, "find_type")) {
|
if (std.mem.eql(u8, name, "find_type")) {
|
||||||
if (args.len != 1) return self.failMsg("comptime find_type: expected one StringId arg");
|
if (args.len != 1) return self.failMsg("comptime find_type: expected one StringId arg");
|
||||||
const raw = frame.get(args[0].index());
|
const sid: types.StringId = @enumFromInt(try self.argHandle(args, frame, 0));
|
||||||
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)));
|
|
||||||
// Not found → the dedicated `unresolved` (0) sentinel, never a real
|
// Not found → the dedicated `unresolved` (0) sentinel, never a real
|
||||||
// type id (mirrors `compiler_lib.handleFindType`).
|
// type id (mirrors `compiler_lib.handleFindType`).
|
||||||
const tid = table.findByName(sid) orelse TypeId.unresolved;
|
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 (std.mem.eql(u8, name, "type_field_count")) {
|
||||||
if (args.len != 1) return self.failMsg("comptime type_field_count: expected one TypeId arg");
|
if (args.len != 1) return self.failMsg("comptime type_field_count: expected one TypeId arg");
|
||||||
const raw = frame.get(args[0].index());
|
const tid = try self.argTypeId(args, frame, 0);
|
||||||
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)));
|
|
||||||
// Same `TypeTable.memberCount` the legacy handler reads → no drift; a
|
// Same `TypeTable.memberCount` the legacy handler reads → no drift; a
|
||||||
// type with no member count bails loudly (no silent 0).
|
// type with no member count bails loudly (no silent 0).
|
||||||
const count = table.memberCount(tid) orelse
|
const count = table.memberCount(tid) orelse
|
||||||
return self.failMsg("comptime type_field_count: type has no field/variant count");
|
return self.failMsg("comptime type_field_count: type has no field/variant count");
|
||||||
return @as(Reg, @bitCast(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
|
return null; // not a known compiler function → caller bails to legacy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
/// Source-sensitive variant of `findByName`: asserts at most one named type
|
||||||
/// matches, then returns it (or null). Quarantines the global first-match
|
/// matches, then returns it (or null). Quarantines the global first-match
|
||||||
/// scan — new resolver code that must not silently pick a first-of-many
|
/// scan — new resolver code that must not silently pick a first-of-many
|
||||||
|
|||||||
Reference in New Issue
Block a user