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 unescape = @import("../../unescape.zig"); const parser_mod = @import("../../parser.zig"); const interp_mod = @import("../interp.zig"); const program_index_mod = @import("../program_index.zig"); const ModuleConstInfo = program_index_mod.ModuleConstInfo; const TypeId = types.TypeId; const Ref = inst_mod.Ref; const FuncId = inst_mod.FuncId; const Function = inst_mod.Function; 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. 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. .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. .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. 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. 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; } // ── Source-const folding ──────────────────────────────────────── /// 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` (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); }