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

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