From 8850fcce70b93d92630d254b2def9f870b4e9b06 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 19 Jun 2026 20:58:34 +0300 Subject: [PATCH] P5.7 Step D: re-express metatype declare() as sx over declare_type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit declare(name) is now an ordinary sx fn in modules/std/meta.sx that calls the abi(.compiler) declare_type primitive — both mint/find the same forward nominal slot. Removed the bespoke .declare arm from callBuiltinVm and the BuiltinId.declare member; dropped the declare interception in tryLowerReflectionCall (the call now routes to the sx fn). preregisterForwardTypes still scans for the literal declare("Name") spelling so *Name self-references forward-register before the body lowers (0618). define/type_info/field_type remain builtins. --- library/modules/std/meta.sx | 47 ++++++++++++++++++++++++++++++++----- src/ir/comptime_vm.zig | 11 +++------ src/ir/inst.zig | 13 ++++------ src/ir/lower/call.zig | 22 +++++------------ 4 files changed, 55 insertions(+), 38 deletions(-) diff --git a/library/modules/std/meta.sx b/library/modules/std/meta.sx index 51572216..86a3658c 100644 --- a/library/modules/std/meta.sx +++ b/library/modules/std/meta.sx @@ -6,8 +6,10 @@ // types would otherwise intern into every module's type table and shift every // `.ir` snapshot. Import it explicitly: #import "modules/std/meta.sx"; // -// All four are comptime-only builtins — reaching one at runtime is a hard error -// (the type must be minted / reflected at compile time). +// `declare` is ordinary sx (over the `abi(.compiler)` primitive `declare_type`); +// `define` / `type_info` / `field_type` are comptime-only compiler builtins — +// reaching one at runtime is a hard error (the type must be minted / reflected +// at compile time). // One variant of a constructed enum: a name plus an optional payload type. // `payload = void` means a tagless variant (e.g. `closed`). @@ -52,24 +54,57 @@ TypeInfo :: enum { `tuple: TupleInfo; } -// The compiler's ONLY type-construction primitives (comptime-only #builtins): +// ── The low-level compiler-API type-construction primitive ─────────────────── +// +// `declare_type` is an `abi(.compiler)` function — the compiler's primitive for +// minting a forward nominal slot, serviced by `comptime_vm.callCompilerFn`. It +// runs at LOWERING time (when a `-> Type` builder's result is first referenced), +// where the compiler still resolves references to the new type. The DSL's +// `declare` below is ordinary sx written over it. (Declared here, not imported +// from `modules/compiler.sx`, so `meta.sx` stays self-contained and doesn't +// intern `compiler.sx`'s `List(string)` types into every importer's table.) +declare_type :: (name: string) -> Type abi(.compiler); + +// ── The metatype DSL: `declare` (sx), `define`/`type_info`/`field_type` (builtins) +// // 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 +// registers the forward type at compile time (it scans +// for the literal `declare("Name")` spelling), 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. +// a loud error; a pointer to it is fine. A trivial alias +// for `declare_type` (both mint the same forward slot). // 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; +// Still a compiler builtin: decoding the `TypeInfo` +// tagged-union VALUE needs comptime-VM tagged-union +// matching (`enum_tag` on a tagged union), which the VM +// doesn't model yet — so `define` can't be plain sx until +// that lands (it would `match` the union). +// type_info($T) — reflect a type INTO a `TypeInfo` value (the inverse of +// `define`). Still a compiler builtin: building the +// tagged-union value byte-compatibly is a compiler job. +// field_type($T, idx) — the i-th field / variant-payload / element type. A +// compiler builtin that folds at LOWER time, so it +// composes inside `type_eq` / `type_name` / any type-arg +// slot — a property `type_field_type` (a runtime value) +// can't provide. define :: (handle: Type, info: TypeInfo) -> Type #builtin; type_info :: ($T: Type) -> TypeInfo #builtin; field_type :: ($T: Type, idx: i64) -> Type #builtin; +// `declare(name)` is plain sx over the `declare_type` primitive: both mint (or +// find) the same forward nominal slot — `declare` adds nothing but a name the +// compiler scans for to forward-register self-references (`*Name`). +declare :: (name: string) -> Type { + return declare_type(name); +} + // --- Type constructors built in sx library code (no compiler machinery) --- // // The channel result types, expressed as type-fns over declare/define. They diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index 6ccdf9d8..a59b19ca 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -1900,14 +1900,9 @@ pub const Vm = struct { /// parity holds). Keeps BOTH paths alive during the VM-default transition. fn callBuiltinVm(self: *Vm, bi: inst_mod.BuiltinCall, ins_ty: TypeId, frame: *Frame, ref_types: []const TypeId) Error!?Reg { switch (bi.builtin) { - // declare(name) → mint an EMPTY nominal slot, returned as a Type value. - .declare => { - const table = try self.requireTable(); - if (bi.args.len != 1) return self.failMsg("comptime declare: expected (name)"); - const s = frame.get(bi.args[0].index()); // string fat-pointer Addr - const text = try self.machine.bytes(try self.sliceData(table, s), @intCast(try self.sliceLen(s))); - return @as(Reg, (self.declareNominal(table, text)).index()); - }, + // `declare(name)` is no longer a builtin — it's plain sx in + // `modules/std/meta.sx` over the `declare_type` compiler-API primitive + // (`callCompilerFn`). The `.declare` BuiltinId was removed with it. // define(handle, info) → complete the declared slot from a TypeInfo VALUE. .define => { const table = try self.requireTable(); diff --git a/src/ir/inst.zig b/src/ir/inst.zig index 5816b9cd..62890569 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -452,17 +452,14 @@ pub const BuiltinId = enum(u16) { type_eq, type_is_unsigned, has_impl, - // The compiler's ONLY comptime type-CONSTRUCTION primitives. Higher-level + // The comptime type-CONSTRUCTION terminator builtin. Higher-level // constructors (one-shot, channel-result, etc.) are ordinary sx built over - // these — the compiler knows none of them by name. Both are comptime-only - // (the interp mutates the type table via its - // `mint` handle); reaching them at runtime / emit is a hard error. - // declare() → mint an EMPTY (undefined) nominal slot, returned - // as a `Type` value. Using the slot before its - // `define` is a loud diagnostic. + // it — the compiler knows none of them by name. Comptime-only (the comptime + // VM mutates the type table); reaching it at runtime / emit is a hard error. + // (`declare` is no longer a builtin — it's plain sx over the `declare_type` + // compiler-API primitive in `modules/std/meta.sx`.) // define(handle, info) → decode the `TypeInfo` VALUE (the name travels in // 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 diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 2c240ff0..f1ab2335 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -1677,22 +1677,12 @@ pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.C // classification covers all 7; it runs before dispatch. if (self.reflectionTypeArgGuard(name, c)) |sentinel| return sentinel; - if (std.mem.eql(u8, name, "declare")) { - // 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; - } - // 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, .type_value); - } + // `declare(name)` is now an ordinary sx function (`modules/std/meta.sx`) + // written over the `abi(.compiler)` primitive `declare_type` — no longer + // intercepted here. (`preregisterForwardTypes` still scans for the literal + // `declare("Name")` spelling so a `*Name` self-reference forward-registers + // before the body lowers; the sx `declare` calls `declare_type`, which + // returns that same forward slot.) if (std.mem.eql(u8, name, "define")) { // Comptime type-construction primitive: complete a declare()'d slot // from a TypeInfo value. `define(handle, info)`.