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:
agra
2026-06-18 09:34:36 +03:00
parent a9302a8b50
commit d23e208430
10 changed files with 316 additions and 33 deletions

View File

@@ -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() };
}

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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