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.
This commit is contained in:
982
src/ir/lower.zig
982
src/ir/lower.zig
File diff suppressed because it is too large
Load Diff
977
src/ir/lower/comptime.zig
Normal file
977
src/ir/lower/comptime.zig
Normal 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(¶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[<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);
|
||||
}
|
||||
Reference in New Issue
Block a user