diff --git a/examples/0614-comptime-metatype-enum.sx b/examples/0614-comptime-metatype-enum.sx index 91346f78..16674267 100644 --- a/examples/0614-comptime-metatype-enum.sx +++ b/examples/0614-comptime-metatype-enum.sx @@ -1,5 +1,5 @@ // Comptime type construction: mint a NEW nominal enum from a `TypeInfo` value -// via the `define(declare(), info)` primitives, then construct one of its +// via the `define(declare("E"), info)` primitives, then construct one of its // variants and match on it — exercising that a programmatically-built enum // (with NO backing AST decl) flows through enum codegen unmodified (layout / // construct / match), byte-identical to a hand-written enum. @@ -9,7 +9,7 @@ #import "modules/std.sx"; #import "modules/std/meta.sx"; -E :: define(declare(), .enum(.{ name = "E", variants = .[ +E :: define(declare("E"), .enum(.{ variants = .[ EnumVariant.{ name = "value", payload = i64 }, EnumVariant.{ name = "closed", payload = void }, ] })); diff --git a/examples/0615-comptime-metatype-typefn-identity.sx b/examples/0615-comptime-metatype-typefn-identity.sx index 115d0770..5a4858f6 100644 --- a/examples/0615-comptime-metatype-typefn-identity.sx +++ b/examples/0615-comptime-metatype-typefn-identity.sx @@ -1,5 +1,5 @@ // Comptime type construction — identity: a type-fn that builds a type with -// `define(declare(), ...)` must memoize by the instantiation's mangled name, so +// `define(declare("Box"), ...)` must memoize by the instantiation's mangled name, so // `Box(i64)` resolved at two INDEPENDENT sites (here: a return type and a // parameter type) is ONE `TypeId`. A value built at one site is therefore // assignable / matchable at the other — nominal identity. If the minted result @@ -9,7 +9,7 @@ #import "modules/std/meta.sx"; Box :: ($T: Type) -> Type { - return define(declare(), .enum(.{ name = "Box", variants = .[ + return define(declare("Box"), .enum(.{ variants = .[ EnumVariant.{ name = "some", payload = T }, EnumVariant.{ name = "none", payload = void }, ] })); diff --git a/library/modules/std/meta.sx b/library/modules/std/meta.sx index c89f0cfd..69c481f3 100644 --- a/library/modules/std/meta.sx +++ b/library/modules/std/meta.sx @@ -16,11 +16,10 @@ EnumVariant :: struct { payload: Type; } -// The shape of an enum/tagged-union being reflected or constructed. `name` is -// the type's name — it travels WITH the shape (so `define` can name the slot and -// `type_info` round-trips it); the compiler derives nothing from a binding LHS. +// The shape of an enum/tagged-union being reflected or constructed. The type's +// NAME is supplied to `declare(name)`, not here — `declare` needs it at compile +// time to register the forward type so the body can reference itself (`*Name`). EnumInfo :: struct { - name: string; variants: []EnumVariant; } @@ -33,20 +32,19 @@ TypeInfo :: enum { } // The compiler's ONLY type-construction primitives (comptime-only #builtins): -// declare() — mint a NEW empty (undefined) nominal type, returned -// as a `Type` handle. Using it before its `define` is a -// loud error; references to it (`*Self`) are fine. -// define(handle, info) — fill a declared handle's body from a `TypeInfo` -// (which carries the type's name), and RETURN the -// handle so the one-shot form chains: -// T :: define(declare(), info); -// The recursive / mutually-recursive form keeps them apart so the handle can be -// referenced inside its own definition: -// List :: declare(); -// define(List, .enum(.{ name = "List", variants = .[ -// EnumVariant.{ name = "cons", payload = *List }, -// EnumVariant.{ name = "nil", payload = void } ] })); -declare :: () -> Type #builtin; +// declare(name) — mint a NEW empty (undefined) nominal type NAMED +// `name`, returned as a `Type` handle. The compiler +// registers the forward type at compile time, so the +// body of `define` can reference it BY NAME — that's how +// self-reference works (`payload = *List` resolves to the +// forward `List`). Using the type before its `define` is +// a loud error; a pointer to it is fine. +// define(handle, info) — fill a declared handle's body from a `TypeInfo`, and +// RETURN the handle so the one-shot form chains: +// List :: define(declare("List"), .enum(.{ variants = .[ +// EnumVariant.{ name = "cons", payload = *List }, +// EnumVariant.{ name = "nil", payload = void } ] })); +declare :: (name: string) -> Type #builtin; define :: (handle: Type, info: TypeInfo) -> Type #builtin; type_info :: ($T: Type) -> TypeInfo #builtin; field_type :: ($T: Type, idx: i64) -> Type #builtin; @@ -61,7 +59,7 @@ field_type :: ($T: Type, idx: i64) -> Type #builtin; // A blocking recv: a value, or the channel was closed (drained). RecvResult :: ($T: Type) -> Type { - return define(declare(), .enum(.{ name = "RecvResult", variants = .[ + return define(declare("RecvResult"), .enum(.{ variants = .[ EnumVariant.{ name = "value", payload = T }, EnumVariant.{ name = "closed", payload = void }, ] })); @@ -70,7 +68,7 @@ RecvResult :: ($T: Type) -> Type { // A non-blocking try-recv: a value, currently empty, or closed — three states // a bool can't express. TryResult :: ($T: Type) -> Type { - return define(declare(), .enum(.{ name = "TryResult", variants = .[ + return define(declare("TryResult"), .enum(.{ variants = .[ EnumVariant.{ name = "value", payload = T }, EnumVariant.{ name = "empty", payload = void }, EnumVariant.{ name = "closed", payload = void }, diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 4031747a..205aade6 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -189,10 +189,6 @@ pub const Interpreter = struct { /// that may run `declare`/`define`. Null elsewhere (unit tests, emit-time /// `#run`) → those builtins bail loudly. mint: ?*types.TypeTable = null, - /// Monotonic suffix for `declare()`'s anonymous slot names, so two - /// undefined slots alive at once don't collide in `findByName` before - /// `define` names them (or a type-fn renames them to the mangled name). - declare_counter: u32 = 0, // Heap: dynamically allocated memory blocks heap: std.ArrayList([]u8), @@ -1980,14 +1976,14 @@ pub const Interpreter = struct { .declare => { const tbl = self.mint orelse return bailDetail("comptime declare(): no type-mint target (declare/define are comptime-only — reached at runtime/emit?)"); - // Mint an EMPTY (undefined) tagged_union slot under a fresh - // anonymous name. The binding site (`E :: …` / type-fn) renames - // it to the real name afterwards. An empty `fields` is the - // "declared, not yet defined" state — `define` fills it. - var buf: [40]u8 = undefined; - const nm = std.fmt.bufPrint(&buf, "__reified_{d}", .{self.declare_counter}) catch "__reified"; - self.declare_counter += 1; + if (bi.args.len != 1) return bailDetail("comptime declare(name): needs the name argument"); + const nm = frame.getRef(bi.args[0]).asString(self) orelse + return bailDetail("comptime declare(): name is not a string"); const name_id = tbl.internString(nm); + // Lowering already registered this named forward slot (so a + // `*Name` self-reference in the body resolved); return THAT slot + // so `define` completes the same one. Mint it if somehow absent. + if (tbl.findByName(name_id)) |existing| return .{ .value = .{ .type_tag = existing } }; const info: types.TypeInfo = .{ .tagged_union = .{ .name = name_id, .fields = &.{}, @@ -2027,11 +2023,10 @@ pub const Interpreter = struct { .aggregate => |f| f, else => return bailDetail("comptime define(): `.enum` payload is not an EnumInfo struct value"), }; - // EnumInfo = `{ name: string, variants: []EnumVariant }`. The name - // travels with the shape — `define` names the slot from it. - if (einfo_fields.len != 2) return bailDetail("comptime define(): EnumInfo must have `name` and `variants`"); - const name = einfo_fields[0].asString(self) orelse return bailDetail("comptime define(): EnumInfo `name` is not a string"); - const elems = decodeVariantElements(einfo_fields[1]) orelse + // EnumInfo = `{ variants: []EnumVariant }`. The name was given to + // `declare` (it's already the slot's name) — `define` only fills the body. + if (einfo_fields.len != 1) return bailDetail("comptime define(): EnumInfo must have a `variants` field"); + const elems = decodeVariantElements(einfo_fields[0]) orelse return bailDetail("comptime define(): `variants` is not a slice/array of EnumVariant"); if (elems.len == 0) return bailDetail("comptime define(): enum has no variants"); @@ -2047,22 +2042,21 @@ pub const Interpreter = struct { fields.append(self.alloc, .{ .name = tbl.internString(vname), .ty = payload_tid }) catch return error.CannotEvalComptime; } - // Complete the declared slot: NAME it from the EnumInfo (the name travels - // with the shape) and fill the body. The name changes the intern key - // (declare minted an anonymous `__reified_N`), so re-key via - // `replaceKeyedInfo`. The nominal id is preserved. + // Complete the declared slot IN PLACE: it already has its name + nominal + // id (from `declare`); fill the body. Name/id unchanged → the intern key + // is stable, so `updatePreservingKey`. const cur = tbl.get(handle); if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d enum slot"); const full: types.TypeInfo = .{ .tagged_union = .{ - .name = tbl.internString(name), + .name = cur.tagged_union.name, .fields = fields.items, .tag_type = .i64, .backing_type = null, .explicit_tag_values = null, .nominal_id = cur.tagged_union.nominal_id, } }; - tbl.replaceKeyedInfo(handle, full); - // Return the handle so the one-shot form chains: `T :: define(declare(), info)`. + tbl.updatePreservingKey(handle, full); + // Return the handle so the one-shot form chains: `T :: define(declare("T"), info)`. return .{ .value = .{ .type_tag = handle } }; } }; diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 832eb4c8..14cc6e1e 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -1677,15 +1677,20 @@ pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.C if (self.reflectionTypeArgGuard(name, c)) |sentinel| return sentinel; if (std.mem.eql(u8, name, "declare")) { - // Comptime type-construction primitive: mint an empty nominal slot. - // Comptime-only — emitted as a builtin_call the interp executes against - // its `mint` table; never reaches codegen (its sx callers are only ever - // comptime-evaluated). - if (c.args.len != 0) { - if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "declare() takes no arguments", .{}); + // Comptime type-construction primitive: mint an empty nominal slot NAMED + // by its (compile-time string) argument. Comptime-only — emitted as a + // builtin_call the interp executes against its `mint` table; never + // reaches codegen (its sx callers are only ever comptime-evaluated). + if (c.args.len != 1) { + if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "declare(name) takes one string argument", .{}); return Ref.none; } - return self.builder.callBuiltin(.declare, &.{}, .any); + // The named forward type is pre-registered by `evalComptimeType`'s + // `preregisterForwardTypes` before this body lowers (so a `*Name` + // self-reference resolves); the interp's `declare` returns that slot. + const name_ref = self.lowerExpr(c.args[0]); + const args_owned = self.alloc.dupe(Ref, &.{name_ref}) catch return Ref.none; + return self.builder.callBuiltin(.declare, args_owned, .any); } if (std.mem.eql(u8, name, "define")) { // Comptime type-construction primitive: complete a declare()'d slot diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index d7e136a4..d709c206 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -393,7 +393,60 @@ pub fn lowerInsertExprValue(self: *Lowering, expr: *const Node) Ref { /// name (the type-fn mangled-name path) renames afterwards via /// `renameNominalType`. Returns null (caller poisons) if evaluation didn't yield /// a Type. +/// Register an empty forward nominal type named by each `declare("Name")` call +/// reachable from `expr` (and, if `expr` is a call to a known fn, that fn's +/// body). Runs before the comptime expression lowers so a `*Name` self-reference +/// resolves to this forward slot. Idempotent (skips an already-registered name). +fn preregisterForwardTypes(self: *Lowering, expr: *const Node) void { + scanDeclareNames(self, expr, 0); + if (expr.data == .call and expr.data.call.callee.data == .identifier) { + if (self.program_index.fn_ast_map.get(expr.data.call.callee.data.identifier.name)) |fd| { + scanDeclareNames(self, fd.body, 0); + } + } +} + +fn scanDeclareNames(self: *Lowering, node: *const Node, depth: u32) void { + if (depth > 64) return; + switch (node.data) { + .call => |c| { + if (c.callee.data == .identifier and + std.mem.eql(u8, c.callee.data.identifier.name, "declare") and + c.args.len == 1 and c.args[0].data == .string_literal) + { + const nm = c.args[0].data.string_literal.raw; + const nid = self.module.types.internString(nm); + const tid = self.module.types.findByName(nid) orelse self.module.types.internNominal(.{ .tagged_union = .{ + .name = nid, + .fields = &.{}, + .tag_type = .i64, + } }, 0); + // Bind the name as a type alias too: a `Name :: ()` decl + // makes `Name` a const_decl author, so a `*Name` self-reference + // resolves through the forward-ALIAS path — which checks + // `type_aliases_by_source`, not `findByName`. Without this the + // alias path returns a pending empty-struct stub instead. + self.putTypeAlias(self.current_source_file, nm, tid); + } + for (c.args) |a| scanDeclareNames(self, a, depth + 1); + }, + .block => |b| for (b.stmts) |s| scanDeclareNames(self, s, depth + 1), + .return_stmt => |r| if (r.value) |v| scanDeclareNames(self, v, depth + 1), + .var_decl => |v| if (v.value) |val| scanDeclareNames(self, val, depth + 1), + .const_decl => |cd| scanDeclareNames(self, cd.value, depth + 1), + .struct_literal => |sl| for (sl.field_inits) |fi| scanDeclareNames(self, fi.value, depth + 1), + .array_literal => |al| for (al.elements) |e| scanDeclareNames(self, e, depth + 1), + else => {}, + } +} + pub fn evalComptimeType(self: *Lowering, expr: *const Node) ?TypeId { + // Pre-register every `declare("Name")` forward type BEFORE lowering, so a + // self-referential `*Name` payload resolves (the name is a known forward + // type when the body lowers). Done up-front rather than at declare's + // lowering because a `*Name` can lower before its `declare` within the same + // body. The interp's `declare` returns this same slot; `define` completes it. + preregisterForwardTypes(self, expr); const func_id = self.createComptimeFunction("__ctype", expr, .any); var interp = interp_mod.Interpreter.init(self.module, self.alloc);