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

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