diff --git a/examples/expected/0619-comptime-metatype-type-info.exit b/examples/expected/0619-comptime-metatype-type-info.exit index d00491fd..573541ac 100644 --- a/examples/expected/0619-comptime-metatype-type-info.exit +++ b/examples/expected/0619-comptime-metatype-type-info.exit @@ -1 +1 @@ -1 +0 diff --git a/examples/expected/0619-comptime-metatype-type-info.stderr b/examples/expected/0619-comptime-metatype-type-info.stderr index 7819cb46..8b137891 100644 --- a/examples/expected/0619-comptime-metatype-type-info.stderr +++ b/examples/expected/0619-comptime-metatype-type-info.stderr @@ -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)); - | ^^^^^ diff --git a/examples/expected/0619-comptime-metatype-type-info.stdout b/examples/expected/0619-comptime-metatype-type-info.stdout index 8b137891..9e5d8da9 100644 --- a/examples/expected/0619-comptime-metatype-type-info.stdout +++ b/examples/expected/0619-comptime-metatype-type-info.stdout @@ -1 +1,3 @@ - +circle r=2.500000 +rect n=7 +empty diff --git a/src/ir/inst.zig b/src/ir/inst.zig index e2586a4e..b3c8c14c 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -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 { diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 205aade6..e236d557 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -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 }`. diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 14cc6e1e..51b44034 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -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))