From 856299ce3662cc0e07a5682be5dce3c0466941ce Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 10 Jun 2026 13:11:33 +0300 Subject: [PATCH] refactor(B2.1): move comptime hooks + const folding to lower/comptime.zig Verbatim relocation of the 26-method comptime cluster (comptime eval hooks, #insert, comptime calls/deps/substitution, source-const folding and module-const selection) plus the three nested const-selection types (SelectedConst, ConstAuthor, ConstSourcePin) into src/ir/lower/ comptime.zig. 26 fn aliases + SelectedConst type alias on Lowering keep all call sites unchanged. Shared file-scope helpers stay in lower.zig per the helpers-stay-home rule, now pub: ConstFoldFrame, constFoldFrameContains, SourceConstCtx. Method pub-flips: findVariantIndex, putGlobal, tryLowerAsExpr, lowerVariadicArgs, resolver, setCurrentSourceFile, diagNonIntegralNarrow, lowerStmt, stampCallerSource, resolveParamType, resolveReturnType. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn. --- src/ir/lower.zig | 982 ++------------------------------------ src/ir/lower/comptime.zig | 977 +++++++++++++++++++++++++++++++++++++ 2 files changed, 1021 insertions(+), 938 deletions(-) create mode 100644 src/ir/lower/comptime.zig diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 1dee575..ff23364 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -34,6 +34,7 @@ const ErrorFlow = @import("error_flow.zig").ErrorFlow; const ObjcLowering = @import("ffi_objc.zig").ObjcLowering; const semantic_diagnostics = @import("semantic_diagnostics.zig"); const lower_error = @import("lower/error.zig"); +const lower_comptime = @import("lower/comptime.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -64,13 +65,13 @@ fn isExportedEntryName(name: []const u8) bool { /// across modules (`a.M` ≠ `b.M`) must NOT trip a false cycle (F3). A pair /// already on the chain is a cyclic definition (`N :: N`; `N :: M + 1; M :: N`) /// with no compile-time value → folds to null. -const ConstFoldFrame = struct { +pub const ConstFoldFrame = struct { name: []const u8, source: ?[]const u8, parent: ?*const ConstFoldFrame, }; -fn constFoldFrameContains(frame: ?*const ConstFoldFrame, name: []const u8, source: ?[]const u8) bool { +pub fn constFoldFrameContains(frame: ?*const ConstFoldFrame, name: []const u8, source: ?[]const u8) bool { var cur = frame; while (cur) |c| : (cur = c.parent) { if (std.mem.eql(u8, c.name, name) and sourcesEql(c.source, source)) return true; @@ -92,7 +93,7 @@ fn sourcesEql(a: ?[]const u8, b: ?[]const u8) bool { /// (`K :: M + 1`, with `M` a same-name shadow too) fold `M` to the SELECTED /// author's `M` — coherently for a const used as a value AND as an array /// dimension / count. `frame` is the cyclic-definition guard. -const SourceConstCtx = struct { +pub const SourceConstCtx = struct { lowering: *Lowering, frame: ?*const ConstFoldFrame, pub fn lookupDimName(self: SourceConstCtx, name: []const u8) ?i64 { @@ -723,7 +724,7 @@ pub const Lowering = struct { self.comptime_constants.put("POINTER_SIZE", .{ .int_val = ptr_size }) catch {}; } - fn findVariantIndex(self: *Lowering, variants: []const types.StringId, name: []const u8) u32 { + pub fn findVariantIndex(self: *Lowering, variants: []const types.StringId, name: []const u8) u32 { const name_id = self.module.types.internString(name); for (variants, 0..) |v, i| { if (v == name_id) return @intCast(i); @@ -876,7 +877,7 @@ pub const Lowering = struct { self.program_index.module_const_map.put(name, info) catch {}; if (source orelse self.main_file) |src| self.program_index.putModuleConstBySource(src, name, info); } - fn putGlobal(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.GlobalInfo) void { + pub fn putGlobal(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.GlobalInfo) void { self.program_index.global_names.put(name, info) catch {}; if (source orelse self.main_file) |src| self.program_index.putGlobalBySource(src, name, info); } @@ -1562,136 +1563,6 @@ pub const Lowering = struct { return false; } - /// Try to convert an array literal's elements into a compile-time - /// ConstantValue.aggregate. `array_ty` is the array's resolved TypeId; its - /// element type drives type-aware serialization of struct-literal and - /// nested-array elements. Returns null if `array_ty` is not an array type or - /// any element is not a compile-time constant. - fn constArrayLiteral(self: *Lowering, elements: []const *const Node, array_ty: TypeId) ?inst_mod.ConstantValue { - if (array_ty.isBuiltin()) return null; - const elem_ty: TypeId = switch (self.module.types.get(array_ty)) { - .array => |a| a.element, - else => return null, - }; - const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null; - for (elements, 0..) |elem, i| { - vals[i] = self.constExprValue(elem, elem_ty) orelse return null; - } - return .{ .aggregate = vals }; - } - - /// Try to convert a single AST expression into a compile-time ConstantValue. - /// `expected_ty` is the destination element/field type — it lets aggregate - /// leaves (struct literals, nested arrays) serialize with the correct shape - /// rather than collapsing to null (issue 0080). Returns null if the - /// expression is not constant-foldable here. - fn constExprValue(self: *Lowering, expr: *const Node, expected_ty: TypeId) ?inst_mod.ConstantValue { - return switch (expr.data) { - .int_literal => |il| .{ .int = il.value }, - .bool_literal => |bl| .{ .boolean = bl.value }, - // A float into an INTEGER destination follows the implicit - // narrowing rule: an integral float folds to its int, a - // non-integral one is a compile error (not a silent bit-coerce). - .float_literal => |fl| blk: { - if (self.isIntEx(expected_ty)) { - if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv }; - self.diagNonIntegralNarrow(expr.span, fl.value, expected_ty); - break :blk null; - } - break :blk inst_mod.ConstantValue{ .float = fl.value }; - }, - .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, - .undef_literal => .zeroinit, - // A `null` in a pointer (or optional-pointer) field is a - // compile-time constant: the zero pointer. Without this arm the - // aggregate is wrongly rejected as non-constant (issue 0081). - .null_literal => .null_val, - .unary_op => |uo| switch (uo.op) { - .negate => switch (uo.operand.data) { - .int_literal => |il| .{ .int = -il.value }, - .float_literal => |fl| .{ .float = -fl.value }, - else => null, - }, - else => null, - }, - .array_literal => |al| self.constArrayLiteral(al.elements, expected_ty), - .struct_literal => |sl| self.constStructLiteral(&sl, expected_ty), - // An enum tag as an aggregate leaf (`[2]Color = .[.green, .blue]`, or - // an enum field inside a global struct) serializes to its tag int - // against the leaf's declared enum type (issue 0082). - .enum_literal => |el| self.constEnumLiteral(&el, expected_ty, expr.span), - else => null, - }; - } - - /// Serialize an enum-literal initializer (`.Variant`) into a static - /// `ConstantValue.int` holding the variant's tag value, resolved against the - /// destination enum type `ty`. The tag respects explicit variant values - /// (`enum { a; b :: 5; }`); the enum's backing width is applied by the - /// const emitters via the destination type's LLVM type. Plain enums only — - /// a tagged-union or non-enum destination is diagnosed loudly rather than - /// silently zero-initialized (issue 0082). - fn constEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral, ty: TypeId, span: ast.Span) ?inst_mod.ConstantValue { - if (!ty.isBuiltin()) { - const info = self.module.types.get(ty); - if (info == .@"enum") { - const e = info.@"enum"; - const name_id = self.module.types.internString(el.name); - for (e.variants, 0..) |variant, i| { - if (variant != name_id) continue; - if (e.explicit_values) |vals| { - if (i < vals.len) return .{ .int = vals[i] }; - } - return .{ .int = @intCast(i) }; - } - if (self.diagnostics) |d| - d.addFmt(.err, span, "'.{s}' is not a variant of enum '{s}'", .{ el.name, self.module.types.getString(e.name) }); - return null; - } - } - if (self.diagnostics) |d| - d.addFmt(.err, span, "enum-literal global initializer '.{s}' is only supported for a plain enum destination type", .{el.name}); - return null; - } - - /// Try to convert a struct literal into a compile-time ConstantValue.aggregate of the - /// struct's fields in declaration order, filling missing fields from the struct's - /// field defaults. Returns null if any value is not constant-foldable. - fn constStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, ty: TypeId) ?inst_mod.ConstantValue { - if (ty.isBuiltin()) return null; - const ti = self.module.types.get(ty); - if (ti != .@"struct") return null; - const struct_fields = ti.@"struct".fields; - const struct_name = self.module.types.getString(ti.@"struct".name); - const field_defaults: []const ?*const Node = self.struct_defaults_map.get(struct_name) orelse &.{}; - - const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; - - const vals = self.alloc.alloc(inst_mod.ConstantValue, struct_fields.len) catch return null; - for (struct_fields, 0..) |sf, fi| { - const sf_name = self.module.types.getString(sf.name); - const init_expr: ?*const Node = blk: { - if (has_names) { - for (sl.field_inits) |init_pair| { - if (init_pair.name) |n| { - if (std.mem.eql(u8, n, sf_name)) break :blk init_pair.value; - } - } - } else if (fi < sl.field_inits.len) { - break :blk sl.field_inits[fi].value; - } - if (fi < field_defaults.len) break :blk field_defaults[fi]; - break :blk null; - }; - if (init_expr) |e| { - vals[fi] = self.constExprValue(e, sf.ty) orelse return null; - } else { - vals[fi] = .zeroinit; - } - } - return .{ .aggregate = vals }; - } - /// Pass 2: Lower main function body and comptime side-effects. fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void { for (decls) |decl| { @@ -3027,7 +2898,7 @@ pub const Lowering = struct { /// Try to lower a node as an expression, returning its value. /// Statement nodes are lowered as statements (returning null). - fn tryLowerAsExpr(self: *Lowering, node: *const Node) ?Ref { + pub fn tryLowerAsExpr(self: *Lowering, node: *const Node) ?Ref { return switch (node.data) { .var_decl, .const_decl, .fn_decl, .return_stmt, .raise_stmt, .assignment, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl => { self.lowerStmt(node); @@ -3037,7 +2908,7 @@ pub const Lowering = struct { }; } - fn lowerStmt(self: *Lowering, node: *const Node) void { + pub fn lowerStmt(self: *Lowering, node: *const Node) void { // Stamp this statement's span onto its instructions (ERR E3.0); see // `lowerExpr`. const saved_span = self.builder.current_span; @@ -4998,92 +4869,6 @@ pub const Lowering = struct { } } - /// Evaluate a compile-time condition for `inline if`. - /// Handles: `ident == .variant`, `ident != .variant`, `ident == int`, `ident != int`. - fn evalComptimeCondition(self: *Lowering, node: *const Node) ?bool { - if (node.data != .binary_op) return null; - const bo = &node.data.binary_op; - if (bo.op != .eq and bo.op != .neq) return null; - - // LHS must be an identifier that's in comptime_constants - const name = switch (bo.lhs.data) { - .identifier => |id| id.name, - else => return null, - }; - const cv = self.comptime_constants.get(name) orelse return null; - - switch (cv) { - .enum_tag => |et| { - // RHS must be an enum literal (.variant) - const variant_name = switch (bo.rhs.data) { - .enum_literal => |el| el.name, - else => return null, - }; - // Look up variant index in the enum type - const enum_info = self.module.types.get(et.ty); - if (enum_info != .@"enum") return null; - const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name); - const result = et.tag == variant_idx; - return if (bo.op == .eq) result else !result; - }, - .int_val => |iv| { - // RHS must be an integer literal - const rhs_val: i64 = switch (bo.rhs.data) { - .int_literal => |il| il.value, - else => return null, - }; - const result = iv == rhs_val; - return if (bo.op == .eq) result else !result; - }, - } - } - - /// Evaluate a compile-time match expression for `inline if ... == { case ... }`. - /// Returns the body of the matching arm, or null if the match can't be resolved. - fn evalComptimeMatch(self: *Lowering, me: *const ast.MatchExpr) ?*const Node { - // Subject must be a comptime constant identifier - const name = switch (me.subject.data) { - .identifier => |id| id.name, - else => return null, - }; - const cv = self.comptime_constants.get(name) orelse return null; - - switch (cv) { - .enum_tag => |et| { - const enum_info = self.module.types.get(et.ty); - if (enum_info != .@"enum") return null; - for (me.arms) |arm| { - if (arm.pattern == null) continue; // default arm - const variant_name = switch (arm.pattern.?.data) { - .enum_literal => |el| el.name, - else => continue, - }; - const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name); - if (et.tag == variant_idx) return arm.body; - } - // No match — try default arm - for (me.arms) |arm| { - if (arm.pattern == null) return arm.body; - } - return null; - }, - .int_val => |iv| { - for (me.arms) |arm| { - if (arm.pattern == null) continue; - const rhs_val: i64 = switch (arm.pattern.?.data) { - .int_literal => |il| il.value, - else => continue, - }; - if (iv == rhs_val) return arm.body; - } - for (me.arms) |arm| { - if (arm.pattern == null) return arm.body; - } - return null; - }, - } - } - fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref { const header_bb = self.freshBlock("while.hdr"); const body_bb = self.freshBlock("while.body"); @@ -5371,21 +5156,6 @@ pub const Lowering = struct { return self.builder.constInt(0, .void); } - /// Evaluate an `inline for` range bound to a comptime integer. Delegates to - /// the shared `program_index.evalConstIntExpr` — the SAME integer folder the - /// array dimension / Vector lane / value-param count paths build on — so a - /// literal, a comptime constant (cursor), a module/generic const - /// (`inline for 0..M`), a `.len` leaf, a DIRECT integral float - /// (`0..-2.0` → -2), and any constant-foldable expression over those - /// (`inline for 0..(M + 1)`) all resolve identically. A range bound is an - /// ENDPOINT, not a count (specs.md §2), so it deliberately does NOT take the - /// `foldCountI64` float-const-leaf fallback the count sites add: it accepts a - /// direct integral float but leaves a float-const-leaf expression to the int - /// folder (negatives are valid here, unlike a count). - fn evalComptimeInt(self: *Lowering, node: *const Node) ?i64 { - return program_index_mod.evalConstIntExpr(node, self); - } - fn lowerMatch(self: *Lowering, me: *const ast.MatchExpr) Ref { // inline if match: evaluate at compile time, only lower the matching arm if (me.is_comptime) { @@ -10243,397 +10013,9 @@ pub const Lowering = struct { // ── Comptime lowering ──────────────────────────────────────────── - /// Lower a `#run expr` that appears as a top-level constant binding: - /// NAME :: #run expr; - /// Creates a comptime function wrapping the expression (for later - /// interpretation), plus a global constant to hold the result. - fn lowerComptimeGlobal(self: *Lowering, name: []const u8, expr: *const Node, type_ann: ?*const Node) void { - // When the user writes `NAME :: #run expr;` with no type annotation, - // infer the global's type from the comptime expression's return - // shape. `resolveType(null)` returns `.s64` for legacy reasons — - // good for primitive helpers, silently wrong for anything else. - const expr_ty = self.inferExprType(expr); - // A failable `#run` (bare, no `catch`/`or`): the comptime function - // returns the full failable tuple so the #run site can inspect the - // error slot, but the GLOBAL is typed as the success value. On a - // comptime error the global never materializes — emit halts with a - // diagnostic + trace (E5.2). A handled `#run … catch/or …` already - // strips the error channel, so it lands here as non-failable. - const is_failable = self.errorChannelOf(expr_ty) != null; - const func_ret: TypeId = if (is_failable) - expr_ty - else if (type_ann) |n| - self.resolveTypeWithBindings(n) - else - expr_ty; - const global_ty: TypeId = if (is_failable) self.failableSuccessType(expr_ty) else func_ret; - const func_id = self.createComptimeFunction(name, expr, func_ret); - - // Add a global constant whose initializer will be filled by the interpreter. - const name_id = self.module.types.internString(name); - const gid = self.module.addGlobal(.{ - .name = name_id, - .ty = global_ty, - .init_val = null, // will be filled by interpreter at emit time - .is_const = true, - .comptime_func = func_id, - }); - - // Register for runtime lookup: identifier resolution emits global_get - self.putGlobal(self.current_source_file, name, .{ .id = gid, .ty = global_ty }); - } - - /// Lower a standalone `#run expr;` at the top level (side-effect only). - /// Creates a comptime function that the interpreter should execute. - fn lowerComptimeSideEffect(self: *Lowering, expr: *const Node) void { - // A failable side-effect `#run f();` returns the failable tuple so the - // emit-time runner can detect an escaping error and halt (E5.2); - // non-failable side effects stay `void`. - const expr_ty = self.inferExprType(expr); - const ret: TypeId = if (self.errorChannelOf(expr_ty) != null) expr_ty else .void; - _ = self.createComptimeFunction("__run", expr, ret); - } - - /// Lower a `#run expr` that appears inline within an expression. - /// Creates a comptime function and emits a `call` to it, so the - /// interpreter can evaluate it and replace with the constant result. - fn lowerInlineComptime(self: *Lowering, expr: *const Node) Ref { - const ret_ty: TypeId = self.target_type orelse self.inferExprType(expr); - const func_id = self.createComptimeFunction("__ct", expr, ret_ty); - // Emit a call to the comptime function. At interpretation time, - // this will be evaluated and the result inlined as a constant. - const func = &self.module.functions.items[@intFromEnum(func_id)]; - const final_args: []const Ref = if (func.has_implicit_ctx) - self.alloc.dupe(Ref, &.{self.current_ctx_ref}) catch &.{} - else - &.{}; - return self.builder.call(func_id, final_args, ret_ty); - } - - /// Lower a `#insert expr` statement. Evaluates `expr` at compile time to get - /// a string, parses it as sx code, and lowers each statement inline. - fn lowerInsertExpr(self: *Lowering, expr: *const Node) void { - _ = self.lowerInsertExprValue(expr); - } - - /// Like lowerInsertExpr but returns the value of the last parsed expression. - fn lowerInsertExprValue(self: *Lowering, expr: *const Node) Ref { - // Step 1: Substitute comptime param nodes (e.g., replace $fmt with its literal) - const substituted = if (self.comptime_param_nodes) |cpn| - self.substituteComptimeNodes(expr, cpn) catch expr - else - expr; - - // Step 2: Evaluate the expression to get a string - const code_str = self.evalComptimeString(substituted) orelse return self.builder.constInt(0, .void); - - // Step 3: Parse the string as sx code and lower each statement - // The last expression's value is captured as the return value - var p = parser_mod.Parser.init(self.alloc, code_str); - var last_val: Ref = self.builder.constInt(0, .void); - while (p.current.tag != .eof) { - const stmt = p.parseStmt() catch break; - if (p.current.tag == .eof) { - // Last statement — try to capture as expression value - // Note: tryLowerAsExpr internally calls lowerStmt for statement nodes, - // so we must NOT call lowerStmt again in the else branch. - if (self.tryLowerAsExpr(stmt)) |val| { - last_val = val; - } - } else { - self.lowerStmt(stmt); - } - } - return last_val; - } - - /// Evaluate an expression at compile time, returning its string value. - /// Returns null if evaluation fails. - fn evalComptimeString(self: *Lowering, expr: *const Node) ?[:0]const u8 { - // Case 1: String literal — return it directly (no need for interpreter) - if (expr.data == .string_literal) { - const lit = expr.data.string_literal; - const str = if (lit.is_raw) - lit.raw - else - unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; - return self.alloc.dupeZ(u8, str) catch null; - } - - // Case 2: Evaluate via IR interpreter, reusing the parent module. - // The parent's `scanDecls` pass has already registered every - // type / protocol / impl / thunk the comptime call may need - // (Allocator, CAllocator, Context, the per-impl thunks). A - // fresh empty module would only lazy-lower function ASTs and - // would miss the type/protocol registrations, which would break - // `context.allocator.X` — the protocol dispatch chain needs - // those types to resolve struct field layout and the alloc/ - // dealloc thunks at the bottom of the dispatch. - const ct_func_id = self.createComptimeFunction("__insert", expr, .string); - - 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); - - const result = interp.call(ct_func_id, &.{}) catch return null; - - const str = result.asString(&interp) orelse switch (result) { - .string => |s| s, - else => return null, - }; - - return self.alloc.dupeZ(u8, str) catch null; - } - - /// Lower the direct callee of a comptime expression into the ct module. - /// Transitive dependencies are resolved lazily via the shared fn_ast_map. - fn lowerComptimeDeps(self: *Lowering, ct: *Lowering, expr: *const Node) void { - if (expr.data != .call) return; - if (expr.data.call.callee.data != .identifier) return; - const name = expr.data.call.callee.data.identifier.name; - if (resolveBuiltin(name) != null) return; - if (self.program_index.fn_ast_map.get(name)) |fd| { - if (ct.resolveFuncByName(name) == null) { - ct.lowerFunction(fd, name, false); - } - } - } - - /// Substitute comptime parameter identifiers with their actual AST nodes. - fn substituteComptimeNodes(self: *Lowering, node: *const Node, cpn: std.StringHashMap(*const Node)) !*const Node { - // Direct identifier match - if (node.data == .identifier) { - if (cpn.get(node.data.identifier.name)) |replacement| { - return replacement; - } - } - - // Recurse into call arguments - if (node.data == .call) { - var changed = false; - const new_args = try self.alloc.alloc(*Node, node.data.call.args.len); - for (node.data.call.args, 0..) |arg, i| { - const substituted = try self.substituteComptimeNodes(arg, cpn); - new_args[i] = @constCast(substituted); - if (substituted != arg) changed = true; - } - if (changed) { - const new_node = try self.alloc.create(Node); - new_node.* = .{ - .span = node.span, - .data = .{ .call = .{ - .callee = node.data.call.callee, - .args = new_args, - } }, - }; - return new_node; - } - } - - return node; - } - - /// Lower a call to a function with comptime params by inlining its body. - /// Comptime params are substituted, `#insert` expressions are evaluated. - fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref { - // Build comptime param substitution map: param_name → call_site AST node - var cpn = std.StringHashMap(*const Node).init(self.alloc); - var call_arg_idx: usize = 0; - // Pack-arg-node registration (step 2 of the variadic heterogeneous - // type packs feature): when the fn declares a pack param, record - // the slice of call-site arg nodes under the pack name so the - // body's `args[$i]` lowering can substitute the i-th arg with - // its concrete-typed value instead of the `[]Any` slice load. - var pack_arg_name: ?[]const u8 = null; - var pack_arg_slice: []const *const Node = &.{}; - - for (fd.params) |param| { - if (param.is_variadic) { - // Variadic param: pack remaining call args into []Any slice - self.lowerVariadicArgs(param.name, call_node.args, call_arg_idx); - // Only heterogeneous pack form `..$args` (is_comptime AND - // is_variadic) registers for typed indexing. Plain - // `args: ..Any` keeps the existing []Any path so stdlib's - // `format`/`print` continue boxing through Any. - if (param.is_comptime and call_arg_idx <= call_node.args.len) { - pack_arg_name = param.name; - pack_arg_slice = call_node.args[call_arg_idx..]; - // Stamp each pack arg with the caller's source so the - // body's typed `args[i]` substitution (via packArgNodeAt, - // lowered under the defining-module pin set below) resolves - // its bare names in the CALLER's visibility context — the - // same treatment the fixed comptime params get below. - // Without it a caller-owned helper passed to an imported - // metaprogram (`std.print("{}", caller_fn())`) resolves - // under the callee's module and is reported "not visible". - for (call_node.args[call_arg_idx..]) |pack_arg| { - self.stampCallerSource(pack_arg); - } - } - break; // variadic is always the last param - } - if (call_arg_idx >= call_node.args.len) break; - if (param.is_comptime) { - self.stampCallerSource(call_node.args[call_arg_idx]); - cpn.put(param.name, call_node.args[call_arg_idx]) catch {}; - call_arg_idx += 1; - } else { - const arg_val = self.lowerExpr(call_node.args[call_arg_idx]); - const pty = self.resolveParamType(¶m); - const slot = self.builder.alloca(pty); - self.builder.store(slot, arg_val); - if (self.scope) |scope| { - scope.put(param.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); - } - call_arg_idx += 1; - } - } - - // Also bind comptime params as local string variables (for `fmt` used in runtime code) - var cpn_iter = cpn.iterator(); - while (cpn_iter.next()) |entry| { - const param_name = entry.key_ptr.*; - const param_node = entry.value_ptr.*; - if (param_node.data == .string_literal) { - // Create a local string variable with the literal value - const str_ref = self.lowerExpr(param_node); - const slot = self.builder.alloca(.string); - self.builder.store(slot, str_ref); - if (self.scope) |scope| { - scope.put(param_name, .{ .ref = slot, .ty = .string, .is_alloca = true }); - } - } - } - - // Install comptime param nodes and lower the function body inline - const saved_cpn = self.comptime_param_nodes; - self.comptime_param_nodes = cpn; - defer self.comptime_param_nodes = saved_cpn; - - // Install pack-arg-node binding. Mirrors `comptime_param_nodes`: - // each call owns its own map, nested calls shadow. `lowerIndexExpr` - // reads the map for `args[]` substitution. - const saved_pan = self.pack_arg_nodes; - var pan_map: std.StringHashMap([]const *const Node) = undefined; - var pan_installed = false; - if (pack_arg_name) |pn| { - pan_map = std.StringHashMap([]const *const Node).init(self.alloc); - pan_map.put(pn, pack_arg_slice) catch {}; - self.pack_arg_nodes = pan_map; - pan_installed = true; - } - defer { - if (pan_installed) pan_map.deinit(); - self.pack_arg_nodes = saved_pan; - } - - // Pin the lowering to the metaprogram's OWN module for the body (and - // its return type + anything it `#insert`s, e.g. `build_format` / `out` - // / `emit` inside `std.print` / `log.*`), so those bare names resolve - // in the defining module's visibility context rather than the call - // site's (issue 0106). The call-site ARGS above are deliberately lowered - // BEFORE this, in the caller's context. Mirrors `lowerFunctionBodyInto`, - // which switches to `func.source_file`. The defining path is stamped on - // the body node by `resolveImports`; a sourceless body keeps the - // caller's context. - const saved_source = self.current_source_file; - defer self.setCurrentSourceFile(saved_source); - if (fd.body.source_file) |src| self.setCurrentSourceFile(src); - - // Lower the body — capture return value for functions with return type - const ret_ty = self.resolveReturnType(fd); - if (ret_ty != .void) { - // Detect whether the body might use `return X;` statements. - // If so, set up the inline-return slot AND a dedicated - // "return-done" basic block so each `return X;` stores to - // the slot and branches to ret_done. After the body lowers, - // we switch to ret_done and load. Pure tail-expression - // bodies (arrow form, or a block whose last stmt is an - // expression) skip the slot+block — keeps the common - // `format`/`#insert`-style path unchanged. - const has_return = fnBodyHasReturn(fd.body); - if (has_return) { - const ret_slot = self.builder.alloca(ret_ty); - const ret_done_bb = self.freshBlock("ct.ret_done"); - const saved_iri = self.inline_return_target; - self.inline_return_target = .{ .slot = ret_slot, .ret_ty = ret_ty, .done_bb = ret_done_bb }; - defer self.inline_return_target = saved_iri; - - // Lower body. Tail-expression bodies (rare here since - // has_return == true) produce a tail value we still - // route through the slot so the load in ret_done picks - // it up. Block-statement bodies whose last stmt is - // `return X;` already br to ret_done from inside - // lowerReturn. - if (self.lowerBlockValue(fd.body)) |val| { - if (!self.currentBlockHasTerminator()) { - const v_ty = self.builder.getRefType(val); - const coerced = if (v_ty != ret_ty) - self.coerceToType(val, v_ty, ret_ty) - else - val; - self.builder.store(ret_slot, coerced); - self.builder.br(ret_done_bb, &.{}); - } - } else if (!self.currentBlockHasTerminator()) { - // Body fell through without producing a tail value - // AND without branching to ret_done — this only - // happens for bodies whose last stmt is a void - // statement (e.g. side-effecting). Slot is - // uninitialised on this path; safer to br anyway - // so the CFG is well-formed. The load in ret_done - // will read uninit, which is the same garbage - // behaviour the regular fn-body lowering would - // produce for a missing return. - self.builder.br(ret_done_bb, &.{}); - } - - self.builder.switchToBlock(ret_done_bb); - return self.builder.load(ret_slot, ret_ty); - } else { - if (self.lowerBlockValue(fd.body)) |val| { - return val; - } - } - } else { - self.lowerBlock(fd.body); - } - - return self.builder.constInt(0, .void); - } - - /// True if `node` (a fn body) contains any top-level `return` statement. - /// Used by inline-comptime lowering to decide whether to allocate a - /// result slot — pure tail-expression bodies skip the slot. Walks past - /// `if`/`while`/`for`/`match` arms (early-return inside a conditional - /// counts) but stops at nested fn/lambda bodies (those have their own - /// return contexts). - fn fnBodyHasReturn(node: *const Node) bool { - return switch (node.data) { - .return_stmt => true, - .block => |b| blk: { - for (b.stmts) |s| if (fnBodyHasReturn(s)) break :blk true; - break :blk false; - }, - .if_expr => |ie| blk: { - if (fnBodyHasReturn(ie.then_branch)) break :blk true; - if (ie.else_branch) |eb| if (fnBodyHasReturn(eb)) break :blk true; - break :blk false; - }, - .while_expr => |we| fnBodyHasReturn(we.body), - .for_expr => |fe| fnBodyHasReturn(fe.body), - .match_expr => |me| blk: { - for (me.arms) |arm| if (fnBodyHasReturn(arm.body)) break :blk true; - break :blk false; - }, - .defer_stmt => |ds| fnBodyHasReturn(ds.expr), - else => false, - }; - } - /// Pack variadic arguments into a []Any slice. Each arg is boxed as Any {tag, value}, /// stored into a stack-allocated array, and the slice {ptr, len} is bound to param_name. - fn lowerVariadicArgs(self: *Lowering, param_name: []const u8, call_args: []const *const Node, start_idx: usize) void { + pub fn lowerVariadicArgs(self: *Lowering, param_name: []const u8, call_args: []const *const Node, start_idx: usize) void { const any_slice_ty = self.module.types.sliceOf(.any); const n = if (call_args.len > start_idx) call_args.len - start_idx else 0; @@ -13267,115 +12649,6 @@ pub const Lowering = struct { return p.is_variadic and (p.is_comptime or p.is_pack); } - /// 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 { - var buf: [64]u8 = undefined; - const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix; - self.comptime_counter += 1; - - // Save current builder + lowering state. The wrapper fn we're - // about to build runs the comptime expression in isolation — - // it must NOT inherit the enclosing call's `inline_return_target` - // (which would re-route a `return` inside the wrapper into a - // slot belonging to a different basic block), pack bindings - // (which would substitute caller's `args` inside the wrapper), - // or comptime-param bindings (which would substitute caller's - // `$fmt` inside the wrapper's #insert children). Without these - // saves, nested comptime calls leak outer state into the - // interp-executed wrapper, producing garbage stores (issue-0046 - // face 1 — storeAtRawPtr null). - const saved_func = self.builder.func; - const saved_block = self.builder.current_block; - const saved_counter = self.builder.inst_counter; - const saved_scope = self.scope; - const saved_ctx_ref = self.current_ctx_ref; - const saved_iri = self.inline_return_target; - const saved_pan = self.pack_arg_nodes; - const saved_ppc = self.pack_param_count; - const saved_pat = self.pack_arg_types; - const saved_cpn = self.comptime_param_nodes; - const saved_block_terminated = self.block_terminated; - const saved_target_type = self.target_type; - const saved_func_defer_base = self.func_defer_base; - self.inline_return_target = null; - self.pack_arg_nodes = null; - self.pack_param_count = null; - self.pack_arg_types = null; - self.comptime_param_nodes = null; - self.block_terminated = false; - self.target_type = null; - self.func_defer_base = self.defer_stack.items.len; - defer { - self.current_ctx_ref = saved_ctx_ref; - self.inline_return_target = saved_iri; - self.pack_arg_nodes = saved_pan; - self.pack_param_count = saved_ppc; - self.pack_arg_types = saved_pat; - self.comptime_param_nodes = saved_cpn; - self.block_terminated = saved_block_terminated; - self.target_type = saved_target_type; - self.func_defer_base = saved_func_defer_base; - } - - // Build params: implicit `__sx_ctx` at slot 0 when the program - // uses Context (so the body's `context.X` reads + transitive calls - // resolve cleanly). The comptime function's top-level invocation - // supplies `&__sx_default_context` (interp via callWithDefaultContext; - // codegen via the comptime-eval glue in emit_llvm). - const wants_ctx = self.implicit_ctx_enabled; - const params_slice = blk: { - if (!wants_ctx) break :blk &[_]Function.Param{}; - const owned = self.alloc.alloc(Function.Param, 1) catch break :blk &[_]Function.Param{}; - owned[0] = .{ - .name = self.module.types.internString("__sx_ctx"), - .ty = self.module.types.ptrTo(.void), - }; - break :blk owned; - }; - - // Create the comptime function - const name_id = self.module.types.internString(name); - const func_id = self.builder.beginFunction(name_id, params_slice, ret_ty); - - // Mark as comptime + has_implicit_ctx - const fn_mut = self.module.getFunctionMut(func_id); - fn_mut.is_comptime = true; - fn_mut.has_implicit_ctx = wants_ctx; - - // Create entry block - const entry_name = self.module.types.internString("entry"); - const entry = self.builder.appendBlock(entry_name, &.{}); - self.builder.switchToBlock(entry); - if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); - - // Create a scope that chains to the enclosing scope (so the - // expression can reference names visible at the #run site). - var ct_scope = Scope.init(self.alloc, saved_scope); - self.scope = &ct_scope; - - // Lower the expression and return it - const result = self.lowerExpr(expr); - if (ret_ty == .void) { - self.builder.retVoid(); - } else { - self.builder.ret(result, ret_ty); - } - - self.builder.finalize(); - - // Restore builder state - self.scope = saved_scope; - ct_scope.deinit(); - self.builder.func = saved_func; - self.builder.current_block = saved_block; - self.builder.inst_counter = saved_counter; - - return func_id; - } - - // ── Block helpers ─────────────────────────────────────────────── - pub fn freshBlock(self: *Lowering, prefix: []const u8) BlockId { return self.freshBlockWithParams(prefix, &.{}); } @@ -13405,7 +12678,7 @@ pub const Lowering = struct { // ── Type resolution ───────────────────────────────────────────── // Delegates to type_bridge for full AST type node resolution. - fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId { + pub fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId { if (fd.return_type) |rt| { return self.resolveTypeWithBindings(rt); } @@ -13476,7 +12749,7 @@ pub const Lowering = struct { }; } - fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId { + pub fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId { // A plain value param with no annotation can only be typed from // context (a lambda's target closure signature). When `resolveParamType` // is reached for one, there is no such context — so it's a genuine @@ -13642,163 +12915,6 @@ pub const Lowering = struct { return self.sourceConstIsFloatTyped(name, null); } - /// Resolve a name to a compile-time integer across the three const tables. - /// A comptime binding (generic value param / inline-for cursor) or a - /// `#run`/`OS`/`ARCH` comptime constant wins first; otherwise the name is a - /// SOURCE-AWARE module const, folded with nested leaves resolved own-wins. - fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 { - if (self.comptime_constants.get(name)) |cv| switch (cv) { - .int_val => |iv| return iv, - else => {}, - }; - if (self.comptime_value_bindings) |cvb| { - if (cvb.get(name)) |v| return v; - } - return self.foldSourceConstInt(name, null); - } - - /// Source-aware INTEGER fold of a module const `name` (E2/F2/R1). Select the - /// SOURCE-AWARE author (own-wins; ≥2 flat-visible → ambiguous → null, the loud - /// diagnostic is the reference site's job), then fold ITS RHS with nested const - /// leaves resolved through `SourceConstCtx` — each leaf re-selects its OWN - /// source author, NOT the global last-wins `module_const_map`. So a shadowed - /// `K :: M + 1` folds `M` to the SELECTED author's `M`, coherently whether `K` - /// is read as a value (`return K`) or used as an array dimension / count - /// (`[K]u8`). `frame` (keyed by name + author-source, F3) cycle-guards a const - /// whose value references another const. Single-author → byte-identical to the - /// legacy fold (the selected `ci` IS the global one and every nested leaf has - /// exactly one author). - fn foldSourceConstInt(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) ?i64 { - return switch (self.selectModuleConst(name)) { - .resolved => |sel| { - if (constFoldFrameContains(frame, name, sel.source)) return null; - if (!program_index_mod.isCountableConstType(&self.module.types, sel.info.ty)) return null; - var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; - const restore = self.pinConstAuthorSource(sel.source); - defer restore.unpin(); - return program_index_mod.evalConstIntExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); - }, - .ambiguous, .none => null, - }; - } - - /// Float counterpart of `foldSourceConstInt` (E2/F2/R1). - fn foldSourceConstFloat(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) ?f64 { - return switch (self.selectModuleConst(name)) { - .resolved => |sel| { - if (constFoldFrameContains(frame, name, sel.source)) return null; - if (!program_index_mod.isCountableConstType(&self.module.types, sel.info.ty)) return null; - var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; - const restore = self.pinConstAuthorSource(sel.source); - defer restore.unpin(); - return program_index_mod.evalConstFloatExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); - }, - .ambiguous, .none => null, - }; - } - - /// Source-aware "is `name` a FLOAT-valued module const" (E2/F2/R1): judge the - /// SELECTED author's value, with nested const leaves resolved source-aware. - fn sourceConstIsFloatTyped(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) bool { - return switch (self.selectModuleConst(name)) { - .resolved => |sel| { - if (constFoldFrameContains(frame, name, sel.source)) return false; - if (program_index_mod.isFloatConstType(sel.info.ty)) return true; - var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; - const restore = self.pinConstAuthorSource(sel.source); - defer restore.unpin(); - return program_index_mod.isFloatValuedExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); - }, - .ambiguous, .none => false, - }; - } - - /// A selected module const plus the SOURCE that authored it. `source` pins the - /// context in which the const's RHS leaves must be folded (F1): a same-name - /// `K :: M + 1` selected from author `a.sx` folds its nested `M` against `a.sx`, - /// not against whichever module read `K`. `source` is null only on the - /// fully-unwired fallback (no source partition at all), where the RHS resolves - /// through the global registration context unchanged. - const SelectedConst = struct { - info: ModuleConstInfo, - source: ?[]const u8, - }; - - const ConstAuthor = union(enum) { - resolved: SelectedConst, - ambiguous, - none, - }; - - /// The source-aware module-const author of `name` from the querying module - /// (E2/F2) — the value-const analogue of `selectNominalLeaf` (types) and - /// `selectPlainCallableAuthor` (functions). Selects over the ONE graph-walk - /// collector and reads the value from the SELECTED author's per-source cache - /// (`module_consts_by_source`), never the global last-wins `module_const_map`: - /// - /// - **own-wins**: the querying module's OWN const author is selected outright. - /// - else the FLAT-import-reachable const authors: exactly one → it; ≥2 distinct - /// → `.ambiguous` (issue 0105 / 0760 — never a silent first-/last-wins pick). - /// - none visible → `.none` (a namespaced-only const must be qualified `ns.X`; - /// a non-const name folds to `.none` too). - /// - /// A main-file body carries a null `current_source_file` (it IS the root), so - /// the querying module is `main_file` there; a fully unwired index (no source - /// at all) falls open to the global registration, byte-identical to the legacy - /// reader for the registration / comptime-host path. - pub fn selectModuleConst(self: *Lowering, name: []const u8) ConstAuthor { - const from = self.current_source_file orelse self.main_file orelse { - if (self.program_index.module_const_map.get(name)) |ci| return .{ .resolved = .{ .info = ci, .source = null } }; - return .none; - }; - var res = self.resolver(); - const set = res.collectVisibleAuthors(name, from, .user_bare_flat); - defer if (set.flat.len > 0) self.alloc.free(set.flat); - if (set.own) |o| if (self.sourceModuleConst(o.source, name)) |ci| return .{ .resolved = .{ .info = ci, .source = o.source } }; - var the_one: ?SelectedConst = null; - var count: usize = 0; - for (set.flat) |fa| { - const ci = self.sourceModuleConst(fa.source, name) orelse continue; - count += 1; - if (count >= 2) return .ambiguous; - the_one = .{ .info = ci, .source = fa.source }; - } - if (the_one) |sc| return .{ .resolved = sc }; - return .none; - } - - /// `source`'s per-source const cache entry for `name` (E0's - /// `module_consts_by_source` write side), or null. - fn sourceModuleConst(self: *Lowering, source: []const u8, name: []const u8) ?ModuleConstInfo { - const inner = self.program_index.module_consts_by_source.get(source) orelse return null; - return inner.get(name); - } - - /// Saved `current_source_file` for a const-author pin; `unpin()` restores it. - const ConstSourcePin = struct { - lowering: *Lowering, - saved: ?[]const u8, - active: bool, - fn unpin(self: ConstSourcePin) void { - if (self.active) self.lowering.setCurrentSourceFile(self.saved); - } - }; - - /// Pin `current_source_file` to a SELECTED const's AUTHOR source while its RHS - /// is folded / lowered, so nested same-name leaves resolve in the author's - /// visibility context (F1): `K :: M + 1` selected from `a.sx` always folds `M` - /// against `a.sx`, regardless of which module read `K`. A null author (the - /// fully-unwired fallback) leaves the context untouched. Single-author programs - /// pin to the source they were already in → byte-identical. - fn pinConstAuthorSource(self: *Lowering, source: ?[]const u8) ConstSourcePin { - if (source) |s| { - const saved = self.current_source_file; - self.setCurrentSourceFile(s); - return .{ .lowering = self, .saved = saved, .active = true }; - } - return .{ .lowering = self, .saved = self.current_source_file, .active = false }; - } - /// Resolve a type node, checking type_bindings first for generic type params. pub fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId { // Pack-index in a type position: `$[]` resolves to the @@ -16473,7 +15589,7 @@ pub const Lowering = struct { /// A `Resolver` facade over the borrowed Phase A import facts (Phase B). Cheap /// by-value; `collectVisibleAuthors`'s `AuthorSet.flat` slice is backed by /// `self.alloc` and owned by the caller (`selectPlainCallableAuthor` frees it). - fn resolver(self: *Lowering) resolver_mod.Resolver { + pub fn resolver(self: *Lowering) resolver_mod.Resolver { return resolver_mod.Resolver.init(&self.program_index, self.alloc); } @@ -17100,7 +16216,7 @@ pub const Lowering = struct { /// Update `self.current_source_file` and mirror it onto `diags.current_source_file`, /// so any diagnostic emitted from inside a function lowered from another module is /// attributed to that module — not whichever file the diagnostics list was init'd with. - fn setCurrentSourceFile(self: *Lowering, source_file: ?[]const u8) void { + pub fn setCurrentSourceFile(self: *Lowering, source_file: ?[]const u8) void { self.current_source_file = source_file; if (self.diagnostics) |d| d.current_source_file = source_file; } @@ -17112,7 +16228,7 @@ pub const Lowering = struct { /// a caller-owned helper passed to an imported metaprogram stays visible. /// Only stamps a node with no source yet, and only when the caller context /// is known; an unknown caller source leaves the node's fall-open intact. - fn stampCallerSource(self: *Lowering, node: *Node) void { + pub fn stampCallerSource(self: *Lowering, node: *Node) void { if (node.source_file != null) return; if (self.current_source_file) |src| node.source_file = src; } @@ -17166,50 +16282,11 @@ pub const Lowering = struct { /// this, so the message + fix-it stay identical across the typed-binding /// coerce arm, the field/param-default sites, the typed-const path, and the /// global-initializer path. - fn diagNonIntegralNarrow(self: *Lowering, span: ast.Span, value: f64, dst_ty: TypeId) void { + pub fn diagNonIntegralNarrow(self: *Lowering, span: ast.Span, value: f64, dst_ty: TypeId) void { if (self.diagnostics) |d| d.addFmt(.err, span, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ value, self.formatTypeName(dst_ty) }); } - /// Apply the unified float→int narrowing rule to a typed-binding initializer - /// EXPRESSION `node` whose declared type is `dst` (a typed local, a struct - /// field default, or a call argument incl. an expanded param default). When - /// `node` is a COMPILE-TIME float narrowing into an integer type: - /// - an INTEGRAL value (`4.0`, `M + 2.0`) folds to its `constInt`; - /// - a NON-integral value (`1.5`, `M + 0.5`) emits the narrowing - /// diagnostic and returns a placeholder so lowering finishes. - /// Returns null — so the caller lowers `node` normally — when the rule does - /// not apply: `dst` is not an integer, `node` is not statically float-typed, - /// or `node` is not a compile-time constant (a genuine runtime float keeps - /// truncating, and `xx` / `cast` keep their explicit-truncation escape since - /// a cast node's inferred type is the destination integer, not a float). - /// Reuses `program_index.evalConstIntExpr` (exact integral fold) + - /// `evalConstFloatExpr` (non-integral detection) + `floatToIntExact`. - fn foldComptimeFloatInit(self: *Lowering, node: *const Node, dst: TypeId) ?Ref { - if (!self.isIntEx(dst)) return null; - // PURE & side-effect-free, so it runs FIRST: a runtime / non-comptime / - // non-numeric node — incl. a `$pack[i]` index expression — folds to null - // and is left to the normal path untouched. (Calling `inferExprType` on - // a pack-index value before this guard would spuriously resolve the - // enclosing pack type outside an active binding.) - const fv = program_index_mod.evalConstFloatExpr(node, self) orelse return null; - // Only a FLOAT-flavored initializer narrows here; a plain comptime int - // (`5`, `M + 2`) is left to the normal integer path. Safe to infer now — - // `evalConstFloatExpr` only succeeds for literal / const-arithmetic - // nodes, never an unbound pack index. `inferExprType` is the primary - // signal, but it reads a const's DECLARED type — which is a placeholder - // `s64` for an untyped float-EXPRESSION const (`ME :: 4.0 + 1.0`), so - // `ME / 2` would look like integer division; `isFloatValuedExpr` (judging - // by VALUE) catches that case so it narrows under the unified rule too. - if (!isFloat(self.inferExprType(node)) and !program_index_mod.isFloatValuedExpr(node, self)) return null; - // Integral comptime float folds to its int (`floatToIntExact`, the same - // facility the array-dim / `$K: Count` paths use); a non-integral one is - // the narrowing error. - if (program_index_mod.floatToIntExact(fv)) |iv| return self.builder.constInt(iv, dst); - self.diagNonIntegralNarrow(node.span, fv, dst); - return self.builder.constInt(0, dst); - } - /// Lower a struct field default `default_expr`, coerced to the field type /// `field_ty`. A compile-time float default narrowing into an integer field /// follows the unified rule via `foldComptimeFloatInit`; everything else @@ -19076,6 +18153,35 @@ pub const Lowering = struct { pub const closureShapeKey = lower_error.closureShapeKey; pub const returnValuePart = lower_error.returnValuePart; pub const shapeKeyOfCallee = lower_error.shapeKeyOfCallee; + + // --- moved to lower/comptime.zig (lower_comptime) --- + pub const SelectedConst = lower_comptime.SelectedConst; + pub const evalComptimeCondition = lower_comptime.evalComptimeCondition; + pub const evalComptimeMatch = lower_comptime.evalComptimeMatch; + pub const evalComptimeInt = lower_comptime.evalComptimeInt; + pub const evalComptimeString = lower_comptime.evalComptimeString; + pub const lowerComptimeGlobal = lower_comptime.lowerComptimeGlobal; + pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect; + pub const lowerComptimeCall = lower_comptime.lowerComptimeCall; + pub const lowerInlineComptime = lower_comptime.lowerInlineComptime; + pub const lowerInsertExpr = lower_comptime.lowerInsertExpr; + pub const lowerInsertExprValue = lower_comptime.lowerInsertExprValue; + pub const lowerComptimeDeps = lower_comptime.lowerComptimeDeps; + pub const substituteComptimeNodes = lower_comptime.substituteComptimeNodes; + pub const fnBodyHasReturn = lower_comptime.fnBodyHasReturn; + pub const createComptimeFunction = lower_comptime.createComptimeFunction; + pub const constExprValue = lower_comptime.constExprValue; + pub const constArrayLiteral = lower_comptime.constArrayLiteral; + pub const constStructLiteral = lower_comptime.constStructLiteral; + pub const constEnumLiteral = lower_comptime.constEnumLiteral; + pub const foldSourceConstInt = lower_comptime.foldSourceConstInt; + pub const foldSourceConstFloat = lower_comptime.foldSourceConstFloat; + pub const sourceConstIsFloatTyped = lower_comptime.sourceConstIsFloatTyped; + pub const comptimeIntNamed = lower_comptime.comptimeIntNamed; + pub const selectModuleConst = lower_comptime.selectModuleConst; + pub const sourceModuleConst = lower_comptime.sourceModuleConst; + pub const pinConstAuthorSource = lower_comptime.pinConstAuthorSource; + pub const foldComptimeFloatInit = lower_comptime.foldComptimeFloatInit; }; /// JNI param/return type resolution: user-declared types pass through diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig new file mode 100644 index 0000000..daf0635 --- /dev/null +++ b/src/ir/lower/comptime.zig @@ -0,0 +1,977 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ast = @import("../../ast.zig"); +const Node = ast.Node; +const types = @import("../types.zig"); +const inst_mod = @import("../inst.zig"); +const mod_mod = @import("../module.zig"); +const type_bridge = @import("../type_bridge.zig"); +const unescape = @import("../../unescape.zig"); +const parser_mod = @import("../../parser.zig"); +const interp_mod = @import("../interp.zig"); +const errors = @import("../../errors.zig"); +const jni_descriptor = @import("../jni_descriptor.zig"); +const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); +const imports_mod = @import("../../imports.zig"); +const ProgramIndex = program_index_mod.ProgramIndex; +const GlobalInfo = program_index_mod.GlobalInfo; +const StructTemplate = program_index_mod.StructTemplate; +const TemplateParam = program_index_mod.TemplateParam; +const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +const ModuleConstInfo = program_index_mod.ModuleConstInfo; +const TypeResolver = @import("../type_resolver.zig").TypeResolver; +const ResolveEnv = @import("../type_resolver.zig").ResolveEnv; +const PackResolver = @import("../packs.zig").PackResolver; +const ExprTyper = @import("../expr_typer.zig").ExprTyper; +const CallResolver = @import("../calls.zig").CallResolver; +const GenericResolver = @import("../generics.zig").GenericResolver; +const ProtocolResolver = @import("../protocols.zig").ProtocolResolver; +const CoercionResolver = @import("../conversions.zig").CoercionResolver; +const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; +const ErrorFlow = @import("../error_flow.zig").ErrorFlow; +const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; +const semantic_diagnostics = @import("../semantic_diagnostics.zig"); + +const TypeId = types.TypeId; +const StringId = types.StringId; +const Ref = inst_mod.Ref; +const BlockId = inst_mod.BlockId; +const FuncId = inst_mod.FuncId; +const Function = inst_mod.Function; +const Module = mod_mod.Module; +const Builder = mod_mod.Builder; + +const lower = @import("../lower.zig"); +const Lowering = lower.Lowering; +const Scope = lower.Scope; +const ConstFoldFrame = lower.ConstFoldFrame; +const constFoldFrameContains = lower.constFoldFrameContains; +const SourceConstCtx = lower.SourceConstCtx; +const resolveBuiltin = Lowering.resolveBuiltin; +const isFloat = Lowering.isFloat; + +/// Try to convert an array literal's elements into a compile-time +/// ConstantValue.aggregate. `array_ty` is the array's resolved TypeId; its +/// element type drives type-aware serialization of struct-literal and +/// nested-array elements. Returns null if `array_ty` is not an array type or +/// any element is not a compile-time constant. +pub fn constArrayLiteral(self: *Lowering, elements: []const *const Node, array_ty: TypeId) ?inst_mod.ConstantValue { + if (array_ty.isBuiltin()) return null; + const elem_ty: TypeId = switch (self.module.types.get(array_ty)) { + .array => |a| a.element, + else => return null, + }; + const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null; + for (elements, 0..) |elem, i| { + vals[i] = self.constExprValue(elem, elem_ty) orelse return null; + } + return .{ .aggregate = vals }; +} + +/// Try to convert a single AST expression into a compile-time ConstantValue. +/// `expected_ty` is the destination element/field type — it lets aggregate +/// leaves (struct literals, nested arrays) serialize with the correct shape +/// rather than collapsing to null (issue 0080). Returns null if the +/// expression is not constant-foldable here. +pub fn constExprValue(self: *Lowering, expr: *const Node, expected_ty: TypeId) ?inst_mod.ConstantValue { + return switch (expr.data) { + .int_literal => |il| .{ .int = il.value }, + .bool_literal => |bl| .{ .boolean = bl.value }, + // A float into an INTEGER destination follows the implicit + // narrowing rule: an integral float folds to its int, a + // non-integral one is a compile error (not a silent bit-coerce). + .float_literal => |fl| blk: { + if (self.isIntEx(expected_ty)) { + if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv }; + self.diagNonIntegralNarrow(expr.span, fl.value, expected_ty); + break :blk null; + } + break :blk inst_mod.ConstantValue{ .float = fl.value }; + }, + .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, + .undef_literal => .zeroinit, + // A `null` in a pointer (or optional-pointer) field is a + // compile-time constant: the zero pointer. Without this arm the + // aggregate is wrongly rejected as non-constant (issue 0081). + .null_literal => .null_val, + .unary_op => |uo| switch (uo.op) { + .negate => switch (uo.operand.data) { + .int_literal => |il| .{ .int = -il.value }, + .float_literal => |fl| .{ .float = -fl.value }, + else => null, + }, + else => null, + }, + .array_literal => |al| self.constArrayLiteral(al.elements, expected_ty), + .struct_literal => |sl| self.constStructLiteral(&sl, expected_ty), + // An enum tag as an aggregate leaf (`[2]Color = .[.green, .blue]`, or + // an enum field inside a global struct) serializes to its tag int + // against the leaf's declared enum type (issue 0082). + .enum_literal => |el| self.constEnumLiteral(&el, expected_ty, expr.span), + else => null, + }; +} + +/// Serialize an enum-literal initializer (`.Variant`) into a static +/// `ConstantValue.int` holding the variant's tag value, resolved against the +/// destination enum type `ty`. The tag respects explicit variant values +/// (`enum { a; b :: 5; }`); the enum's backing width is applied by the +/// const emitters via the destination type's LLVM type. Plain enums only — +/// a tagged-union or non-enum destination is diagnosed loudly rather than +/// silently zero-initialized (issue 0082). +pub fn constEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral, ty: TypeId, span: ast.Span) ?inst_mod.ConstantValue { + if (!ty.isBuiltin()) { + const info = self.module.types.get(ty); + if (info == .@"enum") { + const e = info.@"enum"; + const name_id = self.module.types.internString(el.name); + for (e.variants, 0..) |variant, i| { + if (variant != name_id) continue; + if (e.explicit_values) |vals| { + if (i < vals.len) return .{ .int = vals[i] }; + } + return .{ .int = @intCast(i) }; + } + if (self.diagnostics) |d| + d.addFmt(.err, span, "'.{s}' is not a variant of enum '{s}'", .{ el.name, self.module.types.getString(e.name) }); + return null; + } + } + if (self.diagnostics) |d| + d.addFmt(.err, span, "enum-literal global initializer '.{s}' is only supported for a plain enum destination type", .{el.name}); + return null; +} + +/// Try to convert a struct literal into a compile-time ConstantValue.aggregate of the +/// struct's fields in declaration order, filling missing fields from the struct's +/// field defaults. Returns null if any value is not constant-foldable. +pub fn constStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, ty: TypeId) ?inst_mod.ConstantValue { + if (ty.isBuiltin()) return null; + const ti = self.module.types.get(ty); + if (ti != .@"struct") return null; + const struct_fields = ti.@"struct".fields; + const struct_name = self.module.types.getString(ti.@"struct".name); + const field_defaults: []const ?*const Node = self.struct_defaults_map.get(struct_name) orelse &.{}; + + const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; + + const vals = self.alloc.alloc(inst_mod.ConstantValue, struct_fields.len) catch return null; + for (struct_fields, 0..) |sf, fi| { + const sf_name = self.module.types.getString(sf.name); + const init_expr: ?*const Node = blk: { + if (has_names) { + for (sl.field_inits) |init_pair| { + if (init_pair.name) |n| { + if (std.mem.eql(u8, n, sf_name)) break :blk init_pair.value; + } + } + } else if (fi < sl.field_inits.len) { + break :blk sl.field_inits[fi].value; + } + if (fi < field_defaults.len) break :blk field_defaults[fi]; + break :blk null; + }; + if (init_expr) |e| { + vals[fi] = self.constExprValue(e, sf.ty) orelse return null; + } else { + vals[fi] = .zeroinit; + } + } + return .{ .aggregate = vals }; +} + +/// Evaluate a compile-time condition for `inline if`. +/// Handles: `ident == .variant`, `ident != .variant`, `ident == int`, `ident != int`. +pub fn evalComptimeCondition(self: *Lowering, node: *const Node) ?bool { + if (node.data != .binary_op) return null; + const bo = &node.data.binary_op; + if (bo.op != .eq and bo.op != .neq) return null; + + // LHS must be an identifier that's in comptime_constants + const name = switch (bo.lhs.data) { + .identifier => |id| id.name, + else => return null, + }; + const cv = self.comptime_constants.get(name) orelse return null; + + switch (cv) { + .enum_tag => |et| { + // RHS must be an enum literal (.variant) + const variant_name = switch (bo.rhs.data) { + .enum_literal => |el| el.name, + else => return null, + }; + // Look up variant index in the enum type + const enum_info = self.module.types.get(et.ty); + if (enum_info != .@"enum") return null; + const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name); + const result = et.tag == variant_idx; + return if (bo.op == .eq) result else !result; + }, + .int_val => |iv| { + // RHS must be an integer literal + const rhs_val: i64 = switch (bo.rhs.data) { + .int_literal => |il| il.value, + else => return null, + }; + const result = iv == rhs_val; + return if (bo.op == .eq) result else !result; + }, + } +} + +/// Evaluate a compile-time match expression for `inline if ... == { case ... }`. +/// Returns the body of the matching arm, or null if the match can't be resolved. +pub fn evalComptimeMatch(self: *Lowering, me: *const ast.MatchExpr) ?*const Node { + // Subject must be a comptime constant identifier + const name = switch (me.subject.data) { + .identifier => |id| id.name, + else => return null, + }; + const cv = self.comptime_constants.get(name) orelse return null; + + switch (cv) { + .enum_tag => |et| { + const enum_info = self.module.types.get(et.ty); + if (enum_info != .@"enum") return null; + for (me.arms) |arm| { + if (arm.pattern == null) continue; // default arm + const variant_name = switch (arm.pattern.?.data) { + .enum_literal => |el| el.name, + else => continue, + }; + const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name); + if (et.tag == variant_idx) return arm.body; + } + // No match — try default arm + for (me.arms) |arm| { + if (arm.pattern == null) return arm.body; + } + return null; + }, + .int_val => |iv| { + for (me.arms) |arm| { + if (arm.pattern == null) continue; + const rhs_val: i64 = switch (arm.pattern.?.data) { + .int_literal => |il| il.value, + else => continue, + }; + if (iv == rhs_val) return arm.body; + } + for (me.arms) |arm| { + if (arm.pattern == null) return arm.body; + } + return null; + }, + } +} + +/// Evaluate an `inline for` range bound to a comptime integer. Delegates to +/// the shared `program_index.evalConstIntExpr` — the SAME integer folder the +/// array dimension / Vector lane / value-param count paths build on — so a +/// literal, a comptime constant (cursor), a module/generic const +/// (`inline for 0..M`), a `.len` leaf, a DIRECT integral float +/// (`0..-2.0` → -2), and any constant-foldable expression over those +/// (`inline for 0..(M + 1)`) all resolve identically. A range bound is an +/// ENDPOINT, not a count (specs.md §2), so it deliberately does NOT take the +/// `foldCountI64` float-const-leaf fallback the count sites add: it accepts a +/// direct integral float but leaves a float-const-leaf expression to the int +/// folder (negatives are valid here, unlike a count). +pub fn evalComptimeInt(self: *Lowering, node: *const Node) ?i64 { + return program_index_mod.evalConstIntExpr(node, self); +} + +/// Lower a `#run expr` that appears as a top-level constant binding: +/// NAME :: #run expr; +/// Creates a comptime function wrapping the expression (for later +/// interpretation), plus a global constant to hold the result. +pub fn lowerComptimeGlobal(self: *Lowering, name: []const u8, expr: *const Node, type_ann: ?*const Node) void { + // When the user writes `NAME :: #run expr;` with no type annotation, + // infer the global's type from the comptime expression's return + // shape. `resolveType(null)` returns `.s64` for legacy reasons — + // good for primitive helpers, silently wrong for anything else. + const expr_ty = self.inferExprType(expr); + // A failable `#run` (bare, no `catch`/`or`): the comptime function + // returns the full failable tuple so the #run site can inspect the + // error slot, but the GLOBAL is typed as the success value. On a + // comptime error the global never materializes — emit halts with a + // diagnostic + trace (E5.2). A handled `#run … catch/or …` already + // strips the error channel, so it lands here as non-failable. + const is_failable = self.errorChannelOf(expr_ty) != null; + const func_ret: TypeId = if (is_failable) + expr_ty + else if (type_ann) |n| + self.resolveTypeWithBindings(n) + else + expr_ty; + const global_ty: TypeId = if (is_failable) self.failableSuccessType(expr_ty) else func_ret; + const func_id = self.createComptimeFunction(name, expr, func_ret); + + // Add a global constant whose initializer will be filled by the interpreter. + const name_id = self.module.types.internString(name); + const gid = self.module.addGlobal(.{ + .name = name_id, + .ty = global_ty, + .init_val = null, // will be filled by interpreter at emit time + .is_const = true, + .comptime_func = func_id, + }); + + // Register for runtime lookup: identifier resolution emits global_get + self.putGlobal(self.current_source_file, name, .{ .id = gid, .ty = global_ty }); +} + +/// Lower a standalone `#run expr;` at the top level (side-effect only). +/// Creates a comptime function that the interpreter should execute. +pub fn lowerComptimeSideEffect(self: *Lowering, expr: *const Node) void { + // A failable side-effect `#run f();` returns the failable tuple so the + // emit-time runner can detect an escaping error and halt (E5.2); + // non-failable side effects stay `void`. + const expr_ty = self.inferExprType(expr); + const ret: TypeId = if (self.errorChannelOf(expr_ty) != null) expr_ty else .void; + _ = self.createComptimeFunction("__run", expr, ret); +} + +/// Lower a `#run expr` that appears inline within an expression. +/// Creates a comptime function and emits a `call` to it, so the +/// interpreter can evaluate it and replace with the constant result. +pub fn lowerInlineComptime(self: *Lowering, expr: *const Node) Ref { + const ret_ty: TypeId = self.target_type orelse self.inferExprType(expr); + const func_id = self.createComptimeFunction("__ct", expr, ret_ty); + // Emit a call to the comptime function. At interpretation time, + // this will be evaluated and the result inlined as a constant. + const func = &self.module.functions.items[@intFromEnum(func_id)]; + const final_args: []const Ref = if (func.has_implicit_ctx) + self.alloc.dupe(Ref, &.{self.current_ctx_ref}) catch &.{} + else + &.{}; + return self.builder.call(func_id, final_args, ret_ty); +} + +/// Lower a `#insert expr` statement. Evaluates `expr` at compile time to get +/// a string, parses it as sx code, and lowers each statement inline. +pub fn lowerInsertExpr(self: *Lowering, expr: *const Node) void { + _ = self.lowerInsertExprValue(expr); +} + +/// Like lowerInsertExpr but returns the value of the last parsed expression. +pub fn lowerInsertExprValue(self: *Lowering, expr: *const Node) Ref { + // Step 1: Substitute comptime param nodes (e.g., replace $fmt with its literal) + const substituted = if (self.comptime_param_nodes) |cpn| + self.substituteComptimeNodes(expr, cpn) catch expr + else + expr; + + // Step 2: Evaluate the expression to get a string + const code_str = self.evalComptimeString(substituted) orelse return self.builder.constInt(0, .void); + + // Step 3: Parse the string as sx code and lower each statement + // The last expression's value is captured as the return value + var p = parser_mod.Parser.init(self.alloc, code_str); + var last_val: Ref = self.builder.constInt(0, .void); + while (p.current.tag != .eof) { + const stmt = p.parseStmt() catch break; + if (p.current.tag == .eof) { + // Last statement — try to capture as expression value + // Note: tryLowerAsExpr internally calls lowerStmt for statement nodes, + // so we must NOT call lowerStmt again in the else branch. + if (self.tryLowerAsExpr(stmt)) |val| { + last_val = val; + } + } else { + self.lowerStmt(stmt); + } + } + return last_val; +} + +/// 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 { + // Case 1: String literal — return it directly (no need for interpreter) + if (expr.data == .string_literal) { + const lit = expr.data.string_literal; + const str = if (lit.is_raw) + lit.raw + else + unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; + return self.alloc.dupeZ(u8, str) catch null; + } + + // Case 2: Evaluate via IR interpreter, reusing the parent module. + // The parent's `scanDecls` pass has already registered every + // type / protocol / impl / thunk the comptime call may need + // (Allocator, CAllocator, Context, the per-impl thunks). A + // fresh empty module would only lazy-lower function ASTs and + // would miss the type/protocol registrations, which would break + // `context.allocator.X` — the protocol dispatch chain needs + // those types to resolve struct field layout and the alloc/ + // dealloc thunks at the bottom of the dispatch. + const ct_func_id = self.createComptimeFunction("__insert", expr, .string); + + 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); + + const result = interp.call(ct_func_id, &.{}) catch return null; + + const str = result.asString(&interp) orelse switch (result) { + .string => |s| s, + else => return null, + }; + + return self.alloc.dupeZ(u8, str) catch null; +} + +/// Lower the direct callee of a comptime expression into the ct module. +/// Transitive dependencies are resolved lazily via the shared fn_ast_map. +pub fn lowerComptimeDeps(self: *Lowering, ct: *Lowering, expr: *const Node) void { + if (expr.data != .call) return; + if (expr.data.call.callee.data != .identifier) return; + const name = expr.data.call.callee.data.identifier.name; + if (resolveBuiltin(name) != null) return; + if (self.program_index.fn_ast_map.get(name)) |fd| { + if (ct.resolveFuncByName(name) == null) { + ct.lowerFunction(fd, name, false); + } + } +} + +/// Substitute comptime parameter identifiers with their actual AST nodes. +pub fn substituteComptimeNodes(self: *Lowering, node: *const Node, cpn: std.StringHashMap(*const Node)) !*const Node { + // Direct identifier match + if (node.data == .identifier) { + if (cpn.get(node.data.identifier.name)) |replacement| { + return replacement; + } + } + + // Recurse into call arguments + if (node.data == .call) { + var changed = false; + const new_args = try self.alloc.alloc(*Node, node.data.call.args.len); + for (node.data.call.args, 0..) |arg, i| { + const substituted = try self.substituteComptimeNodes(arg, cpn); + new_args[i] = @constCast(substituted); + if (substituted != arg) changed = true; + } + if (changed) { + const new_node = try self.alloc.create(Node); + new_node.* = .{ + .span = node.span, + .data = .{ .call = .{ + .callee = node.data.call.callee, + .args = new_args, + } }, + }; + return new_node; + } + } + + return node; +} + +/// Lower a call to a function with comptime params by inlining its body. +/// Comptime params are substituted, `#insert` expressions are evaluated. +pub fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref { + // Build comptime param substitution map: param_name → call_site AST node + var cpn = std.StringHashMap(*const Node).init(self.alloc); + var call_arg_idx: usize = 0; + // Pack-arg-node registration (step 2 of the variadic heterogeneous + // type packs feature): when the fn declares a pack param, record + // the slice of call-site arg nodes under the pack name so the + // body's `args[$i]` lowering can substitute the i-th arg with + // its concrete-typed value instead of the `[]Any` slice load. + var pack_arg_name: ?[]const u8 = null; + var pack_arg_slice: []const *const Node = &.{}; + + for (fd.params) |param| { + if (param.is_variadic) { + // Variadic param: pack remaining call args into []Any slice + self.lowerVariadicArgs(param.name, call_node.args, call_arg_idx); + // Only heterogeneous pack form `..$args` (is_comptime AND + // is_variadic) registers for typed indexing. Plain + // `args: ..Any` keeps the existing []Any path so stdlib's + // `format`/`print` continue boxing through Any. + if (param.is_comptime and call_arg_idx <= call_node.args.len) { + pack_arg_name = param.name; + pack_arg_slice = call_node.args[call_arg_idx..]; + // Stamp each pack arg with the caller's source so the + // body's typed `args[i]` substitution (via packArgNodeAt, + // lowered under the defining-module pin set below) resolves + // its bare names in the CALLER's visibility context — the + // same treatment the fixed comptime params get below. + // Without it a caller-owned helper passed to an imported + // metaprogram (`std.print("{}", caller_fn())`) resolves + // under the callee's module and is reported "not visible". + for (call_node.args[call_arg_idx..]) |pack_arg| { + self.stampCallerSource(pack_arg); + } + } + break; // variadic is always the last param + } + if (call_arg_idx >= call_node.args.len) break; + if (param.is_comptime) { + self.stampCallerSource(call_node.args[call_arg_idx]); + cpn.put(param.name, call_node.args[call_arg_idx]) catch {}; + call_arg_idx += 1; + } else { + const arg_val = self.lowerExpr(call_node.args[call_arg_idx]); + const pty = self.resolveParamType(¶m); + const slot = self.builder.alloca(pty); + self.builder.store(slot, arg_val); + if (self.scope) |scope| { + scope.put(param.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); + } + call_arg_idx += 1; + } + } + + // Also bind comptime params as local string variables (for `fmt` used in runtime code) + var cpn_iter = cpn.iterator(); + while (cpn_iter.next()) |entry| { + const param_name = entry.key_ptr.*; + const param_node = entry.value_ptr.*; + if (param_node.data == .string_literal) { + // Create a local string variable with the literal value + const str_ref = self.lowerExpr(param_node); + const slot = self.builder.alloca(.string); + self.builder.store(slot, str_ref); + if (self.scope) |scope| { + scope.put(param_name, .{ .ref = slot, .ty = .string, .is_alloca = true }); + } + } + } + + // Install comptime param nodes and lower the function body inline + const saved_cpn = self.comptime_param_nodes; + self.comptime_param_nodes = cpn; + defer self.comptime_param_nodes = saved_cpn; + + // Install pack-arg-node binding. Mirrors `comptime_param_nodes`: + // each call owns its own map, nested calls shadow. `lowerIndexExpr` + // reads the map for `args[]` substitution. + const saved_pan = self.pack_arg_nodes; + var pan_map: std.StringHashMap([]const *const Node) = undefined; + var pan_installed = false; + if (pack_arg_name) |pn| { + pan_map = std.StringHashMap([]const *const Node).init(self.alloc); + pan_map.put(pn, pack_arg_slice) catch {}; + self.pack_arg_nodes = pan_map; + pan_installed = true; + } + defer { + if (pan_installed) pan_map.deinit(); + self.pack_arg_nodes = saved_pan; + } + + // Pin the lowering to the metaprogram's OWN module for the body (and + // its return type + anything it `#insert`s, e.g. `build_format` / `out` + // / `emit` inside `std.print` / `log.*`), so those bare names resolve + // in the defining module's visibility context rather than the call + // site's (issue 0106). The call-site ARGS above are deliberately lowered + // BEFORE this, in the caller's context. Mirrors `lowerFunctionBodyInto`, + // which switches to `func.source_file`. The defining path is stamped on + // the body node by `resolveImports`; a sourceless body keeps the + // caller's context. + const saved_source = self.current_source_file; + defer self.setCurrentSourceFile(saved_source); + if (fd.body.source_file) |src| self.setCurrentSourceFile(src); + + // Lower the body — capture return value for functions with return type + const ret_ty = self.resolveReturnType(fd); + if (ret_ty != .void) { + // Detect whether the body might use `return X;` statements. + // If so, set up the inline-return slot AND a dedicated + // "return-done" basic block so each `return X;` stores to + // the slot and branches to ret_done. After the body lowers, + // we switch to ret_done and load. Pure tail-expression + // bodies (arrow form, or a block whose last stmt is an + // expression) skip the slot+block — keeps the common + // `format`/`#insert`-style path unchanged. + const has_return = fnBodyHasReturn(fd.body); + if (has_return) { + const ret_slot = self.builder.alloca(ret_ty); + const ret_done_bb = self.freshBlock("ct.ret_done"); + const saved_iri = self.inline_return_target; + self.inline_return_target = .{ .slot = ret_slot, .ret_ty = ret_ty, .done_bb = ret_done_bb }; + defer self.inline_return_target = saved_iri; + + // Lower body. Tail-expression bodies (rare here since + // has_return == true) produce a tail value we still + // route through the slot so the load in ret_done picks + // it up. Block-statement bodies whose last stmt is + // `return X;` already br to ret_done from inside + // lowerReturn. + if (self.lowerBlockValue(fd.body)) |val| { + if (!self.currentBlockHasTerminator()) { + const v_ty = self.builder.getRefType(val); + const coerced = if (v_ty != ret_ty) + self.coerceToType(val, v_ty, ret_ty) + else + val; + self.builder.store(ret_slot, coerced); + self.builder.br(ret_done_bb, &.{}); + } + } else if (!self.currentBlockHasTerminator()) { + // Body fell through without producing a tail value + // AND without branching to ret_done — this only + // happens for bodies whose last stmt is a void + // statement (e.g. side-effecting). Slot is + // uninitialised on this path; safer to br anyway + // so the CFG is well-formed. The load in ret_done + // will read uninit, which is the same garbage + // behaviour the regular fn-body lowering would + // produce for a missing return. + self.builder.br(ret_done_bb, &.{}); + } + + self.builder.switchToBlock(ret_done_bb); + return self.builder.load(ret_slot, ret_ty); + } else { + if (self.lowerBlockValue(fd.body)) |val| { + return val; + } + } + } else { + self.lowerBlock(fd.body); + } + + return self.builder.constInt(0, .void); +} + +/// True if `node` (a fn body) contains any top-level `return` statement. +/// Used by inline-comptime lowering to decide whether to allocate a +/// result slot — pure tail-expression bodies skip the slot. Walks past +/// `if`/`while`/`for`/`match` arms (early-return inside a conditional +/// counts) but stops at nested fn/lambda bodies (those have their own +/// return contexts). +pub fn fnBodyHasReturn(node: *const Node) bool { + return switch (node.data) { + .return_stmt => true, + .block => |b| blk: { + for (b.stmts) |s| if (fnBodyHasReturn(s)) break :blk true; + break :blk false; + }, + .if_expr => |ie| blk: { + if (fnBodyHasReturn(ie.then_branch)) break :blk true; + if (ie.else_branch) |eb| if (fnBodyHasReturn(eb)) break :blk true; + break :blk false; + }, + .while_expr => |we| fnBodyHasReturn(we.body), + .for_expr => |fe| fnBodyHasReturn(fe.body), + .match_expr => |me| blk: { + for (me.arms) |arm| if (fnBodyHasReturn(arm.body)) break :blk true; + break :blk false; + }, + .defer_stmt => |ds| fnBodyHasReturn(ds.expr), + else => false, + }; +} + +/// 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 { + var buf: [64]u8 = undefined; + const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix; + self.comptime_counter += 1; + + // Save current builder + lowering state. The wrapper fn we're + // about to build runs the comptime expression in isolation — + // it must NOT inherit the enclosing call's `inline_return_target` + // (which would re-route a `return` inside the wrapper into a + // slot belonging to a different basic block), pack bindings + // (which would substitute caller's `args` inside the wrapper), + // or comptime-param bindings (which would substitute caller's + // `$fmt` inside the wrapper's #insert children). Without these + // saves, nested comptime calls leak outer state into the + // interp-executed wrapper, producing garbage stores (issue-0046 + // face 1 — storeAtRawPtr null). + const saved_func = self.builder.func; + const saved_block = self.builder.current_block; + const saved_counter = self.builder.inst_counter; + const saved_scope = self.scope; + const saved_ctx_ref = self.current_ctx_ref; + const saved_iri = self.inline_return_target; + const saved_pan = self.pack_arg_nodes; + const saved_ppc = self.pack_param_count; + const saved_pat = self.pack_arg_types; + const saved_cpn = self.comptime_param_nodes; + const saved_block_terminated = self.block_terminated; + const saved_target_type = self.target_type; + const saved_func_defer_base = self.func_defer_base; + self.inline_return_target = null; + self.pack_arg_nodes = null; + self.pack_param_count = null; + self.pack_arg_types = null; + self.comptime_param_nodes = null; + self.block_terminated = false; + self.target_type = null; + self.func_defer_base = self.defer_stack.items.len; + defer { + self.current_ctx_ref = saved_ctx_ref; + self.inline_return_target = saved_iri; + self.pack_arg_nodes = saved_pan; + self.pack_param_count = saved_ppc; + self.pack_arg_types = saved_pat; + self.comptime_param_nodes = saved_cpn; + self.block_terminated = saved_block_terminated; + self.target_type = saved_target_type; + self.func_defer_base = saved_func_defer_base; + } + + // Build params: implicit `__sx_ctx` at slot 0 when the program + // uses Context (so the body's `context.X` reads + transitive calls + // resolve cleanly). The comptime function's top-level invocation + // supplies `&__sx_default_context` (interp via callWithDefaultContext; + // codegen via the comptime-eval glue in emit_llvm). + const wants_ctx = self.implicit_ctx_enabled; + const params_slice = blk: { + if (!wants_ctx) break :blk &[_]Function.Param{}; + const owned = self.alloc.alloc(Function.Param, 1) catch break :blk &[_]Function.Param{}; + owned[0] = .{ + .name = self.module.types.internString("__sx_ctx"), + .ty = self.module.types.ptrTo(.void), + }; + break :blk owned; + }; + + // Create the comptime function + const name_id = self.module.types.internString(name); + const func_id = self.builder.beginFunction(name_id, params_slice, ret_ty); + + // Mark as comptime + has_implicit_ctx + const fn_mut = self.module.getFunctionMut(func_id); + fn_mut.is_comptime = true; + fn_mut.has_implicit_ctx = wants_ctx; + + // Create entry block + const entry_name = self.module.types.internString("entry"); + const entry = self.builder.appendBlock(entry_name, &.{}); + self.builder.switchToBlock(entry); + if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); + + // Create a scope that chains to the enclosing scope (so the + // expression can reference names visible at the #run site). + var ct_scope = Scope.init(self.alloc, saved_scope); + self.scope = &ct_scope; + + // Lower the expression and return it + const result = self.lowerExpr(expr); + if (ret_ty == .void) { + self.builder.retVoid(); + } else { + self.builder.ret(result, ret_ty); + } + + self.builder.finalize(); + + // Restore builder state + self.scope = saved_scope; + ct_scope.deinit(); + self.builder.func = saved_func; + self.builder.current_block = saved_block; + self.builder.inst_counter = saved_counter; + + return func_id; +} + +// ── Block helpers ─────────────────────────────────────────────── + +/// Resolve a name to a compile-time integer across the three const tables. +/// A comptime binding (generic value param / inline-for cursor) or a +/// `#run`/`OS`/`ARCH` comptime constant wins first; otherwise the name is a +/// SOURCE-AWARE module const, folded with nested leaves resolved own-wins. +pub fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 { + if (self.comptime_constants.get(name)) |cv| switch (cv) { + .int_val => |iv| return iv, + else => {}, + }; + if (self.comptime_value_bindings) |cvb| { + if (cvb.get(name)) |v| return v; + } + return self.foldSourceConstInt(name, null); +} + +/// Source-aware INTEGER fold of a module const `name` (E2/F2/R1). Select the +/// SOURCE-AWARE author (own-wins; ≥2 flat-visible → ambiguous → null, the loud +/// diagnostic is the reference site's job), then fold ITS RHS with nested const +/// leaves resolved through `SourceConstCtx` — each leaf re-selects its OWN +/// source author, NOT the global last-wins `module_const_map`. So a shadowed +/// `K :: M + 1` folds `M` to the SELECTED author's `M`, coherently whether `K` +/// is read as a value (`return K`) or used as an array dimension / count +/// (`[K]u8`). `frame` (keyed by name + author-source, F3) cycle-guards a const +/// whose value references another const. Single-author → byte-identical to the +/// legacy fold (the selected `ci` IS the global one and every nested leaf has +/// exactly one author). +pub fn foldSourceConstInt(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) ?i64 { + return switch (self.selectModuleConst(name)) { + .resolved => |sel| { + if (constFoldFrameContains(frame, name, sel.source)) return null; + if (!program_index_mod.isCountableConstType(&self.module.types, sel.info.ty)) return null; + var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; + const restore = self.pinConstAuthorSource(sel.source); + defer restore.unpin(); + return program_index_mod.evalConstIntExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); + }, + .ambiguous, .none => null, + }; +} + +/// Float counterpart of `foldSourceConstInt` (E2/F2/R1). +pub fn foldSourceConstFloat(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) ?f64 { + return switch (self.selectModuleConst(name)) { + .resolved => |sel| { + if (constFoldFrameContains(frame, name, sel.source)) return null; + if (!program_index_mod.isCountableConstType(&self.module.types, sel.info.ty)) return null; + var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; + const restore = self.pinConstAuthorSource(sel.source); + defer restore.unpin(); + return program_index_mod.evalConstFloatExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); + }, + .ambiguous, .none => null, + }; +} + +/// Source-aware "is `name` a FLOAT-valued module const" (E2/F2/R1): judge the +/// SELECTED author's value, with nested const leaves resolved source-aware. +pub fn sourceConstIsFloatTyped(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) bool { + return switch (self.selectModuleConst(name)) { + .resolved => |sel| { + if (constFoldFrameContains(frame, name, sel.source)) return false; + if (program_index_mod.isFloatConstType(sel.info.ty)) return true; + var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; + const restore = self.pinConstAuthorSource(sel.source); + defer restore.unpin(); + return program_index_mod.isFloatValuedExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); + }, + .ambiguous, .none => false, + }; +} + +/// A selected module const plus the SOURCE that authored it. `source` pins the +/// context in which the const's RHS leaves must be folded (F1): a same-name +/// `K :: M + 1` selected from author `a.sx` folds its nested `M` against `a.sx`, +/// not against whichever module read `K`. `source` is null only on the +/// fully-unwired fallback (no source partition at all), where the RHS resolves +/// through the global registration context unchanged. +pub const SelectedConst = struct { + info: ModuleConstInfo, + source: ?[]const u8, +}; + +const ConstAuthor = union(enum) { + resolved: SelectedConst, + ambiguous, + none, +}; + +/// The source-aware module-const author of `name` from the querying module +/// (E2/F2) — the value-const analogue of `selectNominalLeaf` (types) and +/// `selectPlainCallableAuthor` (functions). Selects over the ONE graph-walk +/// collector and reads the value from the SELECTED author's per-source cache +/// (`module_consts_by_source`), never the global last-wins `module_const_map`: +/// +/// - **own-wins**: the querying module's OWN const author is selected outright. +/// - else the FLAT-import-reachable const authors: exactly one → it; ≥2 distinct +/// → `.ambiguous` (issue 0105 / 0760 — never a silent first-/last-wins pick). +/// - none visible → `.none` (a namespaced-only const must be qualified `ns.X`; +/// a non-const name folds to `.none` too). +/// +/// A main-file body carries a null `current_source_file` (it IS the root), so +/// the querying module is `main_file` there; a fully unwired index (no source +/// at all) falls open to the global registration, byte-identical to the legacy +/// reader for the registration / comptime-host path. +pub fn selectModuleConst(self: *Lowering, name: []const u8) ConstAuthor { + const from = self.current_source_file orelse self.main_file orelse { + if (self.program_index.module_const_map.get(name)) |ci| return .{ .resolved = .{ .info = ci, .source = null } }; + return .none; + }; + var res = self.resolver(); + const set = res.collectVisibleAuthors(name, from, .user_bare_flat); + defer if (set.flat.len > 0) self.alloc.free(set.flat); + if (set.own) |o| if (self.sourceModuleConst(o.source, name)) |ci| return .{ .resolved = .{ .info = ci, .source = o.source } }; + var the_one: ?SelectedConst = null; + var count: usize = 0; + for (set.flat) |fa| { + const ci = self.sourceModuleConst(fa.source, name) orelse continue; + count += 1; + if (count >= 2) return .ambiguous; + the_one = .{ .info = ci, .source = fa.source }; + } + if (the_one) |sc| return .{ .resolved = sc }; + return .none; +} + +/// `source`'s per-source const cache entry for `name` (E0's +/// `module_consts_by_source` write side), or null. +pub fn sourceModuleConst(self: *Lowering, source: []const u8, name: []const u8) ?ModuleConstInfo { + const inner = self.program_index.module_consts_by_source.get(source) orelse return null; + return inner.get(name); +} + +/// Saved `current_source_file` for a const-author pin; `unpin()` restores it. +const ConstSourcePin = struct { + lowering: *Lowering, + saved: ?[]const u8, + active: bool, + pub fn unpin(self: ConstSourcePin) void { + if (self.active) self.lowering.setCurrentSourceFile(self.saved); + } +}; + +/// Pin `current_source_file` to a SELECTED const's AUTHOR source while its RHS +/// is folded / lowered, so nested same-name leaves resolve in the author's +/// visibility context (F1): `K :: M + 1` selected from `a.sx` always folds `M` +/// against `a.sx`, regardless of which module read `K`. A null author (the +/// fully-unwired fallback) leaves the context untouched. Single-author programs +/// pin to the source they were already in → byte-identical. +pub fn pinConstAuthorSource(self: *Lowering, source: ?[]const u8) ConstSourcePin { + if (source) |s| { + const saved = self.current_source_file; + self.setCurrentSourceFile(s); + return .{ .lowering = self, .saved = saved, .active = true }; + } + return .{ .lowering = self, .saved = self.current_source_file, .active = false }; +} + +/// Apply the unified float→int narrowing rule to a typed-binding initializer +/// EXPRESSION `node` whose declared type is `dst` (a typed local, a struct +/// field default, or a call argument incl. an expanded param default). When +/// `node` is a COMPILE-TIME float narrowing into an integer type: +/// - an INTEGRAL value (`4.0`, `M + 2.0`) folds to its `constInt`; +/// - a NON-integral value (`1.5`, `M + 0.5`) emits the narrowing +/// diagnostic and returns a placeholder so lowering finishes. +/// Returns null — so the caller lowers `node` normally — when the rule does +/// not apply: `dst` is not an integer, `node` is not statically float-typed, +/// or `node` is not a compile-time constant (a genuine runtime float keeps +/// truncating, and `xx` / `cast` keep their explicit-truncation escape since +/// a cast node's inferred type is the destination integer, not a float). +/// Reuses `program_index.evalConstIntExpr` (exact integral fold) + +/// `evalConstFloatExpr` (non-integral detection) + `floatToIntExact`. +pub fn foldComptimeFloatInit(self: *Lowering, node: *const Node, dst: TypeId) ?Ref { + if (!self.isIntEx(dst)) return null; + // PURE & side-effect-free, so it runs FIRST: a runtime / non-comptime / + // non-numeric node — incl. a `$pack[i]` index expression — folds to null + // and is left to the normal path untouched. (Calling `inferExprType` on + // a pack-index value before this guard would spuriously resolve the + // enclosing pack type outside an active binding.) + const fv = program_index_mod.evalConstFloatExpr(node, self) orelse return null; + // Only a FLOAT-flavored initializer narrows here; a plain comptime int + // (`5`, `M + 2`) is left to the normal integer path. Safe to infer now — + // `evalConstFloatExpr` only succeeds for literal / const-arithmetic + // nodes, never an unbound pack index. `inferExprType` is the primary + // signal, but it reads a const's DECLARED type — which is a placeholder + // `s64` for an untyped float-EXPRESSION const (`ME :: 4.0 + 1.0`), so + // `ME / 2` would look like integer division; `isFloatValuedExpr` (judging + // by VALUE) catches that case so it narrows under the unified rule too. + if (!isFloat(self.inferExprType(node)) and !program_index_mod.isFloatValuedExpr(node, self)) return null; + // Integral comptime float folds to its int (`floatToIntExact`, the same + // facility the array-dim / `$K: Count` paths use); a non-integral one is + // the narrowing error. + if (program_index_mod.floatToIntExact(fv)) |iv| return self.builder.constInt(iv, dst); + self.diagNonIntegralNarrow(node.span, fv, dst); + return self.builder.constInt(0, dst); +}