green(reify): declare/define floor — reify is sx; E :: reify(...) comptime-evaluated

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.
This commit is contained in:
agra
2026-06-16 20:39:02 +03:00
parent ae27cffe9d
commit 442a70b8c9
7 changed files with 241 additions and 14 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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).

View File

@@ -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;

View File

@@ -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

View File

@@ -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 {

View File

@@ -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).