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 type_bridge = @import("../type_bridge.zig"); const unescape = @import("../../unescape.zig"); const errors = @import("../../errors.zig"); const program_index_mod = @import("../program_index.zig"); const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; const GlobalInfo = program_index_mod.GlobalInfo; const CallResolver = @import("../calls.zig").CallResolver; const TypeId = types.TypeId; const Ref = inst_mod.Ref; const BlockId = inst_mod.BlockId; const FuncId = inst_mod.FuncId; const Function = inst_mod.Function; const lower = @import("../lower.zig"); const Lowering = lower.Lowering; 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. // // 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; }; } // 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; // `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; }; // 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 // for any compile-time-resolvable type argument. The gate is the // canonical `isStaticTypeArg` (the one `type_name`/`type_eq` use), // so compound shapes (`*T`, `[]T`, `?T`, `[*]T`, `[N]T`) resolve // statically instead of falling into the runtime-dispatch path // and dying unresolved (issue 0118). An identifier bound in scope // as a runtime `Type` value (the `cast(type) val` category-arm // form) still classifies as non-static and falls through. if (std.mem.eql(u8, id.name, "cast") and c.args.len >= 2) { const type_arg = c.args[0]; if (self.isStaticTypeArg(type_arg)) { 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); } } } } // 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. 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. 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.resolveGlobalRef(id.name, c.callee.span)) |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, // `alias.Type.method()` — strip the alias so the existing // `Type.method` qualified machinery resolves the static. .field_access => self.namespaceRootedMember(fa.object), 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; // The carry gate (issue 0114): a plain-identifier root that is // a namespace ALIAS (not a type / fn global name — those are // the `Type.method` paths below) must be visible under the // carry rule, and its fn members dispatch pinned to the // alias's TARGET module — never the global first-wins // qualified registration, never the last-wins bare fallback. gate: { if (fa.object.data != .identifier) break :gate; const oname = fa.object.data.identifier.name; if (self.program_index.global_names.contains(oname)) break :gate; switch (self.namespaceAliasVerdict(oname)) { .target => |target| { const fd = Lowering.namespaceFnMember(&target, fa.field) orelse break :gate; // Foreign / builtin / #compiler bodies keep their // literal global symbol — the existing bare-name // machinery below resolves them. switch (fd.body.data) { .foreign_expr, .builtin_expr, .compiler_expr => break :gate, else => {}, } if (hasComptimeParams(fd)) return self.lowerComptimeCall(fd, c); if (fd.type_params.len > 0) return self.lowerGenericCall(fd, fa.field, c, args.items); var sf = SelectedFunc{ .decl = fd, .source = target.target_module_path }; const fid = self.selectedFuncId(&sf, fa.field); const func = &self.module.functions.items[@intFromEnum(fid)]; self.packVariadicCallArgs(fd, c, &args); const final_args = self.prependCtxIfNeeded(func, args.items); self.coerceCallArgs(final_args, func.params); if (func.is_variadic) self.promoteCVariadicArgs(final_args, func.params.len); return self.builder.call(fid, final_args, func.ret); }, .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, fa.object.span, "namespace '{s}' is ambiguous: aliases from multiple flat-imported modules point at different targets; declare the alias locally", .{oname}); return Ref.none; }, .none => { if (self.aliasDeclaredAnywhere(oname)) { if (self.diagnostics) |d| d.addFmt(.err, fa.object.span, "namespace '{s}' is not visible; #import the module that declares it", .{oname}); return Ref.none; } }, } } // 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 (undefined symbol at link). // // 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 (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 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; } // ── Generic calls ───────────────────────────────────────────── /// 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`. 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`. /// /// 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; }; // 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; }; // 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 (quiet author-aware lookup — // param typing only; the call site diagnoses ambiguity / visibility) if (self.program_index.global_names.get(bare_name)) |gi_global| { const gi: ?GlobalInfo = switch (self.selectGlobalAuthor(bare_name)) { .resolved => |g| g, .untracked => gi_global, else => null, }; if (gi) |g| { if (!g.ty.isBuiltin()) { const ti = self.module.types.get(g.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 &.{}; }