feat(metatype): implement type_info($T) reflection (enum round-trip)

type_info reflects an enum / tagged-union INTO a TypeInfo value — the
inverse of define's decode — so define(declare(n), type_info(T)) mints
a byte-identical copy with NO literal variant list.

- inst.zig: new BuiltinId.type_info (comptime-only, like declare/define).
- lower/call.zig: replace the 'not yet implemented' bail. Resolve $T at
  lower time, reject non-enum/non-tagged-union loudly with a good span,
  emit callBuiltin(.type_info, [const_type], TypeInfo).
- interp.zig: reflectTypeInfo builds the exact nested-aggregate Value
  defineEnum decodes — variant {name,payload}, slice {data,len}, EnumInfo
  {variants}, TypeInfo {tag0, EnumInfo}. tagged_union reflects field.ty
  (tagless already void); payloadless `enum` reflects void per variant.
- emit: unchanged — type_info is always comptime-evaluated, the existing
  comptime-only else arm (shared with declare/define) never fires.

0619 turns green: a source enum (circle:f64 / rect:i64 / empty) reflected
and reconstructed, constructs and matches like the original.
This commit is contained in:
agra
2026-06-16 22:52:53 +03:00
parent 3805a051cc
commit 1ffda415c2
6 changed files with 92 additions and 26 deletions

View File

@@ -1,17 +1 @@
error: type_info is not yet implemented
--> examples/0619-comptime-metatype-type-info.sx:19:43
|
19 | ShapeCopy :: define(declare("ShapeCopy"), type_info(Shape));
| ^^^^^^^^^
error: cannot infer enum type for '.circle' — use an explicit type or assign to a typed variable
--> examples/0619-comptime-metatype-type-info.sx:30:14
|
30 | describe(.circle(2.5));
| ^^^^^^^
error: cannot infer enum type for '.rect' — use an explicit type or assign to a typed variable
--> examples/0619-comptime-metatype-type-info.sx:31:14
|
31 | describe(.rect(7));
| ^^^^^

View File

@@ -1 +1,3 @@
circle r=2.500000
rect n=7
empty

View File

@@ -461,6 +461,12 @@ pub const BuiltinId = enum(u16) {
// it) and complete the slot.
declare,
define,
// The comptime reflection INVERSE of `define`: read a type's variants
// (name + payload type) out of the type table and CONSTRUCT the same
// `.enum(EnumInfo{ variants })` value `define` decodes. Comptime-only
// (the interp builds the Value aggregate); emit bails (Type is
// comptime-only). `type_info($T)` round-trips through `define`.
type_info,
};
pub const CompilerCall = struct {

View File

@@ -2001,9 +2001,59 @@ pub const Interpreter = struct {
const info_val = frame.getRef(bi.args[1]);
return self.defineEnum(tbl, handle, info_val);
},
.type_info => {
// Reflect a type INTO a `TypeInfo` value — the inverse of
// `define`'s decode. Lowering already validated the arg is an
// enum/tagged-union and passed it as a `const_type`.
if (bi.args.len != 1) return bailDetail("comptime type_info: missing type argument");
const tid = frame.getRef(bi.args[0]).asTypeId() orelse
return bailDetail("comptime type_info: argument is not a Type value");
return self.reflectTypeInfo(tid);
},
}
}
/// Build the `.enum(EnumInfo{ variants })` `TypeInfo` value for `tid` — the
/// exact shape `defineEnum` decodes, so `define(declare(n), type_info(T))`
/// round-trips. A `tagged_union` reflects each field as
/// `EnumVariant{ name, payload = field.ty }` (tagless variants already carry
/// `void`); a payloadless `@"enum"` reflects every variant with `void`.
/// Value layout mirrors how the interp evaluates the hand-written literal:
/// variant = { string(name), type_tag(payload) }
/// variants = { aggregate(variant…), int(len) } (slice fat pointer)
/// EnumInfo = { variants }
/// TypeInfo = { int(0), EnumInfo } (`.enum` tag = 0)
fn reflectTypeInfo(self: *Interpreter, tid: TypeId) InterpError!ExecResult {
var elems = std.ArrayList(Value).empty;
const info = self.module.types.get(tid);
switch (info) {
.tagged_union => |u| {
for (u.fields) |f| {
const nm = self.alloc.dupe(u8, self.module.types.getString(f.name)) catch return error.CannotEvalComptime;
const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = f.ty } }) catch return error.CannotEvalComptime;
elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime;
}
},
.@"enum" => |e| {
for (e.variants) |vname| {
const nm = self.alloc.dupe(u8, self.module.types.getString(vname)) catch return error.CannotEvalComptime;
const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = .void } }) catch return error.CannotEvalComptime;
elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime;
}
},
else => return bailDetail("comptime type_info: only enum/tagged-union types reflect today"),
}
if (elems.items.len == 0) return bailDetail("comptime type_info: type has no variants");
const variants_slice = self.alloc.dupe(Value, &.{
.{ .aggregate = elems.items },
.{ .int = @intCast(elems.items.len) },
}) catch return error.CannotEvalComptime;
const einfo = self.alloc.dupe(Value, &.{.{ .aggregate = variants_slice }}) catch return error.CannotEvalComptime;
const typeinfo = self.alloc.dupe(Value, &.{ .{ .int = 0 }, .{ .aggregate = einfo } }) catch return error.CannotEvalComptime;
return .{ .value = .{ .aggregate = typeinfo } };
}
/// Complete a `declare()`d slot from a `TypeInfo` VALUE. The value is the
/// `.enum(EnumInfo)` tagged-union (`{ tag, EnumInfo }`), EnumInfo is
/// `{ variants }`, and each variant is `{ name: string, payload: Type }`.

View File

@@ -1714,14 +1714,38 @@ pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.C
return self.builder.callBuiltin(.define, args_owned, .any);
}
if (std.mem.eql(u8, name, "type_info")) {
// Comptime reflection-into-data (reflect a type INTO a `TypeInfo`
// value). Until the interpreter-side reflection lands, bail loudly
// rather than fall through to the no-body `#builtin` const_decl path
// (which would mis-lower as a zero-arg call). A silent fall-through
// would hand the caller a garbage TypeInfo value.
if (self.diagnostics) |d|
d.addFmt(.err, c.callee.span, "type_info is not yet implemented", .{});
return Ref.none;
// Comptime reflection-into-data: reflect a type INTO a `TypeInfo`
// value (the inverse of `define`'s decode). Resolve `$T` at lower
// time, then emit a `callBuiltin(.type_info, [const_type])` the
// interp executes against its type table — it reads the variants
// (name + payload) and constructs the same `.enum(EnumInfo{ … })`
// value `define` decodes, so the two round-trip. Result type is
// `TypeInfo`; the whole `define(declare(), type_info(T))` expr is
// comptime-evaluated, so this builtin never reaches codegen.
if (c.args.len != 1) {
if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "type_info($T) takes one type argument", .{});
return Ref.none;
}
const ti_ty = self.module.types.findByName(self.module.types.internString("TypeInfo")) orelse {
if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "type_info needs `TypeInfo` in scope — import `modules/std/meta.sx`", .{});
return Ref.none;
};
const t = self.resolveTypeArg(c.args[0]);
// Only enum / tagged-union reflection ships today (the symmetric
// inverse of `define`, which builds tagged-unions). A loud, well-
// spanned reject here beats a deferred interp bail; struct/tuple
// widening lands later.
const reflectable = !t.isBuiltin() and switch (self.module.types.get(t)) {
.@"enum", .tagged_union => true,
else => false,
};
if (!reflectable) {
if (self.diagnostics) |d| d.addFmt(.err, c.args[0].span, "type_info: '{s}' is not an enum — only enum/tagged-union reflection is supported today", .{self.formatTypeName(t)});
return Ref.none;
}
const type_ref = self.builder.constType(t);
const args_owned = self.alloc.dupe(Ref, &.{type_ref}) catch return Ref.none;
return self.builder.callBuiltin(.type_info, args_owned, ti_ty);
}
if (std.mem.eql(u8, name, "size_of")) {
// size_of(T) → const_int(sizeof(T))