feat(metatype): comptime-eval generic type-fn body locals

A generic ($T) -> Type type-fn comptime-evaluated only its return
EXPRESSION, so a local declared before the return ('vs := …; return
make_enum(…, vs)') was unresolved. Now a body with a prelude (statements
before the return) has its full body evaluated: createComptimeFunction-
WithPrelude lowers the pre-return statements into the comptime function's
scope before the return expr, so the locals resolve.

- comptime.zig: createComptimeFunctionWithPrelude (prelude stmts +
  expr); evalComptimeTypeBody (extract prelude + return expr, scan the
  whole body for declare() forward types); runComptimeTypeFunc factored
  out of evalComptimeType (shared bail/declare-never-defined handling).
- generic.zig: route a type-fn body WITH a prelude through
  evalComptimeTypeBody; no-prelude bodies stay on evalComptimeType (zero
  change for RecvResult/TryResult etc.).

Non-generic builders (whole body already evaluated) and the List-growth
path are unaffected. Suite green (684).
This commit is contained in:
agra
2026-06-17 07:40:09 +03:00
parent 60293bf5dd
commit d87d86df8a
3 changed files with 66 additions and 3 deletions

View File

@@ -1567,6 +1567,8 @@ pub const Lowering = struct {
pub const evalComptimeInt = lower_comptime.evalComptimeInt; pub const evalComptimeInt = lower_comptime.evalComptimeInt;
pub const evalComptimeString = lower_comptime.evalComptimeString; pub const evalComptimeString = lower_comptime.evalComptimeString;
pub const evalComptimeType = lower_comptime.evalComptimeType; pub const evalComptimeType = lower_comptime.evalComptimeType;
pub const evalComptimeTypeBody = lower_comptime.evalComptimeTypeBody;
pub const runComptimeTypeFunc = lower_comptime.runComptimeTypeFunc;
pub const renameNominalType = lower_comptime.renameNominalType; pub const renameNominalType = lower_comptime.renameNominalType;
pub const lowerComptimeGlobal = lower_comptime.lowerComptimeGlobal; pub const lowerComptimeGlobal = lower_comptime.lowerComptimeGlobal;
pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect; pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect;
@@ -1578,6 +1580,7 @@ pub const Lowering = struct {
pub const substituteComptimeNodes = lower_comptime.substituteComptimeNodes; pub const substituteComptimeNodes = lower_comptime.substituteComptimeNodes;
pub const fnBodyHasReturn = lower_comptime.fnBodyHasReturn; pub const fnBodyHasReturn = lower_comptime.fnBodyHasReturn;
pub const createComptimeFunction = lower_comptime.createComptimeFunction; pub const createComptimeFunction = lower_comptime.createComptimeFunction;
pub const createComptimeFunctionWithPrelude = lower_comptime.createComptimeFunctionWithPrelude;
pub const constExprValue = lower_comptime.constExprValue; pub const constExprValue = lower_comptime.constExprValue;
pub const constArrayLiteral = lower_comptime.constArrayLiteral; pub const constArrayLiteral = lower_comptime.constArrayLiteral;
pub const constStructLiteral = lower_comptime.constStructLiteral; pub const constStructLiteral = lower_comptime.constStructLiteral;

View File

@@ -448,7 +448,39 @@ pub fn evalComptimeType(self: *Lowering, expr: *const Node) ?TypeId {
// body. The interp's `declare` returns this same slot; `define` completes it. // body. The interp's `declare` returns this same slot; `define` completes it.
preregisterForwardTypes(self, expr); preregisterForwardTypes(self, expr);
const func_id = self.createComptimeFunction("__ctype", expr, .any); const func_id = self.createComptimeFunction("__ctype", expr, .any);
return self.runComptimeTypeFunc(func_id, expr.span);
}
/// Comptime-evaluate a type-fn BODY that has local statements before its
/// `return` (the plain `evalComptimeType` only sees the return expression, so a
/// local declared before it is unresolved). Lowers the pre-return statements as
/// a prelude, then the return expression. Only used for bodies that actually
/// have a prelude — the no-prelude case stays on `evalComptimeType`.
pub fn evalComptimeTypeBody(self: *Lowering, body: *const Node, ret_expr: *const Node) ?TypeId {
// Scan the WHOLE body for `declare("Name")` (a local `h := declare(…)` is in
// the prelude, not the return) so forward types register before lowering.
preregisterForwardTypes(self, body);
const prelude = preludeBeforeReturn(body);
const func_id = self.createComptimeFunctionWithPrelude("__ctype", prelude, ret_expr, .any);
return self.runComptimeTypeFunc(func_id, ret_expr.span);
}
/// The statements of a block body that PRECEDE its first `return` — the locals a
/// type-fn binds before minting. Empty for a non-block (arrow) body.
fn preludeBeforeReturn(body: *const Node) []const *const Node {
if (body.data != .block) return &.{};
const stmts = body.data.block.stmts;
for (stmts, 0..) |stmt, i| {
if (stmt.data == .return_stmt) return stmts[0..i];
}
return &.{};
}
/// Run a comptime type-construction function and post-process its result: render
/// any interp bail as a build-gating diagnostic (issue 0140) and reject a bare
/// `declare()` never completed by `define()` (a zero-field nominal slot that
/// would otherwise panic at codegen). `span` locates both diagnostics.
pub fn runComptimeTypeFunc(self: *Lowering, func_id: FuncId, span: ast.Span) ?TypeId {
var interp = interp_mod.Interpreter.init(self.module, self.alloc); var interp = interp_mod.Interpreter.init(self.module, self.alloc);
defer interp.deinit(); defer interp.deinit();
if (self.diagnostics) |d| if (d.import_sources) |sm| interp.setSourceMap(sm); if (self.diagnostics) |d| if (d.import_sources) |sm| interp.setSourceMap(sm);
@@ -469,7 +501,7 @@ pub fn evalComptimeType(self: *Lowering, expr: *const Node) ?TypeId {
// unannounced. // unannounced.
if (self.diagnostics) |d| { if (self.diagnostics) |d| {
const detail = interp_mod.Interpreter.last_bail_detail orelse @errorName(err); const detail = interp_mod.Interpreter.last_bail_detail orelse @errorName(err);
d.addFmt(.err, expr.span, "comptime type construction failed: {s}", .{detail}); d.addFmt(.err, span, "comptime type construction failed: {s}", .{detail});
} }
return null; return null;
}; };
@@ -483,7 +515,7 @@ pub fn evalComptimeType(self: *Lowering, expr: *const Node) ?TypeId {
const info = self.module.types.get(tid); const info = self.module.types.get(tid);
if (info == .tagged_union and info.tagged_union.fields.len == 0) { if (info == .tagged_union and info.tagged_union.fields.len == 0) {
if (self.diagnostics) |d| if (self.diagnostics) |d|
d.addFmt(.err, expr.span, "type '{s}' is declared but never defined — complete it with define(handle, info)", .{self.module.types.getString(info.tagged_union.name)}); d.addFmt(.err, span, "type '{s}' is declared but never defined — complete it with define(handle, info)", .{self.module.types.getString(info.tagged_union.name)});
return null; return null;
} }
} }
@@ -799,6 +831,15 @@ pub fn fnBodyHasReturn(node: *const Node) bool {
/// Creates a temporary function marked `is_comptime = true` that wraps /// Creates a temporary function marked `is_comptime = true` that wraps
/// the given expression as its return value. Returns the FuncId. /// the given expression as its return value. Returns the FuncId.
pub fn createComptimeFunction(self: *Lowering, prefix: []const u8, expr: *const Node, ret_ty: TypeId) FuncId { pub fn createComptimeFunction(self: *Lowering, prefix: []const u8, expr: *const Node, ret_ty: TypeId) FuncId {
return self.createComptimeFunctionWithPrelude(prefix, &.{}, expr, ret_ty);
}
/// Like `createComptimeFunction`, but lowers `prelude` statements (e.g. a
/// type-fn body's local declarations) into the comptime function's scope BEFORE
/// the result `expr`, so the expr can reference names they bind. Used to
/// comptime-evaluate a generic type-fn body that has locals before its `return`
/// (the non-prelude path only sees the return expression).
pub fn createComptimeFunctionWithPrelude(self: *Lowering, prefix: []const u8, prelude: []const *const Node, expr: *const Node, ret_ty: TypeId) FuncId {
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;
const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix; const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix;
self.comptime_counter += 1; self.comptime_counter += 1;
@@ -883,6 +924,11 @@ pub fn createComptimeFunction(self: *Lowering, prefix: []const u8, expr: *const
var ct_scope = Scope.init(self.alloc, saved_scope); var ct_scope = Scope.init(self.alloc, saved_scope);
self.scope = &ct_scope; self.scope = &ct_scope;
// Lower any prelude statements (type-fn body locals) so the result
// expression can reference the names they bind. Empty for the common
// single-expression case.
for (prelude) |stmt| self.lowerStmt(stmt);
// Lower the expression and return it // Lower the expression and return it
const result = self.lowerExpr(expr); const result = self.lowerExpr(expr);
if (ret_ty == .void) { if (ret_ty == .void) {

View File

@@ -1759,7 +1759,21 @@ pub fn instantiateTypeFunction(self: *Lowering, alias_name: []const u8, template
// `resolveTypeWithBindings` can't evaluate a Type-returning call. // `resolveTypeWithBindings` can't evaluate a Type-returning call.
if (findReturnTypeExpr(fd.body)) |ret_node| { if (findReturnTypeExpr(fd.body)) |ret_node| {
if (self.returnExprMintsType(ret_node)) { if (self.returnExprMintsType(ret_node)) {
const tid = self.evalComptimeType(ret_node) orelse return .unresolved; // A body with LOCALS before its `return` (e.g. `vs := …; return
// make_enum(…, vs)`) needs its full body comptime-evaluated so those
// locals resolve; the bare return-expr path leaves them unresolved.
// A no-prelude body stays on the simpler `evalComptimeType` path.
const has_prelude = fd.body.data == .block and blk: {
for (fd.body.data.block.stmts) |stmt| {
if (stmt.data == .return_stmt) break :blk false;
break :blk true; // a non-return statement precedes the return
}
break :blk false;
};
const tid = (if (has_prelude)
self.evalComptimeTypeBody(fd.body, ret_node)
else
self.evalComptimeType(ret_node)) orelse return .unresolved;
// Re-key to the instantiation's mangled (or alias) name so the // Re-key to the instantiation's mangled (or alias) name so the
// cache check at the top dedups a second instantiation — Contract 1. // cache check at the top dedups a second instantiation — Contract 1.
self.renameNominalType(tid, if (has_alias) alias_name else mangled_name); self.renameNominalType(tid, if (has_alias) alias_name else mangled_name);