From 442a70b8c9c0285f768f92d3bad5fb92f1c750a0 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 16 Jun 2026 20:39:02 +0300 Subject: [PATCH] =?UTF-8?q?green(reify):=20declare/define=20floor=20?= =?UTF-8?q?=E2=80=94=20reify=20is=20sx;=20E=20::=20reify(...)=20comptime-e?= =?UTF-8?q?valuated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First slice of the re-architecture. The compiler gains two comptime type-construction builtins — declare() (mint an empty/undefined nominal slot) and define(handle, info) (decode a TypeInfo VALUE + complete the slot) — executed by the interpreter against a new `mint` TypeTable handle (setMintTable). reify becomes PLAIN sx in meta.sx: reify :: (info) -> Type { h := declare(); define(h, info); return h; } `E :: f(...)` where f is a non-generic Type-returning fn (reify, and later make_enum) is now comptime-evaluated via evalComptimeTypeNamed: wrap the call in a throwaway comptime fn, run it through the interp with the mint table enabled so declare/define mint the type, read back the type_tag, and rename the anonymous slot to the binding name. The compiler has ZERO reify knowledge at the decl site — the old `E :: reify` hook is deleted. examples/0614 (inline reify) now runs on this floor. Full suite green (673). INTERMEDIATE: reifyType + findReturnReifyCall still serve the type-fn path (0615/0617) and will be deleted in the next slice (type-fn body comptime-eval), after which the compiler has no reify code at all. --- library/modules/std/meta.sx | 22 +++++-- src/ir/inst.zig | 11 ++++ src/ir/interp.zig | 122 ++++++++++++++++++++++++++++++++++++ src/ir/lower.zig | 2 + src/ir/lower/call.zig | 23 +++++++ src/ir/lower/comptime.zig | 43 +++++++++++++ src/ir/lower/decl.zig | 32 +++++++--- 7 files changed, 241 insertions(+), 14 deletions(-) diff --git a/library/modules/std/meta.sx b/library/modules/std/meta.sx index 8df7e504..9c95cf60 100644 --- a/library/modules/std/meta.sx +++ b/library/modules/std/meta.sx @@ -30,13 +30,27 @@ TypeInfo :: enum { `enum: EnumInfo; } -// reify(info) — mint a NEW nominal type from a `TypeInfo` (comptime-only). -// type_info($T) — reflect an existing type into a `TypeInfo`. -// field_type($T, i) — the i-th field/variant payload type of `$T`. -reify :: (info: TypeInfo) -> Type #builtin; +// 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 `define` is a +// loud error. References to it (`*Self`) are fine. +// define(handle, info) — fill a declared handle's body from a `TypeInfo`. +// `reify` and every other constructor below are PLAIN sx built over these — the +// compiler has no `reify` knowledge. +declare :: () -> Type #builtin; +define :: (handle: Type, info: TypeInfo) #builtin; type_info :: ($T: Type) -> TypeInfo #builtin; field_type :: ($T: Type, idx: i64) -> Type #builtin; +// reify(info) — the one-shot, non-recursive sugar: declare + define + return. +// (Recursive / mutually-recursive types use the explicit declare/define split +// so the handle can be referenced inside its own definition.) +reify :: (info: TypeInfo) -> Type { + h := declare(); + define(h, info); + return h; +} + // --- Reify'd shapes built in sx library code (no new compiler machinery) --- // // The channel result types, expressed as type-fns over `reify`. They are the diff --git a/src/ir/inst.zig b/src/ir/inst.zig index 7fd69056..52f9dd51 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -449,6 +449,17 @@ pub const BuiltinId = enum(u16) { type_eq, type_is_unsigned, has_impl, + // Comptime type CONSTRUCTION (REIFY floor). The compiler's ONLY + // type-minting primitives — `reify` / `make_enum` / `RecvResult` etc. + // are sx in `meta.sx`, built over these. 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, return + // it as a `Type` value. Using the slot before + // `define` is a loud diagnostic (F5). + // define(handle, info) → decode the `TypeInfo` VALUE + complete the slot. + declare, + define, }; pub const CompilerCall = struct { diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 0d18c3ac..3debb15b 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -183,6 +183,17 @@ pub const Interpreter = struct { /// resolver degrades to line/col 1:1. source_map: ?*const std.StringHashMap([:0]const u8) = null, + /// Comptime type-MINT target — the SAME `TypeTable` the host (`Lowering`) + /// owns (aliases `self.module.types`; the const view here and the host's + /// mutable view point at one table). Set by the host before a comptime-eval + /// that may run `declare`/`define` (the REIFY floor). 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 the + /// binding site renames them to the real (alias / mangled) name. + declare_counter: u32 = 0, + // Heap: dynamically allocated memory blocks heap: std.ArrayList([]u8), @@ -244,6 +255,13 @@ pub const Interpreter = struct { self.source_map = sm; } + /// Enable the comptime type-construction builtins (`declare`/`define`) by + /// handing the interp the host's mutable `TypeTable`. Called by `Lowering` + /// before a comptime-eval that may mint types (the REIFY floor). + pub fn setMintTable(self: *Interpreter, tbl: *types.TypeTable) void { + self.mint = tbl; + } + pub fn deinit(self: *Interpreter) void { // Free all heap allocations for (self.heap.items) |block| { @@ -1957,11 +1975,115 @@ pub const Interpreter = struct { // `tryConstBoolCondition` in lower.zig. return bailDetail("comptime has_impl: interp-time evaluation not yet wired (use static type args for now — they fold at lower time)"); }, + + // ── Comptime type CONSTRUCTION (REIFY floor) ───────── + .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; + const name_id = tbl.internString(nm); + const info: types.TypeInfo = .{ .tagged_union = .{ + .name = name_id, + .fields = &.{}, + .tag_type = .i64, + } }; + const tid = tbl.internNominal(info, 0); + return .{ .value = .{ .type_tag = tid } }; + }, + .define => { + const tbl = self.mint orelse + return bailDetail("comptime define(): no type-mint target (declare/define are comptime-only — reached at runtime/emit?)"); + if (bi.args.len != 2) return bailDetail("comptime define(handle, info): needs exactly two arguments"); + 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); + }, } } + /// 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 }`. + /// Decodes those into a `tagged_union` byte-identical to a source enum's + /// `buildEnumInfo` output (default `i64` tag, no backing) and fills the slot + /// via `updatePreservingKey` (the handle's name + nominal id are unchanged). + /// Every decode failure is a loud bail — never a silent default. + fn defineEnum(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult { + // Unwrap TypeInfo `.enum(EnumInfo)` → EnumInfo `{ variants }`. + 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(): only the `.enum(...)` TypeInfo variant is supported"); + const einfo = ti_fields[1]; + const einfo_fields = switch (einfo) { + .aggregate => |f| f, + else => return bailDetail("comptime define(): `.enum` payload is not an EnumInfo struct value"), + }; + if (einfo_fields.len < 1) return bailDetail("comptime define(): EnumInfo is missing its `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"); + + var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + for (elems) |elem| { + const ev = switch (elem) { + .aggregate => |f| f, + else => return bailDetail("comptime define(): EnumVariant did not evaluate to a struct value"), + }; + if (ev.len != 2) return bailDetail("comptime define(): EnumVariant must have `name` and `payload`"); + const name = ev[0].asString(self) orelse return bailDetail("comptime define(): EnumVariant `name` is not a string"); + const payload_tid = ev[1].asTypeId() orelse return bailDetail("comptime define(): EnumVariant `payload` is not a Type value"); + fields.append(self.alloc, .{ .name = tbl.internString(name), .ty = payload_tid }) catch return error.CannotEvalComptime; + } + + // Preserve the declared slot's intern key (name + nominal id); fill body. + 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 = 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.updatePreservingKey(handle, full); + return .{ .value = .void_val }; + } }; +/// Normalize an interpreter value into the list of EnumVariant element values. +/// A `[]EnumVariant` slice evaluates to a `{ data, len }` aggregate (`len` an +/// int); a `[N]EnumVariant` array literal evaluates to the element aggregate +/// directly. Returns null for any other shape (the caller bails loudly). +fn decodeVariantElements(result: Value) ?[]const Value { + const fields = switch (result) { + .aggregate => |f| f, + else => return null, + }; + // Slice fat pointer `{ data, len }`: a 2-field aggregate whose 2nd field is + // an integer length. (A 2-VARIANT array can't collide — its 2nd field is an + // EnumVariant aggregate, so `asInt` is null.) + if (fields.len == 2) { + if (fields[1].asInt()) |len_i| { + const len: usize = @intCast(len_i); + switch (fields[0]) { + .aggregate => |arr| return if (len <= arr.len) arr[0..len] else null, + else => return null, + } + } + } + return fields; +} + // ── Frame ─────────────────────────────────────────────────────────────── // Holds SSA values (by Ref index) and local mutable slots (for alloca). diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2f8fe2a3..07c734c1 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1566,6 +1566,8 @@ pub const Lowering = struct { pub const evalComptimeMatch = lower_comptime.evalComptimeMatch; pub const evalComptimeInt = lower_comptime.evalComptimeInt; pub const evalComptimeString = lower_comptime.evalComptimeString; + pub const evalComptimeTypeNamed = lower_comptime.evalComptimeTypeNamed; + pub const renameReifiedType = lower_comptime.renameReifiedType; pub const lowerComptimeGlobal = lower_comptime.lowerComptimeGlobal; pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect; pub const lowerComptimeCall = lower_comptime.lowerComptimeCall; diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index a2c64ad0..9f0eddce 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -1676,6 +1676,29 @@ 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 (REIFY floor): mint an empty + // nominal slot. Comptime-only — emitted as a builtin_call the interp + // executes against its `mint` table; never reaches codegen (reify and + // friends, which call it, are only ever comptime-evaluated). + if (c.args.len != 0) { + if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "declare() takes no arguments", .{}); + return Ref.none; + } + return self.builder.callBuiltin(.declare, &.{}, .any); + } + if (std.mem.eql(u8, name, "define")) { + // Comptime type-construction primitive (REIFY floor): complete a + // declare()'d slot from a TypeInfo value. `define(handle, info)`. + if (c.args.len != 2) { + if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "define(handle, info) takes exactly two arguments", .{}); + return Ref.none; + } + const handle_ref = self.lowerExpr(c.args[0]); + const info_ref = self.lowerExpr(c.args[1]); + const args_owned = self.alloc.dupe(Ref, &.{ handle_ref, info_ref }) catch return Ref.none; + return self.builder.callBuiltin(.define, args_owned, .void); + } if (std.mem.eql(u8, name, "type_info")) { // Comptime reflection-into-data (REIFY). Until the interpreter-side // reflection lands (Phase 2), bail loudly rather than fall through to diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index 529e01c6..d572ef24 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -384,6 +384,49 @@ pub fn lowerInsertExprValue(self: *Lowering, expr: *const Node) Ref { return last_val; } +/// Evaluate a Type-returning expression at compile time → its `TypeId`. +/// The driver of the REIFY floor: `expr` (e.g. `reify(.enum(...))`, a type-fn +/// call) is wrapped in a throwaway comptime fn and run through the interpreter +/// with the type-MINT table enabled, so `declare`/`define` builtins reached +/// inside it mutate the real type table. The result value is a `.type_tag`. +/// When `name` is given, the minted (anonymous) type is renamed to it so +/// `type_name` / diagnostics read the binding's name. Returns null (caller +/// poisons) if evaluation didn't yield a Type. +pub fn evalComptimeTypeNamed(self: *Lowering, expr: *const Node, name: ?[]const u8) ?TypeId { + const func_id = self.createComptimeFunction("__ctype", expr, .any); + + var interp = interp_mod.Interpreter.init(self.module, self.alloc); + defer interp.deinit(); + if (self.diagnostics) |d| if (d.import_sources) |sm| interp.setSourceMap(sm); + interp.setMintTable(&self.module.types); + + const result = interp.call(func_id, &.{}) catch return null; + const tid = result.asTypeId() orelse return null; + if (name) |nm| self.renameReifiedType(tid, nm); + return tid; +} + +/// Rename a freshly-minted (anonymous `__reified_N`) nominal type to its +/// binding's name, re-keying `intern_map` so `findByName(name)` resolves it. +/// A no-op for a non-nominal / already-named-as-requested type. +pub fn renameReifiedType(self: *Lowering, tid: TypeId, name: []const u8) void { + const tbl = &self.module.types; + const new_name_id = tbl.internString(name); + var info = tbl.get(tid); + switch (info) { + .tagged_union => |*u| { + if (u.name == new_name_id) return; + u.name = new_name_id; + }, + .@"enum" => |*e| { + if (e.name == new_name_id) return; + e.name = new_name_id; + }, + else => return, + } + tbl.replaceKeyedInfo(tid, info); +} + /// Evaluate an expression at compile time, returning its string value. /// Returns null if evaluation fails. pub fn evalComptimeString(self: *Lowering, expr: *const Node) ?[:0]const u8 { diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index b91b7543..3bd96fa0 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -43,6 +43,14 @@ const isPackFn = Lowering.isPackFn; /// either DCE away or stay hidden from the dynamic symbol table. /// Anything starting with `Java_` is a JNI native method that Android's /// runtime resolves by name mangling — same rule. +/// True when `fd` declares a `-> Type` return — the signal that a non-generic +/// call to it (`E :: f(...)`) should be comptime-evaluated to mint a type (the +/// REIFY floor). Matches a bare `Type` type-expr return only. +fn fnReturnsTypeValue(fd: *const ast.FnDecl) bool { + const rt = fd.return_type orelse return false; + return rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Type"); +} + fn isExportedEntryName(name: []const u8) bool { return std.mem.eql(u8, name, "main") or std.mem.eql(u8, name, "JNI_OnLoad") or @@ -651,16 +659,20 @@ pub fn scanDecls(self: *Lowering, decls: []const *const Node) void { .field_access => |fa| fa.field, else => "", }; - // `E :: reify(...)` — mint a NEW nominal type from a - // `TypeInfo` literal and register `E` as an alias to it. - // `reifyType` builds the type (or diagnoses + returns null); - // either way `E` is bound (to the minted type, or poisoned - // to `.unresolved` so downstream `E.value` gets a clean - // follow-on rather than a silent default type). - if (std.mem.eql(u8, callee_name, "reify")) { - const tid = self.reifyType(cd.name, call_data) orelse TypeId.unresolved; - self.putTypeAlias(self.current_source_file, cd.name, tid); - continue; + // `E :: f(...)` where `f` is a NON-generic fn returning + // `Type` (e.g. the sx `reify` / `make_enum`): comptime- + // evaluate the call — `declare`/`define` reached inside it + // mint the type — and bind `E` as an alias to the result. + // The compiler has ZERO `reify` knowledge: any Type-returning + // value-fn flows here. Generic type-fns (`$T`) are minted by + // `instantiateTypeFunction` below. Poison on failure so + // `E.x` gets a clean follow-on, never a silent default. + if (self.program_index.fn_ast_map.get(callee_name)) |fd| { + if (fd.type_params.len == 0 and fnReturnsTypeValue(fd)) { + const tid = self.evalComptimeTypeNamed(cd.value, cd.name) orelse TypeId.unresolved; + self.putTypeAlias(self.current_source_file, cd.name, tid); + continue; + } } // A namespaced callee (`ns.Box(..)`) is an explicit qualified // reach, exempt from the bare-head visibility gate (E4).