diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 821686b9..9e16f191 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1567,6 +1567,8 @@ pub const Lowering = struct { pub const evalComptimeInt = lower_comptime.evalComptimeInt; pub const evalComptimeString = lower_comptime.evalComptimeString; 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 lowerComptimeGlobal = lower_comptime.lowerComptimeGlobal; pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect; @@ -1578,6 +1580,7 @@ pub const Lowering = struct { pub const substituteComptimeNodes = lower_comptime.substituteComptimeNodes; pub const fnBodyHasReturn = lower_comptime.fnBodyHasReturn; pub const createComptimeFunction = lower_comptime.createComptimeFunction; + pub const createComptimeFunctionWithPrelude = lower_comptime.createComptimeFunctionWithPrelude; pub const constExprValue = lower_comptime.constExprValue; pub const constArrayLiteral = lower_comptime.constArrayLiteral; pub const constStructLiteral = lower_comptime.constStructLiteral; diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index 48d350ad..ca52b79b 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -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. preregisterForwardTypes(self, expr); 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); defer interp.deinit(); 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. if (self.diagnostics) |d| { 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; }; @@ -483,7 +515,7 @@ pub fn evalComptimeType(self: *Lowering, expr: *const Node) ?TypeId { const info = self.module.types.get(tid); if (info == .tagged_union and info.tagged_union.fields.len == 0) { 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; } } @@ -799,6 +831,15 @@ pub fn fnBodyHasReturn(node: *const Node) bool { /// Creates a temporary function marked `is_comptime = true` that wraps /// 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 { + 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; const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix; 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); 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 const result = self.lowerExpr(expr); if (ret_ty == .void) { diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index d7644bc8..a71f8d49 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -1759,7 +1759,21 @@ pub fn instantiateTypeFunction(self: *Lowering, alias_name: []const u8, template // `resolveTypeWithBindings` can't evaluate a Type-returning call. if (findReturnTypeExpr(fd.body)) |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 // cache check at the top dedups a second instantiation — Contract 1. self.renameNominalType(tid, if (has_alias) alias_name else mangled_name);