From 54db29e60aedf401276a890826c5d5e3db2b1bad Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 10 Jun 2026 14:27:20 +0300 Subject: [PATCH] refactor(B7.1): move call lowering to lower/call.zig Verbatim relocation of the 18-method call cluster (lowerCall moved whole, context diagnostics, foreign-call helper, builtin/function resolution, generic + runtime-dispatch calls, reflection calls + guards, default-arg expansion, call param typing) into src/ir/lower/call.zig. 18 fn aliases keep all call sites unchanged. CaptureInfo (closure-domain type that sat inside the run) travelled and is re-exposed via a Lowering type alias; candidate to relocate to lower/closure.zig in B8.3. Method pub-flips: callResolver, createBareFnTrampoline, ensureGenericInstanceMethodLowered, fixupMethodReceiver, getStructTypeName, isStaticTypeArg, lowerPackFnCall, packSpreadRefs, packVariadicCallArgs, refCapturePointee, resolveParamTypeInSource, typeSizeBytes, headNameOfCallee. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn. --- src/ir/lower.zig | 2183 +--------------------------------------- src/ir/lower/call.zig | 2189 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2224 insertions(+), 2148 deletions(-) create mode 100644 src/ir/lower/call.zig diff --git a/src/ir/lower.zig b/src/ir/lower.zig index bd1a07f..80f82a7 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -43,6 +43,7 @@ const lower_protocol = @import("lower/protocol.zig"); const lower_coerce = @import("lower/coerce.zig"); const lower_ffi = @import("lower/ffi.zig"); const lower_objc_class = @import("lower/objc_class.zig"); +const lower_call = @import("lower/call.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -1001,7 +1002,7 @@ pub const Lowering = struct { /// If `node` names a `for xs: (*x)` by-ref capture (an `*elem`), returns /// the element (pointee) type so a value-position use can auto-deref it. - fn refCapturePointee(self: *Lowering, node: *const Node) ?TypeId { + pub fn refCapturePointee(self: *Lowering, node: *const Node) ?TypeId { if (node.data != .identifier) return null; const scope = self.scope orelse return null; const binding = scope.lookup(node.data.identifier.name) orelse return null; @@ -1647,7 +1648,7 @@ pub const Lowering = struct { /// If a method's first param expects a pointer (*T) but we're passing T by value, /// swap the first arg with the alloca address (implicit address-of). - fn fixupMethodReceiver(self: *Lowering, method_args: *std.ArrayList(Ref), func: *const Function, obj_node: *const Node, obj_ty: TypeId) void { + pub fn fixupMethodReceiver(self: *Lowering, method_args: *std.ArrayList(Ref), func: *const Function, obj_node: *const Node, obj_ty: TypeId) void { // Skip the implicit __sx_ctx param when inspecting the receiver slot. const skip: usize = if (func.has_implicit_ctx) 1 else 0; if (func.params.len <= skip) return; @@ -1704,7 +1705,7 @@ pub const Lowering = struct { } /// Get the name of a struct type (dereferencing pointers). Returns null for non-struct types. - fn getStructTypeName(self: *Lowering, ty: TypeId) ?[]const u8 { + pub fn getStructTypeName(self: *Lowering, ty: TypeId) ?[]const u8 { if (ty.isBuiltin()) { // Map builtin types to their names for method resolution (e.g., s64.eq) return builtinTypeName(ty); @@ -2063,7 +2064,7 @@ pub const Lowering = struct { /// (per-element projection) — return the per-element Refs to splice into a /// call's positional args. Null when it's not a pack spread (e.g. a runtime /// slice `..arr`, handled by the slice-variadic path). Caller owns the slice. - fn packSpreadRefs(self: *Lowering, operand: *const Node, span: ast.Span) ?[]Ref { + pub fn packSpreadRefs(self: *Lowering, operand: *const Node, span: ast.Span) ?[]Ref { const ppc = self.pack_param_count orelse return null; switch (operand.data) { .identifier => |id| { @@ -2979,1166 +2980,6 @@ pub const Lowering = struct { // ── FFI intrinsics (#objc_call / #jni_call / #jni_static_call) ─ - fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref { - var c = c_in; - // A bare reserved-type-name spelling in call position parses as a - // `.type_expr` (e.g. `s2(4)`), but if a function of that name is in - // scope — a backtick-declared sx fn or a `#import c` foreign fn whose C - // name collides with a reserved type spelling — it is a CALL to that - // function. `TypeName(val)` is not a cast (casts are `cast(T, val)`), so - // there is no ambiguity. Rewrite the callee to an identifier so the - // normal call machinery resolves it, symmetric to the bare-value - // reference that already resolves via scope/globals (issue 0089). - // - // Scoped to RAW provenance: only a backtick (`is_raw`) or `#import c` - // foreign fn declaration may legally carry a reserved-name spelling - // (the decl check rejects every bare reserved-name sx fn). Refusing the - // rewrite for a non-raw match keeps a genuine reserved type spelling a - // type — belt-and-suspenders should any future path ever reintroduce a - // non-raw reserved-name callee. - if (c.callee.data == .type_expr) { - const tname = c.callee.data.type_expr.name; - const eff = if (self.scope) |scope| scope.lookupFn(tname) orelse tname else tname; - const fd: ?*const ast.FnDecl = self.program_index.fn_ast_map.get(eff) orelse - self.program_index.fn_ast_map.get(tname); - if (fd) |decl| if (decl.is_raw) { - const id_node = self.alloc.create(Node) catch unreachable; - id_node.* = .{ .span = c.callee.span, .data = .{ .identifier = .{ .name = tname, .is_raw = true } } }; - const rewritten = self.alloc.create(ast.Call) catch unreachable; - rewritten.* = .{ .callee = id_node, .args = c.args }; - c = rewritten; - }; - } - // fix-0102 F2 / R5 §C: select the bare / value-UFCS same-name call author - // ONCE, via `CallResolver.selectedFreeAuthor` — the SINGLE producer of - // this verdict, the exact same one `CallResolver.plan` consumes for typing. - // The call-path consumers (default expansion, param typing, dispatch) all - // read THIS one author object, so plan-typing and lowering-dispatch can no - // longer disagree about which same-name function the call names, and the - // shadow's FuncId is materialized at most once (into `author_verdict`). - // `selectedFreeAuthor` is side-effect-free (it only runs the author - // selector — no return-type inference / type-arg resolution), so computing - // it eagerly here can't emit a premature diagnostic the way the full plan - // would. - var author_verdict = self.callResolver().selectedFreeAuthor(c); - const sel_author: ?*SelectedFunc = switch (author_verdict) { - .func => |*sf| sf, - else => null, - }; - const author_ambiguous = author_verdict == .ambiguous; - // Expand default parameter values for bare identifier callees: - // when the caller omits trailing positional args, fill them in - // from the callee's `param: T = expr` declarations. - if (self.expandCallDefaults(c, sel_author, author_ambiguous)) |expanded| c = expanded; - // Check reflection builtins first (before lowering args — some args are type names, not values) - if (c.callee.data == .identifier) { - if (self.tryLowerReflectionCall(c.callee.data.identifier.name, c)) |ref| return ref; - } - - // Check for runtime dispatch pattern BEFORE lowering args. - // lowerRuntimeDispatchCall handles its own arg lowering, and pre-lowering - // cast(type) val would produce a dead `call_builtin cast : void`. - if (c.callee.data == .identifier) { - const id_name = c.callee.data.identifier.name; - const eff_name = blk: { - const scoped = if (self.scope) |scope| scope.lookupFn(id_name) orelse id_name else id_name; - if (self.program_index.ufcs_alias_map.get(id_name)) |target| { - break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; - } - break :blk scoped; - }; - // C-import visibility: deny calls to C fn_decls not in the caller's module scope - if (!self.isCImportVisible(eff_name)) { - if (self.diagnostics) |d| - d.addFmt(.err, c.callee.span, "C function '{s}' not visible; add #import for the module that declares it", .{eff_name}); - return Ref.none; - } - // Non-transitive `#import` visibility check. Apply only when the - // user-typed name resolved as-is to a top-level fn — local-scope - // mangling (eff_name != id_name) and UFCS alias rewriting are - // compiler indirections and stay exempt. - if (std.mem.eql(u8, eff_name, id_name) and - self.program_index.ufcs_alias_map.get(id_name) == null and - self.program_index.fn_ast_map.contains(eff_name) and - !self.isNameVisible(eff_name)) - { - if (self.diagnostics) |d| - d.addFmt(.err, c.callee.span, "'{s}' is not visible; #import the module that declares it", .{eff_name}); - return Ref.none; - } - if (self.program_index.fn_ast_map.get(eff_name)) |fd| { - if (self.current_match_tags) |tags| { - if (tags.len > 0 and self.hasCastWithRuntimeType(c)) { - return self.lowerRuntimeDispatchCall(fd, eff_name, c, tags); - } - } - } - } - - // Handle closure(fn_or_lambda) — wrap bare functions into closures - if (c.callee.data == .identifier and std.mem.eql(u8, c.callee.data.identifier.name, "closure")) { - if (c.args.len >= 1) { - const arg = c.args[0]; - // If argument is a bare function name, create a proper closure from it - if (arg.data == .identifier) { - const fn_name = arg.data.identifier.name; - // fix-0102d site 2: `closure(fn)` over a genuine flat same-name - // collision must capture the RESOLVED author's FuncId, not the - // first-wins winner's. Plain bare name only; `.ambiguous` - // → loud diagnostic; `.none` → existing first-wins path. - const closure_fid: ?FuncId = blk_cl: { - if (self.program_index.ufcs_alias_map.get(fn_name) == null and - (if (self.scope) |scope| scope.lookup(fn_name) == null else true)) - { - if (self.current_source_file) |caller_file| { - switch (self.selectPlainCallableAuthor(fn_name, caller_file)) { - .func => |sf| { - var selected = sf; - break :blk_cl self.selectedFuncId(&selected, fn_name); - }, - .ambiguous => { - if (self.diagnostics) |d| - d.addFmt(.err, arg.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fn_name}); - return Ref.none; - }, - .none => {}, - } - } - } - if (!self.lowered_functions.contains(fn_name)) { - self.lazyLowerFunction(fn_name); - } - break :blk_cl self.resolveFuncByName(fn_name); - }; - if (closure_fid) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - // Build closure type from user-visible params only — - // skip the implicit __sx_ctx param. - var param_types_list = std.ArrayList(TypeId).empty; - defer param_types_list.deinit(self.alloc); - const skip: usize = if (func.has_implicit_ctx) 1 else 0; - for (func.params[skip..]) |p| { - param_types_list.append(self.alloc, p.ty) catch unreachable; - } - const closure_ty = self.module.types.closureType(param_types_list.items, func.ret); - const closure_info = self.module.types.get(closure_ty).closure; - const tramp_id = self.createBareFnTrampoline(fid, closure_info); - return self.builder.closureCreate(tramp_id, Ref.none, closure_ty); - } - } - // Lambda or other expression — already produces closure_create - return self.lowerExpr(arg); - } - } - - // Early detection of comptime-expanded calls (e.g. print) — skip arg evaluation - // since lowerComptimeCall re-evaluates args from AST (avoiding double evaluation) - if (c.callee.data == .identifier) { - const early_name = blk: { - const id_name = c.callee.data.identifier.name; - const scoped = if (self.scope) |scope| scope.lookupFn(id_name) orelse id_name else id_name; - if (self.program_index.ufcs_alias_map.get(id_name)) |target| { - break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; - } - break :blk scoped; - }; - // fix-0102 F2 / R5 §C: the early pack/comptime/generic dispatch reads - // the SAME author the call resolver SELECTED — not the first-wins - // winner — whenever a genuine flat same-name collision rerouted the - // call (`sel_author != null`). The selector only ever returns a plain - // free fn (`isPlainFreeFn` rejects type-params / comptime / pack), so - // `sel_author.decl` matches none of the arms below and the early path - // falls through to the main dispatch, which CONSUMES `sel_author` and - // binds that author. Without this the early path would dispatch the - // first-wins winner (e.g. a pack `(..$args)`) and disagree with the - // main dispatch — the selected plain author's bare call would invoke - // the wrong function. On the common path (`sel_author == null`) this - // reads the winner exactly as before — byte-identical, since the - // selector reroutes nothing there. - const early_fd: ?*const ast.FnDecl = if (sel_author) |sf| sf.decl else self.program_index.fn_ast_map.get(early_name); - if (early_fd) |fd| { - if (isPackFn(fd)) { - // Protocol packs (`..xs: P`) and comptime type-packs - // (`..$args`) both monomorphize per call shape. - return self.lowerPackFnCall(fd, c); - } - if (hasComptimeParams(fd)) { - return self.lowerComptimeCall(fd, c); - } - // Early detection of generic function calls — skip arg lowering for type params - // because lowerGenericCall resolves type params from AST nodes, not lowered refs. - // Only if the name is NOT shadowed by a local variable (closure, fn ptr, etc.). - // A selected author is never generic (`isPlainFreeFn` excludes - // `type_params > 0`), so this branch fires only on the winner. - const shadowed = if (self.scope) |scope| scope.lookup(c.callee.data.identifier.name) != null else false; - if (fd.type_params.len > 0 and !shadowed) { - // Types are explicit when call args match param count (e.g., are_equal(Point, p1, p2)) - // Types are inferred when call args < param count (e.g., are_equal(p1, p2)) - const types_explicit = c.args.len == fd.params.len; - var lowered_args = std.ArrayList(Ref).empty; - defer lowered_args.deinit(self.alloc); - for (c.args, 0..) |arg, ai| { - // Skip type param args only when types are passed explicitly - if (types_explicit and ai < fd.params.len and isTypeParamDecl(&fd.params[ai], fd.type_params)) { - lowered_args.append(self.alloc, Ref.none) catch unreachable; - } else { - const saved_target = self.target_type; - lowered_args.append(self.alloc, self.lowerExpr(arg)) catch unreachable; - self.target_type = saved_target; - } - } - return self.lowerGenericCall(fd, early_name, c, lowered_args.items); - } - } - } - - // Lower args (with target type propagation for xx conversions) - var args = std.ArrayList(Ref).empty; - defer args.deinit(self.alloc); - // Try to resolve param types for target_type context - const param_types = self.resolveCallParamTypes(c, sel_author); - // For enum_literal callees (.Variant(payload)), resolve the payload target type - // from the union field type so struct literal fields get proper coercion - var enum_payload_ty: ?TypeId = null; - if (c.callee.data == .enum_literal) { - const target = self.target_type orelse .unresolved; - if (!target.isBuiltin()) { - const info = self.module.types.get(target); - if (info == .tagged_union) { - const tag = self.resolveVariantIndex(target, c.callee.data.enum_literal.name); - if (tag < info.tagged_union.fields.len) { - enum_payload_ty = info.tagged_union.fields[tag].ty; - } - } - } - } - for (c.args, 0..) |arg, ai| { - if (arg.data == .spread_expr) { - // Pack spread `..xs` / `..xs.method` → expand to N positional - // args here. A runtime-slice spread (`..arr`) is left as a - // placeholder for the slice-variadic path (packVariadicCallArgs). - if (self.packSpreadRefs(arg.data.spread_expr.operand, arg.span)) |elems| { - defer self.alloc.free(elems); - for (elems) |e| args.append(self.alloc, e) catch unreachable; - continue; - } - args.append(self.alloc, Ref.none) catch unreachable; - continue; - } - const saved_target = self.target_type; - if (ai < param_types.len) { - self.target_type = param_types[ai]; - } - if (enum_payload_ty) |ept| { - if (ai == 0) self.target_type = ept; - } - // Implicit float→int narrowing of a compile-time float argument - // (incl. an expanded `param: T = expr` default) follows the unified - // rule: an integral comptime float folds, a non-integral one errors. - // A runtime float / `xx` cast is unaffected and coerces as before. - if (ai < param_types.len) { - if (self.foldComptimeFloatInit(arg, param_types[ai])) |folded| { - args.append(self.alloc, folded) catch unreachable; - self.target_type = saved_target; - continue; - } - } - // Implicit address-of: when param expects *T and arg is an identifier - // with an alloca of type T, pass the alloca pointer directly (reference - // semantics, so mutations through the pointer are visible to the caller). - if (ai < param_types.len and arg.data == .identifier) { - const pt = param_types[ai]; - if (!pt.isBuiltin()) { - const pti = self.module.types.get(pt); - if (pti == .pointer) { - if (self.scope) |scope| { - if (scope.lookup(arg.data.identifier.name)) |binding| { - // Only apply when the binding type matches the pointee type - if (binding.is_alloca and binding.ty == pti.pointer.pointee) { - const ptr_ty = self.module.types.ptrTo(binding.ty); - args.append(self.alloc, self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty)) catch unreachable; - self.target_type = saved_target; - continue; - } - } - } - } - } - } - // Implicit address-of for compound lvalues (field access / index / - // deref): when the param expects `*T` and the arg is an addressable - // lvalue of type `T`, pass the lvalue's real address (GEP) — same - // reference semantics as the identifier case above. Without this the - // arg would be loaded into a temporary and the callee would mutate a - // throwaway copy (silent data loss — e.g. `make_move(self.board, m)`). - if (ai < param_types.len and (arg.data == .field_access or arg.data == .index_expr or arg.data == .deref_expr)) { - const pt = param_types[ai]; - if (!pt.isBuiltin()) { - const pti = self.module.types.get(pt); - if (pti == .pointer and self.inferExprType(arg) == pti.pointer.pointee) { - // `lowerExprAsPtr` yields the lvalue's address, typed - // either as `*T` already (index/deref) or as the pointee - // `T` (a field "place" ref); normalize to `*T` — exactly - // what `@field_access` does. - const place = self.lowerExprAsPtr(arg); - const place_ty = self.builder.getRefType(place); - const ref: ?Ref = if (place_ty == pt) - place - else if (place_ty == pti.pointer.pointee) - self.builder.emit(.{ .addr_of = .{ .operand = place } }, pt) - else - null; - if (ref) |r| { - args.append(self.alloc, r) catch unreachable; - self.target_type = saved_target; - continue; - } - } - } - } - const val = self.lowerExpr(arg); - self.target_type = saved_target; - // Passing a `*T` where a `T` value is expected — a by-reference loop - // capture (`for xs: (*m)`), a `*T` parameter, or any pointer local — - // otherwise slips through to LLVM as an opaque "call parameter type - // does not match function signature" verifier error. Flag it at the - // call site with a `.*` fix-it. - if (ai < param_types.len) { - const vt = self.builder.getRefType(val); - const vti = self.module.types.get(vt); - if (vti == .pointer and vti.pointer.pointee == param_types[ai]) { - if (self.diagnostics) |d| { - const tn = self.formatTypeName(param_types[ai]); - if (arg.data == .identifier) { - const nm = arg.data.identifier.name; - const lead: []const u8 = if (self.refCapturePointee(arg) != null) "by-reference loop capture" else "argument"; - const fix = std.fmt.allocPrint(self.alloc, "{s}.*", .{nm}) catch nm; - const pid = d.addFmtId(.err, arg.span, "{s} '{s}' has type '*{s}', but '{s}' is expected here", .{ lead, nm, tn, tn }); - d.addHelpFmt(pid, arg.span, fix, "dereference it to pass the value: `{s}`", .{fix}); - } else { - const pid = d.addFmtId(.err, arg.span, "this argument has type '*{s}', but '{s}' is expected here", .{ tn, tn }); - d.addHelpFmt(pid, arg.span, null, "dereference it with `.*` to pass the value", .{}); - } - } - } - } - args.append(self.alloc, val) catch unreachable; - } - - switch (c.callee.data) { - .identifier => |id| { - // Resolve local function name (bare → mangled) and UFCS aliases - const func_name = blk: { - // First try scope lookup for mangled local fn names - const scoped = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name; - // Then try UFCS alias on bare name - if (self.program_index.ufcs_alias_map.get(id.name)) |target| { - // Resolve the alias target through scope too (target may be mangled) - break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; - } - break :blk scoped; - }; - - // Handle cast(TargetType, val) — emit conversion instructions - // Only for compile-time known types (type_expr or known type names) - if (std.mem.eql(u8, id.name, "cast") and c.args.len >= 2) { - const type_arg = c.args[0]; - const is_static_type = blk: { - if (type_arg.data == .type_expr) break :blk true; - if (type_arg.data == .identifier) { - const tname = type_arg.data.identifier.name; - // Check if it's a known type name (not a runtime variable) - if (type_bridge.resolveTypePrimitive(tname) != null) break :blk true; - if (self.type_bindings) |bindings| { - if (bindings.get(tname) != null) break :blk true; - } - // Check if it's a registered struct/enum type name - const name_id = self.module.types.internString(tname); - if (self.module.types.findByName(name_id) != null) break :blk true; - } - break :blk false; - }; - if (is_static_type) { - const dst_ty = self.resolveTypeArg(c.args[0]); - const val = args.items[1]; // already lowered - const src_ty = self.inferExprType(c.args[1]); - // Unbox Any → concrete type - if (src_ty == .any) { - return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty); - } - return self.coerceExplicit(val, src_ty, dst_ty); - } - // Runtime cast — fall through to builtin handling - } - // Check builtins first (these are handled natively by interpreter and emitter) - if (resolveBuiltin(id.name)) |bid| { - const ret_ty: TypeId = switch (bid) { - .size_of, .align_of => .s64, - .sqrt, .sin, .cos, .floor => blk: { - // Math builtins: return type matches argument type ($T -> T) - if (c.args.len > 0) { - const arg_ty = self.inferExprType(c.args[0]); - if (arg_ty == .f32) break :blk TypeId.f32; - } - break :blk TypeId.f64; - }, - else => .void, - }; - return self.builder.callBuiltin(bid, args.items, ret_ty); - } - // Check scope first: local variables (closures, fn ptrs) shadow global functions - if (self.scope) |scope| { - if (scope.lookup(id.name)) |binding| { - if (!binding.ty.isBuiltin()) { - const ty_info = self.module.types.get(binding.ty); - if (ty_info == .closure) { - const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref; - // Closure trampolines carry `__sx_ctx` at - // slot 0; emit_llvm's `call_closure` builds - // the call as [ctx, env, user_args], so we - // prepend ctx here. args[0] becomes ctx. - const owned = if (self.implicit_ctx_enabled) blk: { - const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable; - arr[0] = self.current_ctx_ref; - @memcpy(arr[1..], args.items); - break :blk arr; - } else self.alloc.dupe(Ref, args.items) catch unreachable; - const ret_ty = ty_info.closure.ret; - return self.builder.emit(.{ .call_closure = .{ .callee = callee_ref, .args = owned } }, ret_ty); - } - } - } - } - // fix-0102c / R5 §C: a genuine flat same-name collision — bind the - // author the call resolver selected (own-author-wins, or the single - // flat-reachable author), or reject a bare call to a name ≥2 - // imported modules author. `selectedFreeAuthor` (computed once - // above, and the exact verdict `plan` consumes for typing) is the - // single producer; lowering CONSUMES it rather than re-resolving - // the name, so typing and dispatch read the SAME author and can't - // disagree (fix-0102 F2). Reached only for an identifier callee, so - // `sel_author` / `author_ambiguous` here are the bare verdict. - if (author_ambiguous) { - if (self.diagnostics) |d| - d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{func_name}); - return Ref.none; - } - if (sel_author) |sf| { - const fid = self.selectedFuncId(sf, func_name); - const func = &self.module.functions.items[@intFromEnum(fid)]; - const ret_ty = func.ret; - const params = func.params; - // The RESOLVED author's decl drives variadic packing — not a - // first-wins re-lookup by name, whose variadic shape may - // differ (fix-0102c F1). - self.packVariadicCallArgs(sf.decl, c, &args); - const final_args = self.prependCtxIfNeeded(func, args.items); - self.coerceCallArgs(final_args, params); - if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len); - return self.builder.call(fid, final_args, ret_ty); - } - // Check for comptime-expanded or generic functions - if (self.program_index.fn_ast_map.get(func_name)) |fd| { - if (hasComptimeParams(fd)) { - return self.lowerComptimeCall(fd, c); - } - if (fd.type_params.len > 0) { - // Runtime dispatch already handled above (before arg lowering) - return self.lowerGenericCall(fd, func_name, c, args.items); - } - } - // Check for #compiler free functions - if (self.program_index.fn_ast_map.get(func_name)) |fd_check| { - if (fd_check.body.data == .compiler_expr) { - const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) else TypeId.void; - return self.builder.compilerCall(func_name, args.items, ret_ty); - } - } - - // Look up declared/extern function — try lazy lowering if not yet lowered - { - // First attempt: function may already be declared (from scanDecls) - // but not yet lowered. Try lazy lowering if needed. - if (self.program_index.fn_ast_map.contains(func_name) and !self.lowered_functions.contains(func_name)) { - self.lazyLowerFunction(func_name); - } - if (self.resolveFuncByName(func_name)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - const ret_ty = func.ret; - const params = func.params; - // Pack variadic args into a slice if the function has a variadic param - if (self.program_index.fn_ast_map.get(func_name)) |fd| { - self.packVariadicCallArgs(fd, c, &args); - } - const final_args = self.prependCtxIfNeeded(func, args.items); - // Coerce arguments to match parameter types - self.coerceCallArgs(final_args, params); - if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len); - return self.builder.call(fid, final_args, ret_ty); - } - } - // May be a variable holding a function pointer (non-closure) - if (self.scope) |scope| { - if (scope.lookup(id.name)) |binding| { - const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref; - const ret_ty = if (!binding.ty.isBuiltin()) blk: { - const bti = self.module.types.get(binding.ty); - break :blk if (bti == .function) bti.function.ret else .s64; - } else .s64; - var final_args = std.ArrayList(Ref).empty; - defer final_args.deinit(self.alloc); - if (self.fnPtrTypeWantsCtx(binding.ty)) { - final_args.append(self.alloc, self.current_ctx_ref) catch unreachable; - } - final_args.appendSlice(self.alloc, args.items) catch unreachable; - const owned = self.alloc.dupe(Ref, final_args.items) catch unreachable; - return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, ret_ty); - } - } - // May be a global variable holding a function pointer - if (self.program_index.global_names.get(id.name)) |gi| { - if (!gi.ty.isBuiltin()) { - const gti = self.module.types.get(gi.ty); - if (gti == .function) { - const callee_ref = self.builder.emit(.{ .global_get = gi.id }, gi.ty); - // Coerce args to match fn-ptr param types (including implicit address-of) - for (args.items, 0..) |*arg, ai| { - if (ai < gti.function.params.len) { - const dst_ty = gti.function.params[ai]; - const src_ty = self.inferExprType(c.args[ai]); - // Implicit address-of: passing T where *T expected - if (!dst_ty.isBuiltin()) { - const dti = self.module.types.get(dst_ty); - if (dti == .pointer and dti.pointer.pointee == src_ty and src_ty != .void) { - // For identifier args, pass the alloca directly (reference semantics) - if (c.args[ai].data == .identifier) { - if (self.scope) |scope| { - if (scope.lookup(c.args[ai].data.identifier.name)) |binding| { - if (binding.is_alloca) { - arg.* = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, dst_ty); - continue; - } - } - } - } - // For other expressions, copy semantics - const slot = self.builder.alloca(src_ty); - self.builder.store(slot, arg.*); - arg.* = slot; - continue; - } - } - arg.* = self.coerceToType(arg.*, src_ty, dst_ty); - } - } - var final_args = std.ArrayList(Ref).empty; - defer final_args.deinit(self.alloc); - if (self.fnPtrTypeWantsCtx(gi.ty)) { - final_args.append(self.alloc, self.current_ctx_ref) catch unreachable; - } - final_args.appendSlice(self.alloc, args.items) catch unreachable; - const owned = self.alloc.dupe(Ref, final_args.items) catch unreachable; - return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, gti.function.ret); - } - } - } - // Unresolved function call - return self.emitError(id.name, c.callee.span); - }, - .field_access => |fa| { - // `super.method(args)` from inside a `#jni_main` (or any - // sx-defined `#jni_class`) bodied method. Dispatch via - // CallNonvirtualMethod against the parent class - // resolved from the enclosing fcd's `#extends` clause. - if (fa.object.data == .identifier and - std.mem.eql(u8, fa.object.data.identifier.name, "super")) - { - return self.lowerSuperCall(fa.field, args.items, c.callee.span); - } - - // `Alias.method(args)` where Alias is a foreign-class - // identifier and `method` is a `static` member — JNI - // dispatch via FindClass + GetStaticMethodID + CallStatic*, - // OR (for `new`) via FindClass + GetMethodID("") + - // NewObject. Falls through to existing paths when no match. - if (fa.object.data == .identifier) { - const alias = fa.object.data.identifier.name; - if (self.program_index.foreign_class_map.get(alias)) |fcd| { - for (fcd.members) |m| switch (m) { - .method => |md| if (md.is_static and std.mem.eql(u8, md.name, fa.field)) { - return self.lowerForeignStaticCall(fcd, md, args.items, c.callee.span); - }, - else => {}, - }; - } - } - - // Type constructor call: Sx(f32).user(0.5) — obj is a call that returns a type - if (fa.object.data == .call) { - const inner_call = &fa.object.data.call; - // Generic struct STATIC-METHOD head (`Box(s64).make(..)` or the - // qualified `a.Box(s64).make(..)`): the layout author is chosen - // by the single head choke-point (CP-1) and the method body by - // the instance's STAMPED author (CP-4), so layout-author ≡ - // body-author for BOTH bare and qualified heads (E4 #1 / #2). - if (headNameOfCallee(inner_call.callee)) |hn| { - switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, inner_call.callee.span)) { - .poisoned => return Ref.none, - .template => |t| { - const inst_ty = self.instantiateGenericStruct(&t, inner_call.args); - const inst_name = self.formatTypeName(inst_ty); - if (self.genericInstanceMethod(inst_name, fa.field)) |gm| { - if (self.ensureGenericInstanceMethodLowered(gm)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - const final_args = self.prependCtxIfNeeded(func, args.items); - self.coerceCallArgs(final_args, func.params); - return self.builder.call(fid, final_args, func.ret); - } - } - }, - .not_generic => {}, - } - } - - if (inner_call.callee.data == .identifier) { - const inner_name = inner_call.callee.data.identifier.name; - const resolved = if (self.scope) |scope| (scope.lookupFn(inner_name) orelse inner_name) else inner_name; - - if (self.program_index.fn_ast_map.get(resolved)) |fd| { - if (fd.type_params.len > 0) { - if (self.headFnLeak(inner_name, inner_call.callee.span)) return Ref.none; - // Try instantiate as type function - if (self.instantiateTypeFunction(inner_name, inner_name, fd, inner_call.args)) |result_ty| { - const type_info = self.module.types.get(result_ty); - if (type_info == .tagged_union) { - // Qualified enum construction: Type.variant(payload) - const tag = self.resolveVariantIndex(result_ty, fa.field); - var payload = if (args.items.len > 0) args.items[0] else Ref.none; - if (!payload.isNone()) { - const fields = type_info.tagged_union.fields; - if (tag < fields.len) { - const field_ty = fields[tag].ty; - if (field_ty != .void) { - const payload_ty = self.inferExprType(c.args[0]); - if (field_ty != payload_ty) { - payload = self.coerceToType(payload, payload_ty, field_ty); - } - } - } - } - return self.builder.enumInit(tag, payload, result_ty); - } - if (type_info == .@"enum") { - const tag = self.resolveVariantIndex(result_ty, fa.field); - return self.builder.enumInit(tag, Ref.none, result_ty); - } - } - } - } - } - } - - // Namespace-qualified call (e.g. `std.print`) vs method / UFCS - // call on a value (`recv.method`). This boundary decides whether - // the receiver is prepended, so it MUST agree with the call - // plan's `free_fn_ufcs` (prepends) vs `namespace_fn` (does not) - // classification — source it from the single definition in - // `CallResolver` rather than re-deriving it here. - const is_namespace = !self.callResolver().objectIsValue(fa.object); - - if (is_namespace) { - // Namespace call: module.func(args) — don't prepend object - const func_name = fa.field; - // Also try qualified name: Namespace.method (for struct methods) - const ns_name: ?[]const u8 = switch (fa.object.data) { - .identifier => |id| id.name, - .type_expr => |te| te.name, - else => null, - }; - const qualified_name = if (ns_name) |n| - std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ n, fa.field }) catch func_name - else - func_name; - // Check for comptime-expanded or generic functions (try both names) - const effective_name = if (self.program_index.fn_ast_map.get(qualified_name) != null) qualified_name else func_name; - if (self.program_index.fn_ast_map.get(effective_name)) |fd| { - if (hasComptimeParams(fd)) { - return self.lowerComptimeCall(fd, c); - } - if (fd.type_params.len > 0) { - return self.lowerGenericCall(fd, effective_name, c, args.items); - } - } - if (self.program_index.fn_ast_map.contains(effective_name) and !self.lowered_functions.contains(effective_name)) { - self.lazyLowerFunction(effective_name); - } - if (self.resolveFuncByName(effective_name)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - const ret_ty = func.ret; - const params = func.params; - if (self.program_index.fn_ast_map.get(effective_name)) |fd| { - self.packVariadicCallArgs(fd, c, &args); - } - const final_args = self.prependCtxIfNeeded(func, args.items); - self.coerceCallArgs(final_args, params); - if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len); - return self.builder.call(fid, final_args, ret_ty); - } - // Check if this is Type.variant(payload) — qualified enum construction - if (ns_name) |type_name| { - const type_name_id = self.module.types.internString(type_name); - if (self.module.types.findByName(type_name_id)) |union_ty| { - const type_info = self.module.types.get(union_ty); - if (type_info == .tagged_union) { - const tag = self.resolveVariantIndex(union_ty, func_name); - var payload = if (args.items.len > 0) args.items[0] else Ref.none; - // Coerce payload to match field type - if (!payload.isNone()) { - const fields = type_info.tagged_union.fields; - if (tag < fields.len) { - const field_ty = fields[tag].ty; - const payload_ty = self.inferExprType(c.args[0]); - if (field_ty != payload_ty) { - payload = self.coerceToType(payload, payload_ty, field_ty); - } - } - } - return self.builder.enumInit(tag, payload, union_ty); - } - if (type_info == .@"enum") { - const tag = self.resolveVariantIndex(union_ty, func_name); - return self.builder.enumInit(tag, Ref.none, union_ty); - } - } - } - return self.emitError(func_name, c.callee.span); - } - - // Method call: obj.method(args) → prepend obj (or &obj for *Self receivers) - // For ptr.*.method(): pass the pointer directly instead of loading + re-addressing. - // This ensures mutations through self: *T are visible after the call. - var obj_ty: TypeId = undefined; - var obj: Ref = undefined; - var effective_obj_node: *const Node = fa.object; - if (fa.object.data == .deref_expr) { - effective_obj_node = fa.object.data.deref_expr.operand; - obj_ty = self.inferExprType(effective_obj_node); - obj = self.lowerExpr(effective_obj_node); - } else { - obj_ty = self.inferExprType(fa.object); - obj = self.lowerExpr(fa.object); - } - - // Check if field is a closure type — call as closure, not method - if (!obj_ty.isBuiltin()) { - const field_name_id = self.module.types.internString(fa.field); - const struct_fields = self.getStructFields(obj_ty); - for (struct_fields, 0..) |f, fi| { - if (f.name == field_name_id and !f.ty.isBuiltin()) { - const fti = self.module.types.get(f.ty); - if (fti == .closure) { - // structGet requires an aggregate value; if obj is *T, load through it first. - var agg = obj; - const oi = self.module.types.get(obj_ty); - if (oi == .pointer) { - agg = self.builder.load(obj, oi.pointer.pointee); - } - const closure_val = self.builder.structGet(agg, @intCast(fi), f.ty); - // Prepend ctx for sx-side closure call ABI. - const owned = if (self.implicit_ctx_enabled) blk: { - const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable; - arr[0] = self.current_ctx_ref; - @memcpy(arr[1..], args.items); - break :blk arr; - } else self.alloc.dupe(Ref, args.items) catch unreachable; - return self.builder.emit(.{ .call_closure = .{ .callee = closure_val, .args = owned } }, fti.closure.ret); - } - } - } - } - - // Check if receiver is a protocol type → dispatch through vtable/fn_ptrs - if (self.getProtocolInfo(obj_ty)) |proto_info| { - return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, obj_ty); - } - - // Check if receiver is `?Protocol` — for sentinel-shaped - // optionals (Protocol has ctx as first ptr field, and a - // null ctx is the "none" state) the unwrap is a no-op - // structurally. Treat the optional value as the protocol - // value and dispatch. Calling a method on a null protocol - // is undefined (same as derefing a null pointer); user - // guards with `if x != null` first. - if (!obj_ty.isBuiltin()) { - const opt_info = self.module.types.get(obj_ty); - if (opt_info == .optional) { - const pay_ty = opt_info.optional.child; - if (self.getProtocolInfo(pay_ty)) |proto_info| { - return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, pay_ty); - } - } - } - - var method_args = std.ArrayList(Ref).empty; - defer method_args.deinit(self.alloc); - method_args.append(self.alloc, obj) catch unreachable; - for (args.items) |a| { - method_args.append(self.alloc, a) catch unreachable; - } - - // Foreign-class DSL: `inst.method(args)` where `inst`'s - // type is an alias declared by `#jni_class("...") { ... }` - // (or its parallel forms). Routes to the JNI dispatch - // shape, descriptor derived from the sx signature. - const struct_name = self.getStructTypeName(obj_ty); - if (struct_name) |sname_for_foreign| { - if (self.program_index.foreign_class_map.get(sname_for_foreign)) |fcd| { - return self.lowerForeignMethodCall(fcd, fa.field, obj, args.items, c.callee.span); - } - } - - // Try to resolve the method by struct type name - if (struct_name) |sname| { - // Try direct qualified name: StructName.method - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field; - - // Generic #compiler method dispatch - if (self.program_index.fn_ast_map.get(qualified)) |method_fd| { - if (method_fd.body.data == .compiler_expr) { - const ret_ty = if (method_fd.return_type) |rt| - type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) - else - .void; - return self.builder.compilerCall(qualified, method_args.items, ret_ty); - } - } - - // Generic-struct instance method: select the body via the - // instance's STAMPED author (CP-4), so the dispatched method is - // the one authored alongside this instance's layout — never the - // global last-wins `fn_ast_map["Template.method"]`. - if (self.genericInstanceMethod(sname, fa.field)) |gm| { - if (self.ensureGenericInstanceMethodLowered(gm)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - const ret_ty = func.ret; - const params = func.params; - self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); - self.appendDefaultArgs(gm.fd, &method_args); - const final_args = self.prependCtxIfNeeded(func, method_args.items); - self.coerceCallArgs(final_args, params); - return self.builder.call(fid, final_args, ret_ty); - } - } - - // Generic method on a non-template struct: `obj.method($T, ...)` - // or inferred form `obj.method(val)` where val's type pins $T. - if (self.program_index.fn_ast_map.get(qualified)) |gen_fd| { - if (gen_fd.type_params.len > 0 and gen_fd.body.data != .compiler_expr) { - // Effective AST args: prepend receiver so positions - // line up with fd.params (which has self at index 0). - var eff_args = std.ArrayList(*const Node).empty; - defer eff_args.deinit(self.alloc); - eff_args.append(self.alloc, effective_obj_node) catch unreachable; - for (c.args) |a| eff_args.append(self.alloc, a) catch unreachable; - - var gbindings = self.genericResolver().buildTypeBindings(gen_fd, eff_args.items); - defer gbindings.deinit(); - - const gmangled = self.genericResolver().mangleGenericName(qualified, gen_fd, &gbindings); - if (!self.lowered_functions.contains(gmangled)) { - self.monomorphizeFunction(gen_fd, gmangled, &gbindings); - } - if (self.resolveFuncByName(gmangled)) |gfid| { - const gfunc = &self.module.functions.items[@intFromEnum(gfid)]; - const gret_ty = gfunc.ret; - const gparams = gfunc.params; - // Strip type-decl slots from method_args. method_args[0] is the - // receiver (corresponds to fd.params[0] = self, never a type decl). - // Walk fd.params[1..], advance arg_idx through method_args[1..]. - var gvalue_args = std.ArrayList(Ref).empty; - defer gvalue_args.deinit(self.alloc); - gvalue_args.append(self.alloc, method_args.items[0]) catch unreachable; - const types_explicit = method_args.items.len == gen_fd.params.len; - var arg_idx: usize = 1; - for (gen_fd.params[1..]) |p| { - if (isTypeParamDecl(&p, gen_fd.type_params)) { - if (types_explicit) arg_idx += 1; - continue; - } - if (arg_idx < method_args.items.len) { - gvalue_args.append(self.alloc, method_args.items[arg_idx]) catch unreachable; - } - arg_idx += 1; - } - self.fixupMethodReceiver(&gvalue_args, gfunc, effective_obj_node, obj_ty); - const final_args = self.prependCtxIfNeeded(gfunc, gvalue_args.items); - self.coerceCallArgs(final_args, gparams); - return self.builder.call(gfid, final_args, gret_ty); - } - } - } - - // Try non-generic qualified method - if (self.program_index.fn_ast_map.get(qualified)) |fd| { - if (!self.lowered_functions.contains(qualified)) { - self.lazyLowerFunction(qualified); - } - _ = fd; - } - if (self.resolveFuncByName(qualified)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - const ret_ty = func.ret; - const params = func.params; - const has_ctx = func.has_implicit_ctx; - self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); - // Note: coerceCallArgs can trigger protocol thunk creation - // (module.addFunction), invalidating func pointer. - // Use pre-extracted params/ret_ty (+ has_ctx) instead of - // func.* after this. - const final_args = blk: { - if (!has_ctx) break :blk method_args.items; - const new_args = self.alloc.alloc(Ref, method_args.items.len + 1) catch break :blk method_args.items; - new_args[0] = self.current_ctx_ref; - @memcpy(new_args[1..], method_args.items); - break :blk new_args; - }; - self.coerceCallArgs(final_args, params); - return self.builder.call(fid, final_args, ret_ty); - } - } - - // Try to resolve as bare function name (free-function UFCS: - // `recv.fn(args)` → `fn(recv, args)`). Lazily lower the body — - // a function reached ONLY via UFCS would otherwise be declared - // but never emitted (issue 0063: undefined symbol at link). - // - // fix-0102d site 3 / R5 §C: a free-function UFCS target with a - // genuine flat same-name collision dispatches to the author the - // call PLAN selected for the receiver's source — the SAME author - // plan typed the call's result as, so dispatch and typing can't - // disagree (fix-0102 F2; without this, a string-typed winner over - // an s64 shadow boxes a raw int as a string pointer → segfault). - // The plan is the single producer; lowering consumes its verdict - // (`sel_author` / `cplan.ambiguous_collision`, computed once above) - // rather than re-resolving the field name. `.ambiguous` → loud - // diagnostic; otherwise the existing first-wins lazy path. - const ufcs_fid: ?FuncId = blk_uf: { - if (author_ambiguous) { - if (self.diagnostics) |d| - d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field}); - return Ref.none; - } - if (sel_author) |sf| { - break :blk_uf self.selectedFuncId(sf, fa.field); - } - if (self.program_index.fn_ast_map.get(fa.field)) |_| { - if (!self.lowered_functions.contains(fa.field)) { - self.lazyLowerFunction(fa.field); - } - } - break :blk_uf self.resolveFuncByName(fa.field); - }; - if (ufcs_fid) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - const ret_ty = func.ret; - const params = func.params; - // Same implicit address-of as a struct-defined method: if the - // free function's first param is `*T` and the receiver is a - // value `T`, pass its address instead of a by-value copy - // (issue 0063). - self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); - const final_args = self.prependCtxIfNeeded(func, method_args.items); - self.coerceCallArgs(final_args, params); - return self.builder.call(fid, final_args, ret_ty); - } - return self.emitError(fa.field, c.callee.span); - }, - .enum_literal => |el| { - const target_opt: ?TypeId = self.target_type; - - // Try struct-method dispatch first: .{...}.method() where target is a struct - if (target_opt) |tgt| { - if (!tgt.isBuiltin()) { - const target_info = self.module.types.get(tgt); - if (target_info == .@"struct") { - const struct_name = self.module.types.typeName(tgt); - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, el.name }) catch el.name; - if (self.program_index.fn_ast_map.get(qualified)) |fd| { - if (fd.type_params.len > 0) { - return self.lowerGenericCall(fd, qualified, c, args.items); - } - if (!self.lowered_functions.contains(qualified)) { - self.lazyLowerFunction(qualified); - } - } - if (self.resolveFuncByName(qualified)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - const ret_ty = func.ret; - const params = func.params; - const final_args = self.prependCtxIfNeeded(func, args.items); - self.coerceCallArgs(final_args, params); - return self.builder.call(fid, final_args, ret_ty); - } - } - } - } - - // .Variant(payload) — tagged enum construction. Requires target to be a tagged union. - const target = blk: { - if (target_opt) |tgt| { - if (!tgt.isBuiltin() and self.module.types.get(tgt) == .tagged_union) break :blk tgt; - } - if (self.diagnostics) |diags| { - diags.addFmt(.err, c.callee.span, "cannot infer enum type for '.{s}' \u{2014} use an explicit type or assign to a typed variable", .{el.name}); - } - return self.emitPlaceholder(el.name); - }; - const tag = self.resolveVariantIndex(target, el.name); - var payload = if (args.items.len > 0) args.items[0] else Ref.none; - // Coerce payload to match the field type - if (!payload.isNone() and !target.isBuiltin()) { - const info = self.module.types.get(target); - if (info == .tagged_union) { - const fields = info.tagged_union.fields; - if (tag < fields.len) { - const field_ty = fields[tag].ty; - const payload_ty = self.inferExprType(c.args[0]); - if (field_ty != payload_ty) { - payload = self.coerceToType(payload, payload_ty, field_ty); - } - } - } - } - return self.builder.enumInit(tag, payload, target); - }, - else => { - // Indirect call through expression - const callee_ref = self.lowerExpr(c.callee); - const owned = self.alloc.dupe(Ref, args.items) catch unreachable; - return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, .s64); - }, - } - } - - /// Emit a diagnostic for code that needs `Context` (allocator - /// protocol, `push Context.{...}`, the `context` identifier) when - /// the program hasn't registered the type — i.e. doesn't transitively - /// import `modules/std.sx`. Returns a placeholder Ref so the lowering - /// can keep going and surface any additional errors. - pub fn diagnoseMissingContext(self: *Lowering, what: []const u8) Ref { - if (self.diagnostics) |d| { - const span = ast.Span{ .start = 0, .end = 0 }; - d.addFmt(.err, span, "{s} requires the Context type — add `#import \"modules/std.sx\";` (or a module that imports it)", .{what}); - } - return self.emitPlaceholder("missing-context"); - } - - /// Emit `context.allocator.alloc(size)` dispatch — used by internal - /// compiler-driven heap copies (e.g. the `xx value` protocol-erasure - /// path in `buildProtocolValue`). Routes through whatever allocator is - /// currently installed in `context`, so a surrounding - /// `push Context.{ allocator = my_alloc, ... }` actually backs every - /// allocation including the ones the compiler inserts. - /// - /// If `Context` isn't registered (the program doesn't import std.sx), - /// emits a diagnostic and returns a placeholder. We deliberately do - /// NOT fall back to a direct libc malloc — that was the silent escape - /// hatch that bit us through the implicit-context refactor (see the - /// "Silent unimplemented arms" REJECTED PATTERN in CLAUDE.md). - pub fn allocViaContext(self: *Lowering, size_ref: Ref, void_ptr_ty: TypeId) Ref { - if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) { - return self.diagnoseMissingContext("heap allocation"); - } - const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse { - return self.diagnoseMissingContext("heap allocation"); - }; - const ctx_ty_info = self.module.types.get(ctx_ty); - if (ctx_ty_info != .@"struct" or ctx_ty_info.@"struct".fields.len < 1) { - return self.diagnoseMissingContext("heap allocation"); - } - const allocator_ty = ctx_ty_info.@"struct".fields[0].ty; - const ctx = self.builder.load(self.current_ctx_ref, ctx_ty); - const allocator = self.builder.structGet(ctx, 0, allocator_ty); - // #inline Allocator protocol layout: { ctx, alloc_fn_ptr, dealloc_fn_ptr }. - // field 0 = receiver ctx, field 1 = alloc fn-ptr. - const alloc_ctx = self.builder.structGet(allocator, 0, void_ptr_ty); - const fn_ptr = self.builder.structGet(allocator, 1, void_ptr_ty); - // Allocator thunks are sx-side and carry the implicit __sx_ctx at - // slot 0. Forward our caller's current_ctx_ref so the thunk's body - // (and the concrete alloc method it forwards to) has a real - // Context to thread on. - const args = if (self.implicit_ctx_enabled) - self.alloc.dupe(Ref, &.{ self.current_ctx_ref, alloc_ctx, size_ref }) catch unreachable - else - self.alloc.dupe(Ref, &.{ alloc_ctx, size_ref }) catch unreachable; - return self.builder.emit(.{ .call_indirect = .{ - .callee = fn_ptr, - .args = args, - } }, void_ptr_ty); - } - - /// Emit a call to a foreign-declared function looked up by name. - /// Used for the compiler-internal byte-copy in the protocol-erasure - /// heap path and the closure env-copy path, both of which need - /// libc `memcpy` after the `#builtin` form was dropped. - pub fn callForeign(self: *Lowering, name: []const u8, args: []const Ref, ret_ty: TypeId) Ref { - const fid = self.resolveFuncByName(name) orelse @panic("foreign symbol missing — std.sx not imported?"); - return self.builder.call(fid, args, ret_ty); - } - - /// Prepend the caller's current `__sx_ctx` to `args` when the callee - /// has the implicit context param. Returns either the original `args` - /// (when no prepend is needed) or a newly-allocated slice with ctx at - /// slot 0. The returned slice is mutable so callers can pass it - /// straight into `coerceCallArgs`. Direct callers that built the args - /// themselves with __sx_ctx already prepended (protocol thunks, FFI - /// wrappers in Step 4) should NOT call this — they already manage - /// slot 0. - pub fn prependCtxIfNeeded(self: *Lowering, callee: *const Function, args: []Ref) []Ref { - if (!callee.has_implicit_ctx) return args; - const new_args = self.alloc.alloc(Ref, args.len + 1) catch return args; - new_args[0] = self.current_ctx_ref; - @memcpy(new_args[1..], args); - return new_args; - } - - pub fn resolveFuncByName(self: *Lowering, name: []const u8) ?FuncId { - // Check foreign name map first (e.g., "c_abs" → "abs") - const effective_name = self.foreign_name_map.get(name) orelse name; - const name_id = self.module.types.internString(effective_name); - for (self.module.functions.items, 0..) |func, i| { - if (func.name == name_id) return FuncId.fromIndex(@intCast(i)); - } - return null; - } - - pub fn resolveBuiltin(name: []const u8) ?inst_mod.BuiltinId { - const builtins = .{ - // Note: "print" is NOT here — it's a comptime-expanded function, not a simple builtin - .{ "out", inst_mod.BuiltinId.out }, - .{ "sqrt", inst_mod.BuiltinId.sqrt }, - .{ "sin", inst_mod.BuiltinId.sin }, - .{ "cos", inst_mod.BuiltinId.cos }, - .{ "floor", inst_mod.BuiltinId.floor }, - .{ "size_of", inst_mod.BuiltinId.size_of }, - .{ "align_of", inst_mod.BuiltinId.align_of }, - .{ "cast", inst_mod.BuiltinId.cast }, - }; - inline for (builtins) |entry| { - if (std.mem.eql(u8, name, entry[0])) return entry[1]; - } - return null; - } - - // ── Lambda/closure ──────────────────────────────────────────── - - const CaptureInfo = struct { - name: []const u8, - ty: TypeId, - ref: Ref, // alloca or value ref in the parent scope - is_alloca: bool, - }; - fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref { // Lower the lambda body as a new anonymous function var buf: [64]u8 = undefined; @@ -4495,7 +3336,7 @@ pub const Lowering = struct { /// Create a trampoline function that wraps a bare function for closure auto-promotion. /// The trampoline has signature `(env: *void, args...) -> ret` and simply calls the /// bare function with `(args...)`, ignoring the env parameter. - fn createBareFnTrampoline(self: *Lowering, bare_func_id: FuncId, closure_info: types.TypeInfo.ClosureInfo) FuncId { + pub fn createBareFnTrampoline(self: *Lowering, bare_func_id: FuncId, closure_info: types.TypeInfo.ClosureInfo) FuncId { // Build trampoline params: [__sx_ctx]? + env + closure params. // When the program uses Context, every sx-side trampoline carries // the implicit ctx at slot 0 and forwards it to the wrapped @@ -4818,7 +3659,7 @@ pub const Lowering = struct { } /// Byte size of an IR type matching LLVM's type layout. - fn typeSizeBytes(self: *Lowering, ty: TypeId) usize { + pub fn typeSizeBytes(self: *Lowering, ty: TypeId) usize { return self.module.types.typeSizeBytes(ty); } @@ -4964,7 +3805,7 @@ pub const Lowering = struct { /// Pack variadic args into a slice for regular function calls. /// Detects variadic params in the function decl, packs remaining args into a typed slice, /// and replaces the args list with [fixed_args..., slice_ref]. - fn packVariadicCallArgs(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call, args: *std.ArrayList(Ref)) void { + pub fn packVariadicCallArgs(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call, args: *std.ArrayList(Ref)) void { // `#foreign` variadic uses the C calling convention's `...` tail — // extras are passed through directly with default argument promotion // (handled at the call site), not packed into an sx slice. @@ -5124,339 +3965,6 @@ pub const Lowering = struct { // ── Generic monomorphization ────────────────────────────────── - /// Build `tp.name -> TypeId` bindings for a generic call. - /// `args_ast` must be parallel to `fd.params`; for dot-calls the caller - /// prepends the receiver's AST node so positions align with `fd.params[0] = self`. - /// Caller owns the returned map and must call `.deinit()`. - /// Lower a call to a generic function by monomorphizing it with inferred type arguments. - fn lowerGenericCall(self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, lowered_args: []Ref) Ref { - var bindings = self.genericResolver().buildTypeBindings(fd, call_node.args); - defer bindings.deinit(); - - const types_passed_explicitly = call_node.args.len == fd.params.len; - const mangled_name = self.genericResolver().mangleGenericName(base_name, fd, &bindings); - - if (!self.lowered_functions.contains(mangled_name)) { - self.monomorphizeFunction(fd, mangled_name, &bindings); - } - - if (self.resolveFuncByName(mangled_name)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - const ret_ty = func.ret; - const params = func.params; - // Build value-only args (skip type param declaration args) - var value_args = std.ArrayList(Ref).empty; - defer value_args.deinit(self.alloc); - var arg_idx: usize = 0; - for (fd.params) |p| { - if (isTypeParamDecl(&p, fd.type_params)) { - if (types_passed_explicitly) arg_idx += 1; - continue; - } - if (arg_idx < lowered_args.len) { - value_args.append(self.alloc, lowered_args[arg_idx]) catch unreachable; - } - arg_idx += 1; - } - const final_args = self.prependCtxIfNeeded(func, value_args.items); - self.coerceCallArgs(final_args, params); - return self.builder.call(fid, final_args, ret_ty); - } - - return self.emitError(base_name, call_node.callee.span); - } - - /// Create a monomorphized instance of a generic function. - /// Check if a call has a `cast(runtime_var, val)` argument (runtime type dispatch pattern). - fn hasCastWithRuntimeType(self: *Lowering, c: *const ast.Call) bool { - for (c.args) |arg| { - if (arg.data == .call) { - if (arg.data.call.callee.data == .identifier) { - const name = arg.data.call.callee.data.identifier.name; - if (std.mem.eql(u8, name, "cast") and arg.data.call.args.len == 2) { - const type_arg = arg.data.call.args[0]; - if (type_arg.data == .identifier) { - // It's a runtime type if it's in scope as a variable - if (self.scope) |scope| { - if (scope.lookup(type_arg.data.identifier.name) != null) return true; - } - } - } - } - } - } - return false; - } - - /// Generate runtime dispatch for a generic call inside a type-match arm. - /// For each type tag in match_tags, monomorphizes the generic function and calls it. - fn lowerRuntimeDispatchCall( - self: *Lowering, - fd: *const ast.FnDecl, - base_name: []const u8, - call_node: *const ast.Call, - match_tags: []const u64, - ) Ref { - // Find the cast arg: cast(type_var, any_val) - var cast_arg_idx: usize = 0; - var type_tag_node: ?*const Node = null; - var any_val_node: ?*const Node = null; - for (call_node.args, 0..) |arg, i| { - if (arg.data == .call and arg.data.call.callee.data == .identifier) { - const name = arg.data.call.callee.data.identifier.name; - if (std.mem.eql(u8, name, "cast") and arg.data.call.args.len == 2) { - cast_arg_idx = i; - type_tag_node = arg.data.call.args[0]; - any_val_node = arg.data.call.args[1]; - break; - } - } - } - - // Lower the type tag (runtime value) and Any value BEFORE the switch - const type_tag_raw = self.lowerExpr(type_tag_node orelse return self.emitError("dispatch", call_node.callee.span)); - const type_tag_node_ty = self.inferExprType(type_tag_node.?); - const type_tag = if (type_tag_node_ty == .any) - self.builder.emit(.{ .unbox_any = .{ .operand = type_tag_raw } }, .s64) - else - type_tag_raw; - const any_val = self.lowerExpr(any_val_node orelse return self.emitError("dispatch", call_node.callee.span)); - - // Lower non-cast arguments once (before the switch) - var other_args = std.ArrayList(?Ref).empty; - defer other_args.deinit(self.alloc); - for (call_node.args, 0..) |arg, i| { - if (i == cast_arg_idx) { - other_args.append(self.alloc, null) catch unreachable; // placeholder - } else { - other_args.append(self.alloc, self.lowerExpr(arg)) catch unreachable; - } - } - - // Resolve return type (using first available binding) - const ret_ty: TypeId = blk: { - if (fd.return_type) |rt| { - if (rt.data == .type_expr) { - if (type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) != .unresolved) { - break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); - } - } - } - break :blk .string; // default for to_string functions - }; - - const merge_bb = self.freshBlock("dispatch.merge"); - const default_bb = self.freshBlock("dispatch.default"); - - // Build switch cases - var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty; - defer cases.deinit(self.alloc); - - // For each type tag, create a case block - var case_blocks = std.ArrayList(BlockId).empty; - defer case_blocks.deinit(self.alloc); - - for (match_tags) |tag| { - const case_bb = self.freshBlock("dispatch.case"); - case_blocks.append(self.alloc, case_bb) catch unreachable; - cases.append(self.alloc, .{ - .value = @intCast(tag), - .target = case_bb, - .args = &.{}, - }) catch unreachable; - } - - // Create a result alloca BEFORE the switch (must be before terminator) - var result_slot: ?Ref = null; - if (ret_ty != .void) { - result_slot = self.builder.alloca(ret_ty); - } - - self.builder.switchBr(type_tag, cases.items, default_bb, &.{}); - - for (match_tags, 0..) |tag, ti| { - self.builder.switchToBlock(case_blocks.items[ti]); - - const ty_id = TypeId.fromIndex(@intCast(tag)); - - // Unbox the Any value to the concrete type - const unboxed = self.builder.emit(.{ .unbox_any = .{ - .operand = any_val, - } }, ty_id); - - if (fd.type_params.len > 0) { - // Generic function: build type bindings + monomorphize - var bindings = std.StringHashMap(TypeId).init(self.alloc); - defer bindings.deinit(); - - // Find which type param the cast arg corresponds to - if (cast_arg_idx < fd.params.len) { - const param_te = fd.params[cast_arg_idx].type_expr; - if (param_te.data == .type_expr) { - // Direct: `param: $T` → T = ty_id - const tp_name = param_te.data.type_expr.name; - for (fd.type_params) |tp| { - if (std.mem.eql(u8, tp.name, tp_name)) { - bindings.put(tp.name, ty_id) catch {}; - break; - } - } - } else if (param_te.data == .slice_type_expr) { - // Compound: `param: []$T` → T = element type of ty_id - const elem_te = param_te.data.slice_type_expr.element_type; - if (elem_te.data == .type_expr) { - const tp_name = elem_te.data.type_expr.name; - for (fd.type_params) |tp| { - if (std.mem.eql(u8, tp.name, tp_name)) { - const elem_ty = self.getElementType(ty_id); - bindings.put(tp.name, if (elem_ty != .void) elem_ty else ty_id) catch {}; - break; - } - } - } - } else if (param_te.data == .pointer_type_expr) { - // Compound: `param: *$T` → T = pointee type of ty_id - const pointee_te = param_te.data.pointer_type_expr.pointee_type; - if (pointee_te.data == .type_expr) { - const tp_name = pointee_te.data.type_expr.name; - for (fd.type_params) |tp| { - if (std.mem.eql(u8, tp.name, tp_name)) { - if (!ty_id.isBuiltin()) { - const pinfo = self.module.types.get(ty_id); - if (pinfo == .pointer) { - bindings.put(tp.name, pinfo.pointer.pointee) catch {}; - break; - } - } - bindings.put(tp.name, ty_id) catch {}; - break; - } - } - } - } - } - - // Build mangled name - var mangled_buf: [256]u8 = undefined; - var mangled_len: usize = 0; - for (base_name) |ch| { - if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } - } - for (fd.type_params) |tp| { - for ("__") |ch| { - if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } - } - const bound_ty = bindings.get(tp.name) orelse ty_id; - const type_name_str = self.mangleTypeName(bound_ty); - for (type_name_str) |ch| { - if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } - } - } - const mangled_name = mangled_buf[0..mangled_len]; - - // Monomorphize if not already done - if (!self.lowered_functions.contains(mangled_name)) { - self.monomorphizeFunction(fd, mangled_name, &bindings); - } - - // Build call args (replace cast arg with unboxed value, skip type param decl args) - if (self.resolveFuncByName(mangled_name)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - const callee_ret = func.ret; - const callee_params = func.params; - var call_args = std.ArrayList(Ref).empty; - defer call_args.deinit(self.alloc); - for (fd.params, 0..) |p, pi| { - if (isTypeParamDecl(&p, fd.type_params)) continue; - if (pi == cast_arg_idx) { - call_args.append(self.alloc, unboxed) catch unreachable; - } else if (pi < other_args.items.len) { - if (other_args.items[pi]) |ref| { - call_args.append(self.alloc, ref) catch unreachable; - } - } - } - const final_args = self.prependCtxIfNeeded(func, call_args.items); - self.coerceCallArgs(final_args, callee_params); - const result = self.builder.call(fid, final_args, callee_ret); - if (result_slot) |slot| { - self.builder.store(slot, result); - } - } - } else { - // Non-generic function: call directly with per-tag unboxing + coercion - const resolve_name = base_name; - if (!self.lowered_functions.contains(resolve_name)) { - self.lazyLowerFunction(resolve_name); - } - if (self.resolveFuncByName(resolve_name)) |fid| { - const callee_func = &self.module.functions.items[@intFromEnum(fid)]; - const callee_ret = callee_func.ret; - const callee_params = callee_func.params; - const callee_has_ctx = callee_func.has_implicit_ctx; - var call_args = std.ArrayList(Ref).empty; - defer call_args.deinit(self.alloc); - for (fd.params, 0..) |_, pi| { - if (pi == cast_arg_idx) { - // Coerce unboxed value (typed as ty_id) to param type - var arg = unboxed; - // callee param index shifts by +1 if it carries __sx_ctx - const callee_pi = pi + @as(usize, if (callee_has_ctx) 1 else 0); - if (callee_pi < callee_params.len) { - arg = self.coerceToType(arg, ty_id, callee_params[callee_pi].ty); - } - call_args.append(self.alloc, arg) catch unreachable; - } else if (pi < other_args.items.len) { - if (other_args.items[pi]) |ref| { - call_args.append(self.alloc, ref) catch unreachable; - } - } - } - // Prepend __sx_ctx if needed BEFORE coercion so indices line up. - var final_call_args: []Ref = call_args.items; - if (callee_has_ctx) { - final_call_args = self.alloc.alloc(Ref, call_args.items.len + 1) catch call_args.items; - if (final_call_args.len == call_args.items.len + 1) { - final_call_args[0] = self.current_ctx_ref; - @memcpy(final_call_args[1..], call_args.items); - } - } - // Coerce non-cast args (source type unknown, use s64 default). - // cast_arg_idx is in user-space (skips __sx_ctx); offset by ctx_slots. - const ctx_slots: usize = if (callee_has_ctx) 1 else 0; - for (0..@min(final_call_args.len, callee_params.len)) |ci| { - if (ci < ctx_slots) continue; // skip __sx_ctx slot - if ((ci - ctx_slots) != cast_arg_idx) { - final_call_args[ci] = self.coerceToType(final_call_args[ci], .s64, callee_params[ci].ty); - } - } - const result = self.builder.call(fid, final_call_args, callee_ret); - if (result_slot) |slot| { - self.builder.store(slot, result); - } - } - } - - self.builder.br(merge_bb, &.{}); - } - - // Default block: store a default value and branch to merge - self.builder.switchToBlock(default_bb); - if (result_slot) |slot| { - const empty_id = self.module.types.internString(""); - const default_val = if (ret_ty == .string) self.builder.constString(empty_id) else self.zeroValue(ret_ty); - self.builder.store(slot, default_val); - } - self.builder.br(merge_bb, &.{}); - - // Merge block: load result - self.builder.switchToBlock(merge_bb); - if (result_slot) |slot| { - return self.builder.load(slot, ret_ty); - } - return self.builder.constInt(0, .void); - } - /// Build an `[]Any` slice value from the mono's pack params and /// bind it to the pack name in scope. Each pack-param slot is /// loaded, boxed via `boxAny`, and stored into a stack [N x Any] @@ -5582,7 +4090,7 @@ pub const Lowering = struct { /// emits a direct call. Pack params expand into N positional IR /// params with concrete types; the body's `args[]` and /// `args.len` resolve to those params via the pack bindings. - fn lowerPackFnCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref { + pub fn lowerPackFnCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref { // Split call args along the fd.params boundary: // - non-comptime non-pack params → consume one call arg as a // runtime IR param. @@ -6212,329 +4720,6 @@ pub const Lowering = struct { // ── Reflection builtins ──────────────────────────────────────── - /// Try to lower a call as a reflection builtin (expanded inline during lowering). - /// Returns null if the call is not a recognized reflection builtin. - fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref { - // Strict `$T: Type` guard for the type-introspection builtins. A - // value argument (`6`, `true`, `5.2`, a struct) is rejected with a - // diagnostic instead of being silently reinterpreted as a TypeId - // index / sized via its `typeof` (issue 0090). One shared - // classification covers all 7; it runs before dispatch. - if (self.reflectionTypeArgGuard(name, c)) |sentinel| return sentinel; - - if (std.mem.eql(u8, name, "size_of")) { - // size_of(T) → const_int(sizeof(T)) - const ty = self.resolveTypeArg(c.args[0]); - const size: i64 = @intCast(self.typeSizeBytes(ty)); - return self.builder.constInt(size, .s64); - } - if (std.mem.eql(u8, name, "align_of")) { - const ty = self.resolveTypeArg(c.args[0]); - const a: i64 = @intCast(self.module.types.typeAlignBytes(ty)); - return self.builder.constInt(a, .s64); - } - if (std.mem.eql(u8, name, "field_count")) { - // field_count(T) → const_int(N) - const ty = self.resolveTypeArg(c.args[0]); - const info = self.module.types.get(ty); - const count: i64 = switch (info) { - .@"struct" => |s| @intCast(s.fields.len), - .@"union" => |u| @intCast(u.fields.len), - .tagged_union => |u| @intCast(u.fields.len), - .@"enum" => |e| @intCast(e.variants.len), - .array => |a| @intCast(a.length), - .vector => |v| @intCast(v.length), - else => 0, - }; - return self.builder.constInt(count, .s64); - } - if (std.mem.eql(u8, name, "type_name")) { - // type_name(T): - // - Statically resolvable arg (type expression, pack - // index, generic binding, etc.) → fold to const_string - // at lower time. - // - Dynamic arg (e.g. `list[i]` indexing into a - // `$args`-derived []Type slice) → emit a - // `callBuiltin(.type_name, [arg_ref])`. The interp's - // arm (commit 9600ba5) reads the runtime `.type_tag` - // and returns the per-position name. Without this - // split, the catch-all `else => .s64` in - // `resolveTypeArg` silently returns "s64" for every - // dynamic call — exactly the silent-arm pattern the - // project's REJECTED PATTERNS forbid. - if (self.isStaticTypeArg(c.args[0])) { - const ty = self.resolveTypeArg(c.args[0]); - const tn_str = self.formatTypeName(ty); - const sid = self.module.types.internString(tn_str); - return self.builder.constString(sid); - } - const arg_ref = self.lowerExpr(c.args[0]); - const args_owned = self.alloc.dupe(Ref, &.{arg_ref}) catch return self.builder.constString(self.module.types.internString("")); - return self.builder.callBuiltin(.type_name, args_owned, .string); - } - if (std.mem.eql(u8, name, "type_eq")) { - // type_eq(T1, T2) → const_bool — comptime TypeId equality. - // TypeIds are interned per structural shape so equality on - // them matches the user's intuition: `type_eq(s64, s64)` is - // true, `type_eq(*s64, *s64)` is true, distinct shapes are - // false. Pack-indexed types (`$args[0]`) resolve through - // `resolveTypeArg` → `resolveTypeWithBindings`. - if (c.args.len < 2) return self.builder.constBool(false); - const a = self.resolveTypeArg(c.args[0]); - const b = self.resolveTypeArg(c.args[1]); - return self.builder.constBool(a == b); - } - if (std.mem.eql(u8, name, "type_is_unsigned")) { - // type_is_unsigned(T) → bool. Static arg (a spelled type or - // generic binding) folds to const_bool at lower time. A - // dynamic arg — the runtime `type_of(x)` value queried by - // `any_to_string` — emits a `callBuiltin`: the interp reads - // the boxed TypeId, LLVM GEPs a per-type signedness table. - // Mirrors `type_name`'s static/dynamic split; the same split - // avoids `resolveTypeArg`'s silent `.s64` default lying about - // a runtime Type value. - if (c.args.len < 1) return self.builder.constBool(false); - if (self.isStaticTypeArg(c.args[0])) { - const ty = self.resolveTypeArg(c.args[0]); - return self.builder.constBool(self.module.types.isUnsignedInt(ty)); - } - const arg_ref = self.lowerExpr(c.args[0]); - const args_owned = self.alloc.dupe(Ref, &.{arg_ref}) catch return self.builder.constBool(false); - return self.builder.callBuiltin(.type_is_unsigned, args_owned, .bool); - } - if (std.mem.eql(u8, name, "has_impl")) { - // has_impl(P, T) → const_bool. Returns true when type T has - // a reachable impl for protocol P. P is either: - // - plain protocol name (`Hash`, `Eq`) for unary protocols; - // - parameterised call like `Into(Block)` — for protocols - // with type args, the args must be fully spelled. - // Delegates to `computeHasImpl` (shared with the - // `tryConstBoolCondition` arm so `inline if has_impl(...)` - // folds at compile time). - if (c.args.len < 2) return self.builder.constBool(false); - const ty = self.resolveTypeArg(c.args[1]); - return self.builder.constBool(self.computeHasImpl(c.args[0], ty)); - } - if (std.mem.eql(u8, name, "is_flags")) { - const ty = self.resolveTypeArg(c.args[0]); - if (!ty.isBuiltin()) { - const info = self.module.types.get(ty); - if (info == .@"enum") return self.builder.constBool(info.@"enum".is_flags); - } - return self.builder.constBool(false); - } - if (std.mem.eql(u8, name, "compile_error")) { - // compile_error(msg) — raise a build-time diagnostic at - // the call site. The argument must be a string literal so - // the message text is available at lower time. Returns a - // void-typed const (the call site is consumed for its - // side effect, not its value). - if (self.diagnostics) |diags| { - if (c.args.len < 1) { - diags.addFmt(.err, c.callee.span, "compile_error requires a string argument", .{}); - } else if (c.args[0].data == .string_literal) { - const lit = c.args[0].data.string_literal; - const msg = if (lit.is_raw) - lit.raw - else - unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; - diags.addFmt(.err, c.callee.span, "{s}", .{msg}); - } else { - diags.addFmt(.err, c.callee.span, "compile_error argument must be a string literal", .{}); - } - } - return self.builder.constInt(0, .void); - } - if (std.mem.eql(u8, name, "field_name")) { - // field_name(T, i) → field_name_get instruction - if (c.args.len < 2) return self.builder.constString(self.module.types.internString("")); - const ty = self.resolveTypeArg(c.args[0]); - const idx = self.lowerExpr(c.args[1]); - return self.builder.emit(.{ .field_name_get = .{ - .base = .none, - .index = idx, - .struct_type = ty, - } }, .string); - } - if (std.mem.eql(u8, name, "is_comptime")) { - // True under the comptime interpreter, false in compiled code — the - // op decides per backend (it can't fold here, since the same IR - // serves both). Lets stdlib gate a comptime-only diagnostic branch. - return self.builder.emit(.{ .is_comptime = {} }, .bool); - } - if (std.mem.eql(u8, name, "__interp_print_frames")) { - // Backs `trace.print_interpreter_frames()`: dumps the interp call - // chain at comptime, no-op in compiled code (ERR E4.1). - return self.builder.emit(.{ .interp_print_frames = {} }, .void); - } - if (std.mem.eql(u8, name, "__trace_resolve_frame")) { - // Backs `trace.sx`'s formatter: a raw trace-buffer u64 → a `TraceFrame`. - // Compiled code reinterprets the operand as `*TraceFrame` and loads it; - // the interp unpacks (func_id, span.start) and resolves (ERR E3.0 - // slice 3b). Result type is the `TraceFrame` struct from trace.sx. - const frame_ty = self.module.types.findByName(self.module.types.internString("TraceFrame")) orelse { - if (self.diagnostics) |d| d.addFmt(.err, null, "`__trace_resolve_frame` needs `TraceFrame` (from trace.sx) in scope", .{}); - return self.builder.constInt(0, .void); - }; - const arg = self.lowerExpr(c.args[0]); - return self.builder.emit(.{ .trace_resolve = .{ .operand = arg } }, frame_ty); - } - if (std.mem.eql(u8, name, "error_tag_name")) { - // error_tag_name(e) → look the error-set value's runtime tag id up - // in the always-linked tag-name table. The value IS its u32 tag id. - if (c.args.len < 1) return self.builder.constString(self.module.types.internString("")); - const e = self.lowerExpr(c.args[0]); - return self.builder.emit(.{ .error_tag_name_get = .{ .operand = e } }, .string); - } - if (std.mem.eql(u8, name, "field_value")) { - // field_value(s, i) → field_value_get instruction (structs/unions) - // → index_get + box_any (slices/arrays) - if (c.args.len < 2) return self.builder.constInt(0, .any); - const base = self.lowerExpr(c.args[0]); - const idx = self.lowerExpr(c.args[1]); - const struct_ty = self.inferExprType(c.args[0]); - - // For slices, arrays, and vectors, use index_get to access elements - if (!struct_ty.isBuiltin()) { - const ti = self.module.types.get(struct_ty); - if (ti == .slice or ti == .array or ti == .vector) { - const elem_ty = self.getElementType(struct_ty); - const elem = self.builder.emit(.{ .index_get = .{ .lhs = base, .rhs = idx } }, elem_ty); - return self.builder.boxAny(elem, elem_ty); - } - } - - return self.builder.emit(.{ .field_value_get = .{ - .base = base, - .index = idx, - .struct_type = struct_ty, - } }, .any); - } - if (std.mem.eql(u8, name, "type_of")) { - // type_of(val) — produce a Type value (.any-typed aggregate). - if (c.args.len < 1) return self.builder.constType(.void); - const arg_ty = self.inferExprType(c.args[0]); - if (arg_ty == .any) { - // Runtime: extract tag, rebuild Any with `{.any, tag}` so - // the returned value carries Type semantics (tag field - // says ".any" → the value field holds the type id). - const val = self.lowerExpr(c.args[0]); - const tag_val = self.builder.structGet(val, 0, .s64); - return self.builder.boxAny(tag_val, .any); - } else { - return self.builder.constType(arg_ty); - } - } - if (std.mem.eql(u8, name, "field_index")) { - // field_index(T, val) → extract tag from tagged union - if (c.args.len < 2) return self.builder.constInt(0, .s64); - const val = self.lowerExpr(c.args[1]); - // For tagged unions: extract field 0 (the tag) - return self.builder.emit(.{ .enum_tag = .{ .operand = val } }, .s64); - } - if (std.mem.eql(u8, name, "field_value_int")) { - // field_value_int(T, i) → lookup enum variant value by index - if (c.args.len < 2) return self.builder.constInt(0, .s64); - const ty = self.resolveTypeArg(c.args[0]); - const idx = self.lowerExpr(c.args[1]); - // For enums with explicit values, build a global value array and index into it - if (!ty.isBuiltin()) { - const ti = self.module.types.get(ty); - if (ti == .@"enum") { - if (ti.@"enum".explicit_values) |vals| { - // Build inline switch: for each index, return the explicit value - // Simple approach: build an array of constants and use index_get - var elems = std.ArrayList(Ref).empty; - defer elems.deinit(self.alloc); - for (vals) |v| { - elems.append(self.alloc, self.builder.constInt(v, .s64)) catch unreachable; - } - const arr_ty = self.module.types.arrayOf(.s64, @intCast(vals.len)); - const arr = self.builder.structInit(elems.items, arr_ty); - return self.builder.emit(.{ .index_get = .{ .lhs = arr, .rhs = idx } }, .s64); - } - } - } - // Default: return the index itself (regular enums) - return idx; - } - return null; - } - - /// Strict `$T: Type` classification shared by the 7 type-introspection - /// builtins. An argument denotes a type iff it is a spelled / - /// compile-time type or generic type parameter (the `isStaticTypeArg` - /// shapes), or a runtime `Type` value — which is `.any`-typed at - /// runtime (`type_of(x)`, a `[]Type` element `list[i]`, a `Type`-typed - /// local / field / param). Any other expression — a value of type - /// s64 / f64 / bool / a struct — is NOT a type. - pub fn reflectionArgIsType(self: *Lowering, arg: *const Node) bool { - if (self.isStaticTypeArg(arg)) return true; - return self.inferExprType(arg) == .any; - } - - /// Guard for the type-introspection builtins (`size_of`, `align_of`, - /// `field_count`, `type_name`, `type_eq`, `type_is_unsigned`, - /// `is_flags`): every argument must denote a type. A value argument is - /// rejected with a diagnostic rather than silently reinterpreted as a - /// TypeId index or sized via its `typeof` (issue 0090). - /// - /// Returns null when `name` is not a guarded builtin OR every argument - /// is a type (→ fall through to normal dispatch). Returns a harmless - /// result-typed sentinel Ref when a violation was diagnosed; the - /// emitted `.err` gates the build so the value is never observed. - fn reflectionTypeArgGuard(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref { - const arity: usize = if (std.mem.eql(u8, name, "type_eq")) - 2 - else if (std.mem.eql(u8, name, "size_of") or - std.mem.eql(u8, name, "align_of") or - std.mem.eql(u8, name, "field_count") or - std.mem.eql(u8, name, "type_name") or - std.mem.eql(u8, name, "type_is_unsigned") or - std.mem.eql(u8, name, "is_flags")) - 1 - else - return null; - - var ok = true; - if (c.args.len != arity) { - if (self.diagnostics) |d| { - d.addFmt(.err, c.callee.span, "{s} expects {d} type argument{s}, got {d}", .{ - name, arity, if (arity == 1) @as([]const u8, "") else "s", c.args.len, - }); - } - ok = false; - } else { - for (c.args) |a| { - if (self.reflectionArgIsType(a)) continue; - if (self.diagnostics) |d| { - d.addFmt(.err, a.span, "{s} expects a type, got '{s}'", .{ - name, self.formatTypeName(self.inferExprType(a)), - }); - } - ok = false; - } - } - if (ok) return null; - return self.reflectionErrorSentinel(name); - } - - /// Result-typed placeholder returned after `reflectionTypeArgGuard` - /// diagnoses a non-type argument: a string for `type_name`, a bool for - /// the predicate builtins, an int for the size / count builtins. Never - /// observed at runtime — the diagnostic already fails the build — but - /// keeps the IR well-typed so lowering can finish and report every - /// error in one pass. - fn reflectionErrorSentinel(self: *Lowering, name: []const u8) Ref { - if (std.mem.eql(u8, name, "type_name")) - return self.builder.constString(self.module.types.internString("")); - if (std.mem.eql(u8, name, "type_eq") or - std.mem.eql(u8, name, "type_is_unsigned") or - std.mem.eql(u8, name, "is_flags")) - return self.builder.constBool(false); - return self.builder.constInt(0, .s64); - } - /// Resolve a type argument from a call expression. Handles: /// - Type param bindings ($T → concrete type via type_bindings) /// - Direct type names (Vec4 → lookup in TypeTable) @@ -6554,7 +4739,7 @@ pub const Lowering = struct { /// /// Dynamic shapes (index_expr, field_access, runtime locals, /// etc.) fall to the alternative path that emits a builtin_call. - fn isStaticTypeArg(self: *Lowering, node: *const Node) bool { + pub fn isStaticTypeArg(self: *Lowering, node: *const Node) bool { switch (node.data) { .type_expr => |te| { // A type-keyword name (e.g. `s64`) is always static. @@ -7137,325 +5322,6 @@ pub const Lowering = struct { return false; } - /// After args have been lowered, append the lowered values of any - /// `param: T = default_expr` defaults for positions past `args.items.len`. - /// Stops at the first param without a default. Used at method-dispatch - /// sites whose callee is a field_access (so `expandCallDefaults` can't - /// handle them up front). The default expression is lowered in the - /// caller's current scope, so identifiers like `context.allocator` - /// resolve to the caller's runtime context. - fn appendDefaultArgs(self: *Lowering, fd: *const ast.FnDecl, args: *std.ArrayList(Ref)) void { - if (args.items.len >= fd.params.len) return; - var i: usize = args.items.len; - while (i < fd.params.len) : (i += 1) { - const dflt = fd.params[i].default_expr orelse break; - const v = self.lowerExpr(dflt); - args.append(self.alloc, v) catch unreachable; - } - } - - /// When a bare-identifier call omits trailing positional args and the - /// callee's signature provides defaults for them, return a fresh Call - /// node with the defaults filled in. Returns null when no expansion is - /// needed (callee unknown, all args provided, or no defaults available). - fn expandCallDefaults(self: *Lowering, c: *const ast.Call, sel_author: ?*const SelectedFunc, author_ambiguous: bool) ?*ast.Call { - const fd = blk: { - switch (c.callee.data) { - .identifier => |id| { - const eff_name = blk2: { - const scoped = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name; - if (self.program_index.ufcs_alias_map.get(id.name)) |target| { - break :blk2 if (self.scope) |scope| scope.lookupFn(target) orelse target else target; - } - break :blk2 scoped; - }; - // fix-0102d site 1 / R5 §C: for a genuine flat same-name - // collision the omitted trailing args are filled from the - // author the call resolver selected — its `*FnDecl` defaults — - // not the first-wins winner's. lowering consumes the ONE author - // verdict (`selectedFreeAuthor`, computed once in `lowerCall`) - // rather than re-resolving the name, so default expansion and - // dispatch agree on the author. `.ambiguous` declines to expand - // (the call path emits the single diagnostic); a non-collision - // call keeps the existing first-wins winner, byte-for-byte. - // Reading `.decl` only keeps `materialized` null — inspecting - // defaults must not lower the author (0102d). - if (author_ambiguous) return null; - if (sel_author) |sf| break :blk sf.decl; - break :blk self.program_index.fn_ast_map.get(eff_name) orelse return null; - }, - // Namespace call `mod.fn(args)` — args map directly to params - // (no `self` prepend), so default expansion is the same shape as - // a bare call. A METHOD call `value.method(args)` prepends `self` - // (arg/param counts are offset), so it's excluded: only treat the - // receiver as a namespace when it isn't a value in scope. - .field_access => |fa| { - const obj_name: ?[]const u8 = switch (fa.object.data) { - .identifier => |id| id.name, - .type_expr => |te| te.name, - else => null, - }; - const name = obj_name orelse return null; - if (self.scope) |scope| { - if (scope.lookup(name) != null) return null; // method call on a value - } - if (self.program_index.global_names.contains(name)) return null; - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ name, fa.field }) catch fa.field; - break :blk self.program_index.fn_ast_map.get(qualified) orelse self.program_index.fn_ast_map.get(fa.field) orelse return null; - }, - else => return null, - } - }; - if (c.args.len >= fd.params.len) return null; - var end: usize = c.args.len; - while (end < fd.params.len) : (end += 1) { - if (fd.params[end].default_expr == null) break; - } - if (end == c.args.len) return null; - - var new_args = self.alloc.alloc(*ast.Node, end) catch return null; - for (c.args, 0..) |arg, i| new_args[i] = arg; - var i: usize = c.args.len; - while (i < end) : (i += 1) { - const def = fd.params[i].default_expr.?; - // `#caller_location` resolves at the CALL site, not the callee's - // signature: emit a fresh marker carrying the call's span + file so - // lowering synthesizes the caller's `Source_Location` (ERR E4.1b). - if (def.data == .caller_location) { - const n = self.alloc.create(ast.Node) catch return null; - n.* = .{ .span = c.callee.span, .data = .{ .caller_location = {} }, .source_file = c.callee.source_file }; - new_args[i] = n; - } else { - new_args[i] = def; - } - } - const new_call = self.alloc.create(ast.Call) catch return null; - new_call.* = .{ .callee = c.callee, .args = new_args }; - return new_call; - } - - /// Resolve parameter types for a call expression (for target_type context). - /// Returns empty slice if the function can't be resolved. - /// Return the param types of a Function from the caller's POV — i.e. - /// skipping the synthetic `__sx_ctx` slot when present. lowerCall's - /// arg-lowering uses these to set `target_type` per arg, and user - /// args don't include `__sx_ctx`, so the slot must be elided. - fn userParamTypes(self: *Lowering, func: *const Function) []TypeId { - const start: usize = if (func.has_implicit_ctx) 1 else 0; - var types_list = std.ArrayList(TypeId).empty; - if (func.params.len > start) { - for (func.params[start..]) |p| { - types_list.append(self.alloc, p.ty) catch unreachable; - } - } - return types_list.items; - } - - fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call, sel_author: ?*SelectedFunc) []const TypeId { - // Method calls: obj.method(args) — resolve param types from the method signature, - // skipping the first param (self) since it's prepended later. - if (c.callee.data == .field_access) { - const fa = c.callee.data.field_access; - - // Namespace/static call: `Type.method(args)` where `Type` is a type - // identifier (not a value in scope). Args correspond to ALL params - // — no self prepend — so target_type for arg lowering must include - // the leading param. Skipping it would lose the protocol context - // for `xx ptr` inline-cast args. - if (fa.object.data == .identifier) { - const obj_name = fa.object.data.identifier.name; - const is_value = blk: { - if (self.scope) |scope| { - if (scope.lookup(obj_name) != null) break :blk true; - } - if (self.program_index.global_names.contains(obj_name)) break :blk true; - break :blk false; - }; - if (!is_value) { - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ obj_name, fa.field }) catch return &.{}; - if (self.resolveFuncByName(qualified)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - return self.userParamTypes(func); - } - if (self.program_index.fn_ast_map.get(qualified)) |fd| { - var types_list = std.ArrayList(TypeId).empty; - for (fd.params) |p| { - types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; - } - return types_list.items; - } - } - } - - const obj_ty = self.inferExprType(fa.object); - // Protocol-typed receiver: look up the method on the protocol decl. The - // protocol's ProtocolMethodInfo.param_types already excludes self. - if (self.getProtocolInfo(obj_ty)) |proto_info| { - for (proto_info.methods) |m| { - if (std.mem.eql(u8, m.name, fa.field)) return m.param_types; - } - } - // Optional-protocol receiver (`?GPU`): same as above but the - // protocol type sits inside the optional's payload. - if (!obj_ty.isBuiltin()) { - const opt_info = self.module.types.get(obj_ty); - if (opt_info == .optional) { - if (self.getProtocolInfo(opt_info.optional.child)) |proto_info| { - for (proto_info.methods) |m| { - if (std.mem.eql(u8, m.name, fa.field)) return m.param_types; - } - } - } - } - // Closure-typed struct field: `c.on(args)` lowers to call_closure on - // the field value. Pick up the callee's param types from the closure - // type so each arg gets the right target_type during lowering. - if (!obj_ty.isBuiltin()) { - const field_name_id = self.module.types.internString(fa.field); - const struct_fields = self.getStructFields(obj_ty); - for (struct_fields) |f| { - if (f.name == field_name_id and !f.ty.isBuiltin()) { - const fti = self.module.types.get(f.ty); - if (fti == .closure) return fti.closure.params; - if (fti == .function) return fti.function.params; - } - } - } - if (self.getStructTypeName(obj_ty)) |sname| { - // Foreign-class receiver (`#objc_class` / `#jni_class` / etc.): - // resolve the method from `foreign_class_map` walking `#extends`. - // Without this path, `target_type` for each arg falls back to - // whatever `self.target_type` was on entry — typically the - // enclosing fn's return type — which silently truncates `xx ptr` - // casts inside e.g. a `BOOL`-returning method body. - if (self.program_index.foreign_class_map.get(sname)) |fcd| { - if (self.findForeignMethodInChain(fcd, fa.field)) |found| { - const md = found.method; - const saved_fc = self.current_foreign_class; - defer self.current_foreign_class = saved_fc; - self.current_foreign_class = found.fcd; - const user_param_start: usize = if (md.is_static) 0 else 1; - if (md.params.len > user_param_start) { - var types_list = std.ArrayList(TypeId).empty; - for (md.params[user_param_start..]) |p_node| { - types_list.append(self.alloc, self.resolveType(p_node)) catch unreachable; - } - return types_list.items; - } - return &.{}; - } - } - - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch return &.{}; - // Try already-lowered functions first - if (self.resolveFuncByName(qualified)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - // Skip both `__sx_ctx` (if present) AND `self` param; - // caller args include neither. - const skip: usize = (if (func.has_implicit_ctx) @as(usize, 1) else 0) + 1; - if (func.params.len > skip) { - var types_list = std.ArrayList(TypeId).empty; - for (func.params[skip..]) |p| { - types_list.append(self.alloc, p.ty) catch unreachable; - } - return types_list.items; - } - } - // Try AST map (not yet lowered) - if (self.program_index.fn_ast_map.get(qualified)) |fd| { - if (fd.params.len > 0) { - var types_list = std.ArrayList(TypeId).empty; - for (fd.params[1..]) |p| { - types_list.append(self.alloc, self.resolveParamTypeInSource(fd.body.source_file, &p)) catch unreachable; - } - return types_list.items; - } - } - // Generic-struct instance method param types: select the method - // body via the instance's STAMPED author (CP-4), substituting the - // instance's bindings so `T → concrete`. The param source-pin - // follows the selected `fd` (its own `body.source_file`). - if (self.genericInstanceMethod(sname, fa.field)) |gm| { - if (gm.fd.params.len > 0) { - const saved_bindings = self.type_bindings; - self.type_bindings = gm.bindings.*; - var types_list = std.ArrayList(TypeId).empty; - for (gm.fd.params[1..]) |p| { - types_list.append(self.alloc, self.resolveParamTypeInSource(gm.fd.body.source_file, &p)) catch unreachable; - } - self.type_bindings = saved_bindings; - return types_list.items; - } - } - } - return &.{}; - } - if (c.callee.data != .identifier) return &.{}; - const bare_name = c.callee.data.identifier.name; - const name = blk: { - const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name; - if (self.program_index.ufcs_alias_map.get(bare_name)) |target| { - break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; - } - break :blk scoped; - }; - - // fix-0102c F2 / R5 §C: a genuine flat same-name collision must type this - // call's args against the author the call resolver selected, not the - // first-wins winner's params. lowering consumes the ONE author verdict - // (`selectedFreeAuthor`, computed once in `lowerCall`) rather than - // re-resolving the name, so arg lowering (implicit address-of, coercion) - // matches the author actually dispatched — otherwise a `*T`-param shadow - // gets a `T` value arg that is later bit-cast to a pointer (segfault). The - // FuncId materializes into the SHARED verdict (once), so dispatch reuses - // it. A non-collision call falls to the existing first-wins path below, - // byte-for-byte. - if (sel_author) |sf| { - const fid = self.selectedFuncId(sf, bare_name); - const func = &self.module.functions.items[@intFromEnum(fid)]; - return self.userParamTypes(func); - } - - // Check declared functions - if (self.resolveFuncByName(name)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - return self.userParamTypes(func); - } - - // Check AST map for function signatures - if (self.program_index.fn_ast_map.get(name)) |fd| { - var types_list = std.ArrayList(TypeId).empty; - for (fd.params) |p| { - types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; - } - return types_list.items; - } - - // Check global function pointer variables - if (self.program_index.global_names.get(bare_name)) |gi| { - if (!gi.ty.isBuiltin()) { - const ti = self.module.types.get(gi.ty); - if (ti == .function) { - return ti.function.params; - } - } - } - - // Check local scope for function pointer variables - if (self.scope) |scope| { - if (scope.lookup(bare_name)) |binding| { - if (!binding.ty.isBuiltin()) { - const ti = self.module.types.get(binding.ty); - if (ti == .function) { - return ti.function.params; - } - } - } - } - - return &.{}; - } - /// Check if a param is a type param declaration ($T: Type). /// A type param declaration has param.name == one of the type_params names. pub fn isTypeParamDecl(param: *const ast.Param, type_params: []const ast.StructTypeParam) bool { @@ -7637,7 +5503,7 @@ pub const Lowering = struct { /// own module, so typing a cross-module call's args against it must resolve /// in that module's context, not the call site's (E4 — the param analog of /// `resolveTypeInSource`). `src == null` falls back unchanged. - fn resolveParamTypeInSource(self: *Lowering, src: ?[]const u8, p: *const ast.Param) TypeId { + pub fn resolveParamTypeInSource(self: *Lowering, src: ?[]const u8, p: *const ast.Param) TypeId { const pinned = src orelse return self.resolveParamType(p); const saved = self.current_source_file; defer self.setCurrentSourceFile(saved); @@ -8082,7 +5948,7 @@ pub const Lowering = struct { /// whose object is a plain identifier; a nested / non-identifier object is /// qualified-but-unaliased. const HeadName = struct { name: []const u8, alias: ?[]const u8, is_qualified: bool }; - fn headNameOfCallee(callee: *const Node) ?HeadName { + pub fn headNameOfCallee(callee: *const Node) ?HeadName { return switch (callee.data) { .identifier => |id| .{ .name = id.name, .alias = null, .is_qualified = false }, .field_access => |fa| .{ @@ -8443,7 +6309,7 @@ pub const Lowering = struct { /// selected `fd` for free: `monomorphizeFunction` pins to `fd.body.source_file`, /// which is the template's defining module (the author's own method node). /// Null when the function fails to resolve post-monomorphization. - fn ensureGenericInstanceMethodLowered(self: *Lowering, m: GenericStructMethod) ?FuncId { + pub fn ensureGenericInstanceMethodLowered(self: *Lowering, m: GenericStructMethod) ?FuncId { const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ m.inst_name, m.fd.name }) catch return null; if (!self.lowered_functions.contains(mangled)) { self.monomorphizeFunction(m.fd, mangled, m.bindings); @@ -8908,7 +6774,7 @@ pub const Lowering = struct { return .{ .l = self }; } - fn callResolver(self: *Lowering) CallResolver { + pub fn callResolver(self: *Lowering) CallResolver { return .{ .l = self }; } @@ -9505,4 +7371,25 @@ pub const Lowering = struct { pub const emitObjcDefinedClassDeallocImp = lower_objc_class.emitObjcDefinedClassDeallocImp; pub const internStringConstantGlobal = lower_objc_class.internStringConstantGlobal; pub const lookupGlobalIdByName = lower_objc_class.lookupGlobalIdByName; + + // --- moved to lower/call.zig (lower_call) --- + pub const CaptureInfo = lower_call.CaptureInfo; + pub const lowerCall = lower_call.lowerCall; + pub const diagnoseMissingContext = lower_call.diagnoseMissingContext; + pub const allocViaContext = lower_call.allocViaContext; + pub const callForeign = lower_call.callForeign; + pub const prependCtxIfNeeded = lower_call.prependCtxIfNeeded; + pub const resolveFuncByName = lower_call.resolveFuncByName; + pub const resolveBuiltin = lower_call.resolveBuiltin; + pub const lowerGenericCall = lower_call.lowerGenericCall; + pub const hasCastWithRuntimeType = lower_call.hasCastWithRuntimeType; + pub const lowerRuntimeDispatchCall = lower_call.lowerRuntimeDispatchCall; + pub const tryLowerReflectionCall = lower_call.tryLowerReflectionCall; + pub const reflectionArgIsType = lower_call.reflectionArgIsType; + pub const reflectionTypeArgGuard = lower_call.reflectionTypeArgGuard; + pub const reflectionErrorSentinel = lower_call.reflectionErrorSentinel; + pub const appendDefaultArgs = lower_call.appendDefaultArgs; + pub const expandCallDefaults = lower_call.expandCallDefaults; + pub const userParamTypes = lower_call.userParamTypes; + pub const resolveCallParamTypes = lower_call.resolveCallParamTypes; }; diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig new file mode 100644 index 0000000..2ed9ca6 --- /dev/null +++ b/src/ir/lower/call.zig @@ -0,0 +1,2189 @@ +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 SelectedFunc = Lowering.SelectedFunc; +const isTypeParamDecl = Lowering.isTypeParamDecl; +const isPackFn = Lowering.isPackFn; +const headNameOfCallee = Lowering.headNameOfCallee; +const hasComptimeParams = Lowering.hasComptimeParams; + +pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref { + var c = c_in; + // A bare reserved-type-name spelling in call position parses as a + // `.type_expr` (e.g. `s2(4)`), but if a function of that name is in + // scope — a backtick-declared sx fn or a `#import c` foreign fn whose C + // name collides with a reserved type spelling — it is a CALL to that + // function. `TypeName(val)` is not a cast (casts are `cast(T, val)`), so + // there is no ambiguity. Rewrite the callee to an identifier so the + // normal call machinery resolves it, symmetric to the bare-value + // reference that already resolves via scope/globals (issue 0089). + // + // Scoped to RAW provenance: only a backtick (`is_raw`) or `#import c` + // foreign fn declaration may legally carry a reserved-name spelling + // (the decl check rejects every bare reserved-name sx fn). Refusing the + // rewrite for a non-raw match keeps a genuine reserved type spelling a + // type — belt-and-suspenders should any future path ever reintroduce a + // non-raw reserved-name callee. + if (c.callee.data == .type_expr) { + const tname = c.callee.data.type_expr.name; + const eff = if (self.scope) |scope| scope.lookupFn(tname) orelse tname else tname; + const fd: ?*const ast.FnDecl = self.program_index.fn_ast_map.get(eff) orelse + self.program_index.fn_ast_map.get(tname); + if (fd) |decl| if (decl.is_raw) { + const id_node = self.alloc.create(Node) catch unreachable; + id_node.* = .{ .span = c.callee.span, .data = .{ .identifier = .{ .name = tname, .is_raw = true } } }; + const rewritten = self.alloc.create(ast.Call) catch unreachable; + rewritten.* = .{ .callee = id_node, .args = c.args }; + c = rewritten; + }; + } + // fix-0102 F2 / R5 §C: select the bare / value-UFCS same-name call author + // ONCE, via `CallResolver.selectedFreeAuthor` — the SINGLE producer of + // this verdict, the exact same one `CallResolver.plan` consumes for typing. + // The call-path consumers (default expansion, param typing, dispatch) all + // read THIS one author object, so plan-typing and lowering-dispatch can no + // longer disagree about which same-name function the call names, and the + // shadow's FuncId is materialized at most once (into `author_verdict`). + // `selectedFreeAuthor` is side-effect-free (it only runs the author + // selector — no return-type inference / type-arg resolution), so computing + // it eagerly here can't emit a premature diagnostic the way the full plan + // would. + var author_verdict = self.callResolver().selectedFreeAuthor(c); + const sel_author: ?*SelectedFunc = switch (author_verdict) { + .func => |*sf| sf, + else => null, + }; + const author_ambiguous = author_verdict == .ambiguous; + // Expand default parameter values for bare identifier callees: + // when the caller omits trailing positional args, fill them in + // from the callee's `param: T = expr` declarations. + if (self.expandCallDefaults(c, sel_author, author_ambiguous)) |expanded| c = expanded; + // Check reflection builtins first (before lowering args — some args are type names, not values) + if (c.callee.data == .identifier) { + if (self.tryLowerReflectionCall(c.callee.data.identifier.name, c)) |ref| return ref; + } + + // Check for runtime dispatch pattern BEFORE lowering args. + // lowerRuntimeDispatchCall handles its own arg lowering, and pre-lowering + // cast(type) val would produce a dead `call_builtin cast : void`. + if (c.callee.data == .identifier) { + const id_name = c.callee.data.identifier.name; + const eff_name = blk: { + const scoped = if (self.scope) |scope| scope.lookupFn(id_name) orelse id_name else id_name; + if (self.program_index.ufcs_alias_map.get(id_name)) |target| { + break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; + } + break :blk scoped; + }; + // C-import visibility: deny calls to C fn_decls not in the caller's module scope + if (!self.isCImportVisible(eff_name)) { + if (self.diagnostics) |d| + d.addFmt(.err, c.callee.span, "C function '{s}' not visible; add #import for the module that declares it", .{eff_name}); + return Ref.none; + } + // Non-transitive `#import` visibility check. Apply only when the + // user-typed name resolved as-is to a top-level fn — local-scope + // mangling (eff_name != id_name) and UFCS alias rewriting are + // compiler indirections and stay exempt. + if (std.mem.eql(u8, eff_name, id_name) and + self.program_index.ufcs_alias_map.get(id_name) == null and + self.program_index.fn_ast_map.contains(eff_name) and + !self.isNameVisible(eff_name)) + { + if (self.diagnostics) |d| + d.addFmt(.err, c.callee.span, "'{s}' is not visible; #import the module that declares it", .{eff_name}); + return Ref.none; + } + if (self.program_index.fn_ast_map.get(eff_name)) |fd| { + if (self.current_match_tags) |tags| { + if (tags.len > 0 and self.hasCastWithRuntimeType(c)) { + return self.lowerRuntimeDispatchCall(fd, eff_name, c, tags); + } + } + } + } + + // Handle closure(fn_or_lambda) — wrap bare functions into closures + if (c.callee.data == .identifier and std.mem.eql(u8, c.callee.data.identifier.name, "closure")) { + if (c.args.len >= 1) { + const arg = c.args[0]; + // If argument is a bare function name, create a proper closure from it + if (arg.data == .identifier) { + const fn_name = arg.data.identifier.name; + // fix-0102d site 2: `closure(fn)` over a genuine flat same-name + // collision must capture the RESOLVED author's FuncId, not the + // first-wins winner's. Plain bare name only; `.ambiguous` + // → loud diagnostic; `.none` → existing first-wins path. + const closure_fid: ?FuncId = blk_cl: { + if (self.program_index.ufcs_alias_map.get(fn_name) == null and + (if (self.scope) |scope| scope.lookup(fn_name) == null else true)) + { + if (self.current_source_file) |caller_file| { + switch (self.selectPlainCallableAuthor(fn_name, caller_file)) { + .func => |sf| { + var selected = sf; + break :blk_cl self.selectedFuncId(&selected, fn_name); + }, + .ambiguous => { + if (self.diagnostics) |d| + d.addFmt(.err, arg.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fn_name}); + return Ref.none; + }, + .none => {}, + } + } + } + if (!self.lowered_functions.contains(fn_name)) { + self.lazyLowerFunction(fn_name); + } + break :blk_cl self.resolveFuncByName(fn_name); + }; + if (closure_fid) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + // Build closure type from user-visible params only — + // skip the implicit __sx_ctx param. + var param_types_list = std.ArrayList(TypeId).empty; + defer param_types_list.deinit(self.alloc); + const skip: usize = if (func.has_implicit_ctx) 1 else 0; + for (func.params[skip..]) |p| { + param_types_list.append(self.alloc, p.ty) catch unreachable; + } + const closure_ty = self.module.types.closureType(param_types_list.items, func.ret); + const closure_info = self.module.types.get(closure_ty).closure; + const tramp_id = self.createBareFnTrampoline(fid, closure_info); + return self.builder.closureCreate(tramp_id, Ref.none, closure_ty); + } + } + // Lambda or other expression — already produces closure_create + return self.lowerExpr(arg); + } + } + + // Early detection of comptime-expanded calls (e.g. print) — skip arg evaluation + // since lowerComptimeCall re-evaluates args from AST (avoiding double evaluation) + if (c.callee.data == .identifier) { + const early_name = blk: { + const id_name = c.callee.data.identifier.name; + const scoped = if (self.scope) |scope| scope.lookupFn(id_name) orelse id_name else id_name; + if (self.program_index.ufcs_alias_map.get(id_name)) |target| { + break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; + } + break :blk scoped; + }; + // fix-0102 F2 / R5 §C: the early pack/comptime/generic dispatch reads + // the SAME author the call resolver SELECTED — not the first-wins + // winner — whenever a genuine flat same-name collision rerouted the + // call (`sel_author != null`). The selector only ever returns a plain + // free fn (`isPlainFreeFn` rejects type-params / comptime / pack), so + // `sel_author.decl` matches none of the arms below and the early path + // falls through to the main dispatch, which CONSUMES `sel_author` and + // binds that author. Without this the early path would dispatch the + // first-wins winner (e.g. a pack `(..$args)`) and disagree with the + // main dispatch — the selected plain author's bare call would invoke + // the wrong function. On the common path (`sel_author == null`) this + // reads the winner exactly as before — byte-identical, since the + // selector reroutes nothing there. + const early_fd: ?*const ast.FnDecl = if (sel_author) |sf| sf.decl else self.program_index.fn_ast_map.get(early_name); + if (early_fd) |fd| { + if (isPackFn(fd)) { + // Protocol packs (`..xs: P`) and comptime type-packs + // (`..$args`) both monomorphize per call shape. + return self.lowerPackFnCall(fd, c); + } + if (hasComptimeParams(fd)) { + return self.lowerComptimeCall(fd, c); + } + // Early detection of generic function calls — skip arg lowering for type params + // because lowerGenericCall resolves type params from AST nodes, not lowered refs. + // Only if the name is NOT shadowed by a local variable (closure, fn ptr, etc.). + // A selected author is never generic (`isPlainFreeFn` excludes + // `type_params > 0`), so this branch fires only on the winner. + const shadowed = if (self.scope) |scope| scope.lookup(c.callee.data.identifier.name) != null else false; + if (fd.type_params.len > 0 and !shadowed) { + // Types are explicit when call args match param count (e.g., are_equal(Point, p1, p2)) + // Types are inferred when call args < param count (e.g., are_equal(p1, p2)) + const types_explicit = c.args.len == fd.params.len; + var lowered_args = std.ArrayList(Ref).empty; + defer lowered_args.deinit(self.alloc); + for (c.args, 0..) |arg, ai| { + // Skip type param args only when types are passed explicitly + if (types_explicit and ai < fd.params.len and isTypeParamDecl(&fd.params[ai], fd.type_params)) { + lowered_args.append(self.alloc, Ref.none) catch unreachable; + } else { + const saved_target = self.target_type; + lowered_args.append(self.alloc, self.lowerExpr(arg)) catch unreachable; + self.target_type = saved_target; + } + } + return self.lowerGenericCall(fd, early_name, c, lowered_args.items); + } + } + } + + // Lower args (with target type propagation for xx conversions) + var args = std.ArrayList(Ref).empty; + defer args.deinit(self.alloc); + // Try to resolve param types for target_type context + const param_types = self.resolveCallParamTypes(c, sel_author); + // For enum_literal callees (.Variant(payload)), resolve the payload target type + // from the union field type so struct literal fields get proper coercion + var enum_payload_ty: ?TypeId = null; + if (c.callee.data == .enum_literal) { + const target = self.target_type orelse .unresolved; + if (!target.isBuiltin()) { + const info = self.module.types.get(target); + if (info == .tagged_union) { + const tag = self.resolveVariantIndex(target, c.callee.data.enum_literal.name); + if (tag < info.tagged_union.fields.len) { + enum_payload_ty = info.tagged_union.fields[tag].ty; + } + } + } + } + for (c.args, 0..) |arg, ai| { + if (arg.data == .spread_expr) { + // Pack spread `..xs` / `..xs.method` → expand to N positional + // args here. A runtime-slice spread (`..arr`) is left as a + // placeholder for the slice-variadic path (packVariadicCallArgs). + if (self.packSpreadRefs(arg.data.spread_expr.operand, arg.span)) |elems| { + defer self.alloc.free(elems); + for (elems) |e| args.append(self.alloc, e) catch unreachable; + continue; + } + args.append(self.alloc, Ref.none) catch unreachable; + continue; + } + const saved_target = self.target_type; + if (ai < param_types.len) { + self.target_type = param_types[ai]; + } + if (enum_payload_ty) |ept| { + if (ai == 0) self.target_type = ept; + } + // Implicit float→int narrowing of a compile-time float argument + // (incl. an expanded `param: T = expr` default) follows the unified + // rule: an integral comptime float folds, a non-integral one errors. + // A runtime float / `xx` cast is unaffected and coerces as before. + if (ai < param_types.len) { + if (self.foldComptimeFloatInit(arg, param_types[ai])) |folded| { + args.append(self.alloc, folded) catch unreachable; + self.target_type = saved_target; + continue; + } + } + // Implicit address-of: when param expects *T and arg is an identifier + // with an alloca of type T, pass the alloca pointer directly (reference + // semantics, so mutations through the pointer are visible to the caller). + if (ai < param_types.len and arg.data == .identifier) { + const pt = param_types[ai]; + if (!pt.isBuiltin()) { + const pti = self.module.types.get(pt); + if (pti == .pointer) { + if (self.scope) |scope| { + if (scope.lookup(arg.data.identifier.name)) |binding| { + // Only apply when the binding type matches the pointee type + if (binding.is_alloca and binding.ty == pti.pointer.pointee) { + const ptr_ty = self.module.types.ptrTo(binding.ty); + args.append(self.alloc, self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty)) catch unreachable; + self.target_type = saved_target; + continue; + } + } + } + } + } + } + // Implicit address-of for compound lvalues (field access / index / + // deref): when the param expects `*T` and the arg is an addressable + // lvalue of type `T`, pass the lvalue's real address (GEP) — same + // reference semantics as the identifier case above. Without this the + // arg would be loaded into a temporary and the callee would mutate a + // throwaway copy (silent data loss — e.g. `make_move(self.board, m)`). + if (ai < param_types.len and (arg.data == .field_access or arg.data == .index_expr or arg.data == .deref_expr)) { + const pt = param_types[ai]; + if (!pt.isBuiltin()) { + const pti = self.module.types.get(pt); + if (pti == .pointer and self.inferExprType(arg) == pti.pointer.pointee) { + // `lowerExprAsPtr` yields the lvalue's address, typed + // either as `*T` already (index/deref) or as the pointee + // `T` (a field "place" ref); normalize to `*T` — exactly + // what `@field_access` does. + const place = self.lowerExprAsPtr(arg); + const place_ty = self.builder.getRefType(place); + const ref: ?Ref = if (place_ty == pt) + place + else if (place_ty == pti.pointer.pointee) + self.builder.emit(.{ .addr_of = .{ .operand = place } }, pt) + else + null; + if (ref) |r| { + args.append(self.alloc, r) catch unreachable; + self.target_type = saved_target; + continue; + } + } + } + } + const val = self.lowerExpr(arg); + self.target_type = saved_target; + // Passing a `*T` where a `T` value is expected — a by-reference loop + // capture (`for xs: (*m)`), a `*T` parameter, or any pointer local — + // otherwise slips through to LLVM as an opaque "call parameter type + // does not match function signature" verifier error. Flag it at the + // call site with a `.*` fix-it. + if (ai < param_types.len) { + const vt = self.builder.getRefType(val); + const vti = self.module.types.get(vt); + if (vti == .pointer and vti.pointer.pointee == param_types[ai]) { + if (self.diagnostics) |d| { + const tn = self.formatTypeName(param_types[ai]); + if (arg.data == .identifier) { + const nm = arg.data.identifier.name; + const lead: []const u8 = if (self.refCapturePointee(arg) != null) "by-reference loop capture" else "argument"; + const fix = std.fmt.allocPrint(self.alloc, "{s}.*", .{nm}) catch nm; + const pid = d.addFmtId(.err, arg.span, "{s} '{s}' has type '*{s}', but '{s}' is expected here", .{ lead, nm, tn, tn }); + d.addHelpFmt(pid, arg.span, fix, "dereference it to pass the value: `{s}`", .{fix}); + } else { + const pid = d.addFmtId(.err, arg.span, "this argument has type '*{s}', but '{s}' is expected here", .{ tn, tn }); + d.addHelpFmt(pid, arg.span, null, "dereference it with `.*` to pass the value", .{}); + } + } + } + } + args.append(self.alloc, val) catch unreachable; + } + + switch (c.callee.data) { + .identifier => |id| { + // Resolve local function name (bare → mangled) and UFCS aliases + const func_name = blk: { + // First try scope lookup for mangled local fn names + const scoped = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name; + // Then try UFCS alias on bare name + if (self.program_index.ufcs_alias_map.get(id.name)) |target| { + // Resolve the alias target through scope too (target may be mangled) + break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; + } + break :blk scoped; + }; + + // Handle cast(TargetType, val) — emit conversion instructions + // Only for compile-time known types (type_expr or known type names) + if (std.mem.eql(u8, id.name, "cast") and c.args.len >= 2) { + const type_arg = c.args[0]; + const is_static_type = blk: { + if (type_arg.data == .type_expr) break :blk true; + if (type_arg.data == .identifier) { + const tname = type_arg.data.identifier.name; + // Check if it's a known type name (not a runtime variable) + if (type_bridge.resolveTypePrimitive(tname) != null) break :blk true; + if (self.type_bindings) |bindings| { + if (bindings.get(tname) != null) break :blk true; + } + // Check if it's a registered struct/enum type name + const name_id = self.module.types.internString(tname); + if (self.module.types.findByName(name_id) != null) break :blk true; + } + break :blk false; + }; + if (is_static_type) { + const dst_ty = self.resolveTypeArg(c.args[0]); + const val = args.items[1]; // already lowered + const src_ty = self.inferExprType(c.args[1]); + // Unbox Any → concrete type + if (src_ty == .any) { + return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty); + } + return self.coerceExplicit(val, src_ty, dst_ty); + } + // Runtime cast — fall through to builtin handling + } + // Check builtins first (these are handled natively by interpreter and emitter) + if (resolveBuiltin(id.name)) |bid| { + const ret_ty: TypeId = switch (bid) { + .size_of, .align_of => .s64, + .sqrt, .sin, .cos, .floor => blk: { + // Math builtins: return type matches argument type ($T -> T) + if (c.args.len > 0) { + const arg_ty = self.inferExprType(c.args[0]); + if (arg_ty == .f32) break :blk TypeId.f32; + } + break :blk TypeId.f64; + }, + else => .void, + }; + return self.builder.callBuiltin(bid, args.items, ret_ty); + } + // Check scope first: local variables (closures, fn ptrs) shadow global functions + if (self.scope) |scope| { + if (scope.lookup(id.name)) |binding| { + if (!binding.ty.isBuiltin()) { + const ty_info = self.module.types.get(binding.ty); + if (ty_info == .closure) { + const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref; + // Closure trampolines carry `__sx_ctx` at + // slot 0; emit_llvm's `call_closure` builds + // the call as [ctx, env, user_args], so we + // prepend ctx here. args[0] becomes ctx. + const owned = if (self.implicit_ctx_enabled) blk: { + const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable; + arr[0] = self.current_ctx_ref; + @memcpy(arr[1..], args.items); + break :blk arr; + } else self.alloc.dupe(Ref, args.items) catch unreachable; + const ret_ty = ty_info.closure.ret; + return self.builder.emit(.{ .call_closure = .{ .callee = callee_ref, .args = owned } }, ret_ty); + } + } + } + } + // fix-0102c / R5 §C: a genuine flat same-name collision — bind the + // author the call resolver selected (own-author-wins, or the single + // flat-reachable author), or reject a bare call to a name ≥2 + // imported modules author. `selectedFreeAuthor` (computed once + // above, and the exact verdict `plan` consumes for typing) is the + // single producer; lowering CONSUMES it rather than re-resolving + // the name, so typing and dispatch read the SAME author and can't + // disagree (fix-0102 F2). Reached only for an identifier callee, so + // `sel_author` / `author_ambiguous` here are the bare verdict. + if (author_ambiguous) { + if (self.diagnostics) |d| + d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{func_name}); + return Ref.none; + } + if (sel_author) |sf| { + const fid = self.selectedFuncId(sf, func_name); + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + // The RESOLVED author's decl drives variadic packing — not a + // first-wins re-lookup by name, whose variadic shape may + // differ (fix-0102c F1). + self.packVariadicCallArgs(sf.decl, c, &args); + const final_args = self.prependCtxIfNeeded(func, args.items); + self.coerceCallArgs(final_args, params); + if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len); + return self.builder.call(fid, final_args, ret_ty); + } + // Check for comptime-expanded or generic functions + if (self.program_index.fn_ast_map.get(func_name)) |fd| { + if (hasComptimeParams(fd)) { + return self.lowerComptimeCall(fd, c); + } + if (fd.type_params.len > 0) { + // Runtime dispatch already handled above (before arg lowering) + return self.lowerGenericCall(fd, func_name, c, args.items); + } + } + // Check for #compiler free functions + if (self.program_index.fn_ast_map.get(func_name)) |fd_check| { + if (fd_check.body.data == .compiler_expr) { + const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) else TypeId.void; + return self.builder.compilerCall(func_name, args.items, ret_ty); + } + } + + // Look up declared/extern function — try lazy lowering if not yet lowered + { + // First attempt: function may already be declared (from scanDecls) + // but not yet lowered. Try lazy lowering if needed. + if (self.program_index.fn_ast_map.contains(func_name) and !self.lowered_functions.contains(func_name)) { + self.lazyLowerFunction(func_name); + } + if (self.resolveFuncByName(func_name)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + // Pack variadic args into a slice if the function has a variadic param + if (self.program_index.fn_ast_map.get(func_name)) |fd| { + self.packVariadicCallArgs(fd, c, &args); + } + const final_args = self.prependCtxIfNeeded(func, args.items); + // Coerce arguments to match parameter types + self.coerceCallArgs(final_args, params); + if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len); + return self.builder.call(fid, final_args, ret_ty); + } + } + // May be a variable holding a function pointer (non-closure) + if (self.scope) |scope| { + if (scope.lookup(id.name)) |binding| { + const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref; + const ret_ty = if (!binding.ty.isBuiltin()) blk: { + const bti = self.module.types.get(binding.ty); + break :blk if (bti == .function) bti.function.ret else .s64; + } else .s64; + var final_args = std.ArrayList(Ref).empty; + defer final_args.deinit(self.alloc); + if (self.fnPtrTypeWantsCtx(binding.ty)) { + final_args.append(self.alloc, self.current_ctx_ref) catch unreachable; + } + final_args.appendSlice(self.alloc, args.items) catch unreachable; + const owned = self.alloc.dupe(Ref, final_args.items) catch unreachable; + return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, ret_ty); + } + } + // May be a global variable holding a function pointer + if (self.program_index.global_names.get(id.name)) |gi| { + if (!gi.ty.isBuiltin()) { + const gti = self.module.types.get(gi.ty); + if (gti == .function) { + const callee_ref = self.builder.emit(.{ .global_get = gi.id }, gi.ty); + // Coerce args to match fn-ptr param types (including implicit address-of) + for (args.items, 0..) |*arg, ai| { + if (ai < gti.function.params.len) { + const dst_ty = gti.function.params[ai]; + const src_ty = self.inferExprType(c.args[ai]); + // Implicit address-of: passing T where *T expected + if (!dst_ty.isBuiltin()) { + const dti = self.module.types.get(dst_ty); + if (dti == .pointer and dti.pointer.pointee == src_ty and src_ty != .void) { + // For identifier args, pass the alloca directly (reference semantics) + if (c.args[ai].data == .identifier) { + if (self.scope) |scope| { + if (scope.lookup(c.args[ai].data.identifier.name)) |binding| { + if (binding.is_alloca) { + arg.* = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, dst_ty); + continue; + } + } + } + } + // For other expressions, copy semantics + const slot = self.builder.alloca(src_ty); + self.builder.store(slot, arg.*); + arg.* = slot; + continue; + } + } + arg.* = self.coerceToType(arg.*, src_ty, dst_ty); + } + } + var final_args = std.ArrayList(Ref).empty; + defer final_args.deinit(self.alloc); + if (self.fnPtrTypeWantsCtx(gi.ty)) { + final_args.append(self.alloc, self.current_ctx_ref) catch unreachable; + } + final_args.appendSlice(self.alloc, args.items) catch unreachable; + const owned = self.alloc.dupe(Ref, final_args.items) catch unreachable; + return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, gti.function.ret); + } + } + } + // Unresolved function call + return self.emitError(id.name, c.callee.span); + }, + .field_access => |fa| { + // `super.method(args)` from inside a `#jni_main` (or any + // sx-defined `#jni_class`) bodied method. Dispatch via + // CallNonvirtualMethod against the parent class + // resolved from the enclosing fcd's `#extends` clause. + if (fa.object.data == .identifier and + std.mem.eql(u8, fa.object.data.identifier.name, "super")) + { + return self.lowerSuperCall(fa.field, args.items, c.callee.span); + } + + // `Alias.method(args)` where Alias is a foreign-class + // identifier and `method` is a `static` member — JNI + // dispatch via FindClass + GetStaticMethodID + CallStatic*, + // OR (for `new`) via FindClass + GetMethodID("") + + // NewObject. Falls through to existing paths when no match. + if (fa.object.data == .identifier) { + const alias = fa.object.data.identifier.name; + if (self.program_index.foreign_class_map.get(alias)) |fcd| { + for (fcd.members) |m| switch (m) { + .method => |md| if (md.is_static and std.mem.eql(u8, md.name, fa.field)) { + return self.lowerForeignStaticCall(fcd, md, args.items, c.callee.span); + }, + else => {}, + }; + } + } + + // Type constructor call: Sx(f32).user(0.5) — obj is a call that returns a type + if (fa.object.data == .call) { + const inner_call = &fa.object.data.call; + // Generic struct STATIC-METHOD head (`Box(s64).make(..)` or the + // qualified `a.Box(s64).make(..)`): the layout author is chosen + // by the single head choke-point (CP-1) and the method body by + // the instance's STAMPED author (CP-4), so layout-author ≡ + // body-author for BOTH bare and qualified heads (E4 #1 / #2). + if (headNameOfCallee(inner_call.callee)) |hn| { + switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, inner_call.callee.span)) { + .poisoned => return Ref.none, + .template => |t| { + const inst_ty = self.instantiateGenericStruct(&t, inner_call.args); + const inst_name = self.formatTypeName(inst_ty); + if (self.genericInstanceMethod(inst_name, fa.field)) |gm| { + if (self.ensureGenericInstanceMethodLowered(gm)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const final_args = self.prependCtxIfNeeded(func, args.items); + self.coerceCallArgs(final_args, func.params); + return self.builder.call(fid, final_args, func.ret); + } + } + }, + .not_generic => {}, + } + } + + if (inner_call.callee.data == .identifier) { + const inner_name = inner_call.callee.data.identifier.name; + const resolved = if (self.scope) |scope| (scope.lookupFn(inner_name) orelse inner_name) else inner_name; + + if (self.program_index.fn_ast_map.get(resolved)) |fd| { + if (fd.type_params.len > 0) { + if (self.headFnLeak(inner_name, inner_call.callee.span)) return Ref.none; + // Try instantiate as type function + if (self.instantiateTypeFunction(inner_name, inner_name, fd, inner_call.args)) |result_ty| { + const type_info = self.module.types.get(result_ty); + if (type_info == .tagged_union) { + // Qualified enum construction: Type.variant(payload) + const tag = self.resolveVariantIndex(result_ty, fa.field); + var payload = if (args.items.len > 0) args.items[0] else Ref.none; + if (!payload.isNone()) { + const fields = type_info.tagged_union.fields; + if (tag < fields.len) { + const field_ty = fields[tag].ty; + if (field_ty != .void) { + const payload_ty = self.inferExprType(c.args[0]); + if (field_ty != payload_ty) { + payload = self.coerceToType(payload, payload_ty, field_ty); + } + } + } + } + return self.builder.enumInit(tag, payload, result_ty); + } + if (type_info == .@"enum") { + const tag = self.resolveVariantIndex(result_ty, fa.field); + return self.builder.enumInit(tag, Ref.none, result_ty); + } + } + } + } + } + } + + // Namespace-qualified call (e.g. `std.print`) vs method / UFCS + // call on a value (`recv.method`). This boundary decides whether + // the receiver is prepended, so it MUST agree with the call + // plan's `free_fn_ufcs` (prepends) vs `namespace_fn` (does not) + // classification — source it from the single definition in + // `CallResolver` rather than re-deriving it here. + const is_namespace = !self.callResolver().objectIsValue(fa.object); + + if (is_namespace) { + // Namespace call: module.func(args) — don't prepend object + const func_name = fa.field; + // Also try qualified name: Namespace.method (for struct methods) + const ns_name: ?[]const u8 = switch (fa.object.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => null, + }; + const qualified_name = if (ns_name) |n| + std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ n, fa.field }) catch func_name + else + func_name; + // Check for comptime-expanded or generic functions (try both names) + const effective_name = if (self.program_index.fn_ast_map.get(qualified_name) != null) qualified_name else func_name; + if (self.program_index.fn_ast_map.get(effective_name)) |fd| { + if (hasComptimeParams(fd)) { + return self.lowerComptimeCall(fd, c); + } + if (fd.type_params.len > 0) { + return self.lowerGenericCall(fd, effective_name, c, args.items); + } + } + if (self.program_index.fn_ast_map.contains(effective_name) and !self.lowered_functions.contains(effective_name)) { + self.lazyLowerFunction(effective_name); + } + if (self.resolveFuncByName(effective_name)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + if (self.program_index.fn_ast_map.get(effective_name)) |fd| { + self.packVariadicCallArgs(fd, c, &args); + } + const final_args = self.prependCtxIfNeeded(func, args.items); + self.coerceCallArgs(final_args, params); + if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len); + return self.builder.call(fid, final_args, ret_ty); + } + // Check if this is Type.variant(payload) — qualified enum construction + if (ns_name) |type_name| { + const type_name_id = self.module.types.internString(type_name); + if (self.module.types.findByName(type_name_id)) |union_ty| { + const type_info = self.module.types.get(union_ty); + if (type_info == .tagged_union) { + const tag = self.resolveVariantIndex(union_ty, func_name); + var payload = if (args.items.len > 0) args.items[0] else Ref.none; + // Coerce payload to match field type + if (!payload.isNone()) { + const fields = type_info.tagged_union.fields; + if (tag < fields.len) { + const field_ty = fields[tag].ty; + const payload_ty = self.inferExprType(c.args[0]); + if (field_ty != payload_ty) { + payload = self.coerceToType(payload, payload_ty, field_ty); + } + } + } + return self.builder.enumInit(tag, payload, union_ty); + } + if (type_info == .@"enum") { + const tag = self.resolveVariantIndex(union_ty, func_name); + return self.builder.enumInit(tag, Ref.none, union_ty); + } + } + } + return self.emitError(func_name, c.callee.span); + } + + // Method call: obj.method(args) → prepend obj (or &obj for *Self receivers) + // For ptr.*.method(): pass the pointer directly instead of loading + re-addressing. + // This ensures mutations through self: *T are visible after the call. + var obj_ty: TypeId = undefined; + var obj: Ref = undefined; + var effective_obj_node: *const Node = fa.object; + if (fa.object.data == .deref_expr) { + effective_obj_node = fa.object.data.deref_expr.operand; + obj_ty = self.inferExprType(effective_obj_node); + obj = self.lowerExpr(effective_obj_node); + } else { + obj_ty = self.inferExprType(fa.object); + obj = self.lowerExpr(fa.object); + } + + // Check if field is a closure type — call as closure, not method + if (!obj_ty.isBuiltin()) { + const field_name_id = self.module.types.internString(fa.field); + const struct_fields = self.getStructFields(obj_ty); + for (struct_fields, 0..) |f, fi| { + if (f.name == field_name_id and !f.ty.isBuiltin()) { + const fti = self.module.types.get(f.ty); + if (fti == .closure) { + // structGet requires an aggregate value; if obj is *T, load through it first. + var agg = obj; + const oi = self.module.types.get(obj_ty); + if (oi == .pointer) { + agg = self.builder.load(obj, oi.pointer.pointee); + } + const closure_val = self.builder.structGet(agg, @intCast(fi), f.ty); + // Prepend ctx for sx-side closure call ABI. + const owned = if (self.implicit_ctx_enabled) blk: { + const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable; + arr[0] = self.current_ctx_ref; + @memcpy(arr[1..], args.items); + break :blk arr; + } else self.alloc.dupe(Ref, args.items) catch unreachable; + return self.builder.emit(.{ .call_closure = .{ .callee = closure_val, .args = owned } }, fti.closure.ret); + } + } + } + } + + // Check if receiver is a protocol type → dispatch through vtable/fn_ptrs + if (self.getProtocolInfo(obj_ty)) |proto_info| { + return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, obj_ty); + } + + // Check if receiver is `?Protocol` — for sentinel-shaped + // optionals (Protocol has ctx as first ptr field, and a + // null ctx is the "none" state) the unwrap is a no-op + // structurally. Treat the optional value as the protocol + // value and dispatch. Calling a method on a null protocol + // is undefined (same as derefing a null pointer); user + // guards with `if x != null` first. + if (!obj_ty.isBuiltin()) { + const opt_info = self.module.types.get(obj_ty); + if (opt_info == .optional) { + const pay_ty = opt_info.optional.child; + if (self.getProtocolInfo(pay_ty)) |proto_info| { + return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, pay_ty); + } + } + } + + var method_args = std.ArrayList(Ref).empty; + defer method_args.deinit(self.alloc); + method_args.append(self.alloc, obj) catch unreachable; + for (args.items) |a| { + method_args.append(self.alloc, a) catch unreachable; + } + + // Foreign-class DSL: `inst.method(args)` where `inst`'s + // type is an alias declared by `#jni_class("...") { ... }` + // (or its parallel forms). Routes to the JNI dispatch + // shape, descriptor derived from the sx signature. + const struct_name = self.getStructTypeName(obj_ty); + if (struct_name) |sname_for_foreign| { + if (self.program_index.foreign_class_map.get(sname_for_foreign)) |fcd| { + return self.lowerForeignMethodCall(fcd, fa.field, obj, args.items, c.callee.span); + } + } + + // Try to resolve the method by struct type name + if (struct_name) |sname| { + // Try direct qualified name: StructName.method + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field; + + // Generic #compiler method dispatch + if (self.program_index.fn_ast_map.get(qualified)) |method_fd| { + if (method_fd.body.data == .compiler_expr) { + const ret_ty = if (method_fd.return_type) |rt| + type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) + else + .void; + return self.builder.compilerCall(qualified, method_args.items, ret_ty); + } + } + + // Generic-struct instance method: select the body via the + // instance's STAMPED author (CP-4), so the dispatched method is + // the one authored alongside this instance's layout — never the + // global last-wins `fn_ast_map["Template.method"]`. + if (self.genericInstanceMethod(sname, fa.field)) |gm| { + if (self.ensureGenericInstanceMethodLowered(gm)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); + self.appendDefaultArgs(gm.fd, &method_args); + const final_args = self.prependCtxIfNeeded(func, method_args.items); + self.coerceCallArgs(final_args, params); + return self.builder.call(fid, final_args, ret_ty); + } + } + + // Generic method on a non-template struct: `obj.method($T, ...)` + // or inferred form `obj.method(val)` where val's type pins $T. + if (self.program_index.fn_ast_map.get(qualified)) |gen_fd| { + if (gen_fd.type_params.len > 0 and gen_fd.body.data != .compiler_expr) { + // Effective AST args: prepend receiver so positions + // line up with fd.params (which has self at index 0). + var eff_args = std.ArrayList(*const Node).empty; + defer eff_args.deinit(self.alloc); + eff_args.append(self.alloc, effective_obj_node) catch unreachable; + for (c.args) |a| eff_args.append(self.alloc, a) catch unreachable; + + var gbindings = self.genericResolver().buildTypeBindings(gen_fd, eff_args.items); + defer gbindings.deinit(); + + const gmangled = self.genericResolver().mangleGenericName(qualified, gen_fd, &gbindings); + if (!self.lowered_functions.contains(gmangled)) { + self.monomorphizeFunction(gen_fd, gmangled, &gbindings); + } + if (self.resolveFuncByName(gmangled)) |gfid| { + const gfunc = &self.module.functions.items[@intFromEnum(gfid)]; + const gret_ty = gfunc.ret; + const gparams = gfunc.params; + // Strip type-decl slots from method_args. method_args[0] is the + // receiver (corresponds to fd.params[0] = self, never a type decl). + // Walk fd.params[1..], advance arg_idx through method_args[1..]. + var gvalue_args = std.ArrayList(Ref).empty; + defer gvalue_args.deinit(self.alloc); + gvalue_args.append(self.alloc, method_args.items[0]) catch unreachable; + const types_explicit = method_args.items.len == gen_fd.params.len; + var arg_idx: usize = 1; + for (gen_fd.params[1..]) |p| { + if (isTypeParamDecl(&p, gen_fd.type_params)) { + if (types_explicit) arg_idx += 1; + continue; + } + if (arg_idx < method_args.items.len) { + gvalue_args.append(self.alloc, method_args.items[arg_idx]) catch unreachable; + } + arg_idx += 1; + } + self.fixupMethodReceiver(&gvalue_args, gfunc, effective_obj_node, obj_ty); + const final_args = self.prependCtxIfNeeded(gfunc, gvalue_args.items); + self.coerceCallArgs(final_args, gparams); + return self.builder.call(gfid, final_args, gret_ty); + } + } + } + + // Try non-generic qualified method + if (self.program_index.fn_ast_map.get(qualified)) |fd| { + if (!self.lowered_functions.contains(qualified)) { + self.lazyLowerFunction(qualified); + } + _ = fd; + } + if (self.resolveFuncByName(qualified)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + const has_ctx = func.has_implicit_ctx; + self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); + // Note: coerceCallArgs can trigger protocol thunk creation + // (module.addFunction), invalidating func pointer. + // Use pre-extracted params/ret_ty (+ has_ctx) instead of + // func.* after this. + const final_args = blk: { + if (!has_ctx) break :blk method_args.items; + const new_args = self.alloc.alloc(Ref, method_args.items.len + 1) catch break :blk method_args.items; + new_args[0] = self.current_ctx_ref; + @memcpy(new_args[1..], method_args.items); + break :blk new_args; + }; + self.coerceCallArgs(final_args, params); + return self.builder.call(fid, final_args, ret_ty); + } + } + + // Try to resolve as bare function name (free-function UFCS: + // `recv.fn(args)` → `fn(recv, args)`). Lazily lower the body — + // a function reached ONLY via UFCS would otherwise be declared + // but never emitted (issue 0063: undefined symbol at link). + // + // fix-0102d site 3 / R5 §C: a free-function UFCS target with a + // genuine flat same-name collision dispatches to the author the + // call PLAN selected for the receiver's source — the SAME author + // plan typed the call's result as, so dispatch and typing can't + // disagree (fix-0102 F2; without this, a string-typed winner over + // an s64 shadow boxes a raw int as a string pointer → segfault). + // The plan is the single producer; lowering consumes its verdict + // (`sel_author` / `cplan.ambiguous_collision`, computed once above) + // rather than re-resolving the field name. `.ambiguous` → loud + // diagnostic; otherwise the existing first-wins lazy path. + const ufcs_fid: ?FuncId = blk_uf: { + if (author_ambiguous) { + if (self.diagnostics) |d| + d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field}); + return Ref.none; + } + if (sel_author) |sf| { + break :blk_uf self.selectedFuncId(sf, fa.field); + } + if (self.program_index.fn_ast_map.get(fa.field)) |_| { + if (!self.lowered_functions.contains(fa.field)) { + self.lazyLowerFunction(fa.field); + } + } + break :blk_uf self.resolveFuncByName(fa.field); + }; + if (ufcs_fid) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + // Same implicit address-of as a struct-defined method: if the + // free function's first param is `*T` and the receiver is a + // value `T`, pass its address instead of a by-value copy + // (issue 0063). + self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); + const final_args = self.prependCtxIfNeeded(func, method_args.items); + self.coerceCallArgs(final_args, params); + return self.builder.call(fid, final_args, ret_ty); + } + return self.emitError(fa.field, c.callee.span); + }, + .enum_literal => |el| { + const target_opt: ?TypeId = self.target_type; + + // Try struct-method dispatch first: .{...}.method() where target is a struct + if (target_opt) |tgt| { + if (!tgt.isBuiltin()) { + const target_info = self.module.types.get(tgt); + if (target_info == .@"struct") { + const struct_name = self.module.types.typeName(tgt); + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, el.name }) catch el.name; + if (self.program_index.fn_ast_map.get(qualified)) |fd| { + if (fd.type_params.len > 0) { + return self.lowerGenericCall(fd, qualified, c, args.items); + } + if (!self.lowered_functions.contains(qualified)) { + self.lazyLowerFunction(qualified); + } + } + if (self.resolveFuncByName(qualified)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + const final_args = self.prependCtxIfNeeded(func, args.items); + self.coerceCallArgs(final_args, params); + return self.builder.call(fid, final_args, ret_ty); + } + } + } + } + + // .Variant(payload) — tagged enum construction. Requires target to be a tagged union. + const target = blk: { + if (target_opt) |tgt| { + if (!tgt.isBuiltin() and self.module.types.get(tgt) == .tagged_union) break :blk tgt; + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, c.callee.span, "cannot infer enum type for '.{s}' \u{2014} use an explicit type or assign to a typed variable", .{el.name}); + } + return self.emitPlaceholder(el.name); + }; + const tag = self.resolveVariantIndex(target, el.name); + var payload = if (args.items.len > 0) args.items[0] else Ref.none; + // Coerce payload to match the field type + if (!payload.isNone() and !target.isBuiltin()) { + const info = self.module.types.get(target); + if (info == .tagged_union) { + const fields = info.tagged_union.fields; + if (tag < fields.len) { + const field_ty = fields[tag].ty; + const payload_ty = self.inferExprType(c.args[0]); + if (field_ty != payload_ty) { + payload = self.coerceToType(payload, payload_ty, field_ty); + } + } + } + } + return self.builder.enumInit(tag, payload, target); + }, + else => { + // Indirect call through expression + const callee_ref = self.lowerExpr(c.callee); + const owned = self.alloc.dupe(Ref, args.items) catch unreachable; + return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, .s64); + }, + } +} + +/// Emit a diagnostic for code that needs `Context` (allocator +/// protocol, `push Context.{...}`, the `context` identifier) when +/// the program hasn't registered the type — i.e. doesn't transitively +/// import `modules/std.sx`. Returns a placeholder Ref so the lowering +/// can keep going and surface any additional errors. +pub fn diagnoseMissingContext(self: *Lowering, what: []const u8) Ref { + if (self.diagnostics) |d| { + const span = ast.Span{ .start = 0, .end = 0 }; + d.addFmt(.err, span, "{s} requires the Context type — add `#import \"modules/std.sx\";` (or a module that imports it)", .{what}); + } + return self.emitPlaceholder("missing-context"); +} + +/// Emit `context.allocator.alloc(size)` dispatch — used by internal +/// compiler-driven heap copies (e.g. the `xx value` protocol-erasure +/// path in `buildProtocolValue`). Routes through whatever allocator is +/// currently installed in `context`, so a surrounding +/// `push Context.{ allocator = my_alloc, ... }` actually backs every +/// allocation including the ones the compiler inserts. +/// +/// If `Context` isn't registered (the program doesn't import std.sx), +/// emits a diagnostic and returns a placeholder. We deliberately do +/// NOT fall back to a direct libc malloc — that was the silent escape +/// hatch that bit us through the implicit-context refactor (see the +/// "Silent unimplemented arms" REJECTED PATTERN in CLAUDE.md). +pub fn allocViaContext(self: *Lowering, size_ref: Ref, void_ptr_ty: TypeId) Ref { + if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) { + return self.diagnoseMissingContext("heap allocation"); + } + const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse { + return self.diagnoseMissingContext("heap allocation"); + }; + const ctx_ty_info = self.module.types.get(ctx_ty); + if (ctx_ty_info != .@"struct" or ctx_ty_info.@"struct".fields.len < 1) { + return self.diagnoseMissingContext("heap allocation"); + } + const allocator_ty = ctx_ty_info.@"struct".fields[0].ty; + const ctx = self.builder.load(self.current_ctx_ref, ctx_ty); + const allocator = self.builder.structGet(ctx, 0, allocator_ty); + // #inline Allocator protocol layout: { ctx, alloc_fn_ptr, dealloc_fn_ptr }. + // field 0 = receiver ctx, field 1 = alloc fn-ptr. + const alloc_ctx = self.builder.structGet(allocator, 0, void_ptr_ty); + const fn_ptr = self.builder.structGet(allocator, 1, void_ptr_ty); + // Allocator thunks are sx-side and carry the implicit __sx_ctx at + // slot 0. Forward our caller's current_ctx_ref so the thunk's body + // (and the concrete alloc method it forwards to) has a real + // Context to thread on. + const args = if (self.implicit_ctx_enabled) + self.alloc.dupe(Ref, &.{ self.current_ctx_ref, alloc_ctx, size_ref }) catch unreachable + else + self.alloc.dupe(Ref, &.{ alloc_ctx, size_ref }) catch unreachable; + return self.builder.emit(.{ .call_indirect = .{ + .callee = fn_ptr, + .args = args, + } }, void_ptr_ty); +} + +/// Emit a call to a foreign-declared function looked up by name. +/// Used for the compiler-internal byte-copy in the protocol-erasure +/// heap path and the closure env-copy path, both of which need +/// libc `memcpy` after the `#builtin` form was dropped. +pub fn callForeign(self: *Lowering, name: []const u8, args: []const Ref, ret_ty: TypeId) Ref { + const fid = self.resolveFuncByName(name) orelse @panic("foreign symbol missing — std.sx not imported?"); + return self.builder.call(fid, args, ret_ty); +} + +/// Prepend the caller's current `__sx_ctx` to `args` when the callee +/// has the implicit context param. Returns either the original `args` +/// (when no prepend is needed) or a newly-allocated slice with ctx at +/// slot 0. The returned slice is mutable so callers can pass it +/// straight into `coerceCallArgs`. Direct callers that built the args +/// themselves with __sx_ctx already prepended (protocol thunks, FFI +/// wrappers in Step 4) should NOT call this — they already manage +/// slot 0. +pub fn prependCtxIfNeeded(self: *Lowering, callee: *const Function, args: []Ref) []Ref { + if (!callee.has_implicit_ctx) return args; + const new_args = self.alloc.alloc(Ref, args.len + 1) catch return args; + new_args[0] = self.current_ctx_ref; + @memcpy(new_args[1..], args); + return new_args; +} + +pub fn resolveFuncByName(self: *Lowering, name: []const u8) ?FuncId { + // Check foreign name map first (e.g., "c_abs" → "abs") + const effective_name = self.foreign_name_map.get(name) orelse name; + const name_id = self.module.types.internString(effective_name); + for (self.module.functions.items, 0..) |func, i| { + if (func.name == name_id) return FuncId.fromIndex(@intCast(i)); + } + return null; +} + +pub fn resolveBuiltin(name: []const u8) ?inst_mod.BuiltinId { + const builtins = .{ + // Note: "print" is NOT here — it's a comptime-expanded function, not a simple builtin + .{ "out", inst_mod.BuiltinId.out }, + .{ "sqrt", inst_mod.BuiltinId.sqrt }, + .{ "sin", inst_mod.BuiltinId.sin }, + .{ "cos", inst_mod.BuiltinId.cos }, + .{ "floor", inst_mod.BuiltinId.floor }, + .{ "size_of", inst_mod.BuiltinId.size_of }, + .{ "align_of", inst_mod.BuiltinId.align_of }, + .{ "cast", inst_mod.BuiltinId.cast }, + }; + inline for (builtins) |entry| { + if (std.mem.eql(u8, name, entry[0])) return entry[1]; + } + return null; +} + +// ── Lambda/closure ──────────────────────────────────────────── + +pub const CaptureInfo = struct { + name: []const u8, + ty: TypeId, + ref: Ref, // alloca or value ref in the parent scope + is_alloca: bool, +}; + +/// Build `tp.name -> TypeId` bindings for a generic call. +/// `args_ast` must be parallel to `fd.params`; for dot-calls the caller +/// prepends the receiver's AST node so positions align with `fd.params[0] = self`. +/// Caller owns the returned map and must call `.deinit()`. +/// Lower a call to a generic function by monomorphizing it with inferred type arguments. +pub fn lowerGenericCall(self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, lowered_args: []Ref) Ref { + var bindings = self.genericResolver().buildTypeBindings(fd, call_node.args); + defer bindings.deinit(); + + const types_passed_explicitly = call_node.args.len == fd.params.len; + const mangled_name = self.genericResolver().mangleGenericName(base_name, fd, &bindings); + + if (!self.lowered_functions.contains(mangled_name)) { + self.monomorphizeFunction(fd, mangled_name, &bindings); + } + + if (self.resolveFuncByName(mangled_name)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + // Build value-only args (skip type param declaration args) + var value_args = std.ArrayList(Ref).empty; + defer value_args.deinit(self.alloc); + var arg_idx: usize = 0; + for (fd.params) |p| { + if (isTypeParamDecl(&p, fd.type_params)) { + if (types_passed_explicitly) arg_idx += 1; + continue; + } + if (arg_idx < lowered_args.len) { + value_args.append(self.alloc, lowered_args[arg_idx]) catch unreachable; + } + arg_idx += 1; + } + const final_args = self.prependCtxIfNeeded(func, value_args.items); + self.coerceCallArgs(final_args, params); + return self.builder.call(fid, final_args, ret_ty); + } + + return self.emitError(base_name, call_node.callee.span); +} + +/// Create a monomorphized instance of a generic function. +/// Check if a call has a `cast(runtime_var, val)` argument (runtime type dispatch pattern). +pub fn hasCastWithRuntimeType(self: *Lowering, c: *const ast.Call) bool { + for (c.args) |arg| { + if (arg.data == .call) { + if (arg.data.call.callee.data == .identifier) { + const name = arg.data.call.callee.data.identifier.name; + if (std.mem.eql(u8, name, "cast") and arg.data.call.args.len == 2) { + const type_arg = arg.data.call.args[0]; + if (type_arg.data == .identifier) { + // It's a runtime type if it's in scope as a variable + if (self.scope) |scope| { + if (scope.lookup(type_arg.data.identifier.name) != null) return true; + } + } + } + } + } + } + return false; +} + +/// Generate runtime dispatch for a generic call inside a type-match arm. +/// For each type tag in match_tags, monomorphizes the generic function and calls it. +pub fn lowerRuntimeDispatchCall( + self: *Lowering, + fd: *const ast.FnDecl, + base_name: []const u8, + call_node: *const ast.Call, + match_tags: []const u64, +) Ref { + // Find the cast arg: cast(type_var, any_val) + var cast_arg_idx: usize = 0; + var type_tag_node: ?*const Node = null; + var any_val_node: ?*const Node = null; + for (call_node.args, 0..) |arg, i| { + if (arg.data == .call and arg.data.call.callee.data == .identifier) { + const name = arg.data.call.callee.data.identifier.name; + if (std.mem.eql(u8, name, "cast") and arg.data.call.args.len == 2) { + cast_arg_idx = i; + type_tag_node = arg.data.call.args[0]; + any_val_node = arg.data.call.args[1]; + break; + } + } + } + + // Lower the type tag (runtime value) and Any value BEFORE the switch + const type_tag_raw = self.lowerExpr(type_tag_node orelse return self.emitError("dispatch", call_node.callee.span)); + const type_tag_node_ty = self.inferExprType(type_tag_node.?); + const type_tag = if (type_tag_node_ty == .any) + self.builder.emit(.{ .unbox_any = .{ .operand = type_tag_raw } }, .s64) + else + type_tag_raw; + const any_val = self.lowerExpr(any_val_node orelse return self.emitError("dispatch", call_node.callee.span)); + + // Lower non-cast arguments once (before the switch) + var other_args = std.ArrayList(?Ref).empty; + defer other_args.deinit(self.alloc); + for (call_node.args, 0..) |arg, i| { + if (i == cast_arg_idx) { + other_args.append(self.alloc, null) catch unreachable; // placeholder + } else { + other_args.append(self.alloc, self.lowerExpr(arg)) catch unreachable; + } + } + + // Resolve return type (using first available binding) + const ret_ty: TypeId = blk: { + if (fd.return_type) |rt| { + if (rt.data == .type_expr) { + if (type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) != .unresolved) { + break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + } + } + } + break :blk .string; // default for to_string functions + }; + + const merge_bb = self.freshBlock("dispatch.merge"); + const default_bb = self.freshBlock("dispatch.default"); + + // Build switch cases + var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty; + defer cases.deinit(self.alloc); + + // For each type tag, create a case block + var case_blocks = std.ArrayList(BlockId).empty; + defer case_blocks.deinit(self.alloc); + + for (match_tags) |tag| { + const case_bb = self.freshBlock("dispatch.case"); + case_blocks.append(self.alloc, case_bb) catch unreachable; + cases.append(self.alloc, .{ + .value = @intCast(tag), + .target = case_bb, + .args = &.{}, + }) catch unreachable; + } + + // Create a result alloca BEFORE the switch (must be before terminator) + var result_slot: ?Ref = null; + if (ret_ty != .void) { + result_slot = self.builder.alloca(ret_ty); + } + + self.builder.switchBr(type_tag, cases.items, default_bb, &.{}); + + for (match_tags, 0..) |tag, ti| { + self.builder.switchToBlock(case_blocks.items[ti]); + + const ty_id = TypeId.fromIndex(@intCast(tag)); + + // Unbox the Any value to the concrete type + const unboxed = self.builder.emit(.{ .unbox_any = .{ + .operand = any_val, + } }, ty_id); + + if (fd.type_params.len > 0) { + // Generic function: build type bindings + monomorphize + var bindings = std.StringHashMap(TypeId).init(self.alloc); + defer bindings.deinit(); + + // Find which type param the cast arg corresponds to + if (cast_arg_idx < fd.params.len) { + const param_te = fd.params[cast_arg_idx].type_expr; + if (param_te.data == .type_expr) { + // Direct: `param: $T` → T = ty_id + const tp_name = param_te.data.type_expr.name; + for (fd.type_params) |tp| { + if (std.mem.eql(u8, tp.name, tp_name)) { + bindings.put(tp.name, ty_id) catch {}; + break; + } + } + } else if (param_te.data == .slice_type_expr) { + // Compound: `param: []$T` → T = element type of ty_id + const elem_te = param_te.data.slice_type_expr.element_type; + if (elem_te.data == .type_expr) { + const tp_name = elem_te.data.type_expr.name; + for (fd.type_params) |tp| { + if (std.mem.eql(u8, tp.name, tp_name)) { + const elem_ty = self.getElementType(ty_id); + bindings.put(tp.name, if (elem_ty != .void) elem_ty else ty_id) catch {}; + break; + } + } + } + } else if (param_te.data == .pointer_type_expr) { + // Compound: `param: *$T` → T = pointee type of ty_id + const pointee_te = param_te.data.pointer_type_expr.pointee_type; + if (pointee_te.data == .type_expr) { + const tp_name = pointee_te.data.type_expr.name; + for (fd.type_params) |tp| { + if (std.mem.eql(u8, tp.name, tp_name)) { + if (!ty_id.isBuiltin()) { + const pinfo = self.module.types.get(ty_id); + if (pinfo == .pointer) { + bindings.put(tp.name, pinfo.pointer.pointee) catch {}; + break; + } + } + bindings.put(tp.name, ty_id) catch {}; + break; + } + } + } + } + } + + // Build mangled name + var mangled_buf: [256]u8 = undefined; + var mangled_len: usize = 0; + for (base_name) |ch| { + if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } + } + for (fd.type_params) |tp| { + for ("__") |ch| { + if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } + } + const bound_ty = bindings.get(tp.name) orelse ty_id; + const type_name_str = self.mangleTypeName(bound_ty); + for (type_name_str) |ch| { + if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } + } + } + const mangled_name = mangled_buf[0..mangled_len]; + + // Monomorphize if not already done + if (!self.lowered_functions.contains(mangled_name)) { + self.monomorphizeFunction(fd, mangled_name, &bindings); + } + + // Build call args (replace cast arg with unboxed value, skip type param decl args) + if (self.resolveFuncByName(mangled_name)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const callee_ret = func.ret; + const callee_params = func.params; + var call_args = std.ArrayList(Ref).empty; + defer call_args.deinit(self.alloc); + for (fd.params, 0..) |p, pi| { + if (isTypeParamDecl(&p, fd.type_params)) continue; + if (pi == cast_arg_idx) { + call_args.append(self.alloc, unboxed) catch unreachable; + } else if (pi < other_args.items.len) { + if (other_args.items[pi]) |ref| { + call_args.append(self.alloc, ref) catch unreachable; + } + } + } + const final_args = self.prependCtxIfNeeded(func, call_args.items); + self.coerceCallArgs(final_args, callee_params); + const result = self.builder.call(fid, final_args, callee_ret); + if (result_slot) |slot| { + self.builder.store(slot, result); + } + } + } else { + // Non-generic function: call directly with per-tag unboxing + coercion + const resolve_name = base_name; + if (!self.lowered_functions.contains(resolve_name)) { + self.lazyLowerFunction(resolve_name); + } + if (self.resolveFuncByName(resolve_name)) |fid| { + const callee_func = &self.module.functions.items[@intFromEnum(fid)]; + const callee_ret = callee_func.ret; + const callee_params = callee_func.params; + const callee_has_ctx = callee_func.has_implicit_ctx; + var call_args = std.ArrayList(Ref).empty; + defer call_args.deinit(self.alloc); + for (fd.params, 0..) |_, pi| { + if (pi == cast_arg_idx) { + // Coerce unboxed value (typed as ty_id) to param type + var arg = unboxed; + // callee param index shifts by +1 if it carries __sx_ctx + const callee_pi = pi + @as(usize, if (callee_has_ctx) 1 else 0); + if (callee_pi < callee_params.len) { + arg = self.coerceToType(arg, ty_id, callee_params[callee_pi].ty); + } + call_args.append(self.alloc, arg) catch unreachable; + } else if (pi < other_args.items.len) { + if (other_args.items[pi]) |ref| { + call_args.append(self.alloc, ref) catch unreachable; + } + } + } + // Prepend __sx_ctx if needed BEFORE coercion so indices line up. + var final_call_args: []Ref = call_args.items; + if (callee_has_ctx) { + final_call_args = self.alloc.alloc(Ref, call_args.items.len + 1) catch call_args.items; + if (final_call_args.len == call_args.items.len + 1) { + final_call_args[0] = self.current_ctx_ref; + @memcpy(final_call_args[1..], call_args.items); + } + } + // Coerce non-cast args (source type unknown, use s64 default). + // cast_arg_idx is in user-space (skips __sx_ctx); offset by ctx_slots. + const ctx_slots: usize = if (callee_has_ctx) 1 else 0; + for (0..@min(final_call_args.len, callee_params.len)) |ci| { + if (ci < ctx_slots) continue; // skip __sx_ctx slot + if ((ci - ctx_slots) != cast_arg_idx) { + final_call_args[ci] = self.coerceToType(final_call_args[ci], .s64, callee_params[ci].ty); + } + } + const result = self.builder.call(fid, final_call_args, callee_ret); + if (result_slot) |slot| { + self.builder.store(slot, result); + } + } + } + + self.builder.br(merge_bb, &.{}); + } + + // Default block: store a default value and branch to merge + self.builder.switchToBlock(default_bb); + if (result_slot) |slot| { + const empty_id = self.module.types.internString(""); + const default_val = if (ret_ty == .string) self.builder.constString(empty_id) else self.zeroValue(ret_ty); + self.builder.store(slot, default_val); + } + self.builder.br(merge_bb, &.{}); + + // Merge block: load result + self.builder.switchToBlock(merge_bb); + if (result_slot) |slot| { + return self.builder.load(slot, ret_ty); + } + return self.builder.constInt(0, .void); +} + +/// Try to lower a call as a reflection builtin (expanded inline during lowering). +/// Returns null if the call is not a recognized reflection builtin. +pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref { + // Strict `$T: Type` guard for the type-introspection builtins. A + // value argument (`6`, `true`, `5.2`, a struct) is rejected with a + // diagnostic instead of being silently reinterpreted as a TypeId + // index / sized via its `typeof` (issue 0090). One shared + // classification covers all 7; it runs before dispatch. + if (self.reflectionTypeArgGuard(name, c)) |sentinel| return sentinel; + + if (std.mem.eql(u8, name, "size_of")) { + // size_of(T) → const_int(sizeof(T)) + const ty = self.resolveTypeArg(c.args[0]); + const size: i64 = @intCast(self.typeSizeBytes(ty)); + return self.builder.constInt(size, .s64); + } + if (std.mem.eql(u8, name, "align_of")) { + const ty = self.resolveTypeArg(c.args[0]); + const a: i64 = @intCast(self.module.types.typeAlignBytes(ty)); + return self.builder.constInt(a, .s64); + } + if (std.mem.eql(u8, name, "field_count")) { + // field_count(T) → const_int(N) + const ty = self.resolveTypeArg(c.args[0]); + const info = self.module.types.get(ty); + const count: i64 = switch (info) { + .@"struct" => |s| @intCast(s.fields.len), + .@"union" => |u| @intCast(u.fields.len), + .tagged_union => |u| @intCast(u.fields.len), + .@"enum" => |e| @intCast(e.variants.len), + .array => |a| @intCast(a.length), + .vector => |v| @intCast(v.length), + else => 0, + }; + return self.builder.constInt(count, .s64); + } + if (std.mem.eql(u8, name, "type_name")) { + // type_name(T): + // - Statically resolvable arg (type expression, pack + // index, generic binding, etc.) → fold to const_string + // at lower time. + // - Dynamic arg (e.g. `list[i]` indexing into a + // `$args`-derived []Type slice) → emit a + // `callBuiltin(.type_name, [arg_ref])`. The interp's + // arm (commit 9600ba5) reads the runtime `.type_tag` + // and returns the per-position name. Without this + // split, the catch-all `else => .s64` in + // `resolveTypeArg` silently returns "s64" for every + // dynamic call — exactly the silent-arm pattern the + // project's REJECTED PATTERNS forbid. + if (self.isStaticTypeArg(c.args[0])) { + const ty = self.resolveTypeArg(c.args[0]); + const tn_str = self.formatTypeName(ty); + const sid = self.module.types.internString(tn_str); + return self.builder.constString(sid); + } + const arg_ref = self.lowerExpr(c.args[0]); + const args_owned = self.alloc.dupe(Ref, &.{arg_ref}) catch return self.builder.constString(self.module.types.internString("")); + return self.builder.callBuiltin(.type_name, args_owned, .string); + } + if (std.mem.eql(u8, name, "type_eq")) { + // type_eq(T1, T2) → const_bool — comptime TypeId equality. + // TypeIds are interned per structural shape so equality on + // them matches the user's intuition: `type_eq(s64, s64)` is + // true, `type_eq(*s64, *s64)` is true, distinct shapes are + // false. Pack-indexed types (`$args[0]`) resolve through + // `resolveTypeArg` → `resolveTypeWithBindings`. + if (c.args.len < 2) return self.builder.constBool(false); + const a = self.resolveTypeArg(c.args[0]); + const b = self.resolveTypeArg(c.args[1]); + return self.builder.constBool(a == b); + } + if (std.mem.eql(u8, name, "type_is_unsigned")) { + // type_is_unsigned(T) → bool. Static arg (a spelled type or + // generic binding) folds to const_bool at lower time. A + // dynamic arg — the runtime `type_of(x)` value queried by + // `any_to_string` — emits a `callBuiltin`: the interp reads + // the boxed TypeId, LLVM GEPs a per-type signedness table. + // Mirrors `type_name`'s static/dynamic split; the same split + // avoids `resolveTypeArg`'s silent `.s64` default lying about + // a runtime Type value. + if (c.args.len < 1) return self.builder.constBool(false); + if (self.isStaticTypeArg(c.args[0])) { + const ty = self.resolveTypeArg(c.args[0]); + return self.builder.constBool(self.module.types.isUnsignedInt(ty)); + } + const arg_ref = self.lowerExpr(c.args[0]); + const args_owned = self.alloc.dupe(Ref, &.{arg_ref}) catch return self.builder.constBool(false); + return self.builder.callBuiltin(.type_is_unsigned, args_owned, .bool); + } + if (std.mem.eql(u8, name, "has_impl")) { + // has_impl(P, T) → const_bool. Returns true when type T has + // a reachable impl for protocol P. P is either: + // - plain protocol name (`Hash`, `Eq`) for unary protocols; + // - parameterised call like `Into(Block)` — for protocols + // with type args, the args must be fully spelled. + // Delegates to `computeHasImpl` (shared with the + // `tryConstBoolCondition` arm so `inline if has_impl(...)` + // folds at compile time). + if (c.args.len < 2) return self.builder.constBool(false); + const ty = self.resolveTypeArg(c.args[1]); + return self.builder.constBool(self.computeHasImpl(c.args[0], ty)); + } + if (std.mem.eql(u8, name, "is_flags")) { + const ty = self.resolveTypeArg(c.args[0]); + if (!ty.isBuiltin()) { + const info = self.module.types.get(ty); + if (info == .@"enum") return self.builder.constBool(info.@"enum".is_flags); + } + return self.builder.constBool(false); + } + if (std.mem.eql(u8, name, "compile_error")) { + // compile_error(msg) — raise a build-time diagnostic at + // the call site. The argument must be a string literal so + // the message text is available at lower time. Returns a + // void-typed const (the call site is consumed for its + // side effect, not its value). + if (self.diagnostics) |diags| { + if (c.args.len < 1) { + diags.addFmt(.err, c.callee.span, "compile_error requires a string argument", .{}); + } else if (c.args[0].data == .string_literal) { + const lit = c.args[0].data.string_literal; + const msg = if (lit.is_raw) + lit.raw + else + unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; + diags.addFmt(.err, c.callee.span, "{s}", .{msg}); + } else { + diags.addFmt(.err, c.callee.span, "compile_error argument must be a string literal", .{}); + } + } + return self.builder.constInt(0, .void); + } + if (std.mem.eql(u8, name, "field_name")) { + // field_name(T, i) → field_name_get instruction + if (c.args.len < 2) return self.builder.constString(self.module.types.internString("")); + const ty = self.resolveTypeArg(c.args[0]); + const idx = self.lowerExpr(c.args[1]); + return self.builder.emit(.{ .field_name_get = .{ + .base = .none, + .index = idx, + .struct_type = ty, + } }, .string); + } + if (std.mem.eql(u8, name, "is_comptime")) { + // True under the comptime interpreter, false in compiled code — the + // op decides per backend (it can't fold here, since the same IR + // serves both). Lets stdlib gate a comptime-only diagnostic branch. + return self.builder.emit(.{ .is_comptime = {} }, .bool); + } + if (std.mem.eql(u8, name, "__interp_print_frames")) { + // Backs `trace.print_interpreter_frames()`: dumps the interp call + // chain at comptime, no-op in compiled code (ERR E4.1). + return self.builder.emit(.{ .interp_print_frames = {} }, .void); + } + if (std.mem.eql(u8, name, "__trace_resolve_frame")) { + // Backs `trace.sx`'s formatter: a raw trace-buffer u64 → a `TraceFrame`. + // Compiled code reinterprets the operand as `*TraceFrame` and loads it; + // the interp unpacks (func_id, span.start) and resolves (ERR E3.0 + // slice 3b). Result type is the `TraceFrame` struct from trace.sx. + const frame_ty = self.module.types.findByName(self.module.types.internString("TraceFrame")) orelse { + if (self.diagnostics) |d| d.addFmt(.err, null, "`__trace_resolve_frame` needs `TraceFrame` (from trace.sx) in scope", .{}); + return self.builder.constInt(0, .void); + }; + const arg = self.lowerExpr(c.args[0]); + return self.builder.emit(.{ .trace_resolve = .{ .operand = arg } }, frame_ty); + } + if (std.mem.eql(u8, name, "error_tag_name")) { + // error_tag_name(e) → look the error-set value's runtime tag id up + // in the always-linked tag-name table. The value IS its u32 tag id. + if (c.args.len < 1) return self.builder.constString(self.module.types.internString("")); + const e = self.lowerExpr(c.args[0]); + return self.builder.emit(.{ .error_tag_name_get = .{ .operand = e } }, .string); + } + if (std.mem.eql(u8, name, "field_value")) { + // field_value(s, i) → field_value_get instruction (structs/unions) + // → index_get + box_any (slices/arrays) + if (c.args.len < 2) return self.builder.constInt(0, .any); + const base = self.lowerExpr(c.args[0]); + const idx = self.lowerExpr(c.args[1]); + const struct_ty = self.inferExprType(c.args[0]); + + // For slices, arrays, and vectors, use index_get to access elements + if (!struct_ty.isBuiltin()) { + const ti = self.module.types.get(struct_ty); + if (ti == .slice or ti == .array or ti == .vector) { + const elem_ty = self.getElementType(struct_ty); + const elem = self.builder.emit(.{ .index_get = .{ .lhs = base, .rhs = idx } }, elem_ty); + return self.builder.boxAny(elem, elem_ty); + } + } + + return self.builder.emit(.{ .field_value_get = .{ + .base = base, + .index = idx, + .struct_type = struct_ty, + } }, .any); + } + if (std.mem.eql(u8, name, "type_of")) { + // type_of(val) — produce a Type value (.any-typed aggregate). + if (c.args.len < 1) return self.builder.constType(.void); + const arg_ty = self.inferExprType(c.args[0]); + if (arg_ty == .any) { + // Runtime: extract tag, rebuild Any with `{.any, tag}` so + // the returned value carries Type semantics (tag field + // says ".any" → the value field holds the type id). + const val = self.lowerExpr(c.args[0]); + const tag_val = self.builder.structGet(val, 0, .s64); + return self.builder.boxAny(tag_val, .any); + } else { + return self.builder.constType(arg_ty); + } + } + if (std.mem.eql(u8, name, "field_index")) { + // field_index(T, val) → extract tag from tagged union + if (c.args.len < 2) return self.builder.constInt(0, .s64); + const val = self.lowerExpr(c.args[1]); + // For tagged unions: extract field 0 (the tag) + return self.builder.emit(.{ .enum_tag = .{ .operand = val } }, .s64); + } + if (std.mem.eql(u8, name, "field_value_int")) { + // field_value_int(T, i) → lookup enum variant value by index + if (c.args.len < 2) return self.builder.constInt(0, .s64); + const ty = self.resolveTypeArg(c.args[0]); + const idx = self.lowerExpr(c.args[1]); + // For enums with explicit values, build a global value array and index into it + if (!ty.isBuiltin()) { + const ti = self.module.types.get(ty); + if (ti == .@"enum") { + if (ti.@"enum".explicit_values) |vals| { + // Build inline switch: for each index, return the explicit value + // Simple approach: build an array of constants and use index_get + var elems = std.ArrayList(Ref).empty; + defer elems.deinit(self.alloc); + for (vals) |v| { + elems.append(self.alloc, self.builder.constInt(v, .s64)) catch unreachable; + } + const arr_ty = self.module.types.arrayOf(.s64, @intCast(vals.len)); + const arr = self.builder.structInit(elems.items, arr_ty); + return self.builder.emit(.{ .index_get = .{ .lhs = arr, .rhs = idx } }, .s64); + } + } + } + // Default: return the index itself (regular enums) + return idx; + } + return null; +} + +/// Strict `$T: Type` classification shared by the 7 type-introspection +/// builtins. An argument denotes a type iff it is a spelled / +/// compile-time type or generic type parameter (the `isStaticTypeArg` +/// shapes), or a runtime `Type` value — which is `.any`-typed at +/// runtime (`type_of(x)`, a `[]Type` element `list[i]`, a `Type`-typed +/// local / field / param). Any other expression — a value of type +/// s64 / f64 / bool / a struct — is NOT a type. +pub fn reflectionArgIsType(self: *Lowering, arg: *const Node) bool { + if (self.isStaticTypeArg(arg)) return true; + return self.inferExprType(arg) == .any; +} + +/// Guard for the type-introspection builtins (`size_of`, `align_of`, +/// `field_count`, `type_name`, `type_eq`, `type_is_unsigned`, +/// `is_flags`): every argument must denote a type. A value argument is +/// rejected with a diagnostic rather than silently reinterpreted as a +/// TypeId index or sized via its `typeof` (issue 0090). +/// +/// Returns null when `name` is not a guarded builtin OR every argument +/// is a type (→ fall through to normal dispatch). Returns a harmless +/// result-typed sentinel Ref when a violation was diagnosed; the +/// emitted `.err` gates the build so the value is never observed. +pub fn reflectionTypeArgGuard(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref { + const arity: usize = if (std.mem.eql(u8, name, "type_eq")) + 2 + else if (std.mem.eql(u8, name, "size_of") or + std.mem.eql(u8, name, "align_of") or + std.mem.eql(u8, name, "field_count") or + std.mem.eql(u8, name, "type_name") or + std.mem.eql(u8, name, "type_is_unsigned") or + std.mem.eql(u8, name, "is_flags")) + 1 + else + return null; + + var ok = true; + if (c.args.len != arity) { + if (self.diagnostics) |d| { + d.addFmt(.err, c.callee.span, "{s} expects {d} type argument{s}, got {d}", .{ + name, arity, if (arity == 1) @as([]const u8, "") else "s", c.args.len, + }); + } + ok = false; + } else { + for (c.args) |a| { + if (self.reflectionArgIsType(a)) continue; + if (self.diagnostics) |d| { + d.addFmt(.err, a.span, "{s} expects a type, got '{s}'", .{ + name, self.formatTypeName(self.inferExprType(a)), + }); + } + ok = false; + } + } + if (ok) return null; + return self.reflectionErrorSentinel(name); +} + +/// Result-typed placeholder returned after `reflectionTypeArgGuard` +/// diagnoses a non-type argument: a string for `type_name`, a bool for +/// the predicate builtins, an int for the size / count builtins. Never +/// observed at runtime — the diagnostic already fails the build — but +/// keeps the IR well-typed so lowering can finish and report every +/// error in one pass. +pub fn reflectionErrorSentinel(self: *Lowering, name: []const u8) Ref { + if (std.mem.eql(u8, name, "type_name")) + return self.builder.constString(self.module.types.internString("")); + if (std.mem.eql(u8, name, "type_eq") or + std.mem.eql(u8, name, "type_is_unsigned") or + std.mem.eql(u8, name, "is_flags")) + return self.builder.constBool(false); + return self.builder.constInt(0, .s64); +} + +/// After args have been lowered, append the lowered values of any +/// `param: T = default_expr` defaults for positions past `args.items.len`. +/// Stops at the first param without a default. Used at method-dispatch +/// sites whose callee is a field_access (so `expandCallDefaults` can't +/// handle them up front). The default expression is lowered in the +/// caller's current scope, so identifiers like `context.allocator` +/// resolve to the caller's runtime context. +pub fn appendDefaultArgs(self: *Lowering, fd: *const ast.FnDecl, args: *std.ArrayList(Ref)) void { + if (args.items.len >= fd.params.len) return; + var i: usize = args.items.len; + while (i < fd.params.len) : (i += 1) { + const dflt = fd.params[i].default_expr orelse break; + const v = self.lowerExpr(dflt); + args.append(self.alloc, v) catch unreachable; + } +} + +/// When a bare-identifier call omits trailing positional args and the +/// callee's signature provides defaults for them, return a fresh Call +/// node with the defaults filled in. Returns null when no expansion is +/// needed (callee unknown, all args provided, or no defaults available). +pub fn expandCallDefaults(self: *Lowering, c: *const ast.Call, sel_author: ?*const SelectedFunc, author_ambiguous: bool) ?*ast.Call { + const fd = blk: { + switch (c.callee.data) { + .identifier => |id| { + const eff_name = blk2: { + const scoped = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name; + if (self.program_index.ufcs_alias_map.get(id.name)) |target| { + break :blk2 if (self.scope) |scope| scope.lookupFn(target) orelse target else target; + } + break :blk2 scoped; + }; + // fix-0102d site 1 / R5 §C: for a genuine flat same-name + // collision the omitted trailing args are filled from the + // author the call resolver selected — its `*FnDecl` defaults — + // not the first-wins winner's. lowering consumes the ONE author + // verdict (`selectedFreeAuthor`, computed once in `lowerCall`) + // rather than re-resolving the name, so default expansion and + // dispatch agree on the author. `.ambiguous` declines to expand + // (the call path emits the single diagnostic); a non-collision + // call keeps the existing first-wins winner, byte-for-byte. + // Reading `.decl` only keeps `materialized` null — inspecting + // defaults must not lower the author (0102d). + if (author_ambiguous) return null; + if (sel_author) |sf| break :blk sf.decl; + break :blk self.program_index.fn_ast_map.get(eff_name) orelse return null; + }, + // Namespace call `mod.fn(args)` — args map directly to params + // (no `self` prepend), so default expansion is the same shape as + // a bare call. A METHOD call `value.method(args)` prepends `self` + // (arg/param counts are offset), so it's excluded: only treat the + // receiver as a namespace when it isn't a value in scope. + .field_access => |fa| { + const obj_name: ?[]const u8 = switch (fa.object.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => null, + }; + const name = obj_name orelse return null; + if (self.scope) |scope| { + if (scope.lookup(name) != null) return null; // method call on a value + } + if (self.program_index.global_names.contains(name)) return null; + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ name, fa.field }) catch fa.field; + break :blk self.program_index.fn_ast_map.get(qualified) orelse self.program_index.fn_ast_map.get(fa.field) orelse return null; + }, + else => return null, + } + }; + if (c.args.len >= fd.params.len) return null; + var end: usize = c.args.len; + while (end < fd.params.len) : (end += 1) { + if (fd.params[end].default_expr == null) break; + } + if (end == c.args.len) return null; + + var new_args = self.alloc.alloc(*ast.Node, end) catch return null; + for (c.args, 0..) |arg, i| new_args[i] = arg; + var i: usize = c.args.len; + while (i < end) : (i += 1) { + const def = fd.params[i].default_expr.?; + // `#caller_location` resolves at the CALL site, not the callee's + // signature: emit a fresh marker carrying the call's span + file so + // lowering synthesizes the caller's `Source_Location` (ERR E4.1b). + if (def.data == .caller_location) { + const n = self.alloc.create(ast.Node) catch return null; + n.* = .{ .span = c.callee.span, .data = .{ .caller_location = {} }, .source_file = c.callee.source_file }; + new_args[i] = n; + } else { + new_args[i] = def; + } + } + const new_call = self.alloc.create(ast.Call) catch return null; + new_call.* = .{ .callee = c.callee, .args = new_args }; + return new_call; +} + +/// Resolve parameter types for a call expression (for target_type context). +/// Returns empty slice if the function can't be resolved. +/// Return the param types of a Function from the caller's POV — i.e. +/// skipping the synthetic `__sx_ctx` slot when present. lowerCall's +/// arg-lowering uses these to set `target_type` per arg, and user +/// args don't include `__sx_ctx`, so the slot must be elided. +pub fn userParamTypes(self: *Lowering, func: *const Function) []TypeId { + const start: usize = if (func.has_implicit_ctx) 1 else 0; + var types_list = std.ArrayList(TypeId).empty; + if (func.params.len > start) { + for (func.params[start..]) |p| { + types_list.append(self.alloc, p.ty) catch unreachable; + } + } + return types_list.items; +} + +pub fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call, sel_author: ?*SelectedFunc) []const TypeId { + // Method calls: obj.method(args) — resolve param types from the method signature, + // skipping the first param (self) since it's prepended later. + if (c.callee.data == .field_access) { + const fa = c.callee.data.field_access; + + // Namespace/static call: `Type.method(args)` where `Type` is a type + // identifier (not a value in scope). Args correspond to ALL params + // — no self prepend — so target_type for arg lowering must include + // the leading param. Skipping it would lose the protocol context + // for `xx ptr` inline-cast args. + if (fa.object.data == .identifier) { + const obj_name = fa.object.data.identifier.name; + const is_value = blk: { + if (self.scope) |scope| { + if (scope.lookup(obj_name) != null) break :blk true; + } + if (self.program_index.global_names.contains(obj_name)) break :blk true; + break :blk false; + }; + if (!is_value) { + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ obj_name, fa.field }) catch return &.{}; + if (self.resolveFuncByName(qualified)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + return self.userParamTypes(func); + } + if (self.program_index.fn_ast_map.get(qualified)) |fd| { + var types_list = std.ArrayList(TypeId).empty; + for (fd.params) |p| { + types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; + } + return types_list.items; + } + } + } + + const obj_ty = self.inferExprType(fa.object); + // Protocol-typed receiver: look up the method on the protocol decl. The + // protocol's ProtocolMethodInfo.param_types already excludes self. + if (self.getProtocolInfo(obj_ty)) |proto_info| { + for (proto_info.methods) |m| { + if (std.mem.eql(u8, m.name, fa.field)) return m.param_types; + } + } + // Optional-protocol receiver (`?GPU`): same as above but the + // protocol type sits inside the optional's payload. + if (!obj_ty.isBuiltin()) { + const opt_info = self.module.types.get(obj_ty); + if (opt_info == .optional) { + if (self.getProtocolInfo(opt_info.optional.child)) |proto_info| { + for (proto_info.methods) |m| { + if (std.mem.eql(u8, m.name, fa.field)) return m.param_types; + } + } + } + } + // Closure-typed struct field: `c.on(args)` lowers to call_closure on + // the field value. Pick up the callee's param types from the closure + // type so each arg gets the right target_type during lowering. + if (!obj_ty.isBuiltin()) { + const field_name_id = self.module.types.internString(fa.field); + const struct_fields = self.getStructFields(obj_ty); + for (struct_fields) |f| { + if (f.name == field_name_id and !f.ty.isBuiltin()) { + const fti = self.module.types.get(f.ty); + if (fti == .closure) return fti.closure.params; + if (fti == .function) return fti.function.params; + } + } + } + if (self.getStructTypeName(obj_ty)) |sname| { + // Foreign-class receiver (`#objc_class` / `#jni_class` / etc.): + // resolve the method from `foreign_class_map` walking `#extends`. + // Without this path, `target_type` for each arg falls back to + // whatever `self.target_type` was on entry — typically the + // enclosing fn's return type — which silently truncates `xx ptr` + // casts inside e.g. a `BOOL`-returning method body. + if (self.program_index.foreign_class_map.get(sname)) |fcd| { + if (self.findForeignMethodInChain(fcd, fa.field)) |found| { + const md = found.method; + const saved_fc = self.current_foreign_class; + defer self.current_foreign_class = saved_fc; + self.current_foreign_class = found.fcd; + const user_param_start: usize = if (md.is_static) 0 else 1; + if (md.params.len > user_param_start) { + var types_list = std.ArrayList(TypeId).empty; + for (md.params[user_param_start..]) |p_node| { + types_list.append(self.alloc, self.resolveType(p_node)) catch unreachable; + } + return types_list.items; + } + return &.{}; + } + } + + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch return &.{}; + // Try already-lowered functions first + if (self.resolveFuncByName(qualified)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + // Skip both `__sx_ctx` (if present) AND `self` param; + // caller args include neither. + const skip: usize = (if (func.has_implicit_ctx) @as(usize, 1) else 0) + 1; + if (func.params.len > skip) { + var types_list = std.ArrayList(TypeId).empty; + for (func.params[skip..]) |p| { + types_list.append(self.alloc, p.ty) catch unreachable; + } + return types_list.items; + } + } + // Try AST map (not yet lowered) + if (self.program_index.fn_ast_map.get(qualified)) |fd| { + if (fd.params.len > 0) { + var types_list = std.ArrayList(TypeId).empty; + for (fd.params[1..]) |p| { + types_list.append(self.alloc, self.resolveParamTypeInSource(fd.body.source_file, &p)) catch unreachable; + } + return types_list.items; + } + } + // Generic-struct instance method param types: select the method + // body via the instance's STAMPED author (CP-4), substituting the + // instance's bindings so `T → concrete`. The param source-pin + // follows the selected `fd` (its own `body.source_file`). + if (self.genericInstanceMethod(sname, fa.field)) |gm| { + if (gm.fd.params.len > 0) { + const saved_bindings = self.type_bindings; + self.type_bindings = gm.bindings.*; + var types_list = std.ArrayList(TypeId).empty; + for (gm.fd.params[1..]) |p| { + types_list.append(self.alloc, self.resolveParamTypeInSource(gm.fd.body.source_file, &p)) catch unreachable; + } + self.type_bindings = saved_bindings; + return types_list.items; + } + } + } + return &.{}; + } + if (c.callee.data != .identifier) return &.{}; + const bare_name = c.callee.data.identifier.name; + const name = blk: { + const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name; + if (self.program_index.ufcs_alias_map.get(bare_name)) |target| { + break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; + } + break :blk scoped; + }; + + // fix-0102c F2 / R5 §C: a genuine flat same-name collision must type this + // call's args against the author the call resolver selected, not the + // first-wins winner's params. lowering consumes the ONE author verdict + // (`selectedFreeAuthor`, computed once in `lowerCall`) rather than + // re-resolving the name, so arg lowering (implicit address-of, coercion) + // matches the author actually dispatched — otherwise a `*T`-param shadow + // gets a `T` value arg that is later bit-cast to a pointer (segfault). The + // FuncId materializes into the SHARED verdict (once), so dispatch reuses + // it. A non-collision call falls to the existing first-wins path below, + // byte-for-byte. + if (sel_author) |sf| { + const fid = self.selectedFuncId(sf, bare_name); + const func = &self.module.functions.items[@intFromEnum(fid)]; + return self.userParamTypes(func); + } + + // Check declared functions + if (self.resolveFuncByName(name)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + return self.userParamTypes(func); + } + + // Check AST map for function signatures + if (self.program_index.fn_ast_map.get(name)) |fd| { + var types_list = std.ArrayList(TypeId).empty; + for (fd.params) |p| { + types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; + } + return types_list.items; + } + + // Check global function pointer variables + if (self.program_index.global_names.get(bare_name)) |gi| { + if (!gi.ty.isBuiltin()) { + const ti = self.module.types.get(gi.ty); + if (ti == .function) { + return ti.function.params; + } + } + } + + // Check local scope for function pointer variables + if (self.scope) |scope| { + if (scope.lookup(bare_name)) |binding| { + if (!binding.ty.isBuiltin()) { + const ti = self.module.types.get(binding.ty); + if (ti == .function) { + return ti.function.params; + } + } + } + } + + return &.{}; +}