comptime VM: Phase 3 — find_type + type_field_count reflection readers

First read-only compiler-API reflection readers, bound the same way as the
intern/text_of seed (compiler_lib.bound_fns + Vm.callCompilerFn, native on flat
memory, no marshaling). A type handle is a plain u32 TypeId (like StringId), so
both stay clean scalar host-calls:

  find_type(name: StringId) -> TypeId          (TypeTable.findByName; unresolved/0 if absent)
  type_field_count(t: TypeId) -> i64           (new TypeTable.memberCount; loud-bail, no silent 0)

memberCount is the single source both the legacy handler and the VM read, so the
two paths can't drift. find_type returns a non-optional TypeId using the
unresolved(0) sentinel for not-found rather than ?Type — a Type value is
.any-typed (which the flat-memory VM does not represent) and an optional can't
cross the legacy<->VM eval boundary; unresolved is the project-blessed "no type"
marker.

Example 0628 chains intern -> find_type -> type_field_count (+ a not-found
lookup), folded at #run, VM-HANDLED natively. VM unit test added.

Parity 689/689 (gate OFF and -Dcomptime-flat).
This commit is contained in:
agra
2026-06-18 09:25:26 +03:00
parent 0367d96d9b
commit a9302a8b50
10 changed files with 247 additions and 15 deletions

View File

@@ -47,6 +47,8 @@ pub const BoundFn = struct {
pub const bound_fns = [_]BoundFn{
.{ .sx_name = "intern", .handler = handleIntern },
.{ .sx_name = "text_of", .handler = handleTextOf },
.{ .sx_name = "find_type", .handler = handleFindType },
.{ .sx_name = "type_field_count", .handler = handleTypeFieldCount },
};
/// Look up a compiler function by its sx name. Returns null when the name is not
@@ -82,3 +84,30 @@ fn handleTextOf(interp: *Interpreter, args: []const Value) InterpError!Value {
const id: StringId = @enumFromInt(@as(u32, @intCast(args[0].int)));
return Value{ .string = interp.module.types.getString(id) };
}
/// `find_type(name: StringId) -> TypeId` — look up a named type (struct / enum /
/// union / tagged-union / error-set) by its interned name and return its handle.
/// A name with no matching type yields the dedicated `unresolved` sentinel (a
/// `TypeId` of 0), the codebase-blessed "no type" marker — NOT an `?Type` (a
/// `Type` value is `.any`-typed, which the flat-memory VM does not represent, and
/// an optional can't cross the legacy↔VM eval boundary). The caller checks the
/// handle against 0 / `unresolved`. The VM mirrors this in `comptime_vm.callCompilerFn`.
fn handleFindType(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1 or args[0] != .int) return error.TypeError;
if (args[0].int < 0 or args[0].int > std.math.maxInt(u32)) return error.TypeError;
const name: StringId = @enumFromInt(@as(u32, @intCast(args[0].int)));
const tid = interp.module.types.findByName(name) orelse types.TypeId.unresolved;
return Value{ .int = tid.index() };
}
/// `type_field_count(t: TypeId) -> i64` — the member count of an aggregate type
/// (struct/union/tagged-union fields, enum variants, array/vector length), read
/// through `TypeTable.memberCount`. A type with no member count (scalar, pointer,
/// the `unresolved` sentinel, …) is a loud error — never a silent 0.
fn handleTypeFieldCount(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1 or args[0] != .int) return error.TypeError;
if (args[0].int < 0 or args[0].int > std.math.maxInt(u32)) return error.TypeError;
const tid: types.TypeId = @enumFromInt(@as(u32, @intCast(args[0].int)));
const count = interp.module.types.memberCount(tid) orelse return error.TypeError;
return Value{ .int = count };
}