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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user