Merge branch 'arch-b' (phase B2: lower/comptime.zig)

This commit is contained in:
agra
2026-06-10 13:12:18 +03:00
2 changed files with 1021 additions and 938 deletions

File diff suppressed because it is too large Load Diff

977
src/ir/lower/comptime.zig Normal file
View File

@@ -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 `<pack>.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(&param);
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[<int_literal>]` 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);
}