diff --git a/library/modules/std/meta.sx b/library/modules/std/meta.sx index f02a5c24..d5d13b34 100644 --- a/library/modules/std/meta.sx +++ b/library/modules/std/meta.sx @@ -23,12 +23,26 @@ EnumInfo :: struct { variants: []EnumVariant; } +// One field of a constructed struct: a name plus its type. +StructField :: struct { + name: string; + type: Type; +} + +// The shape of a struct being reflected or constructed. As with `EnumInfo`, the +// type's NAME travels in `declare(name)`, not here. +StructInfo :: struct { + fields: []StructField; +} + // The reflected/constructed type shape. A tagged union over the kinds of type -// that can be minted. Only `` .`enum `` ships today; struct/tuple land later. -// The variant uses the backtick raw-identifier escape so it reads as the -// keyword `enum` (`` .`enum(...) ``) rather than a mangled `enum_`. +// that can be minted — `` .`enum `` and `` .`struct `` ship today (tuple later). +// The variants use the backtick raw-identifier escape so they read as the +// keywords `enum` / `struct` (`` .`enum(...) `` / `` .`struct(...) ``) rather than +// mangled `enum_` / `struct_`. TypeInfo :: enum { `enum: EnumInfo; + `struct: StructInfo; } // The compiler's ONLY type-construction primitives (comptime-only #builtins): diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 515c4df5..9dcab03b 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -2039,7 +2039,7 @@ pub const Interpreter = struct { const handle = frame.getRef(bi.args[0]).asTypeId() orelse return bailDetail("comptime define(): first argument is not a Type handle (use a `declare()` result)"); const info_val = frame.getRef(bi.args[1]); - return self.defineEnum(tbl, handle, info_val); + return self.defineType(tbl, handle, info_val); }, .type_info => { // Reflect a type INTO a `TypeInfo` value — the inverse of @@ -2066,34 +2066,68 @@ pub const Interpreter = struct { 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| { + // The TypeInfo variant tag (declaration order in `meta.sx`: `enum`=0, + // `struct`=1). Each member reflects as `{ string(name), type_tag(ty) }` + // regardless of kind — payload type for an enum variant, field type for a + // struct field (a payloadless `@"enum"` variant carries `void`). + const tag: i64 = switch (info) { + .tagged_union => |u| blk: { 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; } + break :blk 0; }, - .@"enum" => |e| { + .@"enum" => |e| blk: { 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; } + break :blk 0; }, - 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"); + .@"struct" => |s| blk: { + for (s.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; + } + break :blk 1; + }, + else => return bailDetail("comptime type_info: only enum / tagged-union / struct types reflect today"), + }; + if (elems.items.len == 0) return bailDetail("comptime type_info: type has no members"); - const variants_slice = self.alloc.dupe(Value, &.{ + // Wrap: members → `{ data, len }` slice → info struct `{ members }` → + // TypeInfo `{ int(tag), info }`. Identical shape for `.enum` / `.struct`. + const members_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; + const inner = self.alloc.dupe(Value, &.{.{ .aggregate = members_slice }}) catch return error.CannotEvalComptime; + const typeinfo = self.alloc.dupe(Value, &.{ .{ .int = tag }, .{ .aggregate = inner } }) catch return error.CannotEvalComptime; return .{ .value = .{ .aggregate = typeinfo } }; } + /// Complete a `declare()`d slot from a `TypeInfo` VALUE, dispatching on the + /// TypeInfo tag (`{ tag, payload }`): `0` → `.enum(EnumInfo)` (tagged_union), + /// `1` → `.struct(StructInfo)`. The tag is the variant index in `meta.sx`'s + /// `TypeInfo` enum declaration order (`enum` then `struct`). + fn defineType(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult { + const ti_fields = switch (info_val) { + .aggregate => |f| f, + else => return bailDetail("comptime define(): info did not evaluate to a TypeInfo value"), + }; + if (ti_fields.len != 2) return bailDetail("comptime define(): malformed TypeInfo value (expected `{ tag, info }`)"); + const tag = ti_fields[0].asInt() orelse return bailDetail("comptime define(): TypeInfo tag is not an integer"); + return switch (tag) { + 0 => self.defineEnum(tbl, handle, info_val), + 1 => self.defineStruct(tbl, handle, info_val), + else => bailDetail("comptime define(): unknown TypeInfo variant (only `.enum` / `.struct` are supported)"), + }; + } + /// 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 }`. @@ -2160,6 +2194,61 @@ pub const Interpreter = struct { // Return the handle so the one-shot form chains: `T :: define(declare("T"), info)`. return .{ .value = .{ .type_tag = handle } }; } + + /// Complete a `declare()`d slot from a `.struct(StructInfo)` `TypeInfo` VALUE. + /// Mirror of `defineEnum` for structs: StructInfo is `{ fields }`, each field + /// `{ name: string, type: Type }`. Fills the (tagged_union-shaped) declare + /// slot in place as a `.@"struct"`, preserving its name + nominal id. + fn defineStruct(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult { + // Unwrap TypeInfo `.struct(StructInfo)` → StructInfo `{ fields }`. + const ti_fields = info_val.aggregate; // defineType already checked the shape + const sinfo = ti_fields[1]; + const sinfo_fields = switch (sinfo) { + .aggregate => |f| f, + else => return bailDetail("comptime define(): `.struct` payload is not a StructInfo struct value"), + }; + if (sinfo_fields.len != 1) return bailDetail("comptime define(): StructInfo must have a `fields` field"); + const elems = decodeVariantElements(sinfo_fields[0]) orelse + return bailDetail("comptime define(): `fields` is not a slice/array of StructField"); + if (elems.len == 0) return bailDetail("comptime define(): struct has no fields"); + + var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + for (elems) |elem| { + const sf = switch (elem) { + .aggregate => |f| f, + else => return bailDetail("comptime define(): StructField did not evaluate to a struct value"), + }; + if (sf.len != 2) return bailDetail("comptime define(): StructField must have `name` and `type`"); + const fname = sf[0].asString(self) orelse return bailDetail("comptime define(): StructField `name` is not a string"); + const fty = sf[1].asTypeId() orelse return bailDetail("comptime define(): StructField `type` is not a Type value"); + const fname_id = tbl.internString(fname); + // Reject duplicate field names (a struct can't have two same-named + // fields). Dynamic name → set the detail directly (bailDetail is + // comptime-only); evalComptimeType renders it. + for (fields.items) |existing| { + if (existing.name == fname_id) { + last_bail_detail = std.fmt.allocPrint(self.alloc, "comptime define(): duplicate field name '{s}'", .{fname}) catch "comptime define(): duplicate field name"; + return error.CannotEvalComptime; + } + } + fields.append(self.alloc, .{ .name = fname_id, .ty = fty }) catch return error.CannotEvalComptime; + } + + // Complete the declare slot as a struct. It was minted as an (empty) + // tagged_union by `declare`; we keep its TypeId + name + nominal id but + // SWAP THE KIND to struct. A kind change moves the intern key, so use + // `replaceKeyedInfo` (re-keys) rather than `updatePreservingKey` (which + // asserts the key is unchanged — true for the enum path, false here). + const cur = tbl.get(handle); + if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d slot"); + const full: types.TypeInfo = .{ .@"struct" = .{ + .name = cur.tagged_union.name, + .fields = fields.items, + .nominal_id = cur.tagged_union.nominal_id, + } }; + tbl.replaceKeyedInfo(handle, full); + return .{ .value = .{ .type_tag = handle } }; + } }; /// Normalize an interpreter value into the list of EnumVariant element values. diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 51b44034..5f0c0e2b 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -1731,16 +1731,15 @@ pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.C 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. + // Enum / tagged-union and struct reflection ship today (the symmetric + // inverse of `define`, which builds those kinds). A loud, well-spanned + // reject here beats a deferred interp bail; tuple widening lands later. const reflectable = !t.isBuiltin() and switch (self.module.types.get(t)) { - .@"enum", .tagged_union => true, + .@"enum", .tagged_union, .@"struct" => 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)}); + if (self.diagnostics) |d| d.addFmt(.err, c.args[0].span, "type_info: '{s}' is not reflectable — only enum / tagged-union / struct types reflect today", .{self.formatTypeName(t)}); return Ref.none; } const type_ref = self.builder.constType(t);