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 lower_error = @import("lower/error.zig"); const lower_comptime = @import("lower/comptime.zig"); const lower_stmt = @import("lower/stmt.zig"); const lower_control_flow = @import("lower/control_flow.zig"); const lower_decl = @import("lower/decl.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; /// One frame in the chain of module-const names currently being folded by the /// SOURCE-AWARE const evaluator (`Lowering.foldSourceConstInt` and its float /// twins). Stack-allocated per recursive frame, so cycle detection needs no /// allocation — the source-aware analogue of `program_index.ModuleConstFrame`, /// which guards the GLOBAL-map fold (`moduleConstInt`). The frame keys on the /// const's (name, author-source) pair, NOT name alone: same-name nested consts /// across modules (`a.M` ≠ `b.M`) must NOT trip a false cycle (F3). A pair /// already on the chain is a cyclic definition (`N :: N`; `N :: M + 1; M :: N`) /// with no compile-time value → folds to null. pub const ConstFoldFrame = struct { name: []const u8, source: ?[]const u8, parent: ?*const ConstFoldFrame, }; pub fn constFoldFrameContains(frame: ?*const ConstFoldFrame, name: []const u8, source: ?[]const u8) bool { var cur = frame; while (cur) |c| : (cur = c.parent) { if (std.mem.eql(u8, c.name, name) and sourcesEql(c.source, source)) return true; } return false; } fn sourcesEql(a: ?[]const u8, b: ?[]const u8) bool { if (a == null and b == null) return true; if (a == null or b == null) return false; return std.mem.eql(u8, a.?, b.?); } /// Folding context for a SOURCE-AWARE module-const EXPRESSION RHS (E2/F2/R1). /// The leaf-resolution twin of `program_index.ModuleConstCtx`, but every leaf /// name resolves through the querying source's OWN const author /// (`selectModuleConst`, own-wins / ambiguous) instead of the GLOBAL last-wins /// `module_const_map`. This is what makes a same-name shadow's RHS chain /// (`K :: M + 1`, with `M` a same-name shadow too) fold `M` to the SELECTED /// author's `M` — coherently for a const used as a value AND as an array /// dimension / count. `frame` is the cyclic-definition guard. pub const SourceConstCtx = struct { lowering: *Lowering, frame: ?*const ConstFoldFrame, pub fn lookupDimName(self: SourceConstCtx, name: []const u8) ?i64 { return self.lowering.foldSourceConstInt(name, self.frame); } pub fn lookupPackLen(self: SourceConstCtx, name: []const u8) ?i64 { return self.lowering.lookupPackLen(name); } pub fn lookupFloatName(self: SourceConstCtx, name: []const u8) ?f64 { return self.lowering.foldSourceConstFloat(name, self.frame); } pub fn nameIsFloatTyped(self: SourceConstCtx, name: []const u8) bool { return self.lowering.sourceConstIsFloatTyped(name, self.frame); } }; // ── Scope ─────────────────────────────────────────────────────────────── pub const Binding = struct { ref: Ref, ty: TypeId, is_alloca: bool, // true if ref is a pointer that needs load is_ref_capture: bool = false, // `for xs: (*x)` — `ref` is `*elem`; auto-deref in value positions }; // `init` / `deinit` / `put` are pub so collaborator unit tests (e.g. // calls.test.zig) can stand up a lexical scope and exercise the // scope-dependent call forms (closure / fn-pointer callees) without // driving a full function lowering. pub const Scope = struct { map: std.StringHashMap(Binding), fn_names: std.StringHashMap([]const u8), // bare name → mangled name for local functions parent: ?*Scope, pub fn init(alloc: Allocator, parent: ?*Scope) Scope { return .{ .map = std.StringHashMap(Binding).init(alloc), .fn_names = std.StringHashMap([]const u8).init(alloc), .parent = parent, }; } pub fn deinit(self: *Scope) void { self.map.deinit(); self.fn_names.deinit(); } pub fn put(self: *Scope, name: []const u8, binding: Binding) void { self.map.put(name, binding) catch unreachable; } pub fn lookup(self: *const Scope, name: []const u8) ?Binding { if (self.map.get(name)) |b| return b; if (self.parent) |p| return p.lookup(name); return null; } pub fn lookupFn(self: *const Scope, name: []const u8) ?[]const u8 { if (self.fn_names.get(name)) |mangled| return mangled; if (self.parent) |p| return p.lookupFn(name); return null; } }; /// A pending block-scoped cleanup: `defer` (runs on every block exit) or /// `onfail` (runs only when an error leaves the block, binding the in-flight /// tag). Both share one declaration-ordered stack so error-exit cleanup runs /// them interleaved in reverse order (ERR E1.7). const CleanupEntry = struct { body: *const Node, is_onfail: bool, binding: ?[]const u8 = null, }; /// Pure non-transitive visibility walk: `name` is visible from `source` when /// it's in `source`'s own scope or in any module reachable over one `graph` /// edge. The core of the lowering visibility predicate, exposed so a unit test /// can exercise the edge-walk without standing up a whole `Lowering`. Falls open /// (true) when `scopes`/`graph` are null (scoping infra unwired). pub fn nameVisibleOverEdges( scopes: ?*std.StringHashMap(std.StringHashMap(void)), graph: ?*std.StringHashMap(std.StringHashMap(void)), source: []const u8, name: []const u8, ) bool { const sc = scopes orelse return true; const own_scope = sc.get(source) orelse return true; if (own_scope.contains(name)) return true; const g = graph orelse return true; const direct = g.get(source) orelse return true; var it = direct.iterator(); while (it.next()) |kv| { const dep = sc.get(kv.key_ptr.*) orelse continue; if (dep.contains(name)) return true; } return false; } // ── Lowering ──────────────────────────────────────────────────────────── pub const Lowering = struct { module: *Module, builder: Builder, alloc: Allocator, scope: ?*Scope = null, break_target: ?BlockId = null, continue_target: ?BlockId = null, block_counter: u32 = 0, comptime_counter: u32 = 0, main_file: ?[]const u8 = null, // path of the main file; imported functions are declared extern resolved_root: ?*const Node = null, // full AST root (for building comptime modules) comptime_param_nodes: ?std.StringHashMap(*const Node) = null, // active comptime substitutions target_type: ?TypeId = null, // target type for struct/enum literals without explicit names lowered_functions: std.StringHashMap(void), // tracks which functions have been fully lowered /// Identity map: authoring `*const ast.FnDecl` → the FuncId `declareFunction` /// created for it. The name-keyed function table (`resolveFuncByName`) returns /// the FIRST author of a name, so two same-name authors collide there; this /// map addresses each author's OWN slot by decl identity (fix-0102b), letting /// a SHADOWED author lower its body into a distinct FuncId. fn_decl_fids: std.AutoHashMap(*const ast.FnDecl, FuncId), /// FuncId-keyed lowered tracking — the identity twin of `lowered_functions` /// (which keys by name). A shadowed same-name author shares the winner's name /// but not its FuncId, so name-keyed tracking can't tell them apart; this /// records which specific FuncIds have had a real body lowered (fix-0102b). lowered_fids: std.AutoHashMap(FuncId, void), local_fn_counter: u32 = 0, // unique counter for mangling local function names /// Per-declaration nominal identity bookkeeping (E2). The FIRST source to /// register a given top-level type NAME keeps `nominal_id = 0` (structural — /// byte-identical to pre-E2 single-author registration); a later registration /// of the same name from a DIFFERENT source is a same-name SHADOW and gets a /// fresh id from `next_nominal_id`, so the two authors intern to DISTINCT /// TypeIds (closing issue 0105's last-wins collapse). `nominal_name_authors` /// records each name's first author source to make that decision. nominal_name_authors: std.AutoHashMap(types.StringId, []const u8), next_nominal_id: u32 = 0, /// Declaration-name / import / visibility facts (architecture phase A1, /// `ProgramIndex`). Owns `import_flags`; borrows `module_scopes` / /// `import_graph` from the compilation driver. Reached via /// `self.program_index.`; populated by scan/registration code. program_index: ProgramIndex, current_source_file: ?[]const u8 = null, // source file of function currently being lowered // Implicit Context parameter machinery. When the program imports // `std.sx` (and therefore declares `Context :: struct {...}`), every // default-conv sx function gains a synthetic `__sx_ctx: *void` param // at slot 0, and `current_ctx_ref` is bound to that param on each // function-body entry. `lowerCall` / `call_indirect` prepend this ref // to the args of every sx-to-sx call. push Context.{...} rebinds it // to a stack-allocated Context for the lexical body. See // `~/.claude/plans/lets-see-options-for-merry-dijkstra.md`. implicit_ctx_enabled: bool = false, current_ctx_ref: Ref = Ref.none, sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern (non-literal selector fallback) jni_env_stack: std.ArrayList(Ref) = std.ArrayList(Ref).empty, // lexical `#jni_env(env)` Ref stack — top is current scope's env for omitted-env `#jni_call` jni_env_stack_base: usize = 0, // index above which the currently-lowering fn's `#jni_env` scopes live; outer-fn Refs aren't valid in this fn's instruction stream jni_env_tl_get_fid: ?FuncId = null, // extern `sx_jni_env_tl_get` (from library/vendors/sx_jni_runtime/sx_jni_env_tl.c) jni_env_tl_set_fid: ?FuncId = null, // extern `sx_jni_env_tl_set` needs_jni_env_tl_runtime: bool = false, // set when lowering touches the JNI env TL; signals Compilation to auto-link the runtime .c trace_push_fid: ?FuncId = null, // extern `sx_trace_push` (ERR E3.1, from library/vendors/sx_trace_runtime/sx_trace.c) trace_clear_fid: ?FuncId = null, // extern `sx_trace_clear` needs_trace_runtime: bool = false, // set when lowering emits a trace push/clear; signals Compilation to auto-link sx_trace.c chain_fail_target: ?ChainFailTarget = null, // ERR E2.4: when set, a failable `or` chain routes its TOTAL failure here (an absorbing consumer like `catch`) instead of propagating to the function current_foreign_class: ?*const ast.ForeignClassDecl = null, // set while lowering a `#jni_main` (or any sx-defined `#jni_class`) bodied method — `super.method(args)` dispatch resolves the parent class against this fcd's `#extends` current_foreign_method: ?ast.ForeignMethodDecl = null, // the specific method whose body is being lowered; `super.(...)` reuses its signature type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId) current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch) force_block_value: bool = false, // set by lowerBlockValue to extract if-else values block_terminated: bool = false, // set when constant-folded if emits a return/br into current block in_lambda_body: bool = false, // true while lowering a closure-literal body; sharpens the `raise`-not-failable diagnostic (ERR E5.1: tell the user to annotate `-> (T, !)`) defer_stack: std.ArrayList(CleanupEntry) = std.ArrayList(CleanupEntry).empty, // block-scoped defer + onfail cleanup stack func_defer_base: usize = 0, // defer stack base for current function (lowerReturn drains to this) deferred_type_fns: std.ArrayList([]const u8) = std.ArrayList([]const u8).empty, // functions deferred until all types registered processing_deferred: bool = false, // true when processing deferred functions (prevents re-deferral) /// True while emitting the compiler-synthesized default-Context global /// (`emitDefaultContextGlobal`). The built-in allocator infrastructure /// (`CAllocator`/`Allocator`/`Context`) is resolved as compiler internals, /// independent of the user program's import STYLE (a `std :: #import` puts /// `CAllocator` behind a namespace edge from `main`, so the user-visibility /// gate would reject it) — so the bare TYPE leaf falls open here (F1). emitting_default_context: bool = false, /// Names declared as a BLOCK-LOCAL type (a `Foo :: struct/enum/union/error_set` /// or bare type-decl statement inside a fn / init body), keyed by the DECLARING /// source. A local type registers into the global type table and CLOBBERS a /// same-name top-level entry (`registerStructDecl`'s `findByName … orelse intern` /// + `updatePreservingKey`), so after it lowers the name IS the local type /// program-wide (single-author, pre-E2). The source-aware bare-TYPE gate consults /// this so a legitimately block-local type resolves in ITS OWN source (never /// mistaken for a namespaced-only leak, even when a namespaced-only import authors /// a same-name top-level type — R2). It is keyed by source because a local is /// visible ONLY within the source that declares it: an imported template's field /// resolution (run in the template's source context, E3 attempt-4) must NOT bind a /// name the CALLER declared block-local (E3 attempt-5). local_type_names: std.StringHashMap(std.StringHashMap(void)) = std.StringHashMap(std.StringHashMap(void)).init(std.heap.page_allocator), struct_defaults_map: std.StringHashMap([]const ?*const Node) = std.StringHashMap([]const ?*const Node).init(std.heap.page_allocator), // struct name → field defaults struct_instance_bindings: std.StringHashMap(std.StringHashMap(TypeId)) = std.StringHashMap(std.StringHashMap(TypeId)).init(std.heap.page_allocator), // mangled struct name → type param bindings struct_instance_template: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // mangled struct name → template name struct_instance_author: std.StringHashMap(*const ast.StructDecl) = std.StringHashMap(*const ast.StructDecl).init(std.heap.page_allocator), // mangled struct name → authoring StructDecl (CP-2: body-author ≡ layout-author) comptime_value_bindings: ?std.StringHashMap(i64) = null, // comptime value bindings ($N → integer value) protocol_thunk_map: std.StringHashMap([]const FuncId) = std.StringHashMap([]const FuncId).init(std.heap.page_allocator), // "Proto\x00Type" → thunk FuncIds protocol_vtable_type_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // protocol name → vtable struct TypeId protocol_vtable_global_map: std.StringHashMap(inst_mod.GlobalId) = std.StringHashMap(inst_mod.GlobalId).init(std.heap.page_allocator), // "Proto\x00Type" → vtable GlobalId param_impl_map: std.StringHashMap(std.ArrayList(ParamImplEntry)) = std.StringHashMap(std.ArrayList(ParamImplEntry)).init(std.heap.page_allocator), // "Proto\x00\x00" → impl entries (parameterised protocols only; list lets Phase 4/5 detect cross-module overlap) /// Pack-variadic impl entries — separate map keyed by `"Proto\x00"` /// (NO source suffix) so a single impl `Closure(..$args) -> $R` can be /// matched against many concrete source shapes. Concrete impls in /// `param_impl_map` win when both match (specificity rule). param_impl_pack_map: std.StringHashMap(std.ArrayList(PackParamImplEntry)) = std.StringHashMap(std.ArrayList(PackParamImplEntry)).init(std.heap.page_allocator), /// Active pack bindings during monomorphisation. Mirrors `type_bindings` /// but for variadic pack names: `args → [T1, T2, ...]`. Read by /// `resolveTypeWithBindings` on closure_type_expr to substitute /// `Closure(..$args) -> $R` into a concrete closure type. pack_bindings: ?std.StringHashMap([]const TypeId) = null, /// Active when lowering an inlined comptime-call body. `return X;` /// inside the body must NOT emit a `ret` into the caller's LLVM /// function — instead it stores X into `.slot` (typed `.ret_ty`) /// and sets `block_terminated` so the inliner can load the slot /// once the body finishes. Without this, a body like /// `{ return 42; }` truncates the caller's basic block mid-flight /// and trips LLVM's "Terminator found in the middle of a basic /// block" verifier. inline_return_target: ?InlineReturnInfo = null, /// Active pack-arg-node bindings during a comptime call's body lowering. /// Maps the pack-param name (e.g. `args`) to the slice of call-site /// argument AST nodes. `lowerIndexExpr` (and `inferExprType`) check /// this map when the index expression's base is an identifier matching /// a pack name AND the index is a comptime int literal — substitutes /// with the i-th call arg's lowered value so the static type tracks /// the call arg's real type instead of `Any`. The `[]Any` slice path /// remains the runtime-indexed fallback for non-literal indices. pack_arg_nodes: ?std.StringHashMap([]const *const Node) = null, /// Active pack-arity bindings during a pack-fn mono's body lowering. /// Maps the pack-param name (e.g. `args`) to N. `lowerFieldAccess` /// uses this to resolve `args.len` to a compile-time constant Ref /// when no `args` slice is in scope (the mono path doesn't /// materialise the slice). pack_param_count: ?std.StringHashMap(u32) = null, /// Type-only pack binding consulted by `inferExprType` for /// `args[]` (parallel to `pack_arg_nodes` which carries the /// AST substitution used at lowering time). Holds the concrete /// call-site arg types in declaration order — same data the /// mono's pack-param signature uses. Lets generic-`$R` return /// inference resolve `args[i]` to the correct concrete type even /// before the mono's scope is set up. pack_arg_types: ?std.StringHashMap([]const TypeId) = null, /// Active during a protocol-pack mono's body lowering: pack-param name → /// constraint protocol name (`..xs: Box` ⇒ `xs` → `"Box"`). Lets /// `lowerFieldAccess` enforce the interface-only rule — a member access /// `xs[i].` is rejected unless `` is one of the protocol's methods. /// Null / absent for the comptime `..$args` pack (no constraint). pack_constraint: ?std.StringHashMap([]const u8) = null, struct_const_map: std.StringHashMap(StructConstInfo) = std.StringHashMap(StructConstInfo).init(std.heap.page_allocator), // "Struct.CONST" → value info foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames target_config: ?@import("../target.zig").TargetConfig = null, // compilation target (for inline if) comptime_constants: std.StringHashMap(ComptimeValue) = std.StringHashMap(ComptimeValue).init(std.heap.page_allocator), // compile-time known constants (e.g. OS, ARCH) diagnostics: ?*errors.DiagnosticList = null, // error reporting with source locations xx_reentrancy: std.AutoHashMap(u64, void) = std.AutoHashMap(u64, void).init(std.heap.page_allocator), // (src_ty, dst_ty) pairs currently being resolved through user-space Into; prevents infinite monomorphisation when a convert body re-enters the same xx /// Whole-program-converged inferred error sets (ERR E1.4b): top-level /// bare-`!` function name → its sorted escape-tag ids (literal raises + /// pure-failable `try` edges, fix-pointed across the call graph). The /// shared `!` placeholder TypeId stays empty; this side map holds the real /// per-function sets (sidesteps the name-only error-set interning). Read by /// `lowerTry`'s named-caller widening and the empty-inferred warning. inferred_error_sets: std.StringHashMap([]const u32) = std.StringHashMap([]const u32).init(std.heap.page_allocator), /// Whole-program-converged inferred error sets keyed by closure/function /// VALUE-signature shape (ERR E5.1 sub-feature 2): every occurrence of /// `Closure() -> (T, !)` with a structurally identical value-signature /// shares one node; each bare-`!` closure literal of that shape unions its /// escape tags in. Read by `checkEscapeWidening` when a `try` operand is a /// closure/fn-type SLOT call (no static fn name). Key = `closureShapeKey`. shape_inferred_sets: std.StringHashMap([]const u32) = std.StringHashMap([]const u32).init(std.heap.page_allocator), pub const ComptimeValue = union(enum) { int_val: i64, enum_tag: struct { ty: TypeId, tag: u32 }, }; const StructConstInfo = struct { value: *const Node, ty: ?TypeId, // null if no type annotation (inferred) }; /// One impl block for a parameterised protocol (e.g. `impl Into(Block) for Closure() -> void`). /// Stored in `param_impl_map` keyed by (protocol_name, target_args_mangled, source_mangled). /// `defining_module` enables import-scoped visibility + cross-module duplicate diagnostics. pub const ParamImplEntry = struct { methods: []const *const ast.FnDecl, source_ty: TypeId, target_args: []const TypeId, defining_module: []const u8, span: ast.Span, }; const InlineReturnInfo = struct { slot: Ref, ret_ty: TypeId, done_bb: BlockId }; /// ERR E2.4 — where a failable `or` chain's TOTAL failure routes when the /// chain is the operand of an absorbing consumer (`catch`). `bb` is a block /// with a single parameter typed `set` (the error tag); the chain branches /// there with its final error instead of propagating to the function. const ChainFailTarget = struct { bb: BlockId, set: TypeId }; /// Pack-variadic impl entry — `impl Proto(Args...) for Closure(Prefix..., ..$pack) -> $ret`. /// Matches any concrete closure source whose first `prefix_len` param types /// equal `source_pack_ty`'s fixed prefix; the tail binds to `pack_var_name` /// (e.g. "args") and the source's return type binds to `ret_var_name` /// (e.g. "R") when the impl's return is generic. `ret_var_name == null` /// means the return type is concrete and must match exactly. pub const PackParamImplEntry = struct { methods: []const *const ast.FnDecl, source_pack_ty: TypeId, target_args: []const TypeId, defining_module: []const u8, span: ast.Span, pack_var_name: []const u8, ret_var_name: ?[]const u8, }; /// Caller-state protection for lowering a function body re-entrantly — a /// lazily lowered callee, a qualified `ns.fn` alias, or an out-of-line /// same-name author. `enter` snapshots the in-progress builder / scope / /// flag / pack / jni state and installs a fresh set for the nested body; /// `restore` puts the caller's state back. Lowering a callee must be /// transparent to the caller's own lowering — notably `block_terminated`, /// which leaking back would mark the caller's trailing statements /// dead-after-terminator (issue 0100 F2). pub const FnBodyReentry = struct { l: *Lowering, func: ?FuncId, block: ?BlockId, counter: u32, scope: ?*Scope, defer_base: usize, block_terminated: bool, force_block_value: bool, source_file: ?[]const u8, jni_env_base: usize, pack_arg_nodes: ?std.StringHashMap([]const *const Node), pack_param_count: ?std.StringHashMap(u32), pack_arg_types: ?std.StringHashMap([]const TypeId), inline_return_target: ?InlineReturnInfo, pub fn enter(l: *Lowering) FnBodyReentry { const g = FnBodyReentry{ .l = l, .func = l.builder.func, .block = l.builder.current_block, .counter = l.builder.inst_counter, .scope = l.scope, .defer_base = l.func_defer_base, .block_terminated = l.block_terminated, .force_block_value = l.force_block_value, .source_file = l.current_source_file, .jni_env_base = l.jni_env_stack_base, .pack_arg_nodes = l.pack_arg_nodes, .pack_param_count = l.pack_param_count, .pack_arg_types = l.pack_arg_types, .inline_return_target = l.inline_return_target, }; // The `#jni_env` Ref stack is lexical to ONE function's instruction // stream; move the visible base to the current top. Pack-fn mono // state is likewise lexical to the pack-fn body — null it so a // callee sharing a param NAME with the active pack doesn't fold the // outer mono's arity into its own `.len`. l.jni_env_stack_base = l.jni_env_stack.items.len; l.pack_arg_nodes = null; l.pack_param_count = null; l.pack_arg_types = null; l.inline_return_target = null; l.func_defer_base = l.defer_stack.items.len; l.block_terminated = false; l.force_block_value = false; return g; } pub fn restore(g: FnBodyReentry) void { const l = g.l; l.setCurrentSourceFile(g.source_file); l.scope = g.scope; l.func_defer_base = g.defer_base; l.block_terminated = g.block_terminated; l.force_block_value = g.force_block_value; l.builder.func = g.func; l.builder.current_block = g.block; l.builder.inst_counter = g.counter; l.jni_env_stack_base = g.jni_env_base; l.pack_arg_nodes = g.pack_arg_nodes; l.pack_param_count = g.pack_param_count; l.pack_arg_types = g.pack_arg_types; l.inline_return_target = g.inline_return_target; } }; pub fn init(module: *Module) Lowering { return .{ .module = module, .builder = Builder.init(module), .alloc = module.alloc, .lowered_functions = std.StringHashMap(void).init(module.alloc), .fn_decl_fids = std.AutoHashMap(*const ast.FnDecl, FuncId).init(module.alloc), .lowered_fids = std.AutoHashMap(FuncId, void).init(module.alloc), .nominal_name_authors = std.AutoHashMap(types.StringId, []const u8).init(module.alloc), .program_index = ProgramIndex.init(module.alloc), }; } // ── Public entry point ────────────────────────────────────────── pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { // Stamp this node's source span onto the instructions it emits (ERR // E3.0 — feeds DWARF line-info + comptime frame resolution). Save/ // restore so a parent's later emits keep the parent's span after a // child lowers. Skip the empty default so synthetic nodes don't reset // a meaningful enclosing span to offset 0. const saved_span = self.builder.current_span; defer self.builder.current_span = saved_span; if (node.span.start != 0 or node.span.end != 0) self.builder.current_span = .{ .start = node.span.start, .end = node.span.end }; // A node carrying an explicit `source_file` is one spliced into a body // from another module — a substituted caller comptime-`$`-arg (stamped // at the `cpn` build site in lowerComptimeCall / monomorphizePackFn). // Resolve its bare names in THAT module's visibility context, overriding // the body's defining-module pin, then restore so sibling callee nodes // keep the enclosing context. Ordinary expression nodes never carry a // `source_file`, so this is a no-op on the hot path. const restore_source = node.source_file != null; const saved_source = self.current_source_file; if (node.source_file) |sf| self.setCurrentSourceFile(sf); defer if (restore_source) self.setCurrentSourceFile(saved_source); return switch (node.data) { // Bare `$` in expression position → an `[]Type` slice // value where each element is a `const_type(arg_types[i])`. // Per `Type → .any` mapping in type_bridge, the IR slice // type is `[]Any`; the interp stores raw `.type_tag` Values // (NOT Any-boxed) so `args[i]` reads back as a Type value // directly. Step 4 final slice — lets builder fns walk the // whole pack at interp time. .comptime_pack_ref => |cpr| blk: { // `$` is overloaded in expression position: // - Inside a pack-fn mono (or a `tryPackImplMatch` // impl mono), `name` is a pack binding → slice of // element types (`[]Type` lowered as `[]Any`). // - Inside an impl mono whose impl pattern bound a // single-type generic (`$R: Type` in // `Closure(..$args) -> $R`), `name` is in // `type_bindings` → single `const_type(R)` value. // Pack arg types are checked first (the slice form), // then pack_bindings (the impl-mono mirror), then // type_bindings (single-type binding); only if all // miss is it a real "outside an active binding" error. if (self.pack_arg_types) |pat| { if (pat.get(cpr.pack_name)) |arg_tys| { break :blk self.buildPackSliceValue(arg_tys); } } if (self.pack_bindings) |pb| { if (pb.get(cpr.pack_name)) |arg_tys| { break :blk self.buildPackSliceValue(arg_tys); } } if (self.type_bindings) |tb| { if (tb.get(cpr.pack_name)) |ty| { break :blk self.builder.constType(ty); } } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack reference ${s} used outside an active pack binding", .{cpr.pack_name}); } break :blk self.builder.constNull(self.module.types.sliceOf(.any)); }, // Pack-index in expression position: `$[]` → // `const_type(arg_types[index])`. Yields a comptime-only // Type value (`Value.type_tag(TypeId)` in the interp). // OOB / no-active-pack-binding → focused diagnostic; the // emitted Ref is a const_type(.void) placeholder so the // verifier downstream catches misuse rather than silently // succeeding with .void. .pack_index_type_expr => |pi| blk: { if (self.pack_arg_types) |pat| { if (pat.get(pi.pack_name)) |arg_tys| { if (pi.index < arg_tys.len) { break :blk self.builder.constType(arg_tys[pi.index]); } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index value ${s}[{}] out of bounds: '{s}' has {} element{s}", .{ pi.pack_name, pi.index, pi.pack_name, arg_tys.len, if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"), }); } break :blk self.builder.constType(.void); } } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index value ${s}[{}] used outside an active pack binding", .{ pi.pack_name, pi.index, }); } break :blk self.builder.constType(.void); }, .int_literal => |lit| { // If target is a float type, emit as float literal if (self.target_type) |tt| { if (tt == .f32 or tt == .f64) { return self.builder.constFloat(@floatFromInt(lit.value), tt); } } const ty = if (self.target_type) |tt| blk: { break :blk if (self.isIntEx(tt)) tt else .s64; } else .s64; return self.builder.constInt(lit.value, ty); }, .float_literal => |lit| { const fty: TypeId = if (self.target_type) |tt| (if (tt == .f32 or tt == .f64) tt else .f64) else .f64; return self.builder.constFloat(lit.value, fty); }, .bool_literal => |lit| self.builder.constBool(lit.value), .string_literal => |lit| blk: { const str = if (lit.is_raw) lit.raw else unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; const sid = self.module.types.internString(str); break :blk self.builder.constString(sid); }, // A bare `null` / `---` with no surrounding type expectation is a // legitimate typeless literal, not a failed lookup: `.void` is its // intentional default (emitConstNull/emitConstUndef handle void as // null-ptr / undef-i64). Not a candidate for the `.unresolved` tripwire. .null_literal => self.builder.constNull(self.target_type orelse .void), .undef_literal => self.builder.constUndef(self.target_type orelse .void), .identifier => |id| blk: { // A bare pack name in value position has no runtime // representation (Decision 1). Projections (`xs.len`, `xs[i]`, // `xs.value`) are field/index nodes handled elsewhere, so a bare // `xs` reaching here is always a pack-as-value misuse. if (self.isPackName(id.name)) { break :blk self.diagPackAsValue(id.name, node.span, .generic); } if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { break :blk self.builder.load(binding.ref, binding.ty); } break :blk binding.ref; } } // Check compile-time constants (OS, ARCH, POINTER_SIZE) before globals if (self.comptime_constants.get(id.name)) |cv| { switch (cv) { .int_val => |iv| break :blk self.builder.constInt(iv, .s64), .enum_tag => |et| break :blk self.builder.constInt(@intCast(et.tag), et.ty), } } // `context` resolves to a load through the lowering's // current `__sx_ctx` pointer. Every sx function (and // every `push Context.{...}` body) sets `current_ctx_ref` // to a `*Context` it owns, so this is one indirection. if (std.mem.eql(u8, id.name, "context")) { if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) { break :blk self.diagnoseMissingContext("the `context` identifier"); } const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse { break :blk self.diagnoseMissingContext("the `context` identifier"); }; break :blk self.builder.load(self.current_ctx_ref, ctx_ty); } // Check globals (#run constants) if (self.program_index.global_names.get(id.name)) |gi| { break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); } // Check module-level value constants (e.g. AF_INET :s32: 2) if (self.program_index.module_const_map.get(id.name)) |ci_global| { if (!self.isNameVisible(id.name)) { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{id.name}); break :blk self.emitError(id.name, node.span); } // F2: emit the SOURCE-AWARE author's value (own-wins), not the // global last-wins `ci_global`. ≥2 flat-visible same-name const // authors → a loud ambiguity (issue 0105 / 0760), never a silent // pick. `.none` after a visible name is the registration-only // author (no per-source partition) — emit its global value. switch (self.selectModuleConst(id.name)) { .resolved => |sel| break :blk self.emitModuleConst(sel.info, sel.source), .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name}); break :blk self.emitPlaceholder(id.name); }, .none => break :blk self.emitModuleConst(ci_global, null), } } // Check if it's a function name — produce function pointer reference // Resolve mangled name for block-local functions const eff_fn_name = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name; if (self.program_index.fn_ast_map.contains(eff_fn_name)) { // Visibility check only for user-typed bare names (id.name // == eff_fn_name) without a UFCS alias. Mangled local- // scope names and UFCS rewrites are compiler indirections // and stay exempt. if (std.mem.eql(u8, eff_fn_name, id.name) and self.program_index.ufcs_alias_map.get(id.name) == null and !self.isNameVisible(eff_fn_name)) { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{eff_fn_name}); break :blk self.emitError(eff_fn_name, node.span); } // Type-as-value: if target is Any (Type variable), produce a type name string if (self.target_type == .any) { const fd = self.program_index.fn_ast_map.get(eff_fn_name).?; const fn_type_str = self.formatFnTypeString(fd); const sid = self.module.types.internString(fn_type_str); const str = self.builder.constString(sid); break :blk self.builder.boxAny(str, .string); } // fix-0102d site 2: taking a bare same-name fn as a VALUE // (func_ref, fn-ptr / closure coercion) must capture the // RESOLVED author's FuncId for a genuine flat collision, not // the first-wins winner's. Plain bare name only; `.ambiguous` // → loud diagnostic; `.none` → existing first-wins path. The // winner is lazily lowered ONLY on `.none` — a rerouted value // never uses the winner, so its body must not be lowered. const value_fid: ?FuncId = blk_fv: { if (std.mem.eql(u8, eff_fn_name, id.name) and self.program_index.ufcs_alias_map.get(id.name) == null and (if (self.scope) |scope| scope.lookup(id.name) == null else true)) { if (self.current_source_file) |caller_file| { switch (self.selectPlainCallableAuthor(id.name, caller_file)) { .func => |sf| { var selected = sf; break :blk_fv self.selectedFuncId(&selected, id.name); }, .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{id.name}); break :blk self.emitError(id.name, node.span); }, .none => {}, } } } if (!self.lowered_functions.contains(eff_fn_name)) { self.lazyLowerFunction(eff_fn_name); } break :blk_fv self.resolveFuncByName(eff_fn_name); }; if (value_fid) |fid| { // Auto-promote bare function → closure when target_type is closure if (self.target_type) |tt| { if (!tt.isBuiltin()) { const tt_info = self.module.types.get(tt); if (tt_info == .closure) { const tramp_id = self.createBareFnTrampoline(fid, tt_info.closure); break :blk self.builder.closureCreate(tramp_id, Ref.none, tt); } // Coercing a bare fn name to a fn-pointer // type — the call_conv must match. A // default-conv sx fn assigned to a // callconv(.c) slot (e.g. passed to // pthread_create) would otherwise crash at // runtime when the C caller doesn't supply // the implicit __sx_ctx arg. if (tt_info == .function) { const func_cc = self.module.functions.items[@intFromEnum(fid)].call_conv; if (func_cc != tt_info.function.call_conv) { if (self.diagnostics) |d| { const want_cc = if (tt_info.function.call_conv == .c) "callconv(.c)" else "default sx convention"; const have_cc = if (func_cc == .c) "callconv(.c)" else "default sx convention"; d.addFmt(.err, node.span, "call-convention mismatch: '{s}' is declared with {s} but the target type expects {s}", .{ eff_fn_name, have_cc, want_cc }); } break :blk self.emitPlaceholder(eff_fn_name); } } // NOTE: `xx : *void` (e.g. // `class_addMethod(_, _, xx my_imp, _)`) // is intentionally NOT diagnosed here. // Manually-constructed Closure values // legitimately store default-conv sx fns // into a `*void` slot for sx-side dispatch // through the closure trampoline ABI. The // compiler can't distinguish C-side vs // sx-side use from the cast alone. // examples/50-smoke.sx has both shapes. } } break :blk self.builder.emit(.{ .func_ref = fid }, .s64); } } // Type-as-value: a name that resolves to a TypeId // (primitive, alias, registered struct/enum/union, // generic-struct instantiation) evaluates to a // `const_type` in expression position. Works for // direct assignment to a `Type`-typed slot // (`x: Type = Vec4`), comparison (`x == Vec4`), and // pack-arg / Any context (boxing happens at the // consumer). // E4 single-hop visibility + ambiguity gate: a bare type name used // as a VALUE (`x: Type = COnly`, `x == COnly`) reachable only over // 2+ flat hops is not bare-visible (consistent with annotations / // 0763); ≥2 direct flat same-name authors are ambiguous (loud // diagnostic, 0755/0767). A single source-keyed author — including // the querying source's OWN author over a same-name flat import // (own-wins, 0754) — resolves to ITS TypeId, NOT whichever same-name // author a global `findByName` would pick. A value name / generic // param / undeclared name → `.proceed`, falling through below. const ty = blk_ty: { switch (self.headTypeGate(id.name, node.span)) { .ambiguous, .not_visible => break :blk self.emitPlaceholder(id.name), .resolved => |tid| break :blk_ty tid, .proceed => {}, } if (self.type_bindings) |tb| { if (tb.get(id.name)) |t| break :blk_ty t; } if (self.program_index.type_alias_map.get(id.name)) |t| break :blk_ty t; if (type_bridge.resolveTypePrimitive(id.name)) |t| break :blk_ty t; const name_id = self.module.types.internString(id.name); if (self.module.types.findByName(name_id)) |t| break :blk_ty t; break :blk_ty TypeId.void; }; if (ty != .void) { break :blk self.builder.constType(ty); } // Unknown identifier break :blk self.emitError(id.name, node.span); }, .binary_op => |bop| self.lowerBinaryOp(&bop), .unary_op => |uop| blk: { // `xx ` with a slice target materializes the comptime // pack into a runtime `[]elem` (issue 0053). Must run before the // operand is lowered (a bare pack name otherwise hits the // pack-as-value error). if (uop.op == .xx and uop.operand.data == .identifier and self.isPackName(uop.operand.data.identifier.name)) { const pname = uop.operand.data.identifier.name; if (self.target_type) |tt| { if (!tt.isBuiltin() and self.module.types.get(tt) == .slice) { break :blk self.lowerPackToSlice(pname, tt); } } break :blk self.diagPackAsValue(pname, node.span, .generic); } // address_of(index_expr) → emit index_gep (pointer to element) instead of index_get + addr_of if (uop.op == .address_of and uop.operand.data == .index_expr) { const ie = &uop.operand.data.index_expr; const idx = self.lowerExpr(ie.index); const obj_ty = self.inferExprType(ie.object); const elem_ty = self.getElementType(obj_ty); const ptr_ty = self.module.types.ptrTo(elem_ty); // For array targets, use the storage pointer (alloca for a // local, global_addr for a module global) so the resulting // pointer is into live storage, not a loaded copy. const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; const base = if (is_array) (self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object)) else self.lowerExpr(ie.object); break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty); } // address_of(field_access) → use lowerExprAsPtr for GEP chain // Handles all cases: pointer-based, index-based, nested field access if (uop.op == .address_of and uop.operand.data == .field_access) { const inner_ty = self.inferExprType(uop.operand); const ptr_ty = self.module.types.ptrTo(inner_ty); const ptr = self.lowerExprAsPtr(uop.operand); break :blk self.builder.emit(.{ .addr_of = .{ .operand = ptr } }, ptr_ty); } // address_of(identifier) → return alloca directly (pointer to variable) if (uop.op == .address_of and uop.operand.data == .identifier) { const id_name = uop.operand.data.identifier.name; if (self.scope) |scope| { if (scope.lookup(id_name)) |binding| { if (binding.is_alloca) { const ptr_ty = self.module.types.ptrTo(binding.ty); break :blk self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty); } } } // address_of(global) → emit global_addr (pointer to global, not load) if (self.program_index.global_names.get(id_name)) |gi| { const ptr_ty = self.module.types.ptrTo(gi.ty); break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty); } } const operand = self.lowerExpr(uop.operand); break :blk switch (uop.op) { .negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)), .not => self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool), .bit_not => self.builder.emit(.{ .bit_not = .{ .operand = operand } }, self.inferExprType(uop.operand)), .xx => self.lowerXX(operand, uop.operand), .address_of => blk2: { const inner_ty = self.inferExprType(uop.operand); const ptr_ty = self.module.types.ptrTo(inner_ty); break :blk2 self.builder.emit(.{ .addr_of = .{ .operand = operand } }, ptr_ty); }, }; }, .if_expr => |ie| self.lowerIfExpr(&ie), .match_expr => |me| self.lowerMatch(&me), .while_expr => |we| self.lowerWhile(&we), .for_expr => |fe| self.lowerFor(&fe), .break_expr => self.lowerBreak(), .continue_expr => self.lowerContinue(), .call => |c| self.lowerCall(&c), .ffi_intrinsic_call => |fic| self.lowerFfiIntrinsicCall(&fic), .field_access => |fa| self.lowerFieldAccess(&fa, node.span), .struct_literal => |sl| self.lowerStructLiteral(&sl, node.span), .array_literal => |al| self.lowerArrayLiteral(&al), .index_expr => |ie| self.lowerIndexExpr(&ie), .slice_expr => |se| self.lowerSliceExpr(&se), .lambda => |lam| self.lowerLambda(&lam), .force_unwrap => |fu| self.lowerForceUnwrap(&fu), .null_coalesce => |nc| self.lowerNullCoalesce(&nc), .deref_expr => |de| self.lowerDerefExpr(&de), .enum_literal => |el| self.lowerEnumLiteral(&el), .comptime_expr => |ct| self.lowerInlineComptime(ct.expr), .insert_expr => |ins| blk: { break :blk self.lowerInsertExprValue(ins.expr); }, .tuple_literal => |tl| self.lowerTupleLiteral(&tl), .spread_expr => self.emitError("spread_expr", node.span), .chained_comparison => |cc| self.lowerChainedComparison(&cc), // `#jni_env(env) { body }` in expression position — the block's // value becomes the env-scope's value. Save→set→body-value→restore. .jni_env_block => |eb| blk: { const env_ref = self.lowerExpr(eb.env); const fids = self.getJniEnvTlFids(); const ptr_ty = self.module.types.ptrTo(.void); const saved_tl = self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty); const set_args = self.alloc.dupe(Ref, &.{env_ref}) catch unreachable; _ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = set_args } }, .void); self.jni_env_stack.append(self.alloc, env_ref) catch unreachable; const value = self.lowerBlockValue(eb.body) orelse self.builder.constInt(0, .void); _ = self.jni_env_stack.pop(); const restore_args = self.alloc.dupe(Ref, &.{saved_tl}) catch unreachable; _ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void); break :blk value; }, // Statements that can appear in expression position .block => |blk| blk: { // Create a child scope for block-level variable shadowing var block_scope = Scope.init(self.alloc, self.scope); const saved_scope = self.scope; self.scope = &block_scope; const saved_defer_len = self.defer_stack.items.len; defer { self.emitBlockDefers(saved_defer_len); self.scope = saved_scope; block_scope.deinit(); } // This block sits in value position (lowerExpr is reached only // for value contexts — statement blocks go through lowerBlock). // If its last expression's value is discarded by a `;`, the // surrounding expression has no value to use: report it. if (!blk.produces_value and blk.discarded_semi != null) { if (self.diagnostics) |diags| { diags.addFmt(.err, blk.discarded_semi.?, "this block is used as a value but its last expression's value is discarded by this `;` — drop the `;`", .{}); } } // A block in expression position yields its last statement's // value only when it produces one (no trailing `;`); otherwise // it runs as statements and evaluates to void. if (blk.produces_value and blk.stmts.len > 0) { for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| { self.lowerStmt(stmt); } break :blk self.tryLowerAsExpr(blk.stmts[blk.stmts.len - 1]) orelse self.builder.constInt(0, .void); } for (blk.stmts) |stmt| { self.lowerStmt(stmt); } break :blk self.builder.constInt(0, .void); }, // type_expr can appear as a variable reference when the name collides // with a builtin type name (e.g. s2, u8). Check scope first. .type_expr => |te| blk: { if (self.scope) |scope| { if (scope.lookup(te.name)) |binding| { if (binding.is_alloca) { break :blk self.builder.load(binding.ref, binding.ty); } break :blk binding.ref; } } if (self.program_index.global_names.get(te.name)) |gi| { break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); } // Type literal in expression position → first-class // `const_type` Value (i64 = TypeId.index()). Makes // `t : Type = f64;` store a real TypeId; lets // `t == f64` icmp at runtime against the same TypeId. if (self.isKnownTypeName(te.name)) { const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); break :blk self.builder.constType(ty); } break :blk self.emitError(te.name, node.span); }, .try_expr => |te| self.lowerTry(te.operand, node.span), .catch_expr => |ce| self.lowerCatch(&ce, node.span), .caller_location => self.lowerCallerLocation(node), else => self.emitError("unknown_expr", node.span), }; } /// 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 { if (node.data != .identifier) return null; const scope = self.scope orelse return null; const binding = scope.lookup(node.data.identifier.name) orelse return null; if (!binding.is_ref_capture or binding.ty.isBuiltin()) return null; const info = self.module.types.get(binding.ty); return if (info == .pointer) info.pointer.pointee else null; } fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref { // Short-circuit: `a and b` → if a then b else false if (bop.op == .and_op) { const lhs = self.lowerExpr(bop.lhs); const rhs_bb = self.freshBlock("and.rhs"); const merge_bb = self.freshBlockWithParams("and.merge", &.{.bool}); const false_val = self.builder.constBool(false); self.builder.condBr(lhs, rhs_bb, &.{}, merge_bb, &.{false_val}); self.builder.switchToBlock(rhs_bb); const rhs = self.lowerExpr(bop.rhs); self.builder.br(merge_bb, &.{rhs}); self.builder.switchToBlock(merge_bb); return self.builder.blockParam(merge_bb, 0, .bool); } // Short-circuit: `a or b` → if a then true else b if (bop.op == .or_op) { // A failable `or` (value-terminator or chain) routes to the error- // handling lowering, not the optional/boolean unwrap below. Detected // structurally (a `try`-chain's value type is non-failable `T`, so a // type-only `exprIsFailable(lhs)` would miss nested chains). if (self.orIsFailableChain(bop)) { return self.lowerFailableOr(bop); } const lhs = self.lowerExpr(bop.lhs); const rhs_bb = self.freshBlock("or.rhs"); const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool}); const true_val = self.builder.constBool(true); self.builder.condBr(lhs, merge_bb, &.{true_val}, rhs_bb, &.{}); self.builder.switchToBlock(rhs_bb); const rhs = self.lowerExpr(bop.rhs); self.builder.br(merge_bb, &.{rhs}); self.builder.switchToBlock(merge_bb); return self.builder.blockParam(merge_bb, 0, .bool); } // Type-literal comparison fold: when both sides are type-shaped // AST nodes (`s64`, `*u8`, `?T`, `[3]f64`, etc.) OR resolve to // a static TypeId at lower time (`type_of(x)` for any // statically-typed `x`), resolve each and emit a `const_bool`. // Same semantic as `type_eq(A, B)` but using the standard `==` // operator — the user's intuition. Without the fold, both // sides lower as `const_type` undef-i64 and the runtime icmp // returns garbage. if (bop.op == .eq or bop.op == .neq) { if (self.isStaticTypeRef(bop.lhs) and self.isStaticTypeRef(bop.rhs)) { const lhs_ty = self.resolveTypeArg(bop.lhs); const rhs_ty = self.resolveTypeArg(bop.rhs); const eq_result = lhs_ty == rhs_ty; return self.builder.constBool(if (bop.op == .eq) eq_result else !eq_result); } } // Any-shaped `==` (e.g. `t == s64` where `t: Type`): both // operands are 16-byte `{tag, value}` aggregates. LLVM // doesn't accept `icmp` on aggregates directly. Decompose // via `unbox_any` (which extracts the value field at // `.s64`) and compare the i64s. Tag fields are stable // across compilations of the same source so value-only // identity is enough. if (bop.op == .eq or bop.op == .neq) { const lhs_ty = self.inferExprType(bop.lhs); const rhs_ty = self.inferExprType(bop.rhs); if (lhs_ty == .any and rhs_ty == .any) { const lhs = self.lowerExpr(bop.lhs); const rhs = self.lowerExpr(bop.rhs); const lhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = lhs } }, .s64); const rhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = rhs } }, .s64); if (bop.op == .eq) { return self.builder.emit(.{ .cmp_eq = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool); } else { return self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool); } } } // Special case: optional == null / optional != null if (bop.op == .eq or bop.op == .neq) { const lhs_is_null = bop.lhs.data == .null_literal; const rhs_is_null = bop.rhs.data == .null_literal; if (lhs_is_null or rhs_is_null) { const opt_node = if (rhs_is_null) bop.lhs else bop.rhs; const opt_ty = self.inferExprType(opt_node); if (!opt_ty.isBuiltin()) { const info = self.module.types.get(opt_ty); if (info == .optional) { const opt_val = self.lowerExpr(opt_node); const has = self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool); // == null → !has_value, != null → has_value return if (bop.op == .eq) self.builder.emit(.{ .bool_not = .{ .operand = has } }, .bool) else has; } } } } // Error-set equality: an error-set value compares only with an // `error.X` tag literal or another error-set value. Comparing to a raw // integer is a type error (coerce with `xx`). `e == error.X` resolves // X against e's set and validates membership. if (bop.op == .eq or bop.op == .neq) { if (self.tryLowerErrorSetEquality(bop)) |result| return result; } // Set target_type for null literals to match the other operand's type. // This ensures null gets the same LLVM type as the value being compared. if (bop.op == .eq or bop.op == .neq) { const null_on_rhs = bop.rhs.data == .null_literal; const null_on_lhs = bop.lhs.data == .null_literal; if (null_on_rhs or null_on_lhs) { var other_ty = if (null_on_rhs) self.inferExprType(bop.lhs) else self.inferExprType(bop.rhs); // Lower the non-null side first when its type isn't statically // inferable, and take the null's type from the lowered value — // never a guess. var pre_lowered: ?Ref = null; if (other_ty == .unresolved) { pre_lowered = self.lowerExpr(if (null_on_rhs) bop.lhs else bop.rhs); other_ty = self.builder.getRefType(pre_lowered.?); } if (other_ty != .void and other_ty != .unresolved) { const saved_tt = self.target_type; self.target_type = other_ty; const lv = if (null_on_lhs or pre_lowered == null) self.lowerExpr(bop.lhs) else pre_lowered.?; const rv = if (null_on_rhs or pre_lowered == null) self.lowerExpr(bop.rhs) else pre_lowered.?; self.target_type = saved_tt; const cmp_op: inst_mod.Op = if (bop.op == .eq) .{ .cmp_eq = .{ .lhs = lv, .rhs = rv } } else .{ .cmp_ne = .{ .lhs = lv, .rhs = rv } }; return self.builder.emit(cmp_op, .bool); } } } var lhs = self.lowerExpr(bop.lhs); // A `for xs: (*x)` capture is a pointer; in a value position (here, an // operand) it auto-derefs to the element. const lhs_ref_pointee = self.refCapturePointee(bop.lhs); if (lhs_ref_pointee) |p| lhs = self.builder.load(lhs, p); // Set target_type from LHS so enum literals on RHS resolve correctly. // When the LHS isn't statically inferable (e.g. `#objc_call(...)`), use // the lowered operand's concrete type rather than a guess. const lhs_ty = blk: { if (lhs_ref_pointee) |p| break :blk p; const it = self.inferExprType(bop.lhs); break :blk if (it == .unresolved) self.builder.getRefType(lhs) else it; }; const saved_tt = self.target_type; if (lhs_ty != .void) { if (!lhs_ty.isBuiltin()) { const lhs_info = self.module.types.get(lhs_ty); if (lhs_info == .@"enum" or lhs_info == .@"union" or lhs_info == .tagged_union) { self.target_type = lhs_ty; } } else if (lhs_ty == .f32 or lhs_ty == .f64) { self.target_type = lhs_ty; } } var rhs = self.lowerExpr(bop.rhs); const rhs_ref_pointee = self.refCapturePointee(bop.rhs); if (rhs_ref_pointee) |p| rhs = self.builder.load(rhs, p); self.target_type = saved_tt; // Result type follows the shared promotion rule: an int LHS with a // float RHS promotes to the float (`s64 * f32` → `f32`); vectors / // structs keep the LHS type. `inferExprType` reuses the same helper // so static typing agrees with the value produced here. const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs); var ty = arithResultType(lhs_ty, rhs_inferred); // Auto-unwrap optional operands for arithmetic/comparison if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .optional) { ty = info.optional.child; lhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = lhs } }, ty); } } const rhs_ty = rhs_ref_pointee orelse self.inferExprType(bop.rhs); if (!rhs_ty.isBuiltin()) { const rhs_info = self.module.types.get(rhs_ty); if (rhs_info == .optional) { rhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = rhs } }, rhs_info.optional.child); } } // String comparison: use str_eq/str_ne (memcmp-based) instead of pointer comparison if (ty == .string and (bop.op == .eq or bop.op == .neq)) { return if (bop.op == .eq) self.builder.emit(.{ .str_eq = .{ .lhs = lhs, .rhs = rhs } }, .bool) else self.builder.emit(.{ .str_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool); } // Tuple operators if (!ty.isBuiltin()) { const lhs_info = self.module.types.get(ty); if (lhs_info == .tuple) { return self.lowerTupleOp(bop, lhs, rhs, ty); } } // Tuple membership: value in (tuple) if (bop.op == .in_op) { const rhs_ty_raw = self.inferExprType(bop.rhs); if (!rhs_ty_raw.isBuiltin()) { const rhs_info_raw = self.module.types.get(rhs_ty_raw); if (rhs_info_raw == .tuple) { return self.lowerTupleMembership(lhs, rhs, rhs_info_raw.tuple); } } } // Reject scalar ops on incompatible operand types (e.g. // `s64 + string`, `s64 < string`, `s64 & string`). The result type // `ty` is derived from the LHS, so without this the op lowers as // ` : ` and either reinterprets the RHS bytes (arithmetic // / bitwise → garbage) or feeds mismatched LLVM types to `icmp` // (ordering → verifier failure). { const group: enum { none, arith, ordering, bitwise } = switch (bop.op) { .add, .sub, .mul, .div, .mod => .arith, .lt, .lte, .gt, .gte => .ordering, .bit_and, .bit_or, .bit_xor, .shl, .shr => .bitwise, else => .none, }; if (group != .none) { const eff_rhs_ty = blk: { if (rhs_ty == .unresolved) break :blk self.builder.getRefType(rhs); if (!rhs_ty.isBuiltin()) { const ri = self.module.types.get(rhs_ty); if (ri == .optional) break :blk ri.optional.child; } break :blk rhs_ty; }; const ok = switch (group) { .arith => self.isArithOperand(ty) and self.isArithOperand(eff_rhs_ty), .ordering => self.isOrderingOperand(ty) and self.isOrderingOperand(eff_rhs_ty), .bitwise => self.isBitwiseOperand(ty) and self.isBitwiseOperand(eff_rhs_ty), .none => true, }; if (!ok) { if (self.diagnostics) |diags| { diags.addFmt(.err, bop.lhs.span, "cannot apply '{s}' to operands of type '{s}' and '{s}'", .{ binOpSymbol(bop.op), self.formatTypeName(ty), self.formatTypeName(eff_rhs_ty), }); } return self.emitPlaceholder("operand-type-mismatch"); } } } return switch (bop.op) { .add => self.builder.add(lhs, rhs, ty), .sub => self.builder.sub(lhs, rhs, ty), .mul => self.builder.mul(lhs, rhs, ty), .div => self.builder.div(lhs, rhs, ty), .mod => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty), .eq => self.builder.cmpEq(lhs, rhs), .neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool), .lt => self.builder.cmpLt(lhs, rhs), .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool), .gt => self.builder.cmpGt(lhs, rhs), .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool), .and_op => self.builder.emit(.{ .bool_and = .{ .lhs = lhs, .rhs = rhs } }, .bool), .or_op => self.builder.emit(.{ .bool_or = .{ .lhs = lhs, .rhs = rhs } }, .bool), .bit_and => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty), .bit_or => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty), .bit_xor => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty), .shl => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty), .shr => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty), .in_op => self.emitError("in_op", bop.lhs.span), }; } /// Handle tuple binary ops: concat (+), repeat (*), comparison (==, !=, <, <=, >, >=) fn lowerTupleOp(self: *Lowering, bop: *const ast.BinaryOp, lhs: Ref, rhs: Ref, lhs_ty: TypeId) Ref { const lhs_info = self.module.types.get(lhs_ty); const lhs_fields = lhs_info.tuple.fields; switch (bop.op) { .add => { // Tuple concatenation: (a, b) + (c, d) → (a, b, c, d) const rhs_ty = self.inferExprType(bop.rhs); const rhs_fields = if (!rhs_ty.isBuiltin()) blk: { const ri = self.module.types.get(rhs_ty); break :blk if (ri == .tuple) ri.tuple.fields else &[_]TypeId{}; } else &[_]TypeId{}; var all_fields = std.ArrayList(TypeId).empty; defer all_fields.deinit(self.alloc); var all_vals = std.ArrayList(Ref).empty; defer all_vals.deinit(self.alloc); for (lhs_fields, 0..) |f, i| { all_fields.append(self.alloc, f) catch unreachable; all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable; } for (rhs_fields, 0..) |f, i| { all_fields.append(self.alloc, f) catch unreachable; all_vals.append(self.alloc, self.builder.structGet(rhs, @intCast(i), f)) catch unreachable; } const result_ty = self.module.types.intern(.{ .tuple = .{ .fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable, .names = null, } }); const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable; return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty); }, .mul => { // Tuple repeat: (a, b) * 3 → (a, b, a, b, a, b) const count: usize = switch (bop.rhs.data) { .int_literal => |il| @intCast(@as(u64, @bitCast(il.value))), else => 1, }; var all_fields = std.ArrayList(TypeId).empty; defer all_fields.deinit(self.alloc); var all_vals = std.ArrayList(Ref).empty; defer all_vals.deinit(self.alloc); for (0..count) |_| { for (lhs_fields, 0..) |f, i| { all_fields.append(self.alloc, f) catch unreachable; all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable; } } const result_ty = self.module.types.intern(.{ .tuple = .{ .fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable, .names = null, } }); const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable; return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty); }, .eq, .neq => { // Element-wise equality (or single-element tuple vs scalar) const rhs_is_tuple = blk: { const rt = self.inferExprType(bop.rhs); if (!rt.isBuiltin()) { break :blk self.module.types.get(rt) == .tuple; } break :blk false; }; if (!rhs_is_tuple and lhs_fields.len == 1) { // Single-element tuple vs scalar: unwrap and compare const lf = self.builder.structGet(lhs, 0, lhs_fields[0]); const eq = self.builder.cmpEq(lf, rhs); return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = eq } }, .bool) else eq; } var result = self.builder.constBool(true); for (lhs_fields, 0..) |f, i| { const lf = self.builder.structGet(lhs, @intCast(i), f); const rf = self.builder.structGet(rhs, @intCast(i), f); const eq = self.builder.cmpEq(lf, rf); result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = eq } }, .bool); } return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = result } }, .bool) else result; }, .lt, .lte, .gt, .gte => { // Lexicographic comparison return self.lowerTupleLexCompare(bop.op, lhs, rhs, lhs_fields); }, else => return self.builder.constInt(0, .s64), } } fn lowerTupleLexCompare(self: *Lowering, op: ast.BinaryOp.Op, lhs: Ref, rhs: Ref, fields: []const TypeId) Ref { // Lexicographic comparison using boolean logic. // (a0,a1) < (b0,b1) = (a0 < b0) || (a0 == b0 && a1 < b1) // (a0,a1) <= (b0,b1) = (a0 < b0) || (a0 == b0 && a1 <= b1) if (fields.len == 0) return self.builder.constBool(op == .lte or op == .gte); const n = fields.len; // Start with the last field using the actual op const lf_last = self.builder.structGet(lhs, @intCast(n - 1), fields[n - 1]); const rf_last = self.builder.structGet(rhs, @intCast(n - 1), fields[n - 1]); var result = switch (op) { .lt => self.builder.cmpLt(lf_last, rf_last), .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lf_last, .rhs = rf_last } }, .bool), .gt => self.builder.cmpGt(lf_last, rf_last), .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lf_last, .rhs = rf_last } }, .bool), else => unreachable, }; // Work backwards: result = (a[i] < b[i]) || (a[i] == b[i] && result) if (n > 1) { var i: usize = n - 1; while (i > 0) { i -= 1; const lf = self.builder.structGet(lhs, @intCast(i), fields[i]); const rf = self.builder.structGet(rhs, @intCast(i), fields[i]); const strict = if (op == .lt or op == .lte) self.builder.cmpLt(lf, rf) else self.builder.cmpGt(lf, rf); const eq = self.builder.cmpEq(lf, rf); const eq_and_rest = self.builder.emit(.{ .bool_and = .{ .lhs = eq, .rhs = result } }, .bool); result = self.builder.emit(.{ .bool_or = .{ .lhs = strict, .rhs = eq_and_rest } }, .bool); } } return result; } fn lowerTupleMembership(self: *Lowering, value: Ref, tuple: Ref, tuple_info: anytype) Ref { // value in (a, b, c) → value == a || value == b || value == c var result = self.builder.constBool(false); for (tuple_info.fields, 0..) |f, i| { const elem = self.builder.structGet(tuple, @intCast(i), f); const eq = self.builder.cmpEq(value, elem); result = self.builder.emit(.{ .bool_or = .{ .lhs = result, .rhs = eq } }, .bool); } return result; } // ── Control flow ──────────────────────────────────────────────── /// Shared implementation for the `has_impl(P, T)` builtin and its /// `tryConstBoolCondition` arm. The protocol expression is either: /// - Plain `Hash` (identifier / type_expr) → walks /// `protocol_thunk_map["Hash\x00"]`. /// - Parameterised `Into(Block)` (call) → walks `param_impl_map` /// keyed by `"

\x00\x00"`. /// Returns false on any malformed protocol-arg shape (caller /// reports a diagnostic if it wants). pub fn computeHasImpl(self: *Lowering, proto_node: *const Node, ty: TypeId) bool { switch (proto_node.data) { .identifier => |id| return self.protocolResolver().hasImplPlain(id.name, ty), .type_expr => |te| return self.protocolResolver().hasImplPlain(te.name, ty), .call => |c| { const p_name: []const u8 = switch (c.callee.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => return false, }; // Resolve protocol type args. Each goes through // `resolveTypeArg` so type aliases / generics / pack- // indexed types all work as protocol args. var arg_mangles = std.ArrayList(u8).empty; defer arg_mangles.deinit(self.alloc); for (c.args, 0..) |a, i| { if (i > 0) arg_mangles.append(self.alloc, 0) catch return false; const aty = self.resolveTypeArg(a); arg_mangles.appendSlice(self.alloc, self.mangleTypeName(aty)) catch return false; } const ty_mangled = self.mangleTypeName(ty); const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}\x00{s}", .{ p_name, arg_mangles.items, ty_mangled, }) catch return false; return self.param_impl_map.contains(key); }, else => return false, } } fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: ast.Span) Ref { // Check for tagged enum construction: .Variant.{ payload_fields } // This happens when type_expr is an enum_literal and target_type is a union if (sl.type_expr) |te| { if (te.data == .enum_literal) { const variant_name = te.data.enum_literal.name; const union_ty = self.target_type orelse .unresolved; if (!union_ty.isBuiltin()) { const union_info = self.module.types.get(union_ty); if (union_info == .tagged_union) { return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union, span); } } } } // `.{ name = ... }` against a tagged-union target_type. Reject: // the only valid construction forms are `.variant(payload)` and // `.variant.{ field, ... }`. Falling through would lower the // user's values straight into the `(tag, payload_bytes)` slot // pair and emit IR that LLVM later rejects. if (sl.type_expr == null and sl.struct_name == null) { const tu_ty = self.target_type orelse .unresolved; if (!tu_ty.isBuiltin()) { const tu_info = self.module.types.get(tu_ty); if (tu_info == .tagged_union) { if (sl.field_inits.len > 0 and sl.field_inits[0].name != null) { const first_name = sl.field_inits[0].name.?; if (self.diagnostics) |diags| { const ty_name = self.formatTypeName(tu_ty); if (self.findTaggedVariant(tu_info.tagged_union, first_name) != null) { diags.addFmt( .err, span, "cannot construct tagged union '{s}' from `.{{ {s} = ... }}`; use `.{s}(...)` or `.{s}.{{ ... }}`", .{ ty_name, first_name, first_name, first_name }, ); } else { self.emitBadVariant(tu_ty, tu_info.tagged_union, first_name, span); } } return self.builder.enumInit(0, Ref.none, tu_ty); } } } } const ty: TypeId = if (sl.struct_name) |name| // Source-aware (E2): a bare struct-literal type name resolves to the // querying source's OWN same-name author, not the global `findByName` // first-match — so `Box.{...}` in module B builds B's `Box`, never a // flat-imported A's. `.undeclared`/`.pending` keep the empty-struct // stub (byte-identical to the legacy `findByName orelse intern`); // `.ambiguous`/`.not_visible` surface their loud diagnostic + poison. self.resolveNominalLeaf(name, false, span) else if (sl.type_expr) |te| // Generic struct literal: Pair(s32).{ ... } — resolve type from type_expr self.resolveTypeWithBindings(te) else self.target_type orelse .unresolved; // Get struct field types for coercion and ordering const struct_fields = self.getStructFields(ty); // Look up field defaults from AST const struct_name_for_defaults = if (sl.struct_name) |n| n else if (!ty.isBuiltin()) blk: { const ti = self.module.types.get(ty); break :blk if (ti == .@"struct") self.module.types.getString(ti.@"struct".name) else @as(?[]const u8, null); } else @as(?[]const u8, null); const field_defaults: []const ?*const Node = if (struct_name_for_defaults) |sn| (self.struct_defaults_map.get(sn) orelse &.{}) else &.{}; // Check if any field_init has a name (named literal) const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; if (has_names and struct_fields.len > 0) { // Named literal: reorder fields to match struct declaration order // First, lower all field values in source order (to preserve evaluation order) var lowered = std.ArrayList(struct { val: Ref, name: []const u8, node: *const Node }).empty; defer lowered.deinit(self.alloc); for (sl.field_inits) |fi| { const saved_tt = self.target_type; // Set target_type to the field's declared type so array literals // know if the target is a vector, etc. if (fi.name) |fname| { for (struct_fields) |sf| { if (std.mem.eql(u8, self.module.types.getString(sf.name), fname)) { self.target_type = sf.ty; break; } } } const val = self.lowerExpr(fi.value); self.target_type = saved_tt; lowered.append(self.alloc, .{ .val = val, .name = fi.name orelse "", .node = fi.value, }) catch unreachable; } // Build fields in declaration order var fields = std.ArrayList(Ref).empty; defer fields.deinit(self.alloc); for (struct_fields, 0..) |sf, fi| { const sf_name = self.module.types.getString(sf.name); // Find the matching lowered value var found = false; for (lowered.items) |l| { if (std.mem.eql(u8, l.name, sf_name)) { var val = l.val; const src_ty = self.builder.getRefType(val); val = self.coerceToType(val, src_ty, sf.ty); fields.append(self.alloc, val) catch unreachable; found = true; break; } } if (!found) { // Field not specified — use default if available, else zero if (fi < field_defaults.len) { if (field_defaults[fi]) |default_expr| { // Coerce the default to the field type at the IR // level (the implicit narrowing rule) so a float // default folds/errors here instead of being // silently bit-coerced by the backend. fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; } else { fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; } } else { fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; } } } const result = self.builder.structInit(fields.items, ty); if (sl.init_block) |ib| { return self.lowerInitBlock(result, ty, ib); } return result; } // Positional literal: use source order var fields = std.ArrayList(Ref).empty; defer fields.deinit(self.alloc); for (sl.field_inits, 0..) |fi, i| { var val = self.lowerExpr(fi.value); // Coerce field value to match struct field type if (i < struct_fields.len) { const src_ty = self.inferExprType(fi.value); val = self.coerceToType(val, src_ty, struct_fields[i].ty); } fields.append(self.alloc, val) catch unreachable; } // Pad missing fields with defaults or zeroes if (fields.items.len < struct_fields.len) { for (struct_fields[fields.items.len..], fields.items.len..) |sf, fi| { if (fi < field_defaults.len) { if (field_defaults[fi]) |default_expr| { fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; continue; } } fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; } } const result = self.builder.structInit(fields.items, ty); // Lower init block if present if (sl.init_block) |ib| { return self.lowerInitBlock(result, ty, ib); } return result; } /// Lower an init block: store struct value to alloca, bind `self`, execute block, reload. fn lowerInitBlock(self: *Lowering, struct_val: Ref, ty: TypeId, ib: *const Node) Ref { // Store struct value to a temporary alloca const ptr_ty = self.module.types.ptrTo(ty); const slot = self.builder.alloca(ty); self.builder.store(slot, struct_val); // Create a nested scope with `self` bound to the alloca pointer var init_scope = Scope.init(self.alloc, self.scope); defer init_scope.deinit(); const saved_scope = self.scope; self.scope = &init_scope; // `self` is the pointer to the struct (not an alloca itself — it IS the pointer value) init_scope.put("self", .{ .ref = slot, .ty = ptr_ty, .is_alloca = false }); // Lower the init block body self.lowerBlock(ib); // Restore scope self.scope = saved_scope; // Load and return the (possibly modified) struct value return self.builder.load(slot, ty); } /// Get the field list for a struct TypeId, or empty if not a struct. pub fn getStructFields(self: *Lowering, ty: TypeId) []const types.TypeInfo.StructInfo.Field { if (ty.isBuiltin()) return &.{}; var resolved = ty; const info = self.module.types.get(resolved); // Dereference pointer types to get to the underlying struct if (info == .pointer) { resolved = info.pointer.pointee; if (resolved.isBuiltin()) return &.{}; const inner = self.module.types.get(resolved); return switch (inner) { .@"struct" => |s| s.fields, else => &.{}, }; } return switch (info) { .@"struct" => |s| s.fields, else => &.{}, }; } /// 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 { // 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; const first_param_ty = func.params[skip].ty; // Check if first param expects a pointer if (!first_param_ty.isBuiltin()) { const pi = self.module.types.get(first_param_ty); if (pi == .pointer) { // If obj is already a pointer type, it's already correct (no addr_of needed) if (!obj_ty.isBuiltin()) { const oi = self.module.types.get(obj_ty); if (oi == .pointer) return; // already a pointer } // Method expects *T — pass the address of the receiver (value type in alloca) if (obj_node.data == .identifier) { if (self.scope) |scope| { if (scope.lookup(obj_node.data.identifier.name)) |binding| { if (binding.is_alloca) { const ptr_ty = self.module.types.ptrTo(binding.ty); method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty); return; } } } } // Field access: obj.field.method() → GEP to field, pass pointer directly. // This avoids copying the struct value (mutations through *T must be visible). if (obj_node.data == .field_access) { const gep_ref = self.lowerExprAsPtr(obj_node); // GEP returns a pointer in LLVM but its IR type is the field value type. // Wrap with addr_of (no-op in LLVM) to set the IR type to *T, // preventing coerceCallArgs from doing a spurious alloca+store. const ptr_ty = self.module.types.ptrTo(obj_ty); method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = gep_ref } }, ptr_ty); return; } // General case: alloca+store the value and pass the alloca pointer { const slot = self.builder.alloca(obj_ty); self.builder.store(slot, method_args.items[0]); method_args.items[0] = slot; } } else { // Method expects a value `T` but the receiver is a `*T` (e.g. a // `for xs: (*x)` by-ref capture) — deref to pass the value. if (!obj_ty.isBuiltin()) { const oi = self.module.types.get(obj_ty); if (oi == .pointer and oi.pointer.pointee == first_param_ty) { method_args.items[0] = self.builder.load(method_args.items[0], first_param_ty); } } } } } /// Get the name of a struct type (dereferencing pointers). Returns null for non-struct types. 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); } var resolved = ty; const info = self.module.types.get(resolved); if (info == .pointer) { resolved = info.pointer.pointee; if (resolved.isBuiltin()) return builtinTypeName(resolved); } const ri = self.module.types.get(resolved); return switch (ri) { .@"struct" => |s| self.module.types.getString(s.name), else => null, }; } fn builtinTypeName(ty: TypeId) ?[]const u8 { return switch (ty) { .s8 => "s8", .s16 => "s16", .s32 => "s32", .s64 => "s64", .u8 => "u8", .u16 => "u16", .u32 => "u32", .u64 => "u64", .f32 => "f32", .f64 => "f64", .bool => "bool", .string => "string", else => null, }; } /// Resolve the type of a named field on a given type. fn resolveFieldType(self: *Lowering, ty: TypeId, field: []const u8) TypeId { if (std.mem.eql(u8, field, "len")) return .s64; if (std.mem.eql(u8, field, "ptr")) { const elem_ty = self.getElementType(ty); return self.module.types.manyPtrTo(elem_ty); } const field_name_id = self.module.types.internString(field); // Check union fields + promoted fields if (!ty.isBuiltin()) { const info = self.module.types.get(ty); const u_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (info) { .@"union" => |u| u.fields, .tagged_union => |u| u.fields, else => null, }; if (u_fields) |ufields| { for (ufields) |f| { if (f.name == field_name_id) return f.ty; // Check promoted fields from anonymous struct variants if (!f.ty.isBuiltin()) { const fi = self.module.types.get(f.ty); if (fi == .@"struct") { for (fi.@"struct".fields) |sf| { if (sf.name == field_name_id) return sf.ty; } } } } } } // Check tuple fields if (!ty.isBuiltin()) { const ti = self.module.types.get(ty); if (ti == .tuple) { const tuple = ti.tuple; // Try named fields if (tuple.names) |names| { for (names, 0..) |name_id, i| { if (name_id == field_name_id) return tuple.fields[i]; } } // Try numeric index const idx = std.fmt.parseInt(usize, field, 10) catch { return .unresolved; }; if (idx < tuple.fields.len) return tuple.fields[idx]; return .unresolved; } } const struct_fields = self.getStructFields(ty); for (struct_fields) |f| { if (f.name == field_name_id) return f.ty; } return .unresolved; } fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref { // `error.X` — an error-tag literal. The `error` keyword in expression // position parses as identifier "error" (E0.2), so `error.X` is a // field access we intercept here. `error` is reserved, so this is // unambiguous (no struct/pack can be named `error`). if (fa.object.data == .identifier and std.mem.eql(u8, fa.object.data.identifier.name, "error")) { return self.lowerErrorTagLiteral(fa.field, span); } // Pack-arity intercept: `.len` in a pack-fn mono's // body resolves to the comptime-known N. The mono doesn't // materialise the `[]Any` slice that the inline path used, so // `args` isn't in scope as a value. if (self.pack_param_count) |ppc| { if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) { if (ppc.get(fa.object.data.identifier.name)) |n| { return self.builder.constInt(@as(i64, @intCast(n)), .s64); } } } // Pack value projection: `xs.` where `` is a (zero-arg) method of // the pack's constraint protocol projects it over every element → // a tuple `(xs[0].(), …, xs[N-1].())`. (`xs.len` handled above.) if (self.pack_constraint) |pcon| { if (fa.object.data == .identifier) { if (pcon.get(fa.object.data.identifier.name)) |proto| { if (self.lookupProtocolField(proto, fa.field) != null) { return self.lowerPackValueProjection(fa.object.data.identifier.name, fa.field, span); } } } } // Interface-only enforcement (Decision): a member access on a // constrained pack element `xs[i].` may only name a method of the // constraint protocol — not an arbitrary concrete field. Checked here, // on the `xs[i]` (index_expr) base, BEFORE substitution erases the // "constrained to P" context. Protocol method CALLS go through the call // path; a method name passes this check (it's in the protocol). if (self.pack_constraint) |pcon| { if (fa.object.data == .index_expr and fa.object.data.index_expr.object.data == .identifier) { const base_name = fa.object.data.index_expr.object.data.identifier.name; if (pcon.get(base_name)) |proto| { if (self.lookupProtocolField(proto, fa.field) == null) { if (self.diagnostics) |diags| { diags.addFmt(.err, span, "'{s}' is not part of protocol '{s}' — a pack element exposes only the protocol's interface", .{ fa.field, proto }); } return self.builder.constInt(0, .void); } } } } // Check for struct constant access: Struct.CONST if (fa.object.data == .identifier) { const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fa.object.data.identifier.name, fa.field }) catch fa.field; if (self.struct_const_map.get(qualified)) |info| { return self.lowerStructConstant(info); } } // Numeric-limit accessor: `.min` / `.max` folds to a comptime // const of the queried type (sibling of the identifier-receiver // intercepts above). Placed AFTER `Struct.CONST` so a user const named // `min`/`max` wins on its own struct; a builtin type name can never // name a user struct (reserved — issue 0076), so they never collide. if (self.lowerNumericLimit(fa, span)) |ref| return ref; // M1.3 — `obj.class` on any Obj-C-class pointer lowers to // `object_getClass(obj)`. Sugar; the receiver is opaque so // we don't auto-deref. Returns `Class` (alias for *void; // typed Class(T) parameterization is M1.1.b). if (std.mem.eql(u8, fa.field, "class")) { const expr_ty = self.inferExprType(fa.object); if (self.objc().isObjcClassPointer(expr_ty)) { const obj_ref = self.lowerExpr(fa.object); const ptr_void = self.module.types.ptrTo(.void); const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void); const args = self.alloc.alloc(Ref, 1) catch unreachable; args[0] = obj_ref; return self.builder.emit(.{ .call = .{ .callee = get_class_fid, .args = args } }, ptr_void); } } // M2.2 — `obj.field` where `field` is declared with `#property` // on a foreign Obj-C class lowers as `[obj field]` (the synthesized // getter). Receiver stays opaque — no auto-deref. if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| { return self.lowerObjcPropertyGetter(fa.object, prop, fa.field, span); } // M1.2 A.3 — `self.field` (or `obj.field`) on a *sx-defined-class // pointer for a plain instance field (NOT a #property) lowers as // `object_getIvar(obj, load(___state_ivar))` + struct_gep on // the state struct + load. The receiver is the opaque Obj-C id // (matching Apple's `self` semantics); the state lives in the // hidden `__sx_state` ivar. if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| { return self.lowerObjcDefinedStateFieldRead(fa.object, info); } var obj = self.lowerExpr(fa.object); var obj_ty = self.inferExprType(fa.object); // Auto-deref: if the object is a pointer to a struct, load through it if (!obj_ty.isBuiltin()) { const ptr_info = self.module.types.get(obj_ty); if (ptr_info == .pointer) { const pointee = ptr_info.pointer.pointee; obj = self.builder.load(obj, pointee); obj_ty = pointee; } } // Special fields on slices/strings (NOT structs with .len/.ptr fields) if (std.mem.eql(u8, fa.field, "len") or std.mem.eql(u8, fa.field, "ptr")) { // Only use length/data_ptr for slice, string, array, vector types const is_special = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: { const info = self.module.types.get(obj_ty); break :blk info == .slice or info == .array or info == .vector; } else false); if (is_special) { if (std.mem.eql(u8, fa.field, "len")) { return self.builder.emit(.{ .length = .{ .operand = obj } }, .s64); } { const elem_ty = self.getElementType(obj_ty); const mp_ty = self.module.types.manyPtrTo(elem_ty); return self.builder.emit(.{ .data_ptr = .{ .operand = obj } }, mp_ty); } } } // Optional chaining: p?.field if (fa.is_optional) { return self.lowerOptionalChain(obj, fa, span); } return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span); } /// True when an `.identifier` receiver text resolves to an in-scope VALUE /// binding rather than a builtin type. A backtick raw identifier (F0.6) can /// bind a value whose spelling shadows a builtin type name (`` `f64 := … ``); /// such a value is reachable through the same three sources the ordinary /// identifier field-access path consults (see `expr_typer` `.identifier` /// arm): lexical `scope`, program `global_names`, and module value /// constants `module_const_map`. The numeric-limit intercept must defer to /// ordinary field access whenever ANY of the three binds the name, so a /// raw value field read is never hijacked into a numeric-limit fold /// (issues 0092 local / 0093 global + module-const). A single helper used /// by both lowering and inference keeps the two resolvers in lockstep /// (issue-0083 two-resolver defect class). pub fn identifierBindsValue(self: *Lowering, name: []const u8) bool { if (self.scope) |scope| { if (scope.lookup(name) != null) return true; } if (self.program_index.global_names.get(name) != null) return true; if (self.program_index.module_const_map.get(name) != null) return true; return false; } /// Numeric-limit accessor intercept (`.min`/`.max`/`.epsilon`/ /// `.min_positive`/`.true_min`/`.inf`/`.nan`), a sibling of the `error.X` / /// `Struct.CONST` / pack-arity identifier-receiver intercepts in /// `lowerFieldAccess`. Folds the limit to a comptime const of the queried /// type via the shared `TypeResolver` logic (no second computor) + the /// existing `constInt` / `constFloat` const paths: /// - integer `.min`/`.max` → `constInt` (NL.1, via `integerLimitFor`); /// - float `.min`/`.max`/`.epsilon`/`.min_positive`/`.true_min`/`.inf`/ /// `.nan` → `constFloat` (via `floatLimitFor`). /// Returns null when the field is not a limit accessor, or the receiver is not /// a builtin type (a user struct → ordinary field lowering reports /// field-not-found). Two clean diagnostics (then a placeholder, so lowering /// finishes and `hasErrors()` aborts the build): /// - a FLOAT-only accessor on an integer type (`s32.epsilon`, `u8.inf`); /// - any accessor on a builtin NON-numeric receiver /// (`bool`/`string`/`void`/`Any`/`noreturn`). fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) ?Ref { const name = switch (fa.object.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => return null, }; if (!TypeResolver.isLimitField(fa.field)) return null; const ty = TypeResolver.resolveBuiltinName(name, &self.module.types) orelse return null; // A backtick raw identifier (F0.6) can bind a value whose spelling // shadows a builtin type name (`` `f64 := … ``). Field access on that // value is an ordinary field read, not a numeric-limit fold — defer to // the normal field-access path when the receiver identifier resolves to // a value binding through any of scope / globals / module consts // (issues 0092, 0093). A `.type_expr` receiver is unambiguously a type // and can never be value-shadowed. if (fa.object.data == .identifier and self.identifierBindsValue(name)) return null; if (TypeResolver.integerLimitFor(name, fa.field)) |value| { return self.builder.constInt(value, ty); } if (TypeResolver.floatLimitFor(name, fa.field)) |value| { return self.builder.constFloat(value, ty); } // The field is a limit accessor, but it does not apply to this type. if (self.diagnostics) |d| { if (TypeResolver.integerWidthSign(name) != null) { // Integer receiver + a float-only accessor. d.addFmt(.err, span, "type '{s}' has no '.{s}' — '.{s}' applies only to float types (f32/f64); integer types expose only '.min'/'.max'", .{ name, fa.field, fa.field }); } else { // Non-numeric builtin receiver (bool/string/void/Any/noreturn). d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field }); } } return self.emitPlaceholder(fa.field); } /// Lower each pack element to a Ref: `pack_name[i]` when `method` is null, /// or `pack_name[i].method()` when given. Synthesizes the index/field/call /// AST per element and lowers it (substitution turns `xs[i]` into the /// concrete arg; UFCS dispatches the method). Caller owns the returned slice. fn lowerPackElems(self: *Lowering, pack_name: []const u8, method: ?[]const u8, span: ast.Span) []Ref { const n: u32 = if (self.pack_param_count) |ppc| (ppc.get(pack_name) orelse 0) else 0; var refs = std.ArrayList(Ref).empty; var i: u32 = 0; while (i < n) : (i += 1) { const id_node = self.alloc.create(Node) catch break; id_node.* = .{ .span = span, .data = .{ .identifier = .{ .name = pack_name } } }; const idx_node = self.alloc.create(Node) catch break; idx_node.* = .{ .span = span, .data = .{ .int_literal = .{ .value = @intCast(i) } } }; const index_node = self.alloc.create(Node) catch break; index_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } }; var elem_node = index_node; if (method) |m| { const fa_node = self.alloc.create(Node) catch break; fa_node.* = .{ .span = span, .data = .{ .field_access = .{ .object = index_node, .field = m } } }; const call_node = self.alloc.create(Node) catch break; call_node.* = .{ .span = span, .data = .{ .call = .{ .callee = fa_node, .args = &.{} } } }; elem_node = call_node; } refs.append(self.alloc, self.lowerExpr(elem_node)) catch break; } return refs.toOwnedSlice(self.alloc) catch &.{}; } /// Value-position pack projection `xs.`: call the (zero-arg) /// protocol method on each element and collect the results into a tuple /// `(xs[0].(), …, xs[N-1].())`. N=0 yields the empty tuple. fn lowerPackValueProjection(self: *Lowering, pack_name: []const u8, method: []const u8, span: ast.Span) Ref { const refs = self.lowerPackElems(pack_name, method, span); defer self.alloc.free(refs); var tys = std.ArrayList(TypeId).empty; defer tys.deinit(self.alloc); for (refs) |r| tys.append(self.alloc, self.builder.getRefType(r)) catch {}; const tuple_ty = self.module.types.intern(.{ .tuple = .{ .fields = self.alloc.dupe(TypeId, tys.items) catch return self.builder.constInt(0, .void), .names = null, } }); const owned = self.alloc.dupe(Ref, refs) catch return self.builder.constInt(0, .void); return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty); } /// If `operand` is a pack spread — `..xs` (bare pack) or `..xs.method` /// (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 { const ppc = self.pack_param_count orelse return null; switch (operand.data) { .identifier => |id| { if (ppc.contains(id.name)) return self.lowerPackElems(id.name, null, span); }, .field_access => |fa| { if (fa.object.data == .identifier and ppc.contains(fa.object.data.identifier.name)) { return self.lowerPackElems(fa.object.data.identifier.name, fa.field, span); } }, else => {}, } return null; } /// Lower a struct-level constant value (e.g., Phys.GRAVITY). fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref { const val_node = info.value; return switch (val_node.data) { .int_literal => |lit| self.builder.constInt(lit.value, info.ty orelse .s64), .float_literal => |lit| self.builder.constFloat(lit.value, info.ty orelse .f64), .bool_literal => |lit| self.builder.constBool(lit.value), .string_literal => |lit| self.builder.constString(self.module.types.internString(lit.raw)), else => self.lowerExpr(val_node), }; } /// Lower optional chaining: `p?.field` where p is ?T /// Produces ?FieldType: some(unwrap(p).field) if p has value, else null /// If FieldType is already optional (?U), flattens to ?U (no double wrapping) fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess, span: ast.Span) Ref { const obj_ty = self.inferExprType(fa.object); // Get the inner (non-optional) type const inner_ty = if (!obj_ty.isBuiltin()) blk: { const info = self.module.types.get(obj_ty); break :blk if (info == .optional) info.optional.child else obj_ty; } else obj_ty; // Get the field type on the inner type const field_ty = self.resolveFieldType(inner_ty, fa.field); // If field is already optional, flatten (don't double-wrap) const field_already_optional = if (!field_ty.isBuiltin()) self.module.types.get(field_ty) == .optional else false; const result_ty = if (field_already_optional) field_ty else self.module.types.optionalOf(field_ty); // Check if optional has value const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = obj } }, .bool); // Create blocks const some_bb = self.freshBlock("chain.some"); const none_bb = self.freshBlock("chain.none"); const merge_bb = self.freshBlockWithParams("chain.merge", &.{result_ty}); self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); // Some: unwrap, access field (already ?FieldType if flattened, else wrap) self.builder.switchToBlock(some_bb); const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = obj } }, inner_ty); const field_val = self.lowerFieldAccessOnType(unwrapped, inner_ty, fa.field, span); const some_result = if (field_already_optional) field_val else self.builder.emit(.{ .optional_wrap = .{ .operand = field_val } }, result_ty); self.builder.br(merge_bb, &.{some_result}); // None: produce null optional self.builder.switchToBlock(none_bb); const none_result = self.builder.constNull(result_ty); self.builder.br(merge_bb, &.{none_result}); // Merge self.builder.switchToBlock(merge_bb); return self.builder.blockParam(merge_bb, 0, result_ty); } /// Field access on a known type (shared by regular field access and optional chaining) /// Map a Vector swizzle component (`.x`/`.y`/`.z`/`.w` or the colour /// aliases `.r`/`.g`/`.b`/`.a`) to its lane index. Returns null for any /// other field name so the read path (`lowerFieldAccessOnType`) and the /// write path (`lowerAssignment`) share one resolver and reject a /// non-lane field identically (issue 0086). pub fn vectorLaneIndex(field: []const u8) ?u32 { if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) return 0; if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) return 1; if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) return 2; if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) return 3; return null; } fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { const field_name_id = self.module.types.internString(field); // Check if it's a union type if (!obj_ty.isBuiltin()) { const info = self.module.types.get(obj_ty); switch (info) { .tagged_union => |u| { // .tag → extract the enum tag value with the correct tag type if (std.mem.eql(u8, field, "tag")) { return self.builder.emit(.{ .enum_tag = .{ .operand = obj } }, u.tag_type); } // Tagged union — use enum_payload for (u.fields, 0..) |f, i| { if (f.name == field_name_id) { return self.builder.emit(.{ .enum_payload = .{ .base = obj, .field_index = @intCast(i) } }, f.ty); } } // Check promoted fields from anonymous struct variants for (u.fields) |f| { if (!f.ty.isBuiltin()) { const field_info = self.module.types.get(f.ty); if (field_info == .@"struct") { for (field_info.@"struct".fields, 0..) |sf, si| { if (sf.name == field_name_id) { const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty); return self.builder.structGet(reinterpreted, @intCast(si), sf.ty); } } } } } }, .@"union" => |u| { // Untagged union — use union_get to reinterpret bytes for (u.fields, 0..) |f, i| { if (f.name == field_name_id) { return self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = @intCast(i) } }, f.ty); } } // Check promoted fields from anonymous struct variants for (u.fields) |f| { if (!f.ty.isBuiltin()) { const field_info = self.module.types.get(f.ty); if (field_info == .@"struct") { for (field_info.@"struct".fields, 0..) |sf, si| { if (sf.name == field_name_id) { const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty); return self.builder.structGet(reinterpreted, @intCast(si), sf.ty); } } } } } }, else => {}, } } // Vector lane access: .x/.y/.z/.w (or colour aliases .r/.g/.b/.a) → // lane 0/1/2/3. Shares lane-index resolution with the write path // (lowerAssignment) via vectorLaneIndex; a non-lane field falls // through to the field-not-found error below. if (!obj_ty.isBuiltin()) { const vinfo = self.module.types.get(obj_ty); if (vinfo == .vector) { if (Lowering.vectorLaneIndex(field)) |vidx| { return self.builder.structGet(obj, vidx, vinfo.vector.element); } } } // Closure field access: .fn_ptr → field 0, .env → field 1 if (!obj_ty.isBuiltin()) { const cinfo = self.module.types.get(obj_ty); if (cinfo == .closure) { if (std.mem.eql(u8, field, "fn_ptr")) { const fn_ptr_ty = self.module.types.ptrTo(.void); return self.builder.structGet(obj, 0, fn_ptr_ty); } else if (std.mem.eql(u8, field, "env")) { const env_ty = self.module.types.ptrTo(.void); return self.builder.structGet(obj, 1, env_ty); } } } // Tuple field access: .0, .1, etc. or named fields if (!obj_ty.isBuiltin()) { const tinfo = self.module.types.get(obj_ty); if (tinfo == .tuple) { const tuple = tinfo.tuple; // Try named fields first if (tuple.names) |names| { for (names, 0..) |name_id, i| { if (name_id == field_name_id) { return self.builder.structGet(obj, @intCast(i), tuple.fields[i]); } } } // Try numeric index (e.g., "0", "1") const idx = std.fmt.parseInt(u32, field, 10) catch { return self.emitFieldError(obj_ty, field, span); }; if (idx < tuple.fields.len) { return self.builder.structGet(obj, idx, tuple.fields[idx]); } return self.emitFieldError(obj_ty, field, span); } } // Resolve struct field index and type const struct_fields = self.getStructFields(obj_ty); for (struct_fields, 0..) |f, i| { if (f.name == field_name_id) { return self.builder.structGet(obj, @intCast(i), f.ty); } } return self.emitFieldError(obj_ty, field, span); } fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref { const target = self.target_type orelse .unresolved; const tag = self.resolveVariantValue(target, el.name); return self.builder.enumInit(tag, Ref.none, target); } /// Lower an `error.X` tag literal to its global tag id (a `u32`). When the /// destination context (`target_type`) is a named error set, the value is /// typed as that set and `X`'s membership is validated; otherwise the value /// is the raw `u32` global tag id (per the spec's context rule). fn lowerErrorTagLiteral(self: *Lowering, tag_name: []const u8, span: ast.Span) Ref { const tag_id = self.module.types.internTag(tag_name); if (self.target_type) |t| { if (!t.isBuiltin()) { const info = self.module.types.get(t); if (info == .error_set) { // The bare-`!` inferred placeholder (reserved name "!") accepts // any tag — its members aren't known until the whole-program SCC // pass (E1.4) folds in every raised tag. Skip membership for it. if (!std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!")) { var in_set = false; for (info.error_set.tags) |member| { if (member == tag_id) { in_set = true; break; } } if (!in_set) { if (self.diagnostics) |diags| { diags.addFmt(.err, span, "error tag 'error.{s}' is not in error set '{s}'", .{ tag_name, self.module.types.getString(info.error_set.name) }); } } } return self.builder.constInt(@as(i64, @intCast(tag_id)), t); } } } return self.builder.constInt(@as(i64, @intCast(tag_id)), .u32); } /// Lower a tagged enum construction: .Variant.{ field_inits } /// The struct literal provides the payload fields; we wrap them in an enum_init. fn lowerTaggedEnumLiteral( self: *Lowering, sl: *const ast.StructLiteral, variant_name: []const u8, union_ty: TypeId, union_info: types.TypeInfo.TaggedUnionInfo, span: ast.Span, ) Ref { if (self.findTaggedVariant(union_info, variant_name) == null) { self.emitBadVariant(union_ty, union_info, variant_name, span); return self.builder.enumInit(0, Ref.none, union_ty); } const tag = self.resolveVariantValue(union_ty, variant_name); const name_id = self.module.types.internString(variant_name); // Find the payload type for this variant var payload_ty: TypeId = .void; for (union_info.fields) |f| { if (f.name == name_id) { payload_ty = f.ty; break; } } if (payload_ty == .void or sl.field_inits.len == 0) { // No payload or no fields — just tag return self.builder.enumInit(tag, Ref.none, union_ty); } // Lower the payload as a struct init of the payload type const saved_tt = self.target_type; self.target_type = payload_ty; const payload_fields = self.getStructFields(payload_ty); var fields = std.ArrayList(Ref).empty; defer fields.deinit(self.alloc); for (sl.field_inits, 0..) |fi, i| { if (i < payload_fields.len) { const saved_inner = self.target_type; self.target_type = payload_fields[i].ty; var val = self.lowerExpr(fi.value); self.target_type = saved_inner; const src_ty = self.inferExprType(fi.value); val = self.coerceToType(val, src_ty, payload_fields[i].ty); fields.append(self.alloc, val) catch unreachable; } else { fields.append(self.alloc, self.lowerExpr(fi.value)) catch unreachable; } } // Pad missing payload fields with zeroes if (fields.items.len < payload_fields.len) { for (payload_fields[fields.items.len..]) |sf| { fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; } } const payload = self.builder.structInit(fields.items, payload_ty); self.target_type = saved_tt; return self.builder.enumInit(tag, payload, union_ty); } fn findTaggedVariant( self: *Lowering, union_info: types.TypeInfo.TaggedUnionInfo, variant_name: []const u8, ) ?usize { const name_id = self.module.types.internString(variant_name); for (union_info.fields, 0..) |f, i| { if (f.name == name_id) return i; } return null; } fn emitBadVariant( self: *Lowering, union_ty: TypeId, union_info: types.TypeInfo.TaggedUnionInfo, variant_name: []const u8, span: ast.Span, ) void { const diags = self.diagnostics orelse return; const ty_name = self.formatTypeName(union_ty); var list: std.ArrayList(u8) = .empty; for (union_info.fields, 0..) |f, i| { if (i > 0) list.appendSlice(self.alloc, ", ") catch return; list.appendSlice(self.alloc, self.module.types.getString(f.name)) catch return; } diags.addFmt( .err, span, "'{s}' is not a variant of '{s}' (variants are: {s})", .{ variant_name, ty_name, list.items }, ); } /// Resolve a variant name to its runtime value (flags: power-of-2, regular: index). fn resolveVariantValue(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { if (ty.isBuiltin()) return 0; const info = self.module.types.get(ty); const name_id = self.module.types.internString(variant_name); switch (info) { .@"enum" => |e| { for (e.variants, 0..) |v, i| { if (v == name_id) { if (e.explicit_values) |vals| { if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i]))); } return @intCast(i); } } }, .tagged_union => |u| { for (u.fields, 0..) |f, i| { if (f.name == name_id) { if (u.explicit_tag_values) |vals| { if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i]))); } return @intCast(i); } } }, else => {}, } return 0; } /// Resolve a variant name to its tag index within an enum or union type. pub fn resolveVariantIndex(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { if (ty.isBuiltin()) return 0; const info = self.module.types.get(ty); const name_id = self.module.types.internString(variant_name); switch (info) { .tagged_union => |u| { for (u.fields, 0..) |f, i| { if (f.name == name_id) return @intCast(i); } }, .@"enum" => |e| { for (e.variants, 0..) |v, i| { if (v == name_id) return @intCast(i); } }, else => {}, } return 0; } fn lowerArrayLiteral(self: *Lowering, al: *const ast.ArrayLiteral) Ref { var elems = std.ArrayList(Ref).empty; defer elems.deinit(self.alloc); // Determine element type: explicit type_expr > target_type > inference var elem_ty: TypeId = .unresolved; var from_target = false; var is_vector = false; // First, check explicit type annotation on the literal (e.g. Vector(3,f32).[1,2,3]) if (al.type_expr) |te| { const resolved = self.resolveArrayLiteralType(te); if (resolved != .unresolved) { if (!resolved.isBuiltin()) { const info = self.module.types.get(resolved); switch (info) { .array => |a| { elem_ty = a.element; from_target = true; }, .vector => |v| { elem_ty = v.element; from_target = true; is_vector = true; }, .slice => |s| { elem_ty = s.element; from_target = true; }, else => {}, } } } } if (!from_target) { if (self.target_type) |tt| { if (!tt.isBuiltin()) { const info = self.module.types.get(tt); switch (info) { .array => |a| { elem_ty = a.element; from_target = true; }, .slice => |s| { elem_ty = s.element; from_target = true; }, .vector => |v| { elem_ty = v.element; from_target = true; is_vector = true; }, else => {}, } } } } if (!from_target and al.elements.len > 0) { const inferred = self.inferExprType(al.elements[0]); if (inferred != .void) elem_ty = inferred; } for (al.elements) |elem| { const old_tt = self.target_type; self.target_type = elem_ty; var val = self.lowerExpr(elem); self.target_type = old_tt; // A nested `.[...]` element at a slice element type lowers to an // aggregate array `[N]U` (lowerArrayLiteral always yields an array // value); materialize it into a `[]U` slice so the element is a real // {ptr,len} header rather than a raw array the callee would read its // header off of (issue 0085). This per-element coercion recurses with // the literal nesting, so `[][]T` and deeper coerce at every level. if (!elem_ty.isBuiltin()) { const ei = self.module.types.get(elem_ty); if (ei == .slice) { const val_ty = self.builder.getRefType(val); if (!val_ty.isBuiltin()) { const vi = self.module.types.get(val_ty); if (vi == .array and vi.array.element == ei.slice.element) { val = self.coerceToType(val, val_ty, elem_ty); } } } } elems.append(self.alloc, val) catch unreachable; } const result_ty = if (is_vector) self.module.types.vectorOf(elem_ty, @intCast(al.elements.len)) else self.module.types.arrayOf(elem_ty, @intCast(al.elements.len)); return self.builder.structInit(elems.items, result_ty); } /// Resolve the type annotation on an array literal (e.g. Vector(3,f32).[...]). /// Handles call nodes (Vector(3,f32)), parameterized_type_expr, and identifier/type_expr. fn resolveArrayLiteralType(self: *Lowering, te: *const Node) TypeId { switch (te.data) { .call => |cl| { // Vector(3, f32) or Module.Vector(3, f32) const callee_name = switch (cl.callee.data) { .identifier => |id| id.name, .field_access => |fa| fa.field, else => return .unresolved, }; if (std.mem.eql(u8, callee_name, "Vector")) { if (cl.args.len == 2) { const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved; const elem = self.resolveTypeWithBindings(cl.args[1]); return self.module.types.vectorOf(elem, length); } } // Generic-struct typed-literal head (`Box(s64).[...]`): route // through the single layout choke-point (CP-1). A qualified head // `a.Box(s64).[...]` selects a's OWN template via the namespace edge // (Counter-1: was the global last-wins map); a bare head selects the // single bare-VISIBLE author. if (headNameOfCallee(cl.callee)) |hn| { switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) { .template => |t| return self.instantiateGenericStruct(&t, cl.args), .poisoned => return .unresolved, .not_generic => {}, } } return .unresolved; }, .parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span), .identifier => |id| { // E4 single-hop visibility + ambiguity gate: a 2-flat-hop bare type // name in a typed array/vector-literal annotation (`Nums.[1, 2]`) is // not bare-visible (consistent with annotations / 0763); ≥2 direct // flat same-name authors are ambiguous (loud diagnostic, consistent // with the leaf / 0755); a single source-keyed author resolves to // ITS TypeId instead of a global `findByName` first-/last-wins pick. switch (self.headTypeGate(id.name, te.span)) { .ambiguous, .not_visible => return .unresolved, .resolved => |tid| return tid, .proceed => {}, } const name_id = self.module.types.internString(id.name); return self.module.types.findByName(name_id) orelse .unresolved; }, .type_expr => |inner| { if (self.headTypeLeak(inner.name, te.span)) return .unresolved; return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); }, .field_access => |fa| { // Module.Type — try to resolve the field as a type name const name_id = self.module.types.internString(fa.field); return self.module.types.findByName(name_id) orelse .unresolved; }, else => return .unresolved, } } fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref { // Pack-arg substitution: `args[]` inside a body // whose enclosing comptime call bound `args` as a pack name. // Lowering the i-th call-site arg directly gives the concrete // call-arg type — bypasses the `[]Any` slice boxing that would // otherwise lose the type. Non-literal indices fall through to // the standard slice indexing path. if (self.packArgNodeAt(ie)) |arg_node| { return self.lowerExpr(arg_node); } // Out-of-bounds pack indexing: object IS a pack name + index // IS a comptime int literal but exceeds the pack arity. Emit // a focused diagnostic so the user gets "pack index 2 out of // bounds" instead of the generic "unresolved 'args'" that the // fall-through scope-lookup would produce. if (self.diagPackIndexOOB(ie)) { return self.builder.constInt(0, .s64); } // Runtime index into a comptime-only pack (Decision 1): a pack has no // runtime representation, so the index must be a compile-time constant. // A runtime index is a hard error — clearer than the "unresolved // ''" the slice-index fall-through would otherwise produce. if (self.pack_param_count) |ppc| { if (ie.object.data == .identifier) { const pname = ie.object.data.identifier.name; if (ppc.contains(pname) and self.comptimeIndexOf(ie.index) == null) { if (self.diagnostics) |diags| { diags.addFmt(.err, ie.index.span, "pack '{s}' must be indexed by a compile-time constant — a pack is comptime-only and has no runtime value", .{pname}); } return self.builder.constInt(0, .s64); } } } const obj = self.lowerExpr(ie.object); const idx = self.lowerExpr(ie.index); // Infer element type from the object's slice/array type const obj_ty = self.inferExprType(ie.object); const elem_ty = self.getElementType(obj_ty); return self.builder.emit(.{ .index_get = .{ .lhs = obj, .rhs = idx } }, elem_ty); } /// Detect `[]` where the literal exceeds /// the pack arity (or is negative). Emits a diagnostic and /// returns true; caller skips the standard indexing path and /// returns a placeholder Ref. Returns false for non-pack bases, /// non-literal indices, or in-range indices. fn diagPackIndexOOB(self: *Lowering, ie: *const ast.IndexExpr) bool { const ppc = self.pack_param_count orelse return false; if (ie.object.data != .identifier) return false; const pack_name = ie.object.data.identifier.name; const n = ppc.get(pack_name) orelse return false; // Any comptime index (int literal or a comptime-constant cursor) that's // out of range — runtime indices are handled by the caller's // must-be-comptime check. const raw: i64 = self.comptimeIndexOf(ie.index) orelse return false; if (raw >= 0 and @as(u32, @intCast(raw)) < n) return false; if (self.diagnostics) |diags| { diags.addFmt(.err, ie.index.span, "pack index {} out of bounds: '{s}' has {} element{s}", .{ raw, pack_name, n, if (n == 1) @as([]const u8, "") else @as([]const u8, "s"), }); } return true; } /// Returns the call-site arg AST node when `ie` matches /// `[]` with the pack name bound /// in the active `pack_arg_nodes` map and the index in range. /// Otherwise null — caller falls back to standard slice indexing. pub fn packArgNodeAt(self: *Lowering, ie: *const ast.IndexExpr) ?*const Node { const pan = self.pack_arg_nodes orelse return null; if (ie.object.data != .identifier) return null; const arg_nodes = pan.get(ie.object.data.identifier.name) orelse return null; const raw: i64 = self.comptimeIndexOf(ie.index) orelse return null; if (raw < 0) return null; const i: usize = @intCast(raw); if (i >= arg_nodes.len) return null; return arg_nodes[i]; } /// Resolve an index expression to a comptime-known integer: a literal, /// or an identifier bound to an `int_val` in `comptime_constants` (e.g. /// the cursor of an `inline for 0..N (i)` unroll). Otherwise null. pub fn comptimeIndexOf(self: *Lowering, index: *const Node) ?i64 { switch (index.data) { .int_literal => |lit| return lit.value, .identifier => |id| { if (self.comptime_constants.get(id.name)) |cv| { switch (cv) { .int_val => |iv| return iv, else => return null, } } return null; }, else => return null, } } const PackValueKind = enum { storage, call_arg, return_value, runtime_iter, generic }; /// `xs` is a pack name used where a runtime value is required. A pack is /// comptime-only (Decision 1), so this is an error — with a context-tailored /// suggestion for how to express the intent instead. pub fn diagPackAsValue(self: *Lowering, name: []const u8, span: ast.Span, kind: PackValueKind) Ref { if (self.diagnostics) |d| { const id = d.addFmtId(.err, span, "pack '{s}' has no runtime value — a pack is comptime-only and can't be used as a value here", .{name}); switch (kind) { .storage => d.addHelpFmt(id, span, null, "to store it, materialize a tuple: `(..{s})`", .{name}), .call_arg => d.addHelpFmt(id, span, null, "to pass it to a `[]Any`/`[]P` parameter, materialize it with `xx {s}`", .{name}), .return_value => d.addHelpFmt(id, span, null, "to return it, return a tuple `(..{s})` and make the return type that tuple", .{name}), .runtime_iter => d.addHelpFmt(id, span, null, "to iterate at comptime use `inline for 0..{s}.len (i)`; for a runtime loop declare it as `..{s}: []P` (a protocol slice) instead of a pack", .{ name, name }), .generic => d.addHelpFmt(id, span, null, "materialize a tuple `(..{s})` to store it, or `xx {s}` to convert it to an expected `[]Any`/`[]P` slice", .{ name, name }), } } return self.emitPlaceholder(name); } /// True when `name` is a pack parameter bound in the current mono body. pub fn isPackName(self: *Lowering, name: []const u8) bool { const ppc = self.pack_param_count orelse return false; return ppc.contains(name); } /// `xx ` with a slice target: materialize the comptime pack into a /// runtime `[]elem` by lowering each element node and boxing (`[]Any`) or /// `xx`-erasing (`[]P`) it into a stack `[N]elem`, then return the slice. /// This is the explicit pack→slice bridge (issue 0053). fn lowerPackToSlice(self: *Lowering, pack_name: []const u8, slice_ty: TypeId) Ref { const arg_nodes = (self.pack_arg_nodes orelse return self.builder.constInt(0, .unresolved)).get(pack_name) orelse return self.builder.constInt(0, .unresolved); const elem_ty = self.module.types.get(slice_ty).slice.element; const is_any = elem_ty == .any; const elem_is_protocol = blk: { if (elem_ty.isBuiltin()) break :blk false; const ei = self.module.types.get(elem_ty); break :blk ei == .@"struct" and ei.@"struct".is_protocol; }; const slice_slot = self.builder.alloca(slice_ty); const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); if (arg_nodes.len == 0) { self.builder.store(ptr_gep, self.builder.constNull(self.module.types.ptrTo(elem_ty))); self.builder.store(len_gep, self.builder.constInt(0, .s64)); return self.builder.load(slice_slot, slice_ty); } const array_ty = self.module.types.arrayOf(elem_ty, @intCast(arg_nodes.len)); const array_slot = self.builder.alloca(array_ty); for (arg_nodes, 0..) |arg, i| { var val = self.lowerExpr(arg); var source_ty = self.inferExprType(arg); if (source_ty == .unresolved) source_ty = self.builder.getRefType(val); if (is_any) { if (source_ty != .any) val = self.builder.boxAny(val, source_ty); } else if (elem_is_protocol) { if (source_ty != elem_ty) val = self.buildProtocolErasure(val, arg, source_ty, elem_ty); } const ep = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = self.builder.constInt(@intCast(i), .s64) } }, self.module.types.ptrTo(elem_ty)); self.builder.store(ep, val); } const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = self.builder.constInt(0, .s64) } }, self.module.types.ptrTo(elem_ty)); self.builder.store(ptr_gep, data_ptr); self.builder.store(len_gep, self.builder.constInt(@intCast(arg_nodes.len), .s64)); return self.builder.load(slice_slot, slice_ty); } fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref { const obj = self.lowerExpr(se.object); const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64); const hi = if (se.end) |e| self.lowerExpr(e) else self.builder.emit(.{ .length = .{ .operand = obj } }, .s64); // Infer result slice type from the object const obj_ty = self.inferExprType(se.object); // Subslice of string stays string (same {ptr, i64} layout, correct type category) if (obj_ty == .string) { return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, .string); } const elem_ty = self.getElementType(obj_ty); const slice_ty = if (elem_ty != .void) self.module.types.sliceOf(elem_ty) else self.module.types.sliceOf(.u8); return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, slice_ty); } fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref { var elems = std.ArrayList(Ref).empty; defer elems.deinit(self.alloc); var field_type_ids = std.ArrayList(TypeId).empty; defer field_type_ids.deinit(self.alloc); var name_ids = std.ArrayList(types.StringId).empty; defer name_ids.deinit(self.alloc); var has_names = false; // A tuple_init's element values must match its field types exactly // (LLVM `insertvalue` does no implicit conversion). When a contextual // target tuple of matching arity is in scope (annotation, assignment // LHS, call/return slot), its field types drive element lowering so an // ambient scalar `target_type` (e.g. the enclosing fn's int return // type) can't narrow an element below its field width. Otherwise each // element's type is inferred independently. // A pack-spread element `(..xs)` / `(..xs.method)` expands to N fields, // so element-count ≠ field-count and a contextual target tuple can't be // aligned by index — infer field types from the expanded refs instead. var has_spread = false; for (tl.elements) |elem| { if (elem.value.data == .spread_expr) has_spread = true; } // Contextual target tuple field types. Without a spread we require // exact arity (existing behavior); with a spread we index positionally // by output position (so `(..sources)` into a `(VL(T0), …)` field coerces // / erases each spliced element to its slot's type). var target_fields: ?[]const TypeId = null; if (self.target_type) |tt| { if (!tt.isBuiltin()) { const tinfo = self.module.types.get(tt); if (tinfo == .tuple and (has_spread or tinfo.tuple.fields.len == tl.elements.len)) { target_fields = tinfo.tuple.fields; } } } const saved_target = self.target_type; var out_idx: usize = 0; for (tl.elements) |elem| { // Pack-spread element → splice its per-element values as fields. if (elem.value.data == .spread_expr) { const sp_operand = elem.value.data.spread_expr.operand; if (self.packSpreadRefs(sp_operand, elem.value.span)) |refs| { defer self.alloc.free(refs); // Element AST nodes (for protocol-erasure lvalue/name fallback) // when the spread is a bare pack name. const elem_nodes: ?[]const *const Node = if (sp_operand.data == .identifier and self.pack_arg_nodes != null) self.pack_arg_nodes.?.get(sp_operand.data.identifier.name) else null; for (refs, 0..) |r, ri| { var val = r; var vty = self.builder.getRefType(r); if (target_fields) |tf| { if (out_idx < tf.len and tf[out_idx] != vty and tf[out_idx] != .void) { const want = tf[out_idx]; const node = if (elem_nodes) |ens| (if (ri < ens.len) ens[ri] else elem.value) else elem.value; val = self.coerceOrErase(r, vty, want, node); vty = want; } } elems.append(self.alloc, val) catch unreachable; field_type_ids.append(self.alloc, vty) catch unreachable; name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable; out_idx += 1; } continue; } // Not a pack spread (e.g. tuple-value spread) — not yet handled. _ = self.lowerExpr(elem.value); // surfaces the spread_expr diagnostic continue; } const field_ty = if (target_fields) |tf| (if (out_idx < tf.len) tf[out_idx] else self.inferExprType(elem.value)) else self.inferExprType(elem.value); self.target_type = field_ty; var val = self.lowerExpr(elem.value); self.target_type = saved_target; const val_ty = self.builder.getRefType(val); if (val_ty != field_ty and val_ty != .void) { val = self.coerceToType(val, val_ty, field_ty); } elems.append(self.alloc, val) catch unreachable; field_type_ids.append(self.alloc, field_ty) catch unreachable; if (elem.name) |name| { name_ids.append(self.alloc, self.module.types.internString(name)) catch unreachable; has_names = true; } else { name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable; } out_idx += 1; } // Reuse the contextual target tuple type when it drove lowering so the // value's type identity (incl. field names) matches the destination // slot; otherwise build the tuple type from the inferred fields. const tuple_ty = if (target_fields != null and self.target_type != null) self.target_type.? else self.module.types.intern(.{ .tuple = .{ .fields = self.alloc.dupe(TypeId, field_type_ids.items) catch unreachable, .names = if (has_names) self.alloc.dupe(types.StringId, name_ids.items) catch unreachable else null, } }); const owned = self.alloc.dupe(Ref, elems.items) catch unreachable; return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty); } fn lowerDerefExpr(self: *Lowering, de: *const ast.DerefExpr) Ref { const ptr = self.lowerExpr(de.operand); // Resolve pointee type from the pointer type. const ptr_ty = self.inferExprType(de.operand); if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) { return self.builder.emit(.{ .deref = .{ .operand = ptr } }, info.pointer.pointee); } } // Operand isn't a pointer — `.*` is invalid. Diagnose here instead of // emitting a `.deref` with an `.unresolved` result type, which would // otherwise slip through to emit_llvm's "unresolved type reached LLVM // emission" panic with no source location. if (self.diagnostics) |d| { d.addFmt(.err, de.operand.span, "cannot dereference with `.*`: '{s}' is not a pointer", .{self.formatTypeName(ptr_ty)}); } return ptr; } fn lowerForceUnwrap(self: *Lowering, fu: *const ast.ForceUnwrap) Ref { const val = self.lowerExpr(fu.operand); const inner_ty = self.resolveOptionalInner(self.inferExprType(fu.operand)); return self.builder.optionalUnwrap(val, inner_ty); } fn lowerNullCoalesce(self: *Lowering, nc: *const ast.NullCoalesce) Ref { const lhs = self.lowerExpr(nc.lhs); const inner_ty = self.resolveOptionalInner(self.inferExprType(nc.lhs)); // Short-circuit: only evaluate RHS if LHS is null. // IMPORTANT: optional_unwrap must be in the "has value" branch, // not before the condBr — the interpreter errors on unwrapping null. const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = lhs } }, .bool); const then_bb = self.freshBlock("nc.has"); const rhs_bb = self.freshBlock("nc.rhs"); const merge_bb = self.freshBlockWithParams("nc.merge", &.{inner_ty}); // If has value, go to then_bb to unwrap; else go to rhs_bb self.builder.condBr(has_val, then_bb, &.{}, rhs_bb, &.{}); // Then block: unwrap LHS and branch to merge self.builder.switchToBlock(then_bb); const unwrapped = self.builder.optionalUnwrap(lhs, inner_ty); self.builder.br(merge_bb, &.{unwrapped}); // RHS block: evaluate fallback and branch to merge self.builder.switchToBlock(rhs_bb); var rhs = self.lowerExpr(nc.rhs); const rhs_ty = self.builder.getRefType(rhs); if (rhs_ty != inner_ty and rhs_ty != .void and inner_ty != .void) { rhs = self.coerceToType(rhs, rhs_ty, inner_ty); } self.builder.br(merge_bb, &.{rhs}); // Continue at merge self.builder.switchToBlock(merge_bb); return self.builder.blockParam(merge_bb, 0, inner_ty); } fn resolveOptionalInner(self: *Lowering, ty: TypeId) TypeId { if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .optional) return info.optional.child; } return .unresolved; } // ── FFI intrinsics (#objc_call / #jni_call / #jni_static_call) ─ /// Intern an Obj-C selector string into a module-scoped `SEL*` slot. /// First call creates the global; subsequent calls return the same /// `GlobalId`. emit_llvm.zig walks `module.objc_selector_cache` and /// synthesizes a constructor that populates each slot via /// `sel_registerName` exactly once at module load. /// /// Slot name matches clang's convention: `OBJC_SELECTOR_REFERENCES_` /// with `:` replaced by `_` to keep the symbol name valid. fn internObjcSelector(self: *Lowering, sel_str: []const u8) inst_mod.GlobalId { if (self.module.lookupObjcSelector(sel_str)) |gid| return gid; // Mangle selector: replace colons with underscores. Apple's // toolchain does the same (foo:bar: → foo_bar_). var mangled = std.ArrayList(u8).empty; defer mangled.deinit(self.alloc); mangled.appendSlice(self.alloc, "OBJC_SELECTOR_REFERENCES_") catch unreachable; for (sel_str) |ch| { mangled.append(self.alloc, if (ch == ':') '_' else ch) catch unreachable; } const slot_name = self.module.types.internString(mangled.items); const vptr_ty = self.module.types.ptrTo(.void); const gid = self.module.addGlobal(.{ .name = slot_name, .ty = vptr_ty, .init_val = .null_val, .is_extern = false, .is_const = false, }); self.module.appendObjcSelector(sel_str, gid); return gid; } /// Intern an Obj-C class name into a module-scoped `Class*` slot. /// First call creates the global; subsequent calls return the same /// `GlobalId`. emit_llvm.zig walks `module.objc_class_cache` and /// synthesizes a constructor that populates each slot via /// `objc_getClass` exactly once at module load. /// /// Slot name matches clang's convention: `OBJC_CLASSLIST_REFERENCES_`. fn internObjcClassObject(self: *Lowering, class_name: []const u8) inst_mod.GlobalId { if (self.module.lookupObjcClass(class_name)) |gid| return gid; var mangled = std.ArrayList(u8).empty; defer mangled.deinit(self.alloc); mangled.appendSlice(self.alloc, "OBJC_CLASSLIST_REFERENCES_") catch unreachable; mangled.appendSlice(self.alloc, class_name) catch unreachable; const slot_name = self.module.types.internString(mangled.items); const vptr_ty = self.module.types.ptrTo(.void); const gid = self.module.addGlobal(.{ .name = slot_name, .ty = vptr_ty, .init_val = .null_val, .is_extern = false, .is_const = false, }); self.module.appendObjcClass(class_name, gid); return gid; } /// Lazily declare `sel_registerName(name: *u8) -> *void` as an extern. /// Cached per Lowering instance so multiple `#objc_call` sites share /// one declaration. fn getSelRegisterNameFid(self: *Lowering) FuncId { if (self.sel_register_name_fid) |fid| return fid; var params = std.ArrayList(inst_mod.Function.Param).empty; const name_str = self.module.types.internString("name"); const ptr_ty = self.module.types.ptrTo(.u8); params.append(self.alloc, .{ .name = name_str, .ty = ptr_ty }) catch unreachable; const fn_name = self.module.types.internString("sel_registerName"); const ret_ty = self.module.types.ptrTo(.void); const fid = self.builder.declareExtern(fn_name, params.toOwnedSlice(self.alloc) catch unreachable, ret_ty); const func = self.module.getFunctionMut(fid); func.call_conv = .c; self.sel_register_name_fid = fid; return fid; } /// Lower `#objc_call(T)(recv, "sel:", args...)` to: /// %sel = call ptr @sel_registerName(<"sel:">) /// %ret = call @objc_msgSend(recv, %sel, args...) /// For Phase 1.3 only the (void return, no extra args) form is /// fully wired. Extra arities + non-void returns will land in /// subsequent phase-1 steps. fn lowerFfiIntrinsicCall(self: *Lowering, fic: *const ast.FfiIntrinsicCall) Ref { if (fic.kind == .jni_call or fic.kind == .jni_static_call) { return self.lowerJniCall(fic); } if (fic.args.len < 2) { if (self.diagnostics) |d| { d.add(.err, "#objc_call requires at least a receiver and a selector", null); } return Ref.none; } // Resolve the return type from the syntactic slot. const ret_ty = self.resolveType(fic.return_type); if (fic.args.len < 2) { if (self.diagnostics) |d| { d.add(.err, "#objc_call requires at least a receiver and a selector", null); } return Ref.none; } // Receiver expression. const recv = self.lowerExpr(fic.args[0]); // Selector. Literal selectors get interned into a module- // scoped `SEL*` slot — emit_llvm.zig tags the slot into // `__DATA,__objc_selrefs` so dyld populates it at load time // (matches clang's `@selector(...)` lowering exactly). // Non-literal selectors keep the per-call `sel_registerName` // fallback. const sel_arg_node = fic.args[1]; const vptr_ty = self.module.types.ptrTo(.void); const sel = blk: { if (sel_arg_node.data == .string_literal) { const raw = sel_arg_node.data.string_literal.raw; const slot_gid = self.internObjcSelector(raw); const slot_ptr = self.builder.emit(.{ .global_addr = slot_gid }, self.module.types.ptrTo(vptr_ty)); break :blk self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty); } const sel_ref = self.lowerExpr(sel_arg_node); const sel_fid = self.getSelRegisterNameFid(); var sel_args = std.ArrayList(Ref).empty; sel_args.append(self.alloc, sel_ref) catch unreachable; const sel_owned = sel_args.toOwnedSlice(self.alloc) catch unreachable; break :blk self.builder.emit(.{ .call = .{ .callee = sel_fid, .args = sel_owned } }, vptr_ty); }; // Additional args after recv + selector. var extra = std.ArrayList(Ref).empty; var ai: usize = 2; while (ai < fic.args.len) : (ai += 1) { extra.append(self.alloc, self.lowerExpr(fic.args[ai])) catch unreachable; } const extra_owned = extra.toOwnedSlice(self.alloc) catch unreachable; return self.builder.emit(.{ .objc_msg_send = .{ .recv = recv, .sel = sel, .args = extra_owned, } }, ret_ty); } fn lowerJniCall(self: *Lowering, fic: *const ast.FfiIntrinsicCall) Ref { // env is always implicit: lexical-direct from the enclosing `#jni_env(env)` // block (2.16b, cheap), else the thread-local slot the block populated // at runtime (2.16c, one TL load per call). Surface form is uniform: // #jni_call(T)(target, "name", "sig", method-args...) (≥3 args) if (fic.args.len < 3) { if (self.diagnostics) |d| { d.add(.err, "#jni_call requires target, method name, and signature", null); } return Ref.none; } const ret_ty = self.resolveType(fic.return_type); const env_ref = if (self.jni_env_stack.items.len > self.jni_env_stack_base) self.jni_env_stack.items[self.jni_env_stack.items.len - 1] else blk: { const fids = self.getJniEnvTlFids(); const ptr_ty = self.module.types.ptrTo(.void); break :blk self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty); }; const target_idx: usize = 0; const name_idx: usize = 1; const sig_idx: usize = 2; const first_method_arg_idx: usize = 3; const target_ref = self.lowerExpr(fic.args[target_idx]); const name_node = fic.args[name_idx]; const sig_node = fic.args[sig_idx]; const name_ref = self.lowerExpr(name_node); const sig_ref = self.lowerExpr(sig_node); // Capture the (name, sig) literal content when both args are // string literals — emit_llvm uses this as the intern key for // the shared `jclass`/`jmethodID` slot pair (step 1.17). const cache_key: ?inst_mod.CacheKey = if (name_node.data == .string_literal and sig_node.data == .string_literal) inst_mod.CacheKey{ .name_str = name_node.data.string_literal.raw, .sig_str = sig_node.data.string_literal.raw, } else null; var extra = std.ArrayList(Ref).empty; var ai: usize = first_method_arg_idx; while (ai < fic.args.len) : (ai += 1) { extra.append(self.alloc, self.lowerExpr(fic.args[ai])) catch unreachable; } const extra_owned = extra.toOwnedSlice(self.alloc) catch unreachable; return self.builder.emit(.{ .jni_msg_send = .{ .env = env_ref, .target = target_ref, .name = name_ref, .sig = sig_ref, .args = extra_owned, .is_static = fic.kind == .jni_static_call, .cache_key = cache_key, } }, ret_ty); } /// Lower an `inst.method(args)` call where `inst`'s type is a foreign-class /// alias declared by `#jni_class("...") { ... }` (or its parallel forms). /// JNI runtimes lower directly to `jni_msg_send` with a descriptor derived /// from the method's sx signature; Obj-C / Swift runtimes are deferred to /// Phase 3/4 and currently surface a clear diagnostic. fn lowerForeignMethodCall( self: *Lowering, fcd: *const ast.ForeignClassDecl, method_name: []const u8, target: Ref, method_args: []const Ref, span: ast.Span, ) Ref { // M2.3 — walk the `#extends` chain when the method isn't // declared directly on this fcd. The dispatch target stays // the original receiver — objc_msgSend's runtime walks the // class hierarchy by isa, so we just need to find ANY // ancestor that declared the method (for the selector // mangling + signature info). The receiver-class fcd is // still used for `*Self` substitution at the dispatch site // — the inherited method's *Self should resolve to the // child receiver, not the parent. const found = self.findForeignMethodInChain(fcd, method_name) orelse { if (self.diagnostics) |d| { d.addFmt(.err, span, "no method '{s}' on foreign class '{s}' (or any `#extends` ancestor)", .{ method_name, fcd.name }); } return Ref.none; }; const method = found.method; // Obj-C instance dispatch (Phase 3 step 3.0 + M1.2 A.7). // `inst.method(args)` on an `#objc_class` / `#objc_protocol` // receiver derives a selector from the sx method name (default // mangling: split on `_`, each piece becomes a keyword with a // trailing `:`; niladic stays verbatim) and lowers to // `objc_msg_send`. Both foreign and sx-defined classes flow // through the same path — sx-defined classes have their IMPs // registered at module-init (M1.2 A.4b.iii) so `objc_msgSend` // finds them. The Swift runtimes still bail — Phase 4. if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) { return self.lowerObjcMethodCall(fcd, method, target, method_args, span); } if (!fcd.is_foreign) { if (self.diagnostics) |d| { d.addFmt(.err, span, "sx-defined classes on non-Obj-C runtimes can't yet be dispatched into (class '{s}', runtime '{s}')", .{ fcd.name, @tagName(fcd.runtime) }); } return Ref.none; } if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) { if (self.diagnostics) |d| { d.addFmt(.err, span, "method calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)}); } return Ref.none; } if (self.jni_env_stack.items.len == 0) { if (self.diagnostics) |d| { d.addFmt(.err, span, "method call on '{s}' requires an enclosing '#jni_env' scope", .{fcd.name}); } return Ref.none; } const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1]; // Build a ClassRegistry snapshot so descriptor derivation can // resolve `*Foo` cross-class refs to their foreign paths. var registry = jni_descriptor.ClassRegistry.init(self.alloc); defer registry.deinit(); var it = self.program_index.foreign_class_map.iterator(); while (it.next()) |entry| { registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path) catch {}; } const desc_str = jni_descriptor.deriveMethod(self.alloc, .{ .enclosing_path = fcd.foreign_path, .classes = ®istry, }, method) catch |err| { if (self.diagnostics) |d| { d.addFmt(.err, span, "JNI descriptor derivation failed for '{s}.{s}': {s}", .{ fcd.name, method.name, @errorName(err) }); } return Ref.none; }; const name_sid = self.module.types.internString(method_name); const name_ref = self.builder.constString(name_sid); const sig_sid = self.module.types.internString(desc_str); const sig_ref = self.builder.constString(sig_sid); const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void; // Reject return types the JNI emit path can't dispatch — emit_llvm's // CallMethod switch only covers void / bool / s32 / s64 / f32 / f64 // / pointer-returning. Anything else (s8 / s16 / u8 / u16 / aggregates) // would silently lower to LLVMGetUndef and produce wrong arguments at // the call site (chess Android touch shipped broken because s32→s32+ // f32 returns hit the undef path before .f32 was wired up). if (!jni_descriptor.isJniReturnTypeSupported(&self.module.types, ret_ty)) { if (self.diagnostics) |d| { d.addFmt(.err, span, "JNI method '{s}.{s}' returns '{s}', which isn't supported by the JNI call-method lowering yet — only void/bool/s32/s64/f32/f64 and pointers are wired up", .{ fcd.name, method.name, self.module.types.typeName(ret_ty) }); } return Ref.none; } const cache_key: inst_mod.CacheKey = .{ .name_str = method_name, .sig_str = desc_str, }; const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; return self.builder.emit(.{ .jni_msg_send = .{ .env = env_ref, .target = target, .name = name_ref, .sig = sig_ref, .args = args_owned, .is_static = method.is_static, .cache_key = cache_key, } }, ret_ty); } // Pure Obj-C decision helpers (selector derivation, type-encoding, ARC // property-kind, class-pointer recognition, state-struct planning) live in // `ffi_objc.zig` (`ObjcLowering`, a `*Lowering` facade). Reached via // `self.objc()`. Emission-heavy IMP builders + `lowerObjc*Call` stay here. /// Resolve a foreign-class member type, substituting `Self` (and `*Self`) /// with the foreign class's own struct type. Without this substitution /// chained calls like `Cls.alloc().init()` see the inner result as a /// fictitious `Self` struct and the next dispatch lookup fails. fn resolveForeignClassMemberType( self: *Lowering, fcd: *const ast.ForeignClassDecl, type_node: *const ast.Node, ) TypeId { if (type_node.data == .type_expr and std.mem.eql(u8, type_node.data.type_expr.name, "Self")) { return self.foreignClassStructType(fcd); } if (type_node.data == .pointer_type_expr) { const pt = type_node.data.pointer_type_expr; if (pt.pointee_type.data == .type_expr and std.mem.eql(u8, pt.pointee_type.data.type_expr.name, "Self")) { return self.module.types.ptrTo(self.foreignClassStructType(fcd)); } } return self.resolveType(type_node); } pub fn resolveForeignMethodReturnType( self: *Lowering, fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl, ) TypeId { const rt = method.return_type orelse return .void; return self.resolveForeignClassMemberType(fcd, rt); } fn foreignClassStructType(self: *Lowering, fcd: *const ast.ForeignClassDecl) TypeId { const name_id = self.module.types.internString(fcd.name); if (self.module.types.findByName(name_id)) |existing| return existing; return self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); } /// Lower `inst.method(args)` on an `#objc_class` / `#objc_protocol` /// receiver. The selector is derived by `deriveObjcSelector`; arity /// is validated against the keyword count produced by the mangling /// (excluding self). Dispatch then runs through `objc_msg_send`, /// sharing the cached-SEL slot path with explicit `#objc_call`. fn lowerObjcMethodCall( self: *Lowering, fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl, target: Ref, method_args: []const Ref, span: ast.Span, ) Ref { const arity = method_args.len; const derived = self.objc().deriveObjcSelector(method, arity); // Arity validation: the keyword count (number of `:` in the // selector) must equal the number of args passed at the call // site. For methods using the default mangling rule, a mismatch // is an error because the user can fix the sx-side name. For // `#selector("...")` overrides, the user has deliberately // chosen the selector — downgrade to a warning so the build // proceeds, but still surface the typo case (Obj-C's runtime // doesn't validate colon-vs-arg, so this is the last defense). if (arity > 0 and derived.keyword_count != arity) { if (self.diagnostics) |d| { if (derived.is_override) { d.addFmt( .warn, span, "Obj-C selector \"{s}\" (override for '{s}.{s}') has {} keyword(s) but the call passes {} argument(s); the runtime will dispatch but the colon count is inconsistent with the arity — double-check the selector string", .{ derived.sel, fcd.name, method.name, derived.keyword_count, arity }, ); } else { d.addFmt( .err, span, "Obj-C selector for '{s}.{s}' has {} keyword(s) but the call passes {} argument(s); split the sx method name on '_' so it produces exactly {} keyword(s), or override with `#selector(\"...\")`", .{ fcd.name, method.name, derived.keyword_count, arity, arity }, ); return Ref.none; } } } const ret_ty = self.resolveForeignMethodReturnType(fcd, method); // Cache the SEL slot per (selector-string, module) like // `#objc_call` does. The mangling produces the literal selector // string; we don't need a runtime sel_registerName call at the // dispatch site because the global initializer already does it. const vptr_ty = self.module.types.ptrTo(.void); const slot_gid = self.internObjcSelector(derived.sel); const slot_ptr = self.builder.emit(.{ .global_addr = slot_gid }, self.module.types.ptrTo(vptr_ty)); const sel = self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty); const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; return self.builder.emit(.{ .objc_msg_send = .{ .recv = target, .sel = sel, .args = args_owned, } }, ret_ty); } /// Lower `Cls.static_method(args)` on an `#objc_class` / /// `#objc_protocol` alias. Loads the class object through the /// module-scoped cached slot (populated by `objc_getClass` at /// module-init) and dispatches `objc_msg_send` with the same /// selector mangling as instance methods (Phase 3.0). fn lowerObjcStaticCall( self: *Lowering, fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl, method_args: []const Ref, span: ast.Span, ) Ref { const arity = method_args.len; const derived = self.objc().deriveObjcSelector(method, arity); if (arity > 0 and derived.keyword_count != arity) { if (self.diagnostics) |d| { if (derived.is_override) { d.addFmt( .warn, span, "Obj-C selector \"{s}\" (override for static call '{s}.{s}') has {} keyword(s) but the call passes {} argument(s); the runtime will dispatch but the colon count is inconsistent with the arity — double-check the selector string", .{ derived.sel, fcd.name, method.name, derived.keyword_count, arity }, ); } else { d.addFmt( .err, span, "Obj-C selector for static call '{s}.{s}' has {} keyword(s) but the call passes {} argument(s); split the sx method name on '_' so it produces exactly {} keyword(s), or override with `#selector(\"...\")`", .{ fcd.name, method.name, derived.keyword_count, arity, arity }, ); return Ref.none; } } } const ret_ty = self.resolveForeignMethodReturnType(fcd, method); const vptr_ty = self.module.types.ptrTo(.void); // Load the class object from its module-scoped cached slot. // `objc_getClass()` runs once at module-init via the // constructor emit_llvm synthesizes (see `emitObjcClassInit`). const class_slot_gid = self.internObjcClassObject(fcd.foreign_path); const class_slot_ptr = self.builder.emit(.{ .global_addr = class_slot_gid }, self.module.types.ptrTo(vptr_ty)); const class_obj = self.builder.emit(.{ .load = .{ .operand = class_slot_ptr } }, vptr_ty); // M4.0b: intercept `Cls.alloc()` for sx-defined classes — emit the // inline alloc-and-init sequence using the caller's `context.allocator` // instead of going through `objc_msgSend` (which would land in the // +alloc IMP and use `__sx_default_context.allocator`). This honors // a surrounding `push Context.{ allocator = ... }`. if (!fcd.is_foreign and fcd.runtime == .objc_class and method_args.len == 0 and std.mem.eql(u8, method.name, "alloc")) { const ctx_addr = if (self.current_ctx_ref != Ref.none) self.current_ctx_ref else blk: { // Fallback: no current ctx (e.g. compiler-internal callers). // Use the default context — same as the IMP would. const default_ctx_gi = self.program_index.global_names.get("__sx_default_context") orelse { if (self.diagnostics) |d| { d.addFmt(.err, span, "Cls.alloc() on sx-defined class '{s}': no current context and __sx_default_context missing", .{fcd.name}); } return Ref.none; }; break :blk self.builder.emit(.{ .global_addr = default_ctx_gi.id }, vptr_ty); }; const instance = self.emitObjcDefinedAllocAndInit(fcd, class_obj, ctx_addr) orelse return Ref.none; // class_createInstance returns *void; bitcast to the method's // declared return type (typically `*` or `?*`) so // downstream `let f := Cls.alloc();` binds f at the right type // (lowerVarDecl reads the Ref's IR type when no annotation is // present). coerceToType is a no-op for ptr→ptr; we need an // explicit bitcast IR op to retype the Ref. if (ret_ty == vptr_ty) return instance; // Optional-wrapped returns (e.g. `-> ?*Cls`): emit optional_wrap. if (!ret_ty.isBuiltin()) { const ret_info = self.module.types.get(ret_ty); if (ret_info == .optional) { const inner = ret_info.optional.child; const cast = if (inner == vptr_ty) instance else self.builder.emit(.{ .bitcast = .{ .operand = instance, .from = vptr_ty, .to = inner } }, inner); return self.builder.optionalWrap(cast, ret_ty); } } return self.builder.emit(.{ .bitcast = .{ .operand = instance, .from = vptr_ty, .to = ret_ty } }, ret_ty); } // Load the SEL from its slot. const sel_slot_gid = self.internObjcSelector(derived.sel); const sel_slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty)); const sel = self.builder.emit(.{ .load = .{ .operand = sel_slot_ptr } }, vptr_ty); const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; return self.builder.emit(.{ .objc_msg_send = .{ .recv = class_obj, .sel = sel, .args = args_owned, } }, ret_ty); } /// Lower `Alias.new(args)` where `Alias` is a foreign-class identifier /// with `static new :: (...) -> *Self;` — JNI constructor dispatch: /// `FindClass + GetMethodID("", "(args)V") + NewObject(env, /// clazz, mid, args...)`. Returns the new jobject. /// /// Non-`new` static methods aren't supported via this path yet — the /// user can use `#jni_static_call(T)(class, "name", sig, args...)` /// for those. Constructor is the common case for #jni_main bodies /// that need to instantiate Android classes (SurfaceView, etc.). fn lowerForeignStaticCall( self: *Lowering, fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl, method_args: []const Ref, span: ast.Span, ) Ref { // Obj-C static dispatch (Phase 3 step 3.1). `Cls.static_method(args)` // on an `#objc_class` alias loads the class object through a // module-scoped cached slot (populated once per module via // `objc_getClass`) and dispatches with the derived selector. if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) { return self.lowerObjcStaticCall(fcd, method, method_args, span); } if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) { if (self.diagnostics) |d| d.addFmt(.err, span, "static calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)}); return Ref.none; } if (!std.mem.eql(u8, method.name, "new")) { if (self.diagnostics) |d| d.addFmt(.err, span, "static foreign-class call '{s}.{s}' not yet supported via `Alias.method()` syntax \u{2014} only `new` is wired today; use `#jni_static_call` directly for other static methods", .{ fcd.name, method.name }); return Ref.none; } if (self.jni_env_stack.items.len <= self.jni_env_stack_base) { if (self.diagnostics) |d| d.addFmt(.err, span, "constructor `{s}.new(...)` requires an enclosing `#jni_env` scope (or `#jni_main` body)", .{fcd.name}); return Ref.none; } const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1]; // Build class registry snapshot for `*Foo` cross-class refs. var registry = jni_descriptor.ClassRegistry.init(self.alloc); defer registry.deinit(); var it = self.program_index.foreign_class_map.iterator(); while (it.next()) |entry| { registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path) catch {}; } // For `new`, the JNI descriptor's return position is `V` (the // constructor returns void; the new jobject comes back from // `NewObject` itself). Patch the AST by overriding return_type // to null during derivation. const m_for_desc: ast.ForeignMethodDecl = .{ .name = method.name, .params = method.params, .param_names = method.param_names, .return_type = null, .is_static = method.is_static, .jni_descriptor_override = method.jni_descriptor_override, .body = method.body, }; const descriptor = jni_descriptor.deriveMethod(self.alloc, .{ .enclosing_path = fcd.foreign_path, .classes = ®istry, }, m_for_desc) catch |err| { if (self.diagnostics) |d| d.addFmt(.err, span, "JNI descriptor derivation failed for '{s}.new': {s}", .{ fcd.name, @errorName(err) }); return Ref.none; }; // sx-side return type is `*Self` — resolve to a pointer to the // foreign-class struct type so method dispatch on the new // jobject works (`view := SurfaceView.new(ctx); view.getHolder()`). // At LLVM level still ptr; the sx type table is what method // resolution consults. const self_struct_name = self.module.types.internString(fcd.name); const self_struct_id = if (self.module.types.findByName(self_struct_name)) |existing| existing else blk: { const info: types.TypeInfo = .{ .@"struct" = .{ .name = self_struct_name, .fields = &.{} } }; break :blk self.module.types.intern(info); }; const ret_ty = self.module.types.ptrTo(self_struct_id); const name_sid = self.module.types.internString(""); const name_ref = self.builder.constString(name_sid); const sig_sid = self.module.types.internString(descriptor); const sig_ref = self.builder.constString(sig_sid); const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; return self.builder.emit(.{ .jni_msg_send = .{ .env = env_ref, .target = Ref.none, // unused for ctor — class is resolved via parent_class_path .name = name_ref, .sig = sig_ref, .args = args_owned, .is_static = false, .is_constructor = true, .parent_class_path = self.alloc.dupe(u8, fcd.foreign_path) catch fcd.foreign_path, .cache_key = null, } }, ret_ty); } /// Lower `super.method(args)` inside a `#jni_main` / sx-defined /// `#jni_class` bodied method. Resolves the parent class from the /// enclosing fcd's `#extends` clause (default `android.app.Activity`) /// and emits a `JniMsgSend` with `is_nonvirtual=true`, which /// emit_llvm expands into a `FindClass(parent) + GetMethodID + /// CallNonvirtualMethod` chain. /// /// Signature derivation: when `method_name` matches the enclosing /// method's name (the common case — `super.onCreate(b)` from inside /// `onCreate :: (self, b)` override), the enclosing method's /// signature is reused. Other method names require the parent class /// to be declared via `#foreign #jni_class` so the signature can be /// looked up. fn lowerSuperCall( self: *Lowering, method_name: []const u8, method_args: []const Ref, span: ast.Span, ) Ref { const fcd = self.current_foreign_class orelse { if (self.diagnostics) |d| d.addFmt(.err, span, "'super' is only valid inside a `#jni_class` method body", .{}); return Ref.none; }; // Resolve parent foreign_path from the fcd's `#extends`. Default to // android.app.Activity to match the jni_java_emit default. var parent_path: []const u8 = "android/app/Activity"; for (fcd.members) |m| switch (m) { .extends => |alias| { if (self.program_index.foreign_class_map.get(alias)) |parent_fcd| { parent_path = parent_fcd.foreign_path; } else { parent_path = alias; } break; }, else => {}, }; // Resolve method signature. Same-name fast path reuses the // enclosing method's descriptor; cross-method super calls require // the parent class to be declared via `#foreign #jni_class`. var descriptor: []const u8 = ""; var resolved_method: ?ast.ForeignMethodDecl = null; if (self.current_foreign_method) |em| { if (std.mem.eql(u8, em.name, method_name)) { resolved_method = em; } } if (resolved_method == null) { const parent_fcd = blk: for (fcd.members) |m| switch (m) { .extends => |alias| if (self.program_index.foreign_class_map.get(alias)) |pf| break :blk pf else continue, else => {}, } else null; if (parent_fcd) |pf| { for (pf.members) |pm| switch (pm) { .method => |pmd| if (std.mem.eql(u8, pmd.name, method_name)) { resolved_method = pmd; break; }, else => {}, }; } } const method = resolved_method orelse { if (self.diagnostics) |d| d.addFmt(.err, span, "no method '{s}' found for `super.{s}(...)` — declare the parent class via `#foreign #jni_class` to make cross-method super calls available", .{ method_name, method_name }); return Ref.none; }; // Derive descriptor against the parent path (used as enclosing_path // for `*Self` resolution). var registry = jni_descriptor.ClassRegistry.init(self.alloc); defer registry.deinit(); var it = self.program_index.foreign_class_map.iterator(); while (it.next()) |entry| { registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path) catch {}; } descriptor = jni_descriptor.deriveMethod(self.alloc, .{ .enclosing_path = parent_path, .classes = ®istry, }, method) catch |err| { if (self.diagnostics) |d| d.addFmt(.err, span, "super-call descriptor derivation failed for '{s}.{s}': {s}", .{ parent_path, method_name, @errorName(err) }); return Ref.none; }; // env from the lexical stack (pushed by synthesizeJniMainStub). if (self.jni_env_stack.items.len <= self.jni_env_stack_base) { if (self.diagnostics) |d| d.addFmt(.err, span, "`super.{s}(...)` requires an enclosing `#jni_main` method scope (env is unavailable)", .{method_name}); return Ref.none; } const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1]; // `self` is the first param of the synthesized `Java_*` fn. Bound // in scope as `self` by synthesizeJniMainStub. const self_binding = if (self.scope) |s| s.lookup("self") else null; const self_ref = if (self_binding) |b| (if (b.is_alloca) self.builder.load(b.ref, b.ty) else b.ref) else Ref.none; const name_sid = self.module.types.internString(method_name); const name_ref = self.builder.constString(name_sid); const sig_sid = self.module.types.internString(descriptor); const sig_ref = self.builder.constString(sig_sid); const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void; const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; return self.builder.emit(.{ .jni_msg_send = .{ .env = env_ref, .target = self_ref, .name = name_ref, .sig = sig_ref, .args = args_owned, .is_static = false, .is_nonvirtual = true, .parent_class_path = self.alloc.dupe(u8, parent_path) catch parent_path, .cache_key = null, // per-call FindClass + GetMethodID; caching is a follow-up } }, ret_ty); } // ── Calls ─────────────────────────────────────────────────────── 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). 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. 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. 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; const name = std.fmt.bufPrint(&buf, "__lambda_{d}", .{self.block_counter}) catch "__lambda"; self.block_counter += 1; // Collect lambda param names for exclusion from captures var param_names = std.StringHashMap(void).init(self.alloc); defer param_names.deinit(); for (lam.params) |p| { param_names.put(p.name, {}) catch {}; } // Pre-scan lambda body AST for free variables (captures) var captures = std.ArrayList(CaptureInfo).empty; defer captures.deinit(self.alloc); self.collectCaptures(lam.body, ¶m_names, &captures); // Deduplicate captures var seen = std.StringHashMap(void).init(self.alloc); defer seen.deinit(); var deduped = std.ArrayList(CaptureInfo).empty; defer deduped.deinit(self.alloc); for (captures.items) |cap| { if (!seen.contains(cap.name)) { seen.put(cap.name, {}) catch {}; deduped.append(self.alloc, cap) catch {}; } } const capture_list = deduped.items; // Build env struct type if there are captures var env_struct_ty: TypeId = .void; if (capture_list.len > 0) { const env_field_data = self.alloc.alloc(types.TypeInfo.StructInfo.Field, capture_list.len) catch unreachable; for (capture_list, 0..) |cap, i| { var nbuf: [32]u8 = undefined; const fname = std.fmt.bufPrint(&nbuf, "cap_{d}", .{i}) catch "cap"; env_field_data[i] = .{ .name = self.module.types.internString(fname), .ty = cap.ty, }; } const env_name = std.fmt.bufPrint(&buf, "__env_{d}", .{self.block_counter}) catch "__env"; const env_name_id = self.module.types.internString(env_name); env_struct_ty = self.module.types.intern(.{ .@"struct" = .{ .name = env_name_id, .fields = env_field_data, } }); } // Save current builder state const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const saved_scope = self.scope; // Build param list. Convention when implicit_ctx is enabled: // slot 0 = __sx_ctx: *void // slot 1 = env: *void // slot 2+ = user params // Without implicit_ctx, env is slot 0 and user params follow. var params = std.ArrayList(Function.Param).empty; const env_ptr_ty = self.module.types.ptrTo(.void); const lambda_wants_ctx = self.implicit_ctx_enabled and lam.call_conv != .c; if (lambda_wants_ctx) { params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = env_ptr_ty, }) catch unreachable; } params.append(self.alloc, .{ .name = self.module.types.internString("env"), .ty = env_ptr_ty, }) catch unreachable; // Get target closure param types for inference (from Closure(T1, T2) -> R annotations) const target_closure_params: ?[]const TypeId = if (self.target_type) |tt| blk: { if (!tt.isBuiltin()) { const tti = self.module.types.get(tt); if (tti == .closure) break :blk tti.closure.params; // Unwrap ?Closure(...) → Closure(...) if (tti == .optional) { const inner = tti.optional.child; if (!inner.isBuiltin()) { const inner_info = self.module.types.get(inner); if (inner_info == .closure) break :blk inner_info.closure.params; } } } break :blk null; } else null; // User params follow the ctx (optional) + env slots in `params`. const user_param_base: usize = (if (lambda_wants_ctx) @as(usize, 1) else 0) + 1; for (lam.params, 0..) |p, pi| { const pty: TypeId = blk: { // Unannotated lambda params take their type positionally from // the target `Closure(T0, …)` signature. Resolve them here so // `resolveParamType` (which would diagnose a missing annotation) // is only called for params that carry one. if (p.type_expr.data == .inferred_type) { if (target_closure_params != null and pi < target_closure_params.?.len) { break :blk target_closure_params.?[pi]; } if (self.diagnostics) |d| { d.addFmt(.err, p.type_expr.span, "cannot infer type of lambda parameter '{s}'; annotate it or use the lambda where a closure type is expected", .{p.name}); } break :blk .unresolved; } break :blk self.resolveParamType(&p); }; params.append(self.alloc, .{ .name = self.module.types.internString(p.name), .ty = pty, }) catch unreachable; } const ret_ty = blk: { if (lam.return_type) |rt| { break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } // Use target closure return type if available — but only when it's // a resolved type. An `.unresolved` ret comes from an unbound // generic (`Closure(..) -> $R`); fall through to infer it from the // body so the concrete return drives `$R` inference at the call site. if (self.target_type) |tt| { if (!tt.isBuiltin()) { const tti = self.module.types.get(tt); if (tti == .closure and tti.closure.ret != .unresolved) break :blk tti.closure.ret; // Unwrap ?Closure(...) → Closure(...) if (tti == .optional) { const inner = tti.optional.child; if (!inner.isBuiltin()) { const inner_info = self.module.types.get(inner); if (inner_info == .closure and inner_info.closure.ret != .unresolved) break :blk inner_info.closure.ret; } } } } // Arrow lambda without explicit return type — infer from body expression // Temporarily bind params in scope so inferExprType can resolve param types var temp_scope = Scope.init(self.alloc, self.scope); const saved = self.scope; self.scope = &temp_scope; for (lam.params, 0..) |p, i| { const pty = params.items[user_param_base + i].ty; temp_scope.put(p.name, .{ .ref = @enumFromInt(0), .ty = pty, .is_alloca = false }); } const inferred = self.inferExprType(lam.body); self.scope = saved; temp_scope.deinit(); break :blk inferred; }; const name_id = self.module.types.internString(name); const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); if (lam.call_conv == .c) { self.module.getFunctionMut(func_id).call_conv = .c; } self.builder.currentFunc().has_implicit_ctx = lambda_wants_ctx; // Param-slot layout: ctx at 0 (if present), env at ctx_slots, // user args at ctx_slots+1. const lambda_ctx_slots: u32 = if (lambda_wants_ctx) 1 else 0; const env_param_idx: u32 = lambda_ctx_slots; const user_param_base_lam: u32 = lambda_ctx_slots + 1; // Save + rebind current_ctx_ref so the body's sx-to-sx calls // forward the trampoline's own ctx (slot 0). const saved_ctx_ref_lam = self.current_ctx_ref; defer self.current_ctx_ref = saved_ctx_ref_lam; if (lambda_wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); // A lambda is its own function: its `return` must drain only ITS OWN // `defer`s, not the enclosing function's. Open a fresh defer window // (like `lowerFunction`/`monomorphizeFunction`) and restore on exit — // otherwise lowering a closure literal inside a `defer` body re-enters // the enclosing function's defer drain (infinite recursion — issue 0073). const saved_func_defer_base = self.func_defer_base; const saved_defer_len = self.defer_stack.items.len; defer { self.func_defer_base = saved_func_defer_base; self.defer_stack.shrinkRetainingCapacity(saved_defer_len); } self.func_defer_base = saved_defer_len; // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // Create scope WITHOUT parent — captures are bound from env, not parent scope var lambda_scope = Scope.init(self.alloc, null); self.scope = &lambda_scope; // Bind captures from env struct (at env_param_idx) if (capture_list.len > 0) { const env_param_ref = Ref.fromIndex(env_param_idx); // Alloca env struct locally so struct_gep can resolve the type const env_local = self.builder.alloca(env_struct_ty); // Compute env size const env_byte_size_inner = self.computeEnvSize(capture_list); const env_size_val = self.builder.constInt(@intCast(env_byte_size_inner), .s64); // memcpy(local_alloca, env_param, size) _ = self.callForeign("memcpy", &.{ env_local, env_param_ref, env_size_val }, self.module.types.ptrTo(.void)); for (capture_list, 0..) |cap, i| { // GEP into env struct to get field pointer const field_ptr = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty); // Load the captured value into a local alloca const loaded = self.builder.load(field_ptr, cap.ty); const slot = self.builder.alloca(cap.ty); self.builder.store(slot, loaded); lambda_scope.put(cap.name, .{ .ref = slot, .ty = cap.ty, .is_alloca = true }); } } // Also need parent scope for function lookups (but not variable lookups) // Set up fn_names from parent scope chain { var s: ?*Scope = saved_scope; while (s) |scope| { var it = scope.fn_names.iterator(); while (it.next()) |e| { if (!lambda_scope.fn_names.contains(e.key_ptr.*)) { lambda_scope.fn_names.put(e.key_ptr.*, e.value_ptr.*) catch {}; } } s = scope.parent; } } // Bind params (user args start at user_param_base_lam, shifted past ctx + env). // Use the signature types computed above (`params`), which already // applied contextual typing from the target closure to untyped params — // `resolveParamType` alone would drop it and default each to s64. for (lam.params, 0..) |p, i| { const pty = params.items[user_param_base + i].ty; const slot = self.builder.alloca(pty); const param_ref = Ref.fromIndex(user_param_base_lam + @as(u32, @intCast(i))); self.builder.store(slot, param_ref); lambda_scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); } // Lower body — capture last expression as return value. The // `in_lambda_body` flag scopes the lambda-specific `raise`-not-failable // hint; save/restore so a lambda nested inside a regular function (or a // lambda inside a lambda) restores the enclosing context. const saved_in_lambda = self.in_lambda_body; self.in_lambda_body = true; if (ret_ty != .void) { if (self.lowerBlockValue(lam.body)) |val| { if (!self.currentBlockHasTerminator()) { const val_ty = self.builder.getRefType(val); // A value-carrying failable arrow lambda (`-> (T, !) => expr`) // yields the bare success value; the compiler appends the // no-error slot (0) — same as a `return v` in a block body. if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .tuple and self.errorChannelOf(ret_ty) != null) { self.lowerFailableSuccessReturn(val, ret_ty, lam.body.span); } else { const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; self.builder.ret(coerced, ret_ty); } } } } else { self.lowerBlock(lam.body); } self.in_lambda_body = saved_in_lambda; self.ensureTerminator(ret_ty); self.builder.finalize(); // Restore builder state self.scope = saved_scope; lambda_scope.deinit(); self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; // Restore the caller's `current_ctx_ref` BEFORE we emit the env // alloc/memcpy below — those run in the caller's scope, and // `allocViaContext` reads `current_ctx_ref` to find the // installed allocator. Without this, the env_heap dispatch // would still see `Ref.fromIndex(0)` (the lambda's own ctx // param), which doesn't exist in the caller's frame and // silently routes through the default context instead of any // surrounding `push Context.{ allocator = ... }`. self.current_ctx_ref = saved_ctx_ref_lam; // Closure flowing into a BARE function-pointer slot (`(T) -> U`, no env): // the slot is called without the closure env arg, so the closure fn can't // be passed directly. For a capture-free closure whose return type matches // the slot, emit an adapter with the bare ABI. Reject the cases the bare // ABI can't represent: a capturing closure (env has nowhere to live), and // a failable closure into a non-failable slot (foreign code can't observe // the error channel — ERR E5.1 FFI-boundary rule). if (self.target_type) |tt| { if (!tt.isBuiltin() and self.module.types.get(tt) == .function) { const slot_ret = self.module.types.get(tt).function.ret; const widen_ok = self.errorChannelOf(slot_ret) != null and self.errorChannelOf(ret_ty) == null and self.failableSuccessType(slot_ret) == ret_ty; if (capture_list.len > 0) { if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "a capturing closure cannot be passed as a bare function pointer; declare the parameter type as `Closure(...)` so its environment is carried", .{}); } else if (ret_ty == slot_ret or widen_ok) { // Matching ABI, or a non-failable closure widening into a // failable slot (∅ ⊆ slot set) — the adapter wraps {value, 0}. const adapter = self.createClosureToBareFnAdapter(func_id, self.module.types.get(tt).function, ret_ty, lam.body.span); return self.builder.emit(.{ .func_ref = adapter }, tt); } else if (self.errorChannelOf(ret_ty) != null and self.errorChannelOf(slot_ret) == null) { if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "failable closure cannot be assigned to a non-failable function-type slot; foreign code can't observe the error channel — handle the error in a wrapper closure that absorbs it", .{}); } else if (self.diagnostics) |d| { d.addFmt(.err, lam.body.span, "closure return type does not match the function-type slot", .{}); } } } // Create proper closure type (user-visible params only — skip ctx + env). const skip_count: usize = if (lambda_wants_ctx) 2 else 1; var param_types_list = std.ArrayList(TypeId).empty; for (params.items[skip_count..]) |p| { param_types_list.append(self.alloc, p.ty) catch unreachable; } const closure_ty = self.module.types.closureType(param_types_list.items, ret_ty); // Build env and closure in the caller's scope if (capture_list.len > 0) { // Alloca env struct on stack (so struct_gep can resolve the type) const env_local = self.builder.alloca(env_struct_ty); // Store captured values into env struct fields for (capture_list, 0..) |cap, i| { const gep = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty); const val = if (cap.is_alloca) self.builder.load(cap.ref, cap.ty) else cap.ref; self.builder.store(gep, val); } // Copy env to heap (so it outlives the stack frame). // Route through `context.allocator.alloc` rather than calling // libc malloc directly so closures respect a surrounding // `push Context.{ allocator = ... }` and a tracker / arena // counts the env allocation alongside everything else. const env_byte_size = self.computeEnvSize(capture_list); const env_size = self.builder.constInt(@intCast(env_byte_size), .s64); const ptr_void = self.module.types.ptrTo(.void); const env_heap = self.allocViaContext(env_size, ptr_void); // memcpy(heap, stack_alloca, size) _ = self.callForeign("memcpy", &.{ env_heap, env_local, env_size }, ptr_void); return self.builder.closureCreate(func_id, env_heap, closure_ty); } else { return self.builder.closureCreate(func_id, Ref.none, closure_ty); } } /// 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 { // 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 // function (which is also sx-side and expects it at slot 0). var params = std.ArrayList(inst_mod.Function.Param).empty; defer params.deinit(self.alloc); const void_ptr_ty = self.module.types.ptrTo(.void); const wants_ctx = self.implicit_ctx_enabled; if (wants_ctx) { params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable; } const env_name = self.module.types.internString("env"); params.append(self.alloc, .{ .name = env_name, .ty = void_ptr_ty }) catch unreachable; for (closure_info.params, 0..) |pty, i| { var buf: [32]u8 = undefined; const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg"; params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable; } // Generate unique trampoline name const bare_func = self.module.functions.items[bare_func_id.index()]; const bare_name = self.module.types.getString(bare_func.name); var name_buf: [128]u8 = undefined; const tramp_name = std.fmt.bufPrint(&name_buf, "__tramp_{s}", .{bare_name}) catch "__tramp"; const tramp_name_id = self.module.types.internString(tramp_name); // Save builder state const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; // Create function const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable; var func = inst_mod.Function.init(tramp_name_id, owned_params, closure_info.ret); func.has_implicit_ctx = wants_ctx; const func_id = self.module.addFunction(func); self.builder.func = func_id; self.builder.inst_counter = @intCast(owned_params.len); // params occupy refs 0..N-1 const entry_name = self.module.types.internString("entry"); const entry_block = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry_block); // Build call args: forward [__sx_ctx]? + user_params (skip env). // Trampoline slots: 0=ctx (if present), {0|1}=env, then user args. const ctx_slots: usize = if (wants_ctx) 1 else 0; const user_arg_start: u32 = @intCast(ctx_slots + 1); // skip ctx + env var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); if (wants_ctx and bare_func.has_implicit_ctx) { call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable; // forward our ctx } for (closure_info.params, 0..) |_, i| { call_args.append(self.alloc, Ref.fromIndex(user_arg_start + @as(u32, @intCast(i)))) catch unreachable; } const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; const result = self.builder.emit(.{ .call = .{ .callee = bare_func_id, .args = owned_args } }, closure_info.ret); // Return result (or void) if (closure_info.ret != .void) { self.builder.ret(result, closure_info.ret); } else { self.builder.retVoid(); } self.builder.finalize(); // Restore builder state self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return func_id; } /// Adapter for coercing a closure into a BARE function-pointer slot /// (`(T) -> U`, no env). The closure's underlying function has signature /// `[ctx?] + env + user-params`, but a bare fn-ptr slot is *called* without /// the env arg — so the closure fn can't be used directly (the env slot /// would swallow the first user arg). This adapter carries the bare ABI /// (`[ctx?] + user-params`) and forwards to the closure fn with a null env. /// Only sound for capture-free closures (a null env is correct iff the body /// reads no captures); the caller rejects capturing closures. /// /// When `closure_ret` differs from `fn_info.ret`, this is the ∅-widening /// case (a non-failable closure into a failable slot): the closure returns /// the success value and the adapter wraps it into the slot's `{value, 0}` /// failable tuple (ERR E5.1 non-failable→failable widening). fn createClosureToBareFnAdapter(self: *Lowering, closure_func_id: FuncId, fn_info: types.TypeInfo.FunctionInfo, closure_ret: TypeId, span: ast.Span) FuncId { var params = std.ArrayList(inst_mod.Function.Param).empty; defer params.deinit(self.alloc); const void_ptr_ty = self.module.types.ptrTo(.void); const wants_ctx = self.implicit_ctx_enabled; if (wants_ctx) { params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable; } for (fn_info.params, 0..) |pty, i| { var buf: [32]u8 = undefined; const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg"; params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable; } const closure_func = self.module.functions.items[closure_func_id.index()]; const closure_name = self.module.types.getString(closure_func.name); var name_buf: [128]u8 = undefined; const adapter_name = std.fmt.bufPrint(&name_buf, "__cl2fn_{s}", .{closure_name}) catch "__cl2fn"; const adapter_name_id = self.module.types.internString(adapter_name); const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable; var func = inst_mod.Function.init(adapter_name_id, owned_params, fn_info.ret); func.has_implicit_ctx = wants_ctx; const func_id = self.module.addFunction(func); self.builder.func = func_id; self.builder.inst_counter = @intCast(owned_params.len); const entry_name = self.module.types.internString("entry"); const entry_block = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry_block); // Forward [ctx?] + null env + user params to the closure fn. const ctx_slots: usize = if (wants_ctx) 1 else 0; var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); if (wants_ctx) call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable; call_args.append(self.alloc, self.builder.constNull(void_ptr_ty)) catch unreachable; for (fn_info.params, 0..) |_, i| { call_args.append(self.alloc, Ref.fromIndex(@intCast(ctx_slots + i))) catch unreachable; } const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; const result = self.builder.emit(.{ .call = .{ .callee = closure_func_id, .args = owned_args } }, closure_ret); if (closure_ret == fn_info.ret) { if (fn_info.ret != .void) { self.builder.ret(result, fn_info.ret); } else { self.builder.retVoid(); } } else { // ∅-widening: closure returns the success value; wrap `{value, 0}` // into the slot's failable tuple. self.lowerFailableSuccessReturn(result, fn_info.ret, span); } self.builder.finalize(); self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return func_id; } /// Walk an AST node and collect free variable references (identifiers that are /// in the current scope but not in lambda params). fn collectCaptures(self: *Lowering, node: *const Node, param_names: *std.StringHashMap(void), captures: *std.ArrayList(CaptureInfo)) void { switch (node.data) { .identifier => |id| { // Skip lambda params if (param_names.contains(id.name)) return; // Skip function names if (self.program_index.fn_ast_map.contains(id.name)) return; // Skip type names if (self.program_index.struct_template_map.contains(id.name)) return; // Check if it's a variable in the parent scope if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { captures.append(self.alloc, .{ .name = id.name, .ty = binding.ty, .ref = binding.ref, .is_alloca = binding.is_alloca, }) catch {}; } } }, .binary_op => |bo| { self.collectCaptures(bo.lhs, param_names, captures); self.collectCaptures(bo.rhs, param_names, captures); }, .unary_op => |uo| { self.collectCaptures(uo.operand, param_names, captures); }, .call => |cl| { self.collectCaptures(cl.callee, param_names, captures); for (cl.args) |arg| { self.collectCaptures(arg, param_names, captures); } }, .block => |blk| { for (blk.stmts) |stmt| { self.collectCaptures(stmt, param_names, captures); } }, .if_expr => |ie| { self.collectCaptures(ie.condition, param_names, captures); self.collectCaptures(ie.then_branch, param_names, captures); if (ie.else_branch) |eb| self.collectCaptures(eb, param_names, captures); }, .while_expr => |we| { self.collectCaptures(we.condition, param_names, captures); self.collectCaptures(we.body, param_names, captures); }, .return_stmt => |rs| { if (rs.value) |v| self.collectCaptures(v, param_names, captures); }, .var_decl => |vd| { if (vd.value) |v| self.collectCaptures(v, param_names, captures); // Register the local var name so it's not captured param_names.put(vd.name, {}) catch {}; }, .const_decl => |cd| { self.collectCaptures(cd.value, param_names, captures); param_names.put(cd.name, {}) catch {}; }, .assignment => |a| { self.collectCaptures(a.target, param_names, captures); self.collectCaptures(a.value, param_names, captures); }, .destructure_decl => |dd| { self.collectCaptures(dd.value, param_names, captures); for (dd.names) |name| { param_names.put(name, {}) catch {}; } }, .field_access => |fa| { self.collectCaptures(fa.object, param_names, captures); }, .index_expr => |ie| { self.collectCaptures(ie.object, param_names, captures); self.collectCaptures(ie.index, param_names, captures); }, .struct_literal => |sl| { for (sl.field_inits) |fi| { self.collectCaptures(fi.value, param_names, captures); } }, .array_literal => |al| { for (al.elements) |elem| { self.collectCaptures(elem, param_names, captures); } }, .lambda => |inner_lam| { // For nested lambdas, the inner lambda captures from our scope too // But its own params should be excluded var inner_params = std.StringHashMap(void).init(self.alloc); defer inner_params.deinit(); // Copy current param_names var it = param_names.iterator(); while (it.next()) |e| { inner_params.put(e.key_ptr.*, {}) catch {}; } for (inner_lam.params) |p| { inner_params.put(p.name, {}) catch {}; } self.collectCaptures(inner_lam.body, &inner_params, captures); }, .match_expr => |me| { self.collectCaptures(me.subject, param_names, captures); for (me.arms) |arm| { self.collectCaptures(arm.body, param_names, captures); } }, .null_coalesce => |nc| { self.collectCaptures(nc.lhs, param_names, captures); self.collectCaptures(nc.rhs, param_names, captures); }, .deref_expr => |de| { self.collectCaptures(de.operand, param_names, captures); }, .for_expr => |fe| { self.collectCaptures(fe.iterable, param_names, captures); // Register capture name as local so it's not captured param_names.put(fe.capture_name, {}) catch {}; self.collectCaptures(fe.body, param_names, captures); }, .slice_expr => |se| { self.collectCaptures(se.object, param_names, captures); if (se.start) |s| self.collectCaptures(s, param_names, captures); if (se.end) |e| self.collectCaptures(e, param_names, captures); }, .tuple_literal => |tl| { for (tl.elements) |elem| { self.collectCaptures(elem.value, param_names, captures); } }, .force_unwrap => |fu| { self.collectCaptures(fu.operand, param_names, captures); }, .chained_comparison => |cc| { for (cc.operands) |op| { self.collectCaptures(op, param_names, captures); } }, .defer_stmt => |ds| { self.collectCaptures(ds.expr, param_names, captures); }, .ffi_intrinsic_call => |fic| { self.collectCaptures(fic.return_type, param_names, captures); for (fic.args) |arg| { self.collectCaptures(arg, param_names, captures); } }, else => {}, } } /// Compute the byte size of the env struct based on captured value types. fn computeEnvSize(self: *Lowering, capture_list: []const CaptureInfo) usize { // Must match LLVM's struct layout: fields are aligned to their natural alignment var offset: usize = 0; var max_align: usize = 1; for (capture_list) |cap| { const field_size = self.typeSizeBytes(cap.ty); const field_align = self.typeAlignBytes(cap.ty); if (field_align > max_align) max_align = field_align; // Align offset to field alignment offset = (offset + field_align - 1) & ~(field_align - 1); offset += field_size; } // Align total to max field alignment (matches LLVM's struct alignment) return (offset + max_align - 1) & ~(max_align - 1); } /// Byte size of an IR type matching LLVM's type layout. fn typeSizeBytes(self: *Lowering, ty: TypeId) usize { return self.module.types.typeSizeBytes(ty); } fn typeAlignBytes(self: *Lowering, ty: TypeId) usize { return self.module.types.typeAlignBytes(ty); } fn resolveReturnType2(self: *Lowering, rt: ?*const Node) TypeId { if (rt) |r| return type_bridge.resolveAstType(r, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); return .void; } // ── Chained comparison ────────────────────────────────────────── fn lowerChainedComparison(self: *Lowering, cc: *const ast.ChainedComparison) Ref { // a < b < c → (a < b) and (b < c) // Pre-lower all operands so shared ones (e.g., b) aren't evaluated twice. if (cc.operands.len < 2 or cc.ops.len == 0) { return self.builder.constBool(true); } var refs = std.ArrayList(Ref).empty; defer refs.deinit(self.alloc); for (cc.operands) |op| { refs.append(self.alloc, self.lowerExpr(op)) catch unreachable; } var result = self.emitCmp(refs.items[0], refs.items[1], cc.ops[0]); var i: usize = 1; while (i < cc.ops.len) : (i += 1) { const next_cmp = self.emitCmp(refs.items[i], refs.items[i + 1], cc.ops[i]); result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = next_cmp } }, .bool); } return result; } fn emitCmp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.BinaryOp.Op) Ref { return switch (op) { .eq => self.builder.cmpEq(lhs, rhs), .neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool), .lt => self.builder.cmpLt(lhs, rhs), .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool), .gt => self.builder.cmpGt(lhs, rhs), .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool), else => self.builder.constBool(false), }; } // ── Defer/Push/MultiAssign ────────────────────────────────────── /// Pack variadic arguments into a []Any slice. Each arg is boxed as Any {tag, value}, /// stored into a stack-allocated array, and the slice {ptr, len} is bound to param_name. pub fn lowerVariadicArgs(self: *Lowering, param_name: []const u8, call_args: []const *const Node, start_idx: usize) void { const any_slice_ty = self.module.types.sliceOf(.any); const n = if (call_args.len > start_idx) call_args.len - start_idx else 0; if (n == 0) { // Empty slice: {null, 0} const null_ptr = self.builder.constNull(self.module.types.ptrTo(.any)); const zero_len = self.builder.constInt(0, .s64); const slice_slot = self.builder.alloca(any_slice_ty); // Store ptr (field 0) and len (field 1) into the slice alloca const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty); self.builder.store(ptr_gep, null_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); self.builder.store(len_gep, zero_len); if (self.scope) |scope| { scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); } return; } // Allocate stack array [N x Any] const array_ty = self.module.types.arrayOf(.any, @intCast(n)); const array_slot = self.builder.alloca(array_ty); // Box each arg and store into array for (call_args[start_idx..], 0..) |arg, i| { var val = self.lowerExpr(arg); var source_ty = self.inferExprType(arg); // If AST-based inference falls back to .s64 but the lowered ref is a string/struct, use that if (source_ty == .unresolved) { const ref_ty = self.builder.getRefType(val); if (ref_ty == .string or ref_ty == .f32 or ref_ty == .f64 or ref_ty == .bool) { source_ty = ref_ty; } else if (!ref_ty.isBuiltin()) { const ri = self.module.types.get(ref_ty); if (ri == .@"struct" or ri == .slice or ri == .optional or ri == .closure or ri == .tuple) { source_ty = ref_ty; } } } // Auto-unwrap optionals: box inner value if present, else box string "null" if (!source_ty.isBuiltin()) { const opt_info = self.module.types.get(source_ty); if (opt_info == .optional) { const child_ty = opt_info.optional.child; const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool); const some_bb = self.freshBlock("opt.some"); const none_bb = self.freshBlock("opt.none"); const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any}); self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); self.builder.switchToBlock(some_bb); const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); const boxed_inner = self.builder.boxAny(unwrapped, child_ty); self.builder.br(merge_bb, &.{boxed_inner}); self.builder.switchToBlock(none_bb); const null_str_id = self.module.types.internString("null"); const null_str = self.builder.constString(null_str_id); const boxed_null = self.builder.boxAny(null_str, .string); self.builder.br(merge_bb, &.{boxed_null}); self.builder.switchToBlock(merge_bb); val = self.builder.blockParam(merge_bb, 0, TypeId.any); source_ty = .any; } } const boxed = if (source_ty == .any) val else self.builder.boxAny(val, source_ty); // GEP to array[i] and store const idx_ref = self.builder.constInt(@intCast(i), .s64); const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(.any)); self.builder.store(elem_ptr, boxed); } // Build slice {ptr_to_first_element, len} const slice_slot = self.builder.alloca(any_slice_ty); // Get pointer to first element (array_slot is *[N x Any], GEP to element 0 gives *Any) const zero = self.builder.constInt(0, .s64); const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(.any)); const len_ref = self.builder.constInt(@intCast(n), .s64); // Store into slice fields const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty); self.builder.store(ptr_gep, data_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); self.builder.store(len_gep, len_ref); if (self.scope) |scope| { scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); } } /// 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 { // `#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. if (fd.body.data == .foreign_expr and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) { return; } // Find variadic param index. The two surface forms differ in // what `p.type_expr` resolves to: legacy `name: ..T` declares T // (element type), new `..name: []T` declares []T (already a // slice). Unwrap the latter so the per-element packing below // sees T in both cases. var variadic_idx: ?usize = null; var elem_ty: TypeId = .any; for (fd.params, 0..) |p, i| { if (p.is_variadic) { variadic_idx = i; const declared = self.resolveTypeWithBindings(p.type_expr); elem_ty = declared; if (!declared.isBuiltin()) { const info = self.module.types.get(declared); if (info == .slice) elem_ty = info.slice.element; } break; } } const vi = variadic_idx orelse return; // no variadic param // Number of non-variadic args const fixed_count = vi; const variadic_count = if (args.items.len > fixed_count) args.items.len - fixed_count else 0; const slice_ty = self.module.types.sliceOf(elem_ty); // Check for spread operator: sum(..arr) — single spread arg becomes the slice directly if (variadic_count == 1 and fixed_count < c.args.len) { const arg_node = c.args[fixed_count]; if (arg_node.data == .spread_expr) { const spread = arg_node.data.spread_expr; const arr_val = self.lowerExpr(spread.operand); const arr_ty = self.inferExprType(spread.operand); const arr_info = self.module.types.get(arr_ty); // Convert array to slice const slice_val = switch (arr_info) { .array => self.builder.emit(.{ .array_to_slice = .{ .operand = arr_val } }, slice_ty), .slice => arr_val, else => arr_val, }; args.shrinkRetainingCapacity(fixed_count); args.append(self.alloc, slice_val) catch unreachable; return; } } if (variadic_count == 0) { // Empty slice const null_ptr = self.builder.constNull(self.module.types.ptrTo(elem_ty)); const zero_len = self.builder.constInt(0, .s64); const slice_slot = self.builder.alloca(slice_ty); const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty); self.builder.store(ptr_gep, null_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); self.builder.store(len_gep, zero_len); const slice_val = self.builder.load(slice_slot, slice_ty); // Replace args: keep fixed args, append slice args.shrinkRetainingCapacity(fixed_count); args.append(self.alloc, slice_val) catch unreachable; return; } // Determine if we need to box as Any (for ..Any params) or use raw type const is_any = (elem_ty == .any); // `..xs: []P` (slice of a protocol): each concrete arg must be erased to // a protocol value {ctx, vtable}, not stored raw (which would be a // size/type mismatch — a heap of garbage vtables → crash on dispatch). const elem_is_protocol = blk: { if (elem_ty.isBuiltin()) break :blk false; const ei = self.module.types.get(elem_ty); break :blk ei == .@"struct" and ei.@"struct".is_protocol; }; // Allocate stack array [N x ElemType] const array_elem = if (is_any) TypeId.any else elem_ty; const array_ty = self.module.types.arrayOf(array_elem, @intCast(variadic_count)); const array_slot = self.builder.alloca(array_ty); // Store each variadic arg into array for (0..variadic_count) |i| { var val = args.items[fixed_count + i]; if (is_any) { var source_ty = self.inferExprType(c.args[fixed_count + i]); // If AST-based inference falls back to .s64 but the lowered ref has a richer type, use that if (source_ty == .unresolved) { const ref_ty = self.builder.getRefType(val); if (ref_ty != .unresolved and ref_ty != .void) source_ty = ref_ty; } // Auto-unwrap optionals: box inner value if present, else box string "null" if (!source_ty.isBuiltin()) { const opt_info = self.module.types.get(source_ty); if (opt_info == .optional) { const child_ty = opt_info.optional.child; // Branch: has_value? → box inner : box "null" const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool); const some_bb = self.freshBlock("opt.some"); const none_bb = self.freshBlock("opt.none"); const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any}); self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); // Some: unwrap and box inner value self.builder.switchToBlock(some_bb); const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); const boxed_inner = self.builder.boxAny(unwrapped, child_ty); self.builder.br(merge_bb, &.{boxed_inner}); // None: box the string "null" self.builder.switchToBlock(none_bb); const null_str_id = self.module.types.internString("null"); const null_str = self.builder.constString(null_str_id); const boxed_null = self.builder.boxAny(null_str, .string); self.builder.br(merge_bb, &.{boxed_null}); // Merge self.builder.switchToBlock(merge_bb); val = self.builder.blockParam(merge_bb, 0, TypeId.any); source_ty = .any; // already boxed } } if (source_ty != .any) { val = self.builder.boxAny(val, source_ty); } } else if (elem_is_protocol) { // Erase each concrete arg to the protocol value via the same // impl-driven `xx` machinery, so the runtime `[]P` holds real // {ctx, vtable} values and `xs[i].method()` dispatches. const arg_node = c.args[fixed_count + i]; var source_ty = self.inferExprType(arg_node); if (source_ty == .unresolved) source_ty = self.builder.getRefType(val); if (source_ty != elem_ty) { val = self.buildProtocolErasure(val, arg_node, source_ty, elem_ty); } } const idx_ref = self.builder.constInt(@intCast(i), .s64); const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(array_elem)); self.builder.store(elem_ptr, val); } // Build slice {ptr, len} const slice_slot = self.builder.alloca(slice_ty); const zero = self.builder.constInt(0, .s64); const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(array_elem)); const len_ref = self.builder.constInt(@intCast(variadic_count), .s64); const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(array_elem), slice_ty); self.builder.store(ptr_gep, data_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); self.builder.store(len_gep, len_ref); const slice_val = self.builder.load(slice_slot, slice_ty); // Replace args: keep fixed args, append slice args.shrinkRetainingCapacity(fixed_count); args.append(self.alloc, slice_val) catch unreachable; } // ── 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] /// array; the slice {data_ptr, len} is then bound. Used by /// `monomorphizePackFn` so bodies that reference `args` bare or /// index it with a runtime int resolve through the slice (with /// element type `Any`). Literal-indexed accesses keep the /// concrete per-position types via `packArgNodeAt`. /// Build a `[]Type` slice VALUE for a bare `$` reference. /// Differs from `materialisePackSlice` (which boxes each pack /// element as Any so the body's `args[i]` reads an Any) — this /// helper stores raw `.type_tag` Values via `const_type`, so the /// slice is a list-of-Types that builder fns walk at interp time. /// Slice IR type is `[]Any` (since `Type → .any`); the interp /// stores whichever Value the elements actually carry. fn buildPackSliceValue(self: *Lowering, arg_types: []const TypeId) Ref { const any_slice_ty = self.module.types.sliceOf(.any); const any_ptr_ty = self.module.types.ptrTo(.any); if (arg_types.len == 0) { const null_ptr = self.builder.constNull(any_ptr_ty); const zero_len = self.builder.constInt(0, .s64); const slice_slot = self.builder.alloca(any_slice_ty); const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); self.builder.store(ptr_gep, null_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); self.builder.store(len_gep, zero_len); return self.builder.load(slice_slot, any_slice_ty); } const array_ty = self.module.types.arrayOf(.any, @intCast(arg_types.len)); const array_slot = self.builder.alloca(array_ty); for (arg_types, 0..) |ty, i| { // `const_type` produces an `.any`-typed Type value // (`{tag=.any, value=tid}`) — already the canonical Any // shape, so no re-box needed. const type_val = self.builder.constType(ty); const idx_ref = self.builder.constInt(@intCast(i), .s64); const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, any_ptr_ty); self.builder.store(elem_ptr, type_val); } const slice_slot = self.builder.alloca(any_slice_ty); const zero = self.builder.constInt(0, .s64); const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, any_ptr_ty); const len_ref = self.builder.constInt(@intCast(arg_types.len), .s64); const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); self.builder.store(ptr_gep, data_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); self.builder.store(len_gep, len_ref); return self.builder.load(slice_slot, any_slice_ty); } fn materialisePackSlice( self: *Lowering, scope: *Scope, pack_name: []const u8, slot_refs: []const Ref, arg_types: []const TypeId, ) void { const any_slice_ty = self.module.types.sliceOf(.any); const any_ptr_ty = self.module.types.ptrTo(.any); if (arg_types.len == 0) { const null_ptr = self.builder.constNull(any_ptr_ty); const zero_len = self.builder.constInt(0, .s64); const slice_slot = self.builder.alloca(any_slice_ty); const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); self.builder.store(ptr_gep, null_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); self.builder.store(len_gep, zero_len); scope.put(pack_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); return; } const array_ty = self.module.types.arrayOf(.any, @intCast(arg_types.len)); const array_slot = self.builder.alloca(array_ty); for (slot_refs, arg_types, 0..) |slot, ty, i| { const val = self.builder.load(slot, ty); const boxed = if (ty == .any) val else self.builder.boxAny(val, ty); const idx_ref = self.builder.constInt(@intCast(i), .s64); const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, any_ptr_ty); self.builder.store(elem_ptr, boxed); } const slice_slot = self.builder.alloca(any_slice_ty); const zero = self.builder.constInt(0, .s64); const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, any_ptr_ty); const len_ref = self.builder.constInt(@intCast(arg_types.len), .s64); const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); self.builder.store(ptr_gep, data_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); self.builder.store(len_gep, len_ref); scope.put(pack_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); } /// Infer the return type of a pack-fn body for the generic-`$R` /// case. Walks the body looking for the first concrete return /// type: a `return X;` statement's value type, or — failing that — /// the tail expression of an arrow-form body. Caller must have /// `pack_arg_nodes` installed so `args[]` substitutes during /// inference. Falls back to `.s64` if nothing concrete is found /// (matches the broader "default to .s64" convention elsewhere). fn inferPackBodyReturnType(self: *Lowering, body: *const Node) TypeId { // First try explicit `return X;` — walks past structured // control flow but stops at nested fn / lambda bodies. if (self.findReturnValueType(body)) |ty| return ty; // Arrow-form / tail-expression body: the body IS the value. // For block bodies whose last stmt is an expression, walk down. if (body.data == .block) { const stmts = body.data.block.stmts; if (stmts.len == 0) return .void; return self.inferExprType(stmts[stmts.len - 1]); } return self.inferExprType(body); } /// Per-call-shape monomorphisation entry for pack-fns /// (`isPackFn(fd) == true`). Computes a mangled name from the /// call-site arg types, builds the mono if it's not cached, and /// 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 { // Split call args along the fd.params boundary: // - non-comptime non-pack params → consume one call arg as a // runtime IR param. // - comptime non-pack params → consume one call arg, fold its // value into the mangle (NOT a runtime IR param). // - pack param (always last) → consume the remaining call args // as the pack expansion. var pack_arg_types = std.ArrayList(TypeId).empty; defer pack_arg_types.deinit(self.alloc); var pack_start: usize = call_node.args.len; // Constraint protocol of the pack param (`..xs: P`), if any. The // comptime type-pack `..$args` has no constraint to check. var pack_protocol: ?[]const u8 = null; var pack_is_comptime = false; var pack_name: []const u8 = ""; { var fi: usize = 0; for (fd.params) |p| { if (isPackParam(p)) { pack_start = fi; pack_is_comptime = p.is_comptime; pack_name = p.name; if (p.is_pack and p.type_expr.data == .type_expr) { pack_protocol = p.type_expr.data.type_expr.name; } break; } if (fi >= call_node.args.len) break; fi += 1; } } // Lower the PACK args first, taking each type from the lowered value // (`getRefType`) — never a pre-lowering `inferExprType` guess. Knowing // the pack element types up front lets the prefix args (e.g. // `mapper: Closure(..sources.T) -> $R`) resolve against them, so a // lambda arg types its params from the projected closure signature. // (A comptime `..$args` pack keeps `inferExprType` — its args may be // type-position.) // A pack arg is independently typed — it takes its natural type and // (for a comptime `..$args` pack) auto-boxes to `Any` at the call // boundary. It is NEVER coerced to a leftover outer `target_type`, so // clear it: otherwise an `xx ` pack arg (whose result type IS // `target_type`) would cast to the stale target — e.g. `format("…", xx i)` // inside a `-> string` fn mis-typed the arg as `string`, monomorphizing // `__pack_string` and ABI-coercing the 4-byte int as a 16-byte fat // pointer → memory corruption (issue 0057). const saved_pack_tt = self.target_type; self.target_type = null; var pack_refs = std.ArrayList(Ref).empty; defer pack_refs.deinit(self.alloc); for (call_node.args[pack_start..]) |a| { const r = self.lowerExpr(a); pack_refs.append(self.alloc, r) catch return self.builder.constInt(0, .void); if (pack_is_comptime) { const it = self.inferExprType(a); pack_arg_types.append(self.alloc, if (it == .unresolved) self.builder.getRefType(r) else it) catch return self.builder.constInt(0, .void); } else { pack_arg_types.append(self.alloc, self.builder.getRefType(r)) catch return self.builder.constInt(0, .void); } } self.target_type = saved_pack_tt; // Install the pack's element types + constraint so prefix-arg param // types like `Closure(..sources.T)` resolve while lowering the prefix. var pat_map = std.StringHashMap([]const TypeId).init(self.alloc); defer pat_map.deinit(); pat_map.put(pack_name, pack_arg_types.items) catch {}; var pcon_map = std.StringHashMap([]const u8).init(self.alloc); defer pcon_map.deinit(); if (pack_protocol) |proto| pcon_map.put(pack_name, proto) catch {}; const saved_pat = self.pack_arg_types; const saved_pcon = self.pack_constraint; self.pack_arg_types = pat_map; if (pack_protocol != null) self.pack_constraint = pcon_map; var args = std.ArrayList(Ref).empty; defer args.deinit(self.alloc); { var ri: usize = 0; for (fd.params) |p| { if (isPackParam(p)) break; if (ri >= call_node.args.len) break; if (!p.is_comptime) { // Contextually type the arg from the param (so a lambda arg // `(x) => …` takes its param types from a `Closure(...)` param). // The param type is resolved under the pack fn's OWN source // (E4): a fixed-prefix type bare-visible only in the defining // module must resolve there, not the caller's. The arg itself // is lowered AFTER, in the caller's context. const saved_tt = self.target_type; const pty = self.resolveParamTypeInSource(fd.body.source_file, &p); if (pty != .unresolved) self.target_type = pty; args.append(self.alloc, self.lowerExpr(call_node.args[ri])) catch return self.builder.constInt(0, .void); self.target_type = saved_tt; } ri += 1; } } self.pack_arg_types = saved_pat; self.pack_constraint = saved_pcon; // Infer type-param bindings (e.g. `$R` in `mapper: Closure(..) -> $R`) // from the lowered prefix args. `args.items` holds the non-comptime // prefix refs in declaration order; match each prefix param's declared // type against its arg's concrete type to bind the function's // type-params. These flow into the mangle and the mono's // `self.type_bindings` so `-> VL($R)` / `Combined($R, ..)` resolve. var tparam_bindings = std.StringHashMap(TypeId).init(self.alloc); defer tparam_bindings.deinit(); if (fd.type_params.len > 0) { var pref_ref_idx: usize = 0; for (fd.params) |p| { if (isPackParam(p)) break; if (p.is_comptime) continue; if (pref_ref_idx >= args.items.len) break; const arg_ty = self.builder.getRefType(args.items[pref_ref_idx]); for (fd.type_params) |tp| { if (tparam_bindings.contains(tp.name)) continue; if (self.extractTypeParam(p.type_expr, arg_ty, tp.name)) |ety| { if (ety != .unresolved) tparam_bindings.put(tp.name, ety) catch {}; } } pref_ref_idx += 1; } } // Append the (already-lowered) pack args after the prefix args. for (pack_refs.items) |r| args.append(self.alloc, r) catch return self.builder.constInt(0, .void); // Per-position conformance: each pack arg must impl the constraint // protocol. Only enforced for a known protocol constraint — an unknown // name (e.g. a plain type used as a pack constraint) is left alone. if (pack_protocol) |proto| { if (self.program_index.protocol_ast_map.contains(proto)) { for (call_node.args[pack_start..], pack_arg_types.items) |arg_node, arg_ty| { if (!self.protocolResolver().packArgConformsTo(proto, arg_ty)) { if (self.diagnostics) |diags| { diags.addFmt(.err, arg_node.span, "pack argument of type '{s}' does not conform to protocol '{s}'", .{ self.formatTypeName(arg_ty), proto }); } } } } } // Mangle: `__pack__` with comptime values // (if any) folded into a `__ct_` segment per non-pack // comptime param. Distinct call shapes — including different // comptime VALUES — get distinct symbols. var name_buf = std.ArrayList(u8).empty; defer name_buf.deinit(self.alloc); name_buf.appendSlice(self.alloc, fd.name) catch return self.builder.constInt(0, .void); // Comptime values first (deterministic by fd.params order). var ct_fi: usize = 0; for (fd.params) |p| { if (isPackParam(p)) break; if (ct_fi >= call_node.args.len) break; if (p.is_comptime) { name_buf.appendSlice(self.alloc, "__ct_") catch return self.builder.constInt(0, .void); self.genericResolver().appendComptimeValueMangle(&name_buf, call_node.args[ct_fi]); } ct_fi += 1; } // Inferred type-param bindings (deterministic by fd.type_params order). for (fd.type_params) |tp| { if (tparam_bindings.get(tp.name)) |ty| { name_buf.appendSlice(self.alloc, "__tp_") catch return self.builder.constInt(0, .void); name_buf.appendSlice(self.alloc, self.mangleTypeName(ty)) catch return self.builder.constInt(0, .void); } } name_buf.appendSlice(self.alloc, "__pack") catch return self.builder.constInt(0, .void); for (pack_arg_types.items) |t| { name_buf.append(self.alloc, '_') catch return self.builder.constInt(0, .void); name_buf.appendSlice(self.alloc, self.mangleTypeName(t)) catch return self.builder.constInt(0, .void); } const mangled = name_buf.items; if (!self.lowered_functions.contains(mangled)) { self.monomorphizePackFn(fd, mangled, pack_arg_types.items, call_node, &tparam_bindings); } const fid = self.resolveFuncByName(mangled) orelse return self.builder.constInt(0, .void); 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); } /// Build a single mono fn for the given pack-fn + concrete arg types. /// The mono carries N positional pack-params (synthesised names /// `__pack__`) plus any fixed-prefix non-pack params from /// the original declaration. The body lowers normally — real /// `return X;` emits real `ret X`; `args[]` substitutes via /// `pack_arg_nodes`; `args.len` resolves via `pack_param_count`. fn monomorphizePackFn( self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, arg_types: []const TypeId, call_node: *const ast.Call, type_bindings: *const std.StringHashMap(TypeId), ) void { const owned_name = self.alloc.dupe(u8, mangled_name) catch return; self.lowered_functions.put(owned_name, {}) catch {}; // Find the pack param's name and position in fd.params, plus its // constraint protocol (`..xs: Box` ⇒ "Box"; comptime `..$args` has none). var pack_name: []const u8 = ""; var pack_param_idx: usize = std.math.maxInt(usize); var pack_proto: ?[]const u8 = null; for (fd.params, 0..) |p, i| { if (isPackParam(p)) { pack_name = p.name; pack_param_idx = i; if (p.is_pack and p.type_expr.data == .type_expr) { pack_proto = p.type_expr.data.type_expr.name; } break; } } if (pack_param_idx == std.math.maxInt(usize)) return; // Save state — mirrors monomorphizeFunction but also captures // pack/inline-return state since the mono body must NOT route // returns through any caller's inline slot. const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const saved_scope = self.scope; const saved_defer_base = self.func_defer_base; const saved_block_terminated = self.block_terminated; const saved_target = self.target_type; const saved_pan = self.pack_arg_nodes; const saved_ppc = self.pack_param_count; const saved_pat = self.pack_arg_types; const saved_pcon = self.pack_constraint; const saved_iri = self.inline_return_target; const saved_ctx_ref = self.current_ctx_ref; const saved_type_bindings = self.type_bindings; self.func_defer_base = self.defer_stack.items.len; self.block_terminated = false; self.inline_return_target = null; // Generic type-params inferred at the call site (e.g. `$R` from the // mapper's closure return). Installed for the whole mono so // return-type resolution and body lowering substitute them. self.type_bindings = type_bindings.*; defer { self.type_bindings = saved_type_bindings; self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.block_terminated = saved_block_terminated; self.target_type = saved_target; self.pack_arg_nodes = saved_pan; self.pack_param_count = saved_ppc; self.pack_arg_types = saved_pat; self.pack_constraint = saved_pcon; self.inline_return_target = saved_iri; self.current_ctx_ref = saved_ctx_ref; self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } const wants_ctx = self.funcWantsImplicitCtx(fd); // Synthesise pack-param names + AST ident nodes used to bind // `args[]` substitutions during body lowering. Built // BEFORE return-type resolution so the generic-`$R` path can // pre-install the binding for type inference. var pack_synth_names = std.ArrayList([]const u8).empty; defer pack_synth_names.deinit(self.alloc); var pack_arg_idents = std.ArrayList(*const Node).empty; defer pack_arg_idents.deinit(self.alloc); for (arg_types, 0..) |_, i| { const synth_name = std.fmt.allocPrint(self.alloc, "__pack_{s}_{d}", .{ pack_name, i }) catch return; pack_synth_names.append(self.alloc, synth_name) catch return; const ident_node = self.alloc.create(Node) catch return; ident_node.* = .{ .span = fd.body.span, .data = .{ .identifier = .{ .name = synth_name } }, }; pack_arg_idents.append(self.alloc, ident_node) catch return; } // Resolve return type. When the declared type is a generic // name (e.g. `(..$args) -> $R`), `resolveReturnType` would // return an opaque struct TypeId and the mono's signature // would be wrong. Pre-install the pack bindings + infer the // ret type from the body's tail expression / first explicit // `return X;` instead. var pre_pan = std.StringHashMap([]const *const Node).init(self.alloc); defer pre_pan.deinit(); pre_pan.put(pack_name, pack_arg_idents.items) catch return; var pre_ppc = std.StringHashMap(u32).init(self.alloc); defer pre_ppc.deinit(); pre_ppc.put(pack_name, @intCast(arg_types.len)) catch return; var pre_pat = std.StringHashMap([]const TypeId).init(self.alloc); defer pre_pat.deinit(); pre_pat.put(pack_name, arg_types) catch return; var pre_pcon = std.StringHashMap([]const u8).init(self.alloc); defer pre_pcon.deinit(); if (pack_proto) |proto| pre_pcon.put(pack_name, proto) catch return; self.pack_arg_nodes = pre_pan; self.pack_param_count = pre_ppc; self.pack_arg_types = pre_pat; self.pack_constraint = if (pack_proto != null) pre_pcon else null; // Resolve the declared return + fixed-prefix param types in the pack fn's // OWN module (E4), so a 2-flat-hop library type named in the signature is // bare-visible — mirrors the body pin further down and the // `monomorphizeFunction` pin. The comptime call-site args below are // lowered AFTER this restore, in the caller's context (issue 0106). const saved_sig_src = self.current_source_file; if (fd.body.source_file) |src| self.setCurrentSourceFile(src); const declared_is_generic_ret = blk: { const rt = fd.return_type orelse break :blk false; if (rt.data != .type_expr) break :blk false; break :blk rt.data.type_expr.is_generic; }; const ret_ty: TypeId = if (declared_is_generic_ret) self.inferPackBodyReturnType(fd.body) else self.resolveReturnType(fd); self.target_type = ret_ty; // Param list: ctx (if needed) + fixed prefix + N pack params. // Comptime non-pack params are NOT in the runtime signature — // their values are folded into the mangle and substituted via // `comptime_param_nodes` / bound as runtime locals in scope. // NOT deinit'd — `params.items` is stored by reference in // `Function.init` and read back later via `func.params`. var params = std.ArrayList(Function.Param).empty; if (wants_ctx) { params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = self.module.types.ptrTo(.void), }) catch return; } for (fd.params, 0..) |p, i| { if (i == pack_param_idx) continue; if (p.is_comptime) continue; // folded into mangle, not in IR const pty = self.resolveParamType(&p); params.append(self.alloc, .{ .name = self.module.types.internString(p.name), .ty = pty, }) catch return; } for (arg_types, 0..) |ty, i| { params.append(self.alloc, .{ .name = self.module.types.internString(pack_synth_names.items[i]), .ty = ty, }) catch return; } self.setCurrentSourceFile(saved_sig_src); const name_id = self.module.types.internString(owned_name); _ = self.builder.beginFunction(name_id, params.items, ret_ty); self.builder.currentFunc().has_implicit_ctx = wants_ctx; const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); var scope = Scope.init(self.alloc, null); defer scope.deinit(); self.scope = &scope; // Bind non-pack params. Walk fd.params + call_node.args // together; comptime non-pack params bind both as runtime // locals (so bare-name body access works) AND as // comptime_param_nodes entries (so `#insert` substitution // works). Non-comptime non-pack params consume IR param // slots in order. var cpn = std.StringHashMap(*const Node).init(self.alloc); defer cpn.deinit(); var param_idx: u32 = if (wants_ctx) 1 else 0; var ct_arg_idx: usize = 0; for (fd.params, 0..) |p, i| { if (i == pack_param_idx) break; if (p.is_comptime) { if (ct_arg_idx < call_node.args.len) { const call_arg = call_node.args[ct_arg_idx]; self.stampCallerSource(call_arg); cpn.put(p.name, call_arg) catch return; // Bind as a runtime local for bare-name access. // Lower the call arg as a value, then alloca + store. const val = self.lowerExpr(call_arg); const val_ty = self.builder.getRefType(val); const slot = self.builder.alloca(val_ty); self.builder.store(slot, val); scope.put(p.name, .{ .ref = slot, .ty = val_ty, .is_alloca = true }); } ct_arg_idx += 1; continue; } // Pin to the pack fn's OWN module (E4): a fixed-prefix param whose // type is bare-visible only in the defining module must resolve // there, not in the caller's restored context. Mirrors the // signature build above and `resolveParamTypeInSource` at the // cross-module call-arg typing sites. const pty = self.resolveParamTypeInSource(fd.body.source_file, &p); const slot = self.builder.alloca(pty); self.builder.store(slot, Ref.fromIndex(param_idx)); scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); param_idx += 1; ct_arg_idx += 1; } // Install comptime_param_nodes for the body lowering. const saved_cpn = self.comptime_param_nodes; self.comptime_param_nodes = cpn; defer self.comptime_param_nodes = saved_cpn; var pack_param_slots = std.ArrayList(Ref).empty; defer pack_param_slots.deinit(self.alloc); for (arg_types, 0..) |ty, i| { const synth_name = pack_synth_names.items[i]; const slot = self.builder.alloca(ty); self.builder.store(slot, Ref.fromIndex(param_idx)); scope.put(synth_name, .{ .ref = slot, .ty = ty, .is_alloca = true }); pack_param_slots.append(self.alloc, slot) catch return; param_idx += 1; } // Pack bindings remain installed from the pre-resolution // (generic-`$R`) inference step above. No need to reinstall. // Materialise an `[]Any` slice value for the pack name so // bare `args` (forwarding) and `args[]` (loops) // resolve at runtime. Per-position type info is lost via // Any boxing — that's the inherent cost of treating a // heterogeneous pack as a uniform value. Literal-indexed // access still goes through `packArgNodeAt` and keeps the // concrete per-position types. self.materialisePackSlice(&scope, pack_name, pack_param_slots.items, arg_types); // Pin to the metaprogram's OWN module for the BODY lowering only, so its // bare names (and anything it `#insert`s — e.g. `build_format` / `out` / // `emit` inside `std.print`) resolve in the defining module's visibility // context, not the call site's (issue 0106). The comptime-param call-site // args above were deliberately lowered FIRST, in the caller's context. // Mirrors `lowerFunctionBodyInto`, which switches to `func.source_file`; // the defining path is stamped on the body node by `resolveImports`. A // synthesized/sourceless body keeps the caller's context. const saved_source = self.current_source_file; defer self.setCurrentSourceFile(saved_source); if (fd.body.source_file) |src| self.setCurrentSourceFile(src); if (ret_ty != .void) { const body_val = self.lowerBlockValue(fd.body); if (!self.currentBlockHasTerminator()) { if (body_val) |val| { const val_ty = self.builder.getRefType(val); const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; self.builder.ret(coerced, ret_ty); } else { self.ensureTerminator(ret_ty); } } } else { self.lowerBlock(fd.body); self.ensureTerminator(ret_ty); } self.builder.finalize(); } fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, bindings: *std.StringHashMap(TypeId)) void { // Mark as lowered before lowering (prevents infinite recursion) // Need to dupe the name since mangled_name may be stack-allocated const owned_name = self.alloc.dupe(u8, mangled_name) catch return; self.lowered_functions.put(owned_name, {}) catch {}; // Save builder state const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const saved_scope = self.scope; const saved_bindings = self.type_bindings; const saved_defer_base = self.func_defer_base; const saved_block_terminated = self.block_terminated; const saved_target = self.target_type; // Pack-fn mono state is lexical to the pack-fn body. A generic // function called from inside a pack-fn mono (e.g. // `build(args: []Type, $ret: Type)` invoked from // `probe(..$args) { build($args, void) }`) must not inherit the // caller's pack maps — `lowerFieldAccess`'s `.len` // intercept would otherwise constant-fold the callee's // same-named param to whichever shape triggered the first mono // and bake the wrong arity into the cached IR. Same shape of // fix as `lazyLowerFunction` (issue-0048, commit 0ede097). const saved_pan = self.pack_arg_nodes; const saved_ppc = self.pack_param_count; const saved_pat = self.pack_arg_types; const saved_iri = self.inline_return_target; self.pack_arg_nodes = null; self.pack_param_count = null; self.pack_arg_types = null; self.inline_return_target = null; defer { self.pack_arg_nodes = saved_pan; self.pack_param_count = saved_ppc; self.pack_arg_types = saved_pat; self.inline_return_target = saved_iri; } self.func_defer_base = self.defer_stack.items.len; self.block_terminated = false; // Install type bindings self.type_bindings = bindings.*; // Pin to the template's defining module for the whole monomorphization // (return type, param types, body), so a library-internal bare TYPE ref // — e.g. `List(T).append`'s `alloc: Allocator` default-param type, or a // body reference to a type visible only in the template's module — // resolves where it is visible, not at the (possibly cross-module) call // site. This is the issue-0100-F1 plain-fn pin extended to generic // instantiation; without it the non-transitive bare-TYPE gate (E4) would // reject a 2-flat-hop library type the call site cannot see directly. // A synthesized / sourceless body keeps the caller's context. const saved_source_mono = self.current_source_file; defer self.setCurrentSourceFile(saved_source_mono); if (fd.body.source_file) |src| self.setCurrentSourceFile(src); // Resolve return type with type bindings active. The body's tail // expression inherits this as its target_type so bare `.{...}` // literals resolve to the monomorphised return type instead of // whatever leaked in from the caller (e.g. caller's xx target). const ret_ty = self.resolveReturnType(fd); self.target_type = ret_ty; const wants_ctx = self.funcWantsImplicitCtx(fd); const saved_ctx_ref_mono = self.current_ctx_ref; defer self.current_ctx_ref = saved_ctx_ref_mono; // Build param list (substituting type params, skipping type param declarations). // Prepend `__sx_ctx: *void` at slot 0 if the function gets the implicit param. var params = std.ArrayList(Function.Param).empty; if (wants_ctx) { params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = self.module.types.ptrTo(.void), }) catch unreachable; } for (fd.params) |p| { if (isTypeParamDecl(&p, fd.type_params)) continue; const pty = self.resolveParamType(&p); params.append(self.alloc, .{ .name = self.module.types.internString(p.name), .ty = pty, }) catch unreachable; } // Create the monomorphized function const name_id = self.module.types.internString(owned_name); const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); _ = func_id; self.builder.currentFunc().has_implicit_ctx = wants_ctx; // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); // Create scope and bind params var scope = Scope.init(self.alloc, null); defer scope.deinit(); self.scope = &scope; { var param_idx: u32 = if (wants_ctx) 1 else 0; for (fd.params) |p| { if (isTypeParamDecl(&p, fd.type_params)) continue; const pty = self.resolveParamType(&p); const slot = self.builder.alloca(pty); const param_ref = Ref.fromIndex(param_idx); self.builder.store(slot, param_ref); scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); param_idx += 1; } } // Handle builtin function bodies (e.g. #builtin sqrt monomorphized to sqrt__f32) if (fd.body.data == .builtin_expr) { // Emit builtin call with param 0, then return if (resolveBuiltin(fd.name)) |bid| { const param0 = Ref.fromIndex(0); const result = self.builder.callBuiltin(bid, &.{param0}, ret_ty); self.builder.ret(result, ret_ty); } else { self.ensureTerminator(ret_ty); } self.builder.finalize(); } else { // Lower the function body if (ret_ty != .void) { const body_val = self.lowerBlockValue(fd.body); if (!self.currentBlockHasTerminator()) { if (body_val) |val| { const val_ty = self.builder.getRefType(val); const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; self.builder.ret(coerced, ret_ty); } else { self.ensureTerminator(ret_ty); } } } else { self.lowerBlock(fd.body); self.ensureTerminator(ret_ty); } self.builder.finalize(); } // Restore builder state self.type_bindings = saved_bindings; self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.block_terminated = saved_block_terminated; self.target_type = saved_target; self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } // ── 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) /// - type_expr AST nodes /// True iff `node` matches an AST shape that `resolveTypeArg` /// can resolve to a concrete TypeId without falling through to /// the silent `.s64` default. Used by `tryLowerReflectionCall` /// to split static-fold from dynamic-builtin-call paths. /// /// Static-arg shapes mirror the explicit arms of `resolveTypeArg`: /// - type_expr / identifier (type name or bound generic) /// - pack_index_type_expr (`$pack[]`) /// - compound type literals (pointer, array, slice, optional, /// many_pointer, function_type_expr) /// - parameterised type-constructor `call` (Vector, List, etc.) /// - tuple_literal as a tuple TYPE /// /// 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 { switch (node.data) { .type_expr => |te| { // A type-keyword name (e.g. `s64`) is always static. // A user-defined name that happens to be in scope as // a runtime variable (`x: Type = s64; type_name(x)`) // is NOT static — route through the dynamic builtin // call so the runtime lookup table fires. if (self.scope) |scope| { if (scope.lookup(te.name) != null) return false; } return true; }, .identifier => |id| { if (self.scope) |scope| { if (scope.lookup(id.name) != null) return false; } return true; }, .pack_index_type_expr, .pointer_type_expr, .many_pointer_type_expr, .array_type_expr, .slice_type_expr, .optional_type_expr, .function_type_expr, .tuple_literal, .call, => return true, else => return false, } } /// True iff `node` is a Type-shaped expression that resolves to a /// concrete TypeId at lower time WITHOUT being a runtime variable /// reference. Differs from `isStaticTypeArg` in that we exclude /// identifiers that are in scope as runtime locals/globals — those /// are runtime Type values (e.g. `t: Type = f64`) and the /// comparison fold can't statically resolve them. fn isStaticTypeRef(self: *Lowering, node: *const Node) bool { switch (node.data) { .type_expr => |te| { // Compound type names (`s64`, `Point`, `Vec4`) resolve // statically. If the name is also a runtime var in // scope, it's a value reference, not a type ref. if (self.scope) |scope| { if (scope.lookup(te.name) != null) return false; } return self.isKnownTypeName(te.name) or self.module.types.findByName(self.module.types.internString(te.name)) != null or self.program_index.type_alias_map.get(te.name) != null; }, .identifier => |id| { if (self.scope) |scope| { if (scope.lookup(id.name) != null) return false; } return self.isKnownTypeName(id.name) or self.module.types.findByName(self.module.types.internString(id.name)) != null or self.program_index.type_alias_map.get(id.name) != null; }, .pointer_type_expr, .many_pointer_type_expr, .array_type_expr, .slice_type_expr, .optional_type_expr, .function_type_expr, .pack_index_type_expr, => return true, .call => |cl| { // `type_of(x)` resolves statically when `x`'s type is // known — which it always is for a typed expression. if (cl.callee.data == .identifier and std.mem.eql(u8, cl.callee.data.identifier.name, "type_of") and cl.args.len == 1) { return true; } return false; }, else => return false, } } /// Resolve a tuple LITERAL used in a type position (`(s32, s32)` reinterpreted /// as a tuple type at a type-demanding site such as `size_of`). Every element /// must itself denote a type; a non-type element — e.g. the `1` in /// `(s32, 1)` — is a user error. Emit a diagnostic pointing at the offending /// element and return `.unresolved`; never fabricate a tuple with a bogus /// field (issue 0067). type_bridge.resolveAstType builds the tuple only after /// this validation passes. fn resolveTupleLiteralTypeArg(self: *Lowering, node: *const Node) TypeId { for (node.data.tuple_literal.elements) |el| { if (!type_bridge.isTypeShapedAstNode(el.value, &self.module.types)) { if (self.diagnostics) |diags| { diags.addFmt(.err, el.value.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `(s32, s32)`", .{@tagName(el.value.data)}); } return .unresolved; } // E4 single-hop visibility gate: each element leaf is resolved through // the source-aware resolver, so a 2-flat-hop inner leaf (`(COnly, s64)`) // emits "not visible" + poisons rather than leaking through // `type_bridge`'s ungated global lookup. A valid element resolves to the // same TypeId the delegated build produces below (no diagnostic, no // drift); only the poison short-circuits. if (self.resolveTypeWithBindings(el.value) == .unresolved) return .unresolved; } return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId { // Pack-index access in a type-arg slot (e.g. `type_name($args[0])` // or `type_eq($args[i], s64)`). Same shape as the // `resolveTypeWithBindings` arm — looks up the bound pack types // and returns the i-th. OOB and no-active-binding emit focused // diagnostics rather than silently defaulting to .s64 (the // catch-all `else` below) — that fall-through is exactly the // "silent unimplemented arm" the project's REJECTED PATTERNS // forbid. if (node.data == .pack_index_type_expr) { const pi = node.data.pack_index_type_expr; if (self.pack_arg_types) |pat| { if (pat.get(pi.pack_name)) |arg_tys| { if (pi.index < arg_tys.len) return arg_tys[pi.index]; if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index ${s}[{}] out of bounds: '{s}' has {} element{s}", .{ pi.pack_name, pi.index, pi.pack_name, arg_tys.len, if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"), }); } return .unresolved; } } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index ${s}[{}] used outside an active pack binding", .{ pi.pack_name, pi.index, }); } return .unresolved; } // Bare `$` in a type-arg position. Single-type generic // bindings (`$R: Type` in `Closure(..$args) -> $R`) live in // `type_bindings`; if the name is bound there, return the // bound TypeId directly. Pack bindings would otherwise resolve // to a slice value, not a single Type — the caller (e.g. // `type_name(...)`) expects a single arg. if (node.data == .comptime_pack_ref) { const cpr = node.data.comptime_pack_ref; if (self.type_bindings) |tb| { if (tb.get(cpr.pack_name)) |ty| return ty; } } switch (node.data) { .identifier => |id| { // Check type bindings first (from generic monomorphization) if (self.type_bindings) |tb| { if (tb.get(id.name)) |ty| return ty; } // E4 single-hop visibility + ambiguity gate: a bare type name // reachable only over 2+ flat hops is not bare-visible in a // reflection / type-arg slot (consistent with normal annotations / // 0763); ≥2 direct flat same-name authors are ambiguous (loud // diagnostic, consistent with the leaf / 0755) instead of a global // first-/last-wins pick; a single source-keyed author resolves to // ITS TypeId. A genuinely-undeclared name is NOT authored as a type // anywhere → `.proceed`, falling to the "unresolved type" // diagnostic below. switch (self.headTypeGate(id.name, node.span)) { .ambiguous, .not_visible => return .unresolved, .resolved => |tid| return tid, .proceed => {}, } if (self.program_index.type_alias_map.get(id.name)) |alias_ty| return alias_ty; const name_id = self.module.types.internString(id.name); if (self.module.types.findByName(name_id)) |t| return t; if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "unresolved type: '{s}'", .{id.name}); } return .unresolved; }, .type_expr => |te| { if (self.headTypeLeak(te.name, node.span)) return .unresolved; if (self.program_index.type_alias_map.get(te.name)) |alias_ty| return alias_ty; return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); }, .call => |cl| { // `type_of(x)` resolves to `inferExprType(x)` at lower // time when `x`'s type is statically known (which it // is for any expression — type inference always // produces a concrete TypeId). Lets // `type_of(a) == s64` fold the same as // `inferExprType(a) == s64`. if (cl.callee.data == .identifier and std.mem.eql(u8, cl.callee.data.identifier.name, "type_of") and cl.args.len == 1) { return self.inferExprType(cl.args[0]); } // Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32)) return self.resolveTypeCallWithBindings(&cl); }, // Wrapped / structural forms (`*T`, `[N]T`, `[]T`, `?T`, fn-ptr, tuple) // route through the gated `resolveTypeWithBindings`, whose // `resolveCompound` recurses each element through the source-aware leaf // (`resolveNominalLeaf`) — so a 2-hop inner leaf (`*COnly`, `[2]COnly`, // `(COnly, s64)`) is rejected exactly as in a normal annotation, instead // of `type_bridge.resolveAstType`'s ungated global lookup (E4). .tuple_literal, .pointer_type_expr, .many_pointer_type_expr, .array_type_expr, .slice_type_expr, .optional_type_expr, .function_type_expr, => return self.resolveTypeWithBindings(node), else => return .unresolved, } } /// Format a type name for display (e.g. "*Point", "[]s32", "[3]f64"). pub fn formatTypeName(self: *Lowering, ty: TypeId) []const u8 { // Builtin types: use their canonical name if (ty == .s8) return "s8"; if (ty == .s16) return "s16"; if (ty == .s32) return "s32"; if (ty == .s64) return "s64"; if (ty == .u8) return "u8"; if (ty == .u16) return "u16"; if (ty == .u32) return "u32"; if (ty == .u64) return "u64"; if (ty == .f32) return "f32"; if (ty == .f64) return "f64"; if (ty == .bool) return "bool"; if (ty == .void) return "void"; if (ty == .string) return "string"; if (ty == .any) return "Any"; if (ty == .usize) return "usize"; if (ty == .isize) return "isize"; const info = self.module.types.get(ty); return switch (info) { .@"struct" => |s| self.module.types.getString(s.name), .@"union" => |u| self.module.types.getString(u.name), .tagged_union => |u| self.module.types.getString(u.name), .@"enum" => |e| self.module.types.getString(e.name), .pointer => |p| blk: { const inner = self.formatTypeName(p.pointee); break :blk std.fmt.allocPrint(self.alloc, "*{s}", .{inner}) catch "pointer"; }, .many_pointer => |p| blk: { const inner = self.formatTypeName(p.element); break :blk std.fmt.allocPrint(self.alloc, "[*]{s}", .{inner}) catch "many_pointer"; }, .slice => |s| blk: { const inner = self.formatTypeName(s.element); break :blk std.fmt.allocPrint(self.alloc, "[]{s}", .{inner}) catch "slice"; }, .array => |a| blk: { const inner = self.formatTypeName(a.element); break :blk std.fmt.allocPrint(self.alloc, "[{d}]{s}", .{ a.length, inner }) catch "array"; }, .signed => |w| std.fmt.allocPrint(self.alloc, "s{d}", .{w}) catch "signed", .unsigned => |w| std.fmt.allocPrint(self.alloc, "u{d}", .{w}) catch "unsigned", .optional => |o| blk: { const inner = self.formatTypeName(o.child); break :blk std.fmt.allocPrint(self.alloc, "?{s}", .{inner}) catch "optional"; }, .vector => |v| blk: { const inner = self.formatTypeName(v.element); break :blk std.fmt.allocPrint(self.alloc, "Vector({d},{s})", .{ v.length, inner }) catch "vector"; }, else => @tagName(info), }; } /// Format a function type string like "() -> s32" or "(s32, s32) -> s32". fn formatFnTypeString(self: *Lowering, fd: *const ast.FnDecl) []const u8 { var buf: [512]u8 = undefined; var pos: usize = 0; buf[pos] = '('; pos += 1; for (fd.params, 0..) |p, i| { if (i > 0) { @memcpy(buf[pos..][0..2], ", "); pos += 2; } const pty = self.resolveParamType(&p); const name = self.formatTypeName(pty); @memcpy(buf[pos..][0..name.len], name); pos += name.len; } buf[pos] = ')'; pos += 1; const ret_ty = self.resolveReturnType(fd); if (ret_ty != .void) { @memcpy(buf[pos..][0..4], " -> "); pos += 4; const rname = self.formatTypeName(ret_ty); @memcpy(buf[pos..][0..rname.len], rname); pos += rname.len; } const result = self.alloc.alloc(u8, pos) catch unreachable; @memcpy(result, buf[0..pos]); return result; } /// Format a type name for function name mangling (identifier-safe). /// E.g. *Point → "ptr_Point", []s32 → "slice_s32", [3]f64 → "array_3_f64". /// Check if a param type expression references a type param name (possibly nested). pub fn matchTypeParam(_: *Lowering, type_node: *const Node, tp_name: []const u8) bool { return switch (type_node.data) { .type_expr => |te| std.mem.eql(u8, te.name, tp_name), .identifier => |id| std.mem.eql(u8, id.name, tp_name), .slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name), .pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name), .many_pointer_type_expr => |mp| matchTypeParamStatic(mp.element_type, tp_name), .optional_type_expr => |ot| matchTypeParamStatic(ot.inner_type, tp_name), .array_type_expr => |at| matchTypeParamStatic(at.element_type, tp_name), .closure_type_expr => |ct| blk: { for (ct.param_types) |pt| if (matchTypeParamStatic(pt, tp_name)) break :blk true; if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true; break :blk false; }, else => false, }; } fn matchTypeParamStatic(type_node: *const Node, tp_name: []const u8) bool { return switch (type_node.data) { .type_expr => |te| std.mem.eql(u8, te.name, tp_name), .identifier => |id| std.mem.eql(u8, id.name, tp_name), .slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name), .pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name), .many_pointer_type_expr => |mp| matchTypeParamStatic(mp.element_type, tp_name), .optional_type_expr => |ot| matchTypeParamStatic(ot.inner_type, tp_name), .array_type_expr => |at| matchTypeParamStatic(at.element_type, tp_name), .closure_type_expr => |ct| blk: { for (ct.param_types) |pt| if (matchTypeParamStatic(pt, tp_name)) break :blk true; if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true; break :blk false; }, else => false, }; } /// Extract the concrete type that corresponds to a type param from an arg type. /// E.g., param type []$T with arg type []s64 → T = s64. pub fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId, tp_name: []const u8) ?TypeId { return switch (type_node.data) { .type_expr => |te| if (std.mem.eql(u8, te.name, tp_name)) arg_ty else null, .identifier => |id| if (std.mem.eql(u8, id.name, tp_name)) arg_ty else null, .slice_type_expr => |st| blk: { // arg_ty should be a slice → extract element type if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); break :blk switch (info) { .slice => |s| self.extractTypeParam(st.element_type, s.element, tp_name), else => null, }; }, .pointer_type_expr => |pt| blk: { if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); break :blk switch (info) { .pointer => |p| self.extractTypeParam(pt.pointee_type, p.pointee, tp_name), else => null, }; }, .many_pointer_type_expr => |mp| blk: { if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); break :blk switch (info) { .many_pointer => |p| self.extractTypeParam(mp.element_type, p.element, tp_name), else => null, }; }, .optional_type_expr => |ot| blk: { if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); break :blk switch (info) { .optional => |o| self.extractTypeParam(ot.inner_type, o.child, tp_name), else => null, }; }, .array_type_expr => |at| blk: { if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); break :blk switch (info) { .array => |a| self.extractTypeParam(at.element_type, a.element, tp_name), else => null, }; }, .closure_type_expr => |ct| blk: { if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); const c_params: []const TypeId, const c_ret: TypeId = switch (info) { .closure => |c| .{ c.params, c.ret }, .function => |f| .{ f.params, f.ret }, else => break :blk null, }; // Prefer the return position (`Closure(...) -> $R`), then params. if (ct.return_type) |rt| { if (self.extractTypeParam(rt, c_ret, tp_name)) |ety| break :blk ety; } for (ct.param_types, 0..) |pt, i| { if (i >= c_params.len) break; if (self.extractTypeParam(pt, c_params[i], tp_name)) |ety| break :blk ety; } break :blk null; }, else => null, }; } /// Mangle a TypeId into its mono-key fragment. Thin delegation to the /// canonical owner (`GenericResolver`, `generics.zig`); kept on `Lowering` /// because ~30 cross-cutting callers (impl-map keys, conversion keys, shape /// keys) reach it here, well beyond generic monomorphization. pub fn mangleTypeName(self: *Lowering, ty: TypeId) []const u8 { return self.genericResolver().mangleTypeName(ty); } /// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values. /// Returns a list of TypeId index values that match the category. pub fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 { var tags = std.ArrayList(u64).empty; // Fixed builtin categories if (std.mem.eql(u8, name, "int")) { tags.append(self.alloc, TypeId.s8.index()) catch {}; tags.append(self.alloc, TypeId.s16.index()) catch {}; tags.append(self.alloc, TypeId.s32.index()) catch {}; tags.append(self.alloc, TypeId.s64.index()) catch {}; tags.append(self.alloc, TypeId.u8.index()) catch {}; tags.append(self.alloc, TypeId.u16.index()) catch {}; tags.append(self.alloc, TypeId.u32.index()) catch {}; tags.append(self.alloc, TypeId.u64.index()) catch {}; tags.append(self.alloc, TypeId.usize.index()) catch {}; tags.append(self.alloc, TypeId.isize.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "float")) { tags.append(self.alloc, TypeId.f32.index()) catch {}; tags.append(self.alloc, TypeId.f64.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "bool")) { tags.append(self.alloc, TypeId.bool.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "string")) { tags.append(self.alloc, TypeId.string.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "void")) { tags.append(self.alloc, TypeId.void.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "type") or std.mem.eql(u8, name, "Type")) { tags.append(self.alloc, TypeId.any.index()) catch {}; return tags.items; } // Dynamic categories: scan TypeTable for matching types const Category = enum { @"struct", @"enum", @"union", slice, array, pointer, vector, optional, error_set }; const cat: ?Category = if (std.mem.eql(u8, name, "struct")) .@"struct" else if (std.mem.eql(u8, name, "enum") or std.mem.eql(u8, name, "union")) .@"enum" else if (std.mem.eql(u8, name, "slice")) .slice else if (std.mem.eql(u8, name, "array")) .array else if (std.mem.eql(u8, name, "pointer")) .pointer else if (std.mem.eql(u8, name, "vector")) .vector else if (std.mem.eql(u8, name, "optional")) .optional else if (std.mem.eql(u8, name, "error_set")) .error_set else null; if (cat) |c| { for (self.module.types.infos.items, 0..) |info, idx| { const matches = switch (c) { .@"struct" => info == .@"struct", .@"enum" => info == .@"enum" or info == .tagged_union, .@"union" => info == .@"union" or info == .tagged_union, .slice => info == .slice, .array => info == .array, .pointer => info == .pointer or info == .many_pointer, .vector => info == .vector, .optional => info == .optional, .error_set => info == .error_set, }; if (matches) { tags.append(self.alloc, @intCast(idx)) catch {}; } } } // Specific type name (e.g., Point, Color) — look up in type registry if (tags.items.len == 0) { const name_id = self.module.types.internString(name); if (self.module.types.findByName(name_id)) |tid| { tags.append(self.alloc, tid.index()) catch {}; } } return tags.items; } /// Check if a match expression is a type-category match (patterns are type/category names). pub fn inferMatchResultType(self: *Lowering, me: *const ast.MatchExpr) TypeId { // Infer result type from the first non-null arm body. // If we skip null_literal arms and find a concrete type T, and there // were null arms, the result is ?T (optional). var has_null = false; var saw_unresolved = false; var saw_noreturn = false; for (me.arms) |arm| { const last_node = if (arm.body.data == .block) blk: { if (arm.body.data.block.stmts.len > 0) { break :blk arm.body.data.block.stmts[arm.body.data.block.stmts.len - 1]; } break :blk arm.body; } else arm.body; if (last_node.data == .null_literal) { has_null = true; continue; } // First arm with a statically-inferable type determines the result. // An arm whose type isn't inferable from the AST alone (e.g. a bare // enum literal) doesn't decide — keep looking; the caller falls back // to the contextual target type if none of the arms resolve. const arm_ty = self.inferExprType(last_node); // A diverging arm (`noreturn` — `return` / `raise` / `break` / // `continue`) doesn't produce a value, so it doesn't decide the // result type; keep looking. The match is `noreturn` only if EVERY // arm diverges (handled after the loop). if (arm_ty == .noreturn) { saw_noreturn = true; continue; } if (arm_ty == .unresolved) { saw_unresolved = true; continue; } if (has_null and arm_ty != .void) { return self.module.types.optionalOf(arm_ty); } return arm_ty; } if (saw_unresolved) return .unresolved; if (saw_noreturn) return .noreturn; // all arms diverge return .void; } pub fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool { for (me.arms) |arm| { if (arm.pattern) |pat| { const name = switch (pat.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => continue, }; const categories = [_][]const u8{ "int", "float", "bool", "string", "void", "type", "Type", "struct", "enum", "union", "slice", "array", "pointer", "vector", }; for (categories) |cat| { if (std.mem.eql(u8, name, cat)) return true; } // Also match specific struct/enum type names (e.g., case Point:) if (name.len > 0 and name[0] >= 'A' and name[0] <= 'Z') return true; } } 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 { for (type_params) |tp| { if (std.mem.eql(u8, param.name, tp.name)) return true; } return false; } /// Check if a function has comptime (non-Type) value parameters. pub fn hasComptimeParams(fd: *const ast.FnDecl) bool { for (fd.params) |p| { if (p.is_comptime) return true; } return false; } /// A plain free function: no type params (not generic) and an ordinary sx /// body (not `#foreign` / `#builtin` / `#compiler`). Only these get an /// out-of-line identity-addressable slot — the bare-call disambiguation /// (fix-0102c) and the shadow-author lowering pass leave every other shape /// to the existing name-keyed dispatch. pub fn isPlainFreeFn(fd: *const ast.FnDecl) bool { if (fd.type_params.len > 0) return false; return switch (fd.body.data) { .foreign_expr, .builtin_expr, .compiler_expr => false, else => true, }; } /// Pack-fn: has a trailing heterogeneous pack param (`is_variadic /// AND is_comptime`). Mixed shapes — non-pack comptime params /// before the pack — are also accepted; the mono folds those /// comptime VALUES into the mangled name and binds them as both /// comptime substitutions (for #insert) and runtime locals (for /// bare-name body references). pub fn isPackFn(fd: *const ast.FnDecl) bool { for (fd.params) |p| { if (isPackParam(p)) return true; } return false; } /// A trailing pack parameter: the comptime type-pack `..$args` /// (`is_comptime`) or the protocol-constrained pack `..xs: P` (`is_pack`). /// Both monomorphize per call shape via `lowerPackFnCall`; the slice /// variadic (`..xs: []T`) is neither and stays a runtime slice. fn isPackParam(p: ast.Param) bool { return p.is_variadic and (p.is_comptime or p.is_pack); } pub fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId { if (fd.return_type) |rt| { return self.resolveTypeWithBindings(rt); } // No explicit annotation — the type is inferred from the body, which // references the function's own parameters (`(x: s32) => x * 2`). Those // params aren't pushed into `self.scope` until body lowering, so bind // them into a temporary scope here; otherwise `inferExprType` can't // resolve `x`, the inference yields `.unresolved`, and that reaches LLVM // emission as `func.ret` (issue 0059). Whether it slipped through used to // depend on a same-named binding lingering from earlier lowering. var tmp_scope = Scope.init(self.alloc, self.scope); defer tmp_scope.deinit(); const saved_scope = self.scope; self.scope = &tmp_scope; defer self.scope = saved_scope; for (fd.params, 0..) |p, i| { // Bind only plain annotated value params — that's all the body's // return type can depend on by name. Skip variadic / pack / comptime // params (their concrete types come from per-call substitution) and // unannotated ones (no context here). Resolve the type directly via // resolveTypeWithBindings rather than resolveParamType: the latter // does variadic/pack bookkeeping that must run exactly once, at body // lowering — calling it here too corrupts that state. if (p.is_variadic or p.is_pack or p.is_comptime) continue; if (p.type_expr.data == .inferred_type) continue; const pty = self.resolveTypeWithBindings(p.type_expr); tmp_scope.put(p.name, .{ .ref = Ref.fromIndex(@intCast(i)), .ty = pty, .is_alloca = false }); } // Arrow functions without explicit return type: infer from body expression. if (fd.is_arrow) { return self.inferExprType(fd.body); } // Not arrow: an explicit `return ` statement wins. Otherwise // default to void — the body's tail expression is a side-effect // statement, not an implicit return. if (self.findReturnValueType(fd.body)) |ty| return ty; return .void; } /// Walk a function body and return the type of the first `return ;` /// statement encountered. Does not descend into nested function or lambda /// declarations (those have their own return types). fn findReturnValueType(self: *Lowering, node: *const Node) ?TypeId { return switch (node.data) { .return_stmt => |rs| if (rs.value) |v| self.inferExprType(v) else null, .block => |blk| blk: { for (blk.stmts) |s| { if (self.findReturnValueType(s)) |t| break :blk t; } break :blk null; }, .if_expr => |ie| blk: { if (self.findReturnValueType(ie.then_branch)) |t| break :blk t; if (ie.else_branch) |eb| { if (self.findReturnValueType(eb)) |t| break :blk t; } break :blk null; }, .while_expr => |we| self.findReturnValueType(we.body), .for_expr => |fe| self.findReturnValueType(fe.body), .match_expr => |me| blk: { for (me.arms) |arm| { if (self.findReturnValueType(arm.body)) |t| break :blk t; } break :blk null; }, else => null, }; } pub fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId { // A plain value param with no annotation can only be typed from // context (a lambda's target closure signature). When `resolveParamType` // is reached for one, there is no such context — so it's a genuine // "missing annotation" error, not an 8-byte-int guess. (Comptime/ // variadic pack params also carry `inferred_type` but get their types // from per-call substitution, so they're exempt here.) if (p.type_expr.data == .inferred_type and !p.is_comptime and !p.is_variadic and !p.is_pack) { if (self.diagnostics) |d| { d.addFmt(.err, p.type_expr.span, "parameter '{s}' has no type annotation", .{p.name}); } return .unresolved; } const declared_ty = self.resolveTypeWithBindings(p.type_expr); if (p.is_variadic) { // Two surface forms: // - legacy `name: ..T` — declared_ty is the element type; // wrap to receive a `[]T` slice. // - new `..name: []T` — declared_ty is already the slice // type; use it as-is. Wrapping here would double up to // `[][]T` and downstream LLVM emission crashes when the // caller's argument-marshal pack produces a `[]T` that // doesn't match the callee's stored param shape. if (!declared_ty.isBuiltin()) { const info = self.module.types.get(declared_ty); if (info == .slice) return declared_ty; } return self.module.types.sliceOf(declared_ty); } return declared_ty; } pub fn resolveType(self: *Lowering, type_ann: *const Node) TypeId { return self.resolveTypeWithBindings(type_ann); } /// Resolve a type node with the visibility context pinned to `src`, the /// DEFINING module of a namespaced callee, restoring the caller's context /// after. A namespaced callee's declared return type may name a type that is /// bare-visible only inside the callee's own module — namespaced-only from the /// call site's view. Post-E1 the bare leaf is source-aware, so resolving that /// return type in the CALL SITE's context would wrongly reject it (the type /// analog of the issue-0100-F1 source pin that lowers a namespaced fn body in /// its own module's context). `src == null` falls back to the call site's /// context unchanged. pub fn resolveTypeInSource(self: *Lowering, src: ?[]const u8, type_ann: *const Node) TypeId { const pinned = src orelse return self.resolveType(type_ann); const saved = self.current_source_file; defer self.setCurrentSourceFile(saved); self.setCurrentSourceFile(pinned); return self.resolveType(type_ann); } /// `resolveParamType` with the visibility context pinned to `src`, the /// DEFINING module of the param's function. An imported method's /// default-param type (`alloc: Allocator`) is bare-visible only inside its /// 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 { const pinned = src orelse return self.resolveParamType(p); const saved = self.current_source_file; defer self.setCurrentSourceFile(saved); self.setCurrentSourceFile(pinned); return self.resolveParamType(p); } /// Construct a `TypeResolver` view over the current lowering state (borrows /// only; cheap by-value, reflects current `diagnostics` / `program_index`). pub fn typeResolver(self: *Lowering) TypeResolver { return .{ .alloc = self.alloc, .types = &self.module.types, .diagnostics = self.diagnostics, .index = &self.program_index, }; } /// Snapshot the active resolution context (Principle 2) for `TypeResolver`. /// A2.2 wires the type bindings + literal target; the pack/comptime fields /// are populated as A2.3 moves the cases that consume them. fn resolveEnv(self: *Lowering) ResolveEnv { return .{ .type_bindings = if (self.type_bindings) |*tb| tb else null, .target_type = self.target_type, }; } /// Inner-type recursion hook for `TypeResolver.resolveCompound`: resolves a /// child type node through the full stateful resolver, so generic structs / /// bindings / aliases in element position keep their resolution. pub fn resolveInner(self: *Lowering, node: *const Node) TypeId { return self.resolveTypeWithBindings(node); } /// Fixed-array dimension hook for `TypeResolver.resolveCompound`. A literal /// `[16]T` and a named-const `N :: 16; [N]T` must resolve to the SAME length: /// the dimension folds to a compile-time integer (looked up in the comptime / /// value / module-const tables the stateful lowering owns) and is narrowed to /// `u32` through the single range-checked `program_index.foldDimU32` — never a /// bare `@intCast`, so an oversized-but-valid `i64` dim (`[5_000_000_000]`) /// diagnoses instead of panicking the compiler (issue 0087). A dimension that /// isn't a compile-time integer (or doesn't fit a `u32`) is a hard error: /// emit a diagnostic so the driver aborts (`hasErrors()`), then return a /// harmless `0` so body lowering finishes without touching the `.unresolved` /// sentinel (which would `@panic` in `sizeOf` mid-lowering, before the /// diagnostic surfaces). The diagnostic — not the returned length — is what /// guarantees no garbage ships (issue 0083). pub fn resolveArrayLen(self: *Lowering, len_node: *const Node) ?u32 { const result = program_index_mod.foldDimU32(len_node, self, 0); if (result == .ok) return result.ok; // A non-const / oversized / negative dim is a hard error. Emit the // shared diagnostic (single wording source — `program_index.reportDimError`, // also used by the stateless alias path so the two cannot diverge) and // return null so `resolveCompound` yields the `.unresolved` sentinel — NO // fabricated length (issue 0083: a `0` here gives a 0-byte alloca and OOB // element access). Lowering the binding never computes the failed type's // size: `alloca` records the type but defers `sizeOf` to LLVM emission, // which the emitted diagnostic pre-empts via `hasErrors()`, and a // downstream use of the `.unresolved`-typed value is poison-suppressed (a // field access stays silent — `emitFieldError`). So the failure surfaces // as ONE clean diagnostic and never reaches the `sizeOf` panic. if (self.diagnostics) |d| program_index_mod.reportDimError(d, len_node.span, result); return null; } /// Leaf-name lookup for the shared dimension evaluator: a name bound to a /// compile-time integer across the three const tables. pub fn lookupDimName(self: *Lowering, name: []const u8) ?i64 { return self.comptimeIntNamed(name); } /// Pack-length leaf for the shared integer-expression evaluator: a pack /// name's monomorphised arity (e.g. an `inline for 0..xs.len` bound). /// Resolves through `pack_param_count`, which is populated when a comptime /// call binds a pack name. A name with no active pack binding is not a /// compile-time integer leaf here → null. pub fn lookupPackLen(self: *Lowering, name: []const u8) ?i64 { if (self.pack_param_count) |ppc| { if (ppc.get(name)) |n| return @intCast(n); } return null; } /// Float-valued leaf for the shared float-expression evaluator: a name bound /// to a NUMERIC module const whose compile-time value is a (non-integral) /// float — the FLOAT counterpart of `lookupDimName`, routed through the SAME /// `module_const_map` so the unified narrowing rule resolves a float-const /// leaf (`F : f64 : 2.5`) exactly as it resolves an int-const leaf. Integer / /// integral-float leaves and comptime int bindings are already resolved by the /// `evalConstIntExpr` delegation inside `evalConstFloatExpr`; this surfaces the /// non-integral float const so the rule can reject it. pub fn lookupFloatName(self: *Lowering, name: []const u8) ?f64 { return self.foldSourceConstFloat(name, null); } /// True iff `name` is a FLOAT-valued module const (`F : f64 : 2.5`, /// `K : f64 : 4.0`, untyped `M :: 4.0`, untyped-EXPR `ME :: 4.0 + 1.0`). The /// int folder's division arm consults this so a `/` with a float-const operand /// is recognised as float division (issue 0095 / F0.11-6). Comptime / generic /// value bindings are always integer-valued, so only the module-const table /// can name a float. pub fn nameIsFloatTyped(self: *Lowering, name: []const u8) bool { return self.sourceConstIsFloatTyped(name, null); } /// Resolve a type node, checking type_bindings first for generic type params. pub fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId { // Pack-index in a type position: `$[]` resolves to the // i-th element type of the active pack binding (step 3 of the // variadic heterogeneous type packs feature). Unblocks parametric // trampoline bodies (`(*void, $args[0]) -> $args[1]`) in stdlib's // generic Into(Block) impl. OOB indices / a missing binding emit a // diagnostic and return the `.unresolved` sentinel — never a plausible // `.s64`, which would silently fabricate an 8-byte int. if (node.data == .pack_index_type_expr) { const pi = node.data.pack_index_type_expr; if (self.pack_arg_types) |pat| { if (pat.get(pi.pack_name)) |arg_tys| { if (pi.index < arg_tys.len) return arg_tys[pi.index]; if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index type ${s}[{}] out of bounds: '{s}' has {} element{s}", .{ pi.pack_name, pi.index, pi.pack_name, arg_tys.len, if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"), }); } return .unresolved; } } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index type ${s}[{}] used outside an active pack binding", .{ pi.pack_name, pi.index, }); } return .unresolved; } // `*Self` substitution inside foreign-class member declarations // — both foreign and sx-defined — resolves to the class's own // 0-field stub struct (i.e. the opaque Obj-C pointer type). // This matches the Obj-C idiom where `self` IS the object. // `self.field` access on sx-defined classes is rewritten by // lowerFieldAccess to go through the `__sx_state` ivar // (object_getIvar + struct_gep) when needed — see M1.2 A.3. if (node.data == .type_expr and std.mem.eql(u8, node.data.type_expr.name, "Self")) { if (self.current_foreign_class) |fcd| { if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) { return self.foreignClassStructType(fcd); } } } // Structural type shapes — `*T`, `[*]T`, `[]T`, `?T`, `[N]T`, functions, // PLAIN closures, and PLAIN tuples — are owned by // `TypeResolver.resolveCompound` (A2.3b). Element types recurse through // the full stateful resolver (`resolveInner` → here) so generic structs // / bindings keep their resolution. resolveCompound returns null only // for the pack-shaped forms (`Closure(..p)`, spread tuples) below. if (TypeResolver.resolveCompound(&self.module.types, node, self)) |t| return t; // Generic type-param binding (`$T`, or a bare return-type `T` without // the `$` prefix) — owned by TypeResolver via the explicit ResolveEnv. // The parameterized / call / closure / function arms that used to live // here were redundant with the unconditional handling just below (both // read the active bindings through the same resolvers), so they're gone. if (TypeResolver.resolveBinding(node, self.resolveEnv())) |t| return t; // Even without active type_bindings, handle parameterized types with struct templates if (node.data == .parameterized_type_expr) { return self.resolveParameterizedWithBindings(&node.data.parameterized_type_expr, node.span); } if (node.data == .call) { return self.resolveTypeCallWithBindings(&node.data.call); } // Plain structural shapes were handled by resolveCompound above. What // reaches here is the PACK-shaped subset, owned by `PackResolver` // (packs.zig): pack-shaped `Closure(..p)` and spread tuples. (Functions // are never pack-shaped at the type level — resolveCompound owns them // all, so there is no function arm here.) switch (node.data) { .closure_type_expr => |ct| { return self.packResolver().resolveClosureTypeWithBindings(&ct); }, .tuple_type_expr => |tt| { return self.packResolver().resolveTupleTypeWithBindings(&tt); }, // `(..$Ts)` in a type position (e.g. a struct field) parses as a // tuple LITERAL whose elements include a pack spread; PackResolver // expands it (returns null when no spread, so we fall through). .tuple_literal => |tl| { if (self.packResolver().resolveTupleLiteralType(&tl)) |t| return t; }, else => {}, } // An unbound generic type param (`$R` with no active binding) must not // fabricate an empty-struct stub — that surfaces as `R{}` downstream. // Return `.unresolved` so callers (e.g. lambda return-type inference, // call-site `$R` inference) treat it as not-yet-known. if (node.data == .type_expr and node.data.type_expr.is_generic) { // A VALUE param (`$N: u32`) named in a TYPE position (`x: N`) is bound // to a compile-time integer, not a type, so `resolveBinding` above // found no TYPE binding and it lands here. In the MAIN file the // `UnknownTypeChecker` owns this diagnostic (and halts before codegen); // an imported template's fields are resolved in the template's source // context (see `instantiateGenericStruct`) and are checker-trusted, so // this leaf is the sole guard — emit the tailored hint, mirroring the // imported `.undeclared` leaf. A genuinely-unbound type param (`$R`, // no value binding) stays a silent `.unresolved`. const nm = node.data.type_expr.name; const bound_value = if (self.comptime_value_bindings) |cvb| cvb.contains(nm) else false; if (bound_value) { const is_main = if (self.main_file) |mf| (if (self.current_source_file) |csf| std.mem.eql(u8, csf, mf) else true) else true; if (!is_main) { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is a value parameter, not a type; introduce a generic type parameter with `${s}: Type`", .{ nm, nm }); } } return .unresolved; } // Bare type names resolve through the source-aware `selectNominalLeaf` // (E1): the nominal author is selected over the ONE graph-walk collector // and resolved against the source-keyed caches, not the global // `findByName` first-match / global alias map. Other node kinds (inline // type decls, error types) still route through type_bridge, which reads // the global compat maps (cut over in a later phase). switch (node.data) { .type_expr => |te| return self.resolveNominalLeaf(te.name, te.is_raw, node.span), .identifier => |id| return self.resolveNominalLeaf(id.name, id.is_raw, node.span), // A non-spread tuple literal in a type position is a tuple-type // literal (`(s32, s32)`); validate its elements are types and reject // non-type elements loudly (issue 0067). .tuple_literal => return self.resolveTupleLiteralTypeArg(node), else => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map), } } /// Bind a `PackResolver` to this Lowering for pack-aware TYPE-position /// resolution (`Closure(..p)` / `(Params...) -> R` / `(..xs)` tuples and /// their `..xs.T` projections). A2.3 moved that logic into `packs.zig`. fn packResolver(self: *Lowering) PackResolver { return .{ .l = self }; } /// Resolve a `Vector(N, T)` lane count to a positive compile-time integer /// through the shared `program_index.foldDimU32` folder (min 1) — so a literal /// (`Vector(4, f32)`), a module/generic const (`Vector(N, f32)`), and a const /// expression (`Vector(M + 1, f32)`) all resolve identically, and the i64→u32 /// narrowing is range-checked (an oversized lane diagnoses instead of /// panicking — issue 0087). A non-const lane (`Vector(get(), f32)`) or a /// non-positive one emits a clean diagnostic and returns null; the caller /// yields `.unresolved` rather than fabricating a `<0 x float>` lane count /// that crashes LLVM verification. fn resolveVectorLane(self: *Lowering, lane_node: *const Node) ?u32 { switch (program_index_mod.foldDimU32(lane_node, self, 1)) { .ok => |n| return n, .too_large => |v| { if (self.diagnostics) |d| d.addFmt(.err, lane_node.span, "Vector lane count {} does not fit in u32", .{v}); return null; }, .non_integral_float => |v| { if (self.diagnostics) |d| d.addFmt(.err, lane_node.span, "Vector lane count must be an integer, but '{d}' is a non-integral float", .{v}); return null; }, .not_const, .below_min => { if (self.diagnostics) |d| d.addFmt(.err, lane_node.span, "Vector lane count must be a positive compile-time integer constant", .{}); return null; }, } } /// Resolve a generic value-param argument (`$K: u32`) to its compile-time /// integer AND verify it fits the param's declared integer type. The folded /// value is bound and mangled into the instantiation name, so a module/generic /// const arg (`Vec(N, f32)`), a const expression (`Make(M + 1, s64)`), an /// integral float (`Box(4.0)` → 4), and a literal (`Vec(3, f32)`) all bind the /// same value a literal would. An out-of-range arg (`Box(5_000_000_000)` for a /// `u32` param) or a non-const arg emits a clean diagnostic and returns null; /// the caller bails rather than binding a truncated / fabricated value under a /// wrong mangled name. /// /// `type_name` is the param's declared constraint type (`"u32"`, null if /// unknown). A `u32` count routes through the shared /// `program_index.foldDimU32` — the SAME fold-and-narrow gate an array dim / /// Vector lane uses — so the documented "single u32 gate for value-param /// counts" holds; any other integer type range-checks against /// `program_index.intTypeRange`; an unrecognised type folds without bounding. fn resolveValueParamArg(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: ?[]const u8) ?i64 { // Resolve an ALIASED integer constraint (`$K: Count` where `Count :: u32`, // `$K: Small` where `Small :: s8`) to its underlying builtin so the range // gate below treats it exactly like `$K: u32` / `$K: s8` (issue 0083 — an // alias previously slipped past `intTypeRange`, so `Box(5_000_000_000)` // with `$K: Count` bound a truncated value). A non-integer / unrecognised // constraint yields null → no range bound (fold only), as before. const tn_canon: ?[]const u8 = if (type_name) |tn| self.canonicalIntConstraintName(tn) else null; if (tn_canon) |tn| { if (std.mem.eql(u8, tn, "u32")) { switch (program_index_mod.foldDimU32(arg_node, self, 0)) { .ok => |n| return n, .not_const, .non_integral_float => { self.diagValueParamNotConst(arg_node, param_name); return null; }, .below_min => |v| { self.diagValueParamRange(arg_node, param_name, tn, v); return null; }, .too_large => |v| { self.diagValueParamRange(arg_node, param_name, tn, v); return null; }, } } } // Non-`u32` integer constraint: fold through the SAME unified count fold // so an integral float arg (`Box(4.0)`, `Make(F + 1.5, ...)`) binds the // integer it equals, exactly as the `u32` gate above does; a non-integral // float / non-const arg is not a valid count. const v = switch (program_index_mod.foldCountI64(arg_node, self)) { .int => |iv| iv, .non_integral, .not_const => { self.diagValueParamNotConst(arg_node, param_name); return null; }, }; if (tn_canon) |tn| { if (program_index_mod.intTypeRange(tn)) |r| { if (v < r.min or v > r.max) { self.diagValueParamRange(arg_node, param_name, tn, v); return null; } } } return v; } /// Resolve a generic value-param constraint type NAME to its canonical builtin /// integer type name, chasing a type alias (`Count :: u32` → "u32", /// `Small :: s8` → "s8") so an ALIASED integer constraint range-checks exactly /// like the builtin it names. Returns the name unchanged when it is already a /// builtin integer; null when it isn't an integer type (directly or via alias) /// — the caller then folds without a range bound rather than guessing. The /// alias map + type table are the same single sources every other resolver /// reads, so this can't diverge from how the alias is laid out elsewhere. fn canonicalIntConstraintName(self: *Lowering, name: []const u8) ?[]const u8 { if (program_index_mod.intTypeRange(name) != null) return name; if (self.program_index.type_alias_map.get(name)) |tid| { const canon = self.module.types.typeName(tid); if (program_index_mod.intTypeRange(canon) != null) return canon; } return null; } fn diagValueParamNotConst(self: *Lowering, arg_node: *const Node, param_name: []const u8) void { if (self.diagnostics) |d| d.addFmt(.err, arg_node.span, "generic value parameter '{s}' must be a compile-time integer constant", .{param_name}); } fn diagValueParamRange(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: []const u8, value: i64) void { if (self.diagnostics) |d| d.addFmt(.err, arg_node.span, "value {} does not fit in {s} parameter {s}", .{ value, type_name, param_name }); } /// The poison-vs-proceed projection of `headTypeGate` for an UNQUALIFIED /// parameterized type HEAD that names a generic STRUCT, a parameterized /// PROTOCOL, or a type-returning function used as a head (`Box(s64)`, /// `VL(s64)`) — and the alias-registration / type-match sites that likewise /// only need "poison or proceed". Returns TRUE (the gate's loud diagnostic is /// already emitted) when the head is `.not_visible` (a 2-flat-hop leak) or /// `.ambiguous` (≥2 direct flat same-name authors — consistent with the leaf / /// 0755); FALSE when it resolves or falls open. See `headTypeGate` for the full /// non-transitive visibility + ambiguity model and the fall-open conditions. fn headTypeLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool { // A head site INSTANTIATES (template / type-fn) rather than substituting a // nominal TypeId, so it consumes only the poison-vs-proceed bit of the // full author outcome: `.ambiguous` / `.not_visible` (loud diagnostic // already emitted by `headTypeGate`) poison; `.resolved` / `.proceed` // proceed to instantiation. return switch (self.headTypeGate(name, span)) { .ambiguous, .not_visible => true, .proceed, .resolved => false, }; } /// Control-flow outcome of the generic-struct LAYOUT-head selector. Carries no /// diagnostic for the caller to emit — `selectGenericStructHead` emits inline. const HeadTemplate = union(enum) { template: StructTemplate, // visible bare author OR qualified author → instantiate poisoned, // gate already diagnosed → caller returns .unresolved / Ref.none not_generic, // name is not a generic struct head → caller's non-struct path }; /// THE single selector every generic-struct LAYOUT-head site funnels through — /// no head site reads `struct_template_map` for selection directly. Decides the /// authoring template for a head named `name`, qualified by namespace `alias` /// (non-null only for `ns.Box(..)` with an identifier object) and flagged /// `is_qualified` (any `.field_access` callee, including a non-identifier /// object). Emits the visibility / missing-member diagnostics INLINE at `span`, /// at the same program point and ordering the sites used before (0767/0769/0775), /// and returns a control-flow-only outcome: /// - qualified, namespace authors `name` as a generic struct → that author. /// - qualified, namespace exists but lacks `name` → diagnose missing member, /// `.poisoned` (never the bare global map, E4 #2). /// - qualified, namespace authors `name` but NOT as a generic struct (a /// type-fn / named type) → `.not_generic` (caller's non-struct path). /// - qualified with no usable alias (nested-ns object) → the global template /// if one exists (pre-existing behavior; no namespace edge to consult). /// - bare, ≥2 visible authors / 2-flat-hop only → `headTypeLeak` diagnosed → /// `.poisoned`. /// - bare, single visible author → that author (own / 1-hop flat), source-keyed. /// - bare, visible author IS the canonical map author → the global template /// (byte-identical single-author path). /// - not in `struct_template_map` at all → `.not_generic`. pub fn selectGenericStructHead(self: *Lowering, name: []const u8, alias: ?[]const u8, is_qualified: bool, span: ?ast.Span) HeadTemplate { if (is_qualified) { if (alias) |a| { if (self.qualifiedStructTemplate(a, name)) |tmpl| return .{ .template = tmpl }; if (self.qualifiedMemberMissing(a, name)) { if (self.diagnostics) |d| d.addFmt(.err, span, "namespace '{s}' has no member '{s}'", .{ a, name }); return .poisoned; } return .not_generic; } // Qualified but un-aliasable object (nested namespace / non-identifier): // no namespace edge to select from — use the global template if present. if (self.program_index.struct_template_map.getPtr(name)) |tmpl| return .{ .template = tmpl.* }; return .not_generic; } if (self.program_index.struct_template_map.getPtr(name)) |tmpl| { if (self.headTypeLeak(name, span)) return .poisoned; if (self.bareVisibleStructTemplate(name)) |vt| return .{ .template = vt }; return .{ .template = tmpl.* }; } return .not_generic; } /// Decompose a head callee NODE (`.identifier Box` or `.field_access ns.Box`) /// into the `(name, alias, is_qualified)` triple `selectGenericStructHead` /// consumes. `alias` is the namespace identifier only for a `.field_access` /// 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 { return switch (callee.data) { .identifier => |id| .{ .name = id.name, .alias = null, .is_qualified = false }, .field_access => |fa| .{ .name = fa.field, .alias = if (fa.object.data == .identifier) fa.object.data.identifier.name else null, .is_qualified = true, }, else => null, }; } /// The complete source-aware author outcome of an UNQUALIFIED bare TYPE head — /// the unified non-transitive visibility + ambiguity gate every bare-type- /// reference site OUTSIDE the nominal leaf routes through (E4 attempt-5): /// reflection / type-arg slots, typed array/vector-literal heads, parameterized /// generic / protocol / type-fn heads, type-as-value, and type-category match /// arms. Mirrors `selectNominalLeaf`'s author model so a 2-flat-hop type is /// `.not_visible`, ≥2 direct flat same-name authors are `.ambiguous` (the LOUD /// diagnostic, consistent with the leaf / 0755 — never a silent global /// `findByName` / `struct_template_map` first-/last-wins pick), and a single /// direct flat author resolves to ITS source-keyed TypeId. Falls open /// (`.proceed`) when import facts are unwired, the source context is absent, /// the default-Context emitter is running (built-in infrastructure resolves /// independent of the user's import style, F1), the querying source is the OWN /// author, a single flat author is not registered yet (a forward / foreign / /// generic template — the caller instantiates it), or `name` is a block-local /// of this source / no type author at all. Library-internal heads stay visible /// because every instantiation kind is source-pinned to the template's defining /// module (E3/E4 #1): the query originates THERE, where the head is a direct /// flat import. A namespaced `ns.Box(..)` head is an explicit qualified reach /// and is exempt (the caller skips this gate). const HeadTypeGate = union(enum) { proceed, resolved: TypeId, ambiguous, not_visible, }; pub fn headTypeGate(self: *Lowering, name: []const u8, span: ?ast.Span) HeadTypeGate { if (self.emitting_default_context) return .proceed; if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return .proceed; const from = self.current_source_file orelse return .proceed; var res_walk = self.resolver(); const author_set = res_walk.collectVisibleAuthors(name, from, .user_bare_flat); defer if (author_set.flat.len > 0) self.alloc.free(author_set.flat); // Own author wins outright (own-wins, 0754). Pending / unregistered → .proceed. if (author_set.own) |own| switch (own.raw) { .const_decl => { if (self.program_index.type_aliases_by_source.get(own.source)) |inner| { if (inner.get(name)) |tid| return .{ .resolved = tid }; } return .proceed; }, else => if (isNamedTypeKind(own.raw)) { if (self.namedRefTid(own.raw, name)) |tid| return .{ .resolved = tid }; return .proceed; }, }; // Flat type authors var flat_type_count: usize = 0; var found_tid: ?TypeId = null; var flat_tid_count: usize = 0; for (author_set.flat) |fa| { const is_type = switch (fa.raw) { .const_decl => blk: { if (self.program_index.type_aliases_by_source.get(fa.source)) |inner| break :blk inner.contains(name); break :blk false; }, else => isNamedTypeKind(fa.raw), }; if (!is_type) continue; flat_type_count += 1; const fa_tid: ?TypeId = switch (fa.raw) { .const_decl => blk: { if (self.program_index.type_aliases_by_source.get(fa.source)) |inner| break :blk inner.get(name); break :blk null; }, else => self.namedRefTid(fa.raw, name), }; if (fa_tid) |t| { flat_tid_count += 1; if (found_tid) |f| { if (t != f) { if (self.diagnostics) |d| d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); return .ambiguous; } } else found_tid = t; } } if (flat_type_count > 0) { // ≥2 authors but not all resolved to one TypeId → ambiguous if (flat_type_count >= 2 and !(flat_tid_count == flat_type_count and found_tid != null)) { if (self.diagnostics) |d| d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); return .ambiguous; } if (found_tid) |t| return .{ .resolved = t }; return .proceed; // single author exists but TypeId not registered } if (self.localTypeInSource(from, name)) return .proceed; if (!self.nameAuthoredAsTypeAnywhere(name)) return .proceed; if (self.diagnostics) |d| d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name}); return .not_visible; } /// Single-hop non-transitive visibility + ambiguity gate for an UNQUALIFIED /// type-returning FUNCTION head used as a type (`Make(N, T)` where /// `Make :: ($K, $T) -> Type`). A type-fn is a `fn_decl`, so visibility is /// decided from the ELIGIBLE FUNCTION authors directly reachable from the use /// site (`flatFnAuthorVisible`) — NOT the module-scope NAME predicate /// (`isNameVisible`), which a same-name NON-function (a value const, a named /// type) would wrongly vouch for. Returns TRUE (loud diagnostic already /// emitted) when the head is AMBIGUOUS (≥2 distinct direct flat same-name /// type-fn authors, no own author — consistent with the parameterized struct / /// protocol heads and the leaf, 0755/0767, never a silent `fn_ast_map` /// first-/last-wins pick) or NOT-VISIBLE (its only directly-visible same-name /// author is a non-function and the real type-fn author is ≥2 flat hops away). /// A scope-local (mangled) type-fn or the querying source's OWN function author /// wins outright (own-wins) and is exempt; falls open when unwired / /// default-context. Diagnostic mirrors the type form (the head IS used as a type /// here). pub fn headFnLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool { if (self.emitting_default_context) return false; const from = self.current_source_file orelse return false; if (self.scope) |s| if (s.lookupFn(name) != null) return false; // Fall open when the import facts aren't wired (comptime callers, // directory imports without a main file): the author collector would // otherwise return an empty set and wrongly report a genuinely-visible // type-fn as not-visible. Mirrors `headTypeGate`'s guard. if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return false; // ≥2 distinct direct flat type-fn authors with no own author — a genuine // collision the source cannot disambiguate. Diagnose loudly BEFORE the // visibility short-circuit, which would otherwise let the single // `fn_ast_map[name]` author silently win. if (self.flatFnAuthorAmbiguous(name, from)) { if (self.diagnostics) |d| d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); return true; } // KIND-AWARE: visible iff a directly-reachable (own or 1-hop flat) author // is itself a TYPE-FUNCTION. A same-name 1-hop non-function (attempt-7) OR // ordinary non-type function (attempt-8) does NOT vouch for a type-fn head // whose real author is 2 flat hops away. if (self.flatFnAuthorVisible(name, from)) return false; if (self.diagnostics) |d| d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name}); return true; } /// TRUE iff bare `name` has ≥2 DISTINCT direct flat-import authors that are /// TYPE-FUNCTIONS (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param — an ordinary /// same-name function does not count) and the querying source authors NONE /// itself. The querying source's OWN /// author wins outright (own-wins), so an own author short-circuits to "not /// ambiguous" — the existing single-author path instantiates it. Diamond /// imports of the SAME author collapse in `collectVisibleAuthors`'s /// author-identity de-dup, so two edges onto one type-fn are NOT ambiguous. The /// type-fn ambiguity analogue of `flatTypeAuthorCount`'s `.ambiguous` for named /// type / template heads. fn flatFnAuthorAmbiguous(self: *Lowering, name: []const u8, from: []const u8) bool { var res = self.resolver(); const set = res.collectVisibleAuthors(name, from, .user_bare_flat); defer if (set.flat.len > 0) self.alloc.free(set.flat); if (set.own != null) return false; // own-wins var fn_authors: usize = 0; for (set.flat) |fa| { if (typeFnAuthor(fa.raw)) fn_authors += 1; } return fn_authors >= 2; } /// TRUE iff bare `name` has at least one DIRECTLY-visible author — the /// querying source's OWN author or a 1-hop flat-import author — that is a /// TYPE-FUNCTION (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param). The KIND-AWARE /// analogue of `isNameVisible` for a type-fn head: a same-name 1-hop /// NON-function (a value const `Make :: 123`, a named type) does NOT vouch /// (attempt-7), and — crucially — neither does a same-name 1-hop ORDINARY /// function (`Make :: () -> s32`, zero `$`-params), which cannot be the type /// head being instantiated (attempt-8). So a type-fn whose only directly- /// visible same-name author is a non-fn OR a non-type-fn — its real author 2 /// flat hops away — is correctly invisible. Mirrors `flatFnAuthorAmbiguous`'s /// type-fn-only author view. fn flatFnAuthorVisible(self: *Lowering, name: []const u8, from: []const u8) bool { var res = self.resolver(); const set = res.collectVisibleAuthors(name, from, .user_bare_flat); defer if (set.flat.len > 0) self.alloc.free(set.flat); if (set.own) |own| { if (typeFnAuthor(own.raw)) return true; } for (set.flat) |fa| { if (typeFnAuthor(fa.raw)) return true; } return false; } /// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)). pub fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId { // A namespaced callee (`ns.Box(..)`) is an explicit qualified reach and is // exempt from the bare-head visibility gate; only a plain identifier head // is policed (E4). const is_qualified = cl.callee.data == .field_access; const callee_name: []const u8 = switch (cl.callee.data) { .identifier => |id| id.name, .field_access => |fa| fa.field, else => return .unresolved, }; // Built-in: Vector(N, T) if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) { const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved; const elem = self.resolveTypeWithBindings(cl.args[1]); return self.module.types.vectorOf(elem, length); } // Generic-struct head: route through the single layout choke-point (CP-1). // Bare → the single bare-VISIBLE author (own / 1-hop flat), source-keyed; // qualified `ns.Box(..)` → ns's OWN template (or a missing-member diagnostic); // never the global last-wins map for a visible-shadowed or qualified head. if (headNameOfCallee(cl.callee)) |hn| { switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) { .template => |t| return self.instantiateGenericStruct(&t, cl.args), .poisoned => return .unresolved, .not_generic => {}, } } // User-defined type-returning function: Complex(u32), Sx(f32) // Also resolve via scope fn_names (local functions get mangled names) const resolved_name = if (self.scope) |scope| (scope.lookupFn(callee_name) orelse callee_name) else callee_name; if (self.program_index.fn_ast_map.get(resolved_name)) |fd| { if (fd.type_params.len > 0) { if (!is_qualified and self.headFnLeak(callee_name, cl.callee.span)) return .unresolved; if (self.instantiateTypeFunction(callee_name, callee_name, fd, cl.args)) |ty| { return ty; } } } // Try as a named type const name_id = self.module.types.internString(callee_name); return self.module.types.findByName(name_id) orelse .unresolved; } /// Resolve a parameterized type expr, substituting bindings for type/value params. /// Handles both built-in types (Vector) and user-defined generic structs. /// `span` locates the reference for the unresolved-base diagnostic. fn resolveParameterizedWithBindings(self: *Lowering, pt: *const ast.ParameterizedTypeExpr, span: ?ast.Span) TypeId { const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name; const table = &self.module.types; // A namespaced base (`ns.Box(..)`) is an explicit qualified reach and is // exempt from the bare-head visibility gate; only a dotless head is // policed (E4). const is_qualified = std.mem.indexOfScalar(u8, pt.name, '.') != null; // Vector(N, T) — built-in parameterized type. A backtick raw base // (`` `Vector(…) ``) is the LITERAL user type named `Vector`, so it // skips this intrinsic and resolves through the template map (0089). if (!pt.is_raw and std.mem.eql(u8, base_name, "Vector")) { if (pt.args.len == 2) { const length = self.resolveVectorLane(pt.args[0]) orelse return .unresolved; const elem = self.resolveTypeWithBindings(pt.args[1]); return table.vectorOf(elem, length); } } // Generic-struct base: route through the single layout choke-point (CP-1). // Bare → the single bare-VISIBLE author (own / 1-hop flat), source-keyed; // qualified `ns.Box(..)` → ns's OWN template (or a missing-member diagnostic); // never the global last-wins map for a visible-shadowed or qualified head. { const alias: ?[]const u8 = if (std.mem.indexOfScalar(u8, pt.name, '.')) |dot| pt.name[0..dot] else null; switch (self.selectGenericStructHead(base_name, alias, is_qualified, span)) { .template => |t| return self.instantiateGenericStruct(&t, pt.args), .poisoned => return .unresolved, .not_generic => {}, } } // Parameterized protocol used as a value type (`VL(s64)`): materialize a // 16-byte protocol value with the type-arg bound (not a 0-field stub). if (self.program_index.protocol_ast_map.get(base_name)) |pd| { if (pd.type_params.len > 0) { if (!is_qualified and self.headTypeLeak(base_name, span)) return .unresolved; return self.instantiateParamProtocol(pd, pt.args); } } // User-defined type-returning function used as a TYPE annotation // (`b : Make(N, s64)` where `Make :: ($K: u32, $T: Type) -> Type`). The // `.call`-node path (`resolveTypeCallWithBindings`) already routes here; // a `parameterized_type_expr` must too, or the function name falls through // to the empty-struct stub below and `b.field` / `b.len` fails. const resolved_name = if (self.scope) |scope| (scope.lookupFn(base_name) orelse base_name) else base_name; if (self.program_index.fn_ast_map.get(resolved_name)) |fd| { if (fd.type_params.len > 0) { if (!is_qualified and self.headFnLeak(base_name, span)) return .unresolved; if (self.instantiateTypeFunction(base_name, base_name, fd, pt.args)) |ty| { return ty; } } } // The base names no known type constructor — not Vector, not a generic // struct template, not a parameterized protocol, not a type-returning // function. A silent 0-field stub here would mis-size every downstream // `b.field` / `b.len`; emit the diagnostic and poison with `.unresolved` // (the `.call`-node sibling `resolveTypeCallWithBindings` already poisons). if (self.diagnostics) |d| d.addFmt(.err, span, "unknown type '{s}'", .{base_name}); return .unresolved; } /// Instantiate a generic struct template with concrete args. /// E.g., Vec(3, f32) → struct Vec__3_f32 { data: Vector(3, f32) } /// A generic-struct instance method selected via the STAMPED authoring decl: /// the `fn_decl` to monomorphize, the instance's stored type bindings, and the /// instance (mangled / alias) name the monomorphized function is keyed under. const GenericStructMethod = struct { fd: *const ast.FnDecl, bindings: *std.StringHashMap(TypeId), inst_name: []const u8, }; /// THE single body-axis reader: select `method` of generic-struct instance /// `inst_name` via the instance's STAMPED author (`struct_instance_author`), /// so body-author ≡ layout-author by construction — never the global last-wins /// `fn_ast_map["Template.method"]` a 2-flat-hop same-name template's method /// could win. Null when `inst_name` is NOT a generic instance (no author stamp) /// — the caller's existing non-generic `fn_ast_map` path then handles it /// (non-generic structs, free fns, FFI), or when the confirmed author declares /// no such `method` (a normal unresolved-method, handled downstream). A /// confirmed instance whose author is present but whose bindings are missing is /// a LOUD invariant failure — instantiation writes both together (CP-2). fn genericInstanceMethod(self: *Lowering, inst_name: []const u8, method: []const u8) ?GenericStructMethod { const author = self.struct_instance_author.get(inst_name) orelse return null; const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse std.debug.panic("generic struct instance '{s}' has an author but no bindings", .{inst_name}); // INLINE struct method (`Box :: struct { make :: ... }`): selected via the // instance's STAMPED author, so the body is the one authored alongside the // layout — never the global last-wins `fn_ast_map["Template.method"]` a // 2-flat-hop same-name template's method could win (finding #1). if (structMethodFn(author, method)) |fd| return .{ .fd = fd, .bindings = bindings, .inst_name = inst_name }; // IMPL-block method (`impl P for Box { ... }`): registered under the // template name in `fn_ast_map`, not on the struct decl, so it is keyed by // template name (protocol dispatch). The author confirms this IS a generic // instance; the method body is the template's registered impl method. const tmpl_name = self.struct_instance_template.get(inst_name) orelse return null; const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, method }) catch return null; if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd| return .{ .fd = fd, .bindings = bindings, .inst_name = inst_name }; return null; } /// Monomorphize (once) the selected generic-instance method under /// `.` and return its FuncId. The source-pin follows the /// 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 { 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); } return self.resolveFuncByName(mangled); } /// Debug invariant (CP coverage lock): the two generic-instance maps written /// in lockstep at the SAME two writers (instantiation + alias copy) — /// `struct_instance_template` and `struct_instance_author` — must have /// coincident keysets. A future writer that registers an instance's layout /// without stamping its author (a silent body-axis reopen) trips this in a /// debug `zig build test`, not in production. pub fn assertInstanceMapsCoincide(self: *Lowering) void { if (!std.debug.runtime_safety) return; var it = self.struct_instance_template.keyIterator(); while (it.next()) |k| { if (!self.struct_instance_author.contains(k.*)) std.debug.panic("generic instance '{s}' has a template but no author stamp", .{k.*}); } var it2 = self.struct_instance_author.keyIterator(); while (it2.next()) |k| { if (!self.struct_instance_template.contains(k.*)) std.debug.panic("generic instance '{s}' has an author but no template stamp", .{k.*}); } } fn instantiateGenericStruct(self: *Lowering, tmpl: *const StructTemplate, args: []const *const Node) TypeId { const table = &self.module.types; // Build mangled name dynamically: StructName__arg1_arg2 var name_parts = std.ArrayList(u8).empty; name_parts.appendSlice(self.alloc, tmpl.name) catch {}; // A qualified `ns.Box(..)` head can select a generic template whose bare // name also belongs to a DIFFERENT module's same-name template (the one // that won the last-wins `struct_template_map`). Both would mangle to // `Box__s64` and the second instantiation would alias the first's layout. // Tag the NON-canonical author's mangled name with its source so each // author's instantiation is a distinct type. The canonical (bare-map) // author keeps the untagged name — no churn for single-author generics. if (self.program_index.struct_template_map.get(tmpl.name)) |canon| { const canon_src = canon.source_file orelse ""; const this_src = tmpl.source_file orelse ""; if (!std.mem.eql(u8, canon_src, this_src)) { var tag_buf: [24]u8 = undefined; const tag = std.fmt.bufPrint(&tag_buf, "$m{x}", .{std.hash.Wyhash.hash(0, this_src)}) catch ""; name_parts.appendSlice(self.alloc, tag) catch {}; } } // Bind type params to args and build name suffix const saved_type_bindings = self.type_bindings; const saved_value_bindings = self.comptime_value_bindings; const saved_pack_bindings = self.pack_bindings; const saved_pack_arg_types = self.pack_arg_types; var tb = std.StringHashMap(TypeId).init(self.alloc); var cvb = std.StringHashMap(i64).init(self.alloc); var pb = std.StringHashMap([]const TypeId).init(self.alloc); for (tmpl.type_params, 0..) |tp, i| { if (i >= args.len) break; // `..$Ts: []Type` — bind the REMAINING args as a type pack. if (tp.is_variadic) { var pack_tys = std.ArrayList(TypeId).empty; for (args[i..]) |a| { // A spread arg `..sources.T` expands to the source pack's // per-element (projected) types; a plain arg is one type. if (a.data == .spread_expr) { if (self.packResolver().packTypeElems(a.data.spread_expr.operand)) |elems| { defer self.alloc.free(elems); for (elems) |ty| { pack_tys.append(self.alloc, ty) catch {}; name_parts.appendSlice(self.alloc, "__") catch {}; name_parts.appendSlice(self.alloc, self.formatTypeName(ty)) catch {}; } continue; } } const ty = self.resolveTypeWithBindings(a); pack_tys.append(self.alloc, ty) catch {}; name_parts.appendSlice(self.alloc, "__") catch {}; name_parts.appendSlice(self.alloc, self.formatTypeName(ty)) catch {}; } pb.put(tp.name, pack_tys.toOwnedSlice(self.alloc) catch &.{}) catch {}; break; // a pack param is always last } name_parts.appendSlice(self.alloc, "__") catch {}; if (tp.is_type_param) { const ty = self.resolveTypeWithBindings(args[i]); tb.put(tp.name, ty) catch {}; const tname = self.formatTypeName(ty); name_parts.appendSlice(self.alloc, tname) catch {}; } else { // Value param (e.g., $N: u32) — fold to a compile-time integer // and range-check against its declared type. const val = self.resolveValueParamArg(args[i], tp.name, tp.value_type) orelse return .unresolved; cvb.put(tp.name, val) catch {}; var val_buf: [32]u8 = undefined; const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0"; name_parts.appendSlice(self.alloc, val_str) catch {}; } } const mangled_name = name_parts.items; // Check if already instantiated const name_id = table.internString(mangled_name); if (table.findByName(name_id)) |existing| { // Already registered — check if it has fields const info = table.get(existing); if (info == .@"struct" and info.@"struct".fields.len > 0) { // A confirmed generic instance must never be returned without an // author stamp — the body axis (CP-4) keys method selection off // it. The template/bindings were written at first instantiation; // re-stamp the author from THIS `tmpl` if the dedup fast-path is // the first to reach this mangled name (e.g. a layout interned by // a forward reference before any method dispatch). if (!self.struct_instance_author.contains(mangled_name)) { const owned = self.alloc.dupe(u8, mangled_name) catch return existing; self.struct_instance_author.put(owned, tmpl.decl) catch {}; } return existing; } } // Set up bindings and resolve fields. `pack_bindings` makes a // pack-shaped field type like `(..$Ts)` resolve to the bound type list. self.type_bindings = tb; self.comptime_value_bindings = cvb; self.pack_bindings = pb; self.pack_arg_types = pb; // Resolve the field type nodes in the TEMPLATE's source context, not the // (possibly cross-module) instantiation site. A field naming a type // visible only in the template's module then resolves correctly, and the // source-aware nominal leaf classifies main vs imported by the TEMPLATE's // file — so an undeclared field type (`y: Missing`) or a value param used // as a type (`x: N` for `$N: u32`) is diagnosed at the right authority // (the leaf for an imported template, the `UnknownTypeChecker` for a // main-file one) instead of silently fabricating a stub / poisoning with // `.unresolved` that panics at LLVM emission. const saved_src = self.current_source_file; const saved_diag_src = if (self.diagnostics) |d| d.current_source_file else null; if (tmpl.source_file) |sf| { self.current_source_file = sf; if (self.diagnostics) |d| d.current_source_file = sf; } var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (tmpl.field_names, tmpl.field_type_nodes) |fname, ftype_node| { const field_ty = self.resolveTypeWithBindings(ftype_node); fields.append(self.alloc, .{ .name = table.internString(fname), .ty = field_ty, }) catch unreachable; } self.current_source_file = saved_src; if (self.diagnostics) |d| d.current_source_file = saved_diag_src; // Restore bindings self.type_bindings = saved_type_bindings; self.comptime_value_bindings = saved_value_bindings; self.pack_bindings = saved_pack_bindings; self.pack_arg_types = saved_pack_arg_types; // Register the monomorphized struct const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; const id = if (table.findByName(name_id)) |existing| existing else table.intern(info); table.updatePreservingKey(id, info); // Bind the template name to this concrete instance so a method's // `self: *Combined` (the template name) resolves to `*Combined__s64_s64` // — otherwise `self.field` hits the 0-field generic stub. tb.put(tmpl.name, id) catch {}; // Store the type bindings, template name, and authoring decl for method // resolution. The author is stamped from the SAME `tmpl` that built the // layout above, so the body axis (CP-4) selects this instance's methods // via the layout author — never the global last-wins `fn_ast_map`. const owned_mangled = self.alloc.dupe(u8, mangled_name) catch return id; self.struct_instance_bindings.put(owned_mangled, tb) catch {}; self.struct_instance_template.put(owned_mangled, tmpl.name) catch {}; self.struct_instance_author.put(owned_mangled, tmpl.decl) catch {}; return id; } /// Instantiate a type-returning function: `Foo :: Complex(u32)` where /// `Complex :: ($T:Type) -> Type { return struct { value: T; count: u32; }; }` /// Walks the function body to find the returned struct/enum, resolves field types /// with the provided type bindings, and registers the result. pub fn instantiateTypeFunction(self: *Lowering, alias_name: []const u8, template_name: []const u8, fd: *const ast.FnDecl, args: []const *const Node) ?TypeId { const table = &self.module.types; // Build type bindings from params + args const saved_type_bindings = self.type_bindings; const saved_value_bindings = self.comptime_value_bindings; var tb = std.StringHashMap(TypeId).init(self.alloc); var cvb = std.StringHashMap(i64).init(self.alloc); // Build mangled name var name_parts = std.ArrayList(u8).empty; name_parts.appendSlice(self.alloc, template_name) catch {}; for (fd.type_params, 0..) |tp, i| { if (i >= args.len) break; name_parts.appendSlice(self.alloc, "__") catch {}; // Check if this is a Type param ($T: Type) or a value param ($N: u32) const is_type_param = if (tp.constraint.data == .type_expr) std.mem.eql(u8, tp.constraint.data.type_expr.name, "Type") else true; // default to type param if (is_type_param) { const ty = self.resolveTypeWithBindings(args[i]); tb.put(tp.name, ty) catch {}; const tname = self.formatTypeName(ty); name_parts.appendSlice(self.alloc, tname) catch {}; } else { // Value param (e.g., $N: u32) — fold to a compile-time integer // and range-check against its declared type. A failed bind has // already diagnosed itself, so poison to `.unresolved` rather // than `null`: `null` makes the caller fall through to the // empty-struct placeholder named after the fn, which then // cascades a bogus `field not found` on any later access. The // struct binder (`instantiateGenericStruct`) poisons the same way. const vp_type: ?[]const u8 = if (tp.constraint.data == .type_expr) tp.constraint.data.type_expr.name else null; const val = self.resolveValueParamArg(args[i], tp.name, vp_type) orelse return .unresolved; cvb.put(tp.name, val) catch {}; var val_buf: [32]u8 = undefined; const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0"; name_parts.appendSlice(self.alloc, val_str) catch {}; } } const mangled_name = name_parts.items; // Check if already instantiated const mangled_name_id = table.internString(mangled_name); if (table.findByName(mangled_name_id)) |existing| { const info = table.get(existing); if ((info == .@"struct" and info.@"struct".fields.len > 0) or info == .@"union" or info == .tagged_union) { return existing; } } // Activate bindings self.type_bindings = tb; self.comptime_value_bindings = cvb; defer { self.type_bindings = saved_type_bindings; self.comptime_value_bindings = saved_value_bindings; } // Resolve the type fn's body (inline struct/union fields, or the returned // type expression) in its OWN module (E4), so a 2-flat-hop library type // named there is bare-visible — not the cross-module call site. The arg // exprs above were already resolved in the caller's context. const saved_tf_src = self.current_source_file; defer self.setCurrentSourceFile(saved_tf_src); if (fd.body.source_file) |src| self.setCurrentSourceFile(src); // Determine if alias_name is a real alias (e.g., "Foo" for "Complex(u32)") // or just the template name itself (inline use like "Sx(f32)") const has_alias = !std.mem.eql(u8, alias_name, template_name); // Try struct first if (findStructInBody(fd.body)) |struct_decl| { // Resolve struct fields with type bindings active var struct_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (struct_decl.field_names, struct_decl.field_types) |fname, ftype_node| { const field_ty = self.resolveTypeWithBindings(ftype_node); struct_fields.append(self.alloc, .{ .name = table.internString(fname), .ty = field_ty, }) catch {}; } // Always register under mangled name const mangled_info: types.TypeInfo = .{ .@"struct" = .{ .name = mangled_name_id, .fields = struct_fields.items, } }; const mangled_id = if (table.findByName(mangled_name_id)) |existing| existing else table.intern(mangled_info); table.updatePreservingKey(mangled_id, mangled_info); // If there's a real alias, also register under alias name and in alias map if (has_alias) { const alias_name_id = table.internString(alias_name); const alias_info: types.TypeInfo = .{ .@"struct" = .{ .name = alias_name_id, .fields = struct_fields.items, } }; const alias_id = if (table.findByName(alias_name_id)) |existing| existing else table.intern(alias_info); table.updatePreservingKey(alias_id, alias_info); // Store defaults if any if (struct_decl.field_defaults.len > 0) { self.struct_defaults_map.put(alias_name, struct_decl.field_defaults) catch {}; } return alias_id; } return mangled_id; } // Try tagged enum/union if (findUnionInBody(fd.body)) |enum_decl| { return self.instantiateTypeUnion(if (has_alias) alias_name else mangled_name, mangled_name, &enum_decl); } // General case: the body returns a TYPE EXPRESSION that is not an inline // struct/union/enum — `return [K]T`, `Vector(K, T)`, `*T`, an alias, etc. // Resolve it with the value/type bindings active (so `[K]T` folds K to a // compile-time integer). The result is interned structurally, so // `Make(N, s64)`, `Make(3, s64)`, and `Make(M + 1, s64)` all yield the // same TypeId. `.unresolved` means the return wasn't a type expression // (e.g. a value-returning function in a type position) → fall through to // the caller's fallback rather than fabricating a type. if (findReturnTypeExpr(fd.body)) |ret_node| { const ty = self.resolveTypeWithBindings(ret_node); if (ty != .unresolved) return ty; } return null; } /// The type expression a type-returning function yields: the value of its /// `return` (block body) or the bare expression (arrow body / `=> [K]T`). /// Used for a non-struct/union return shape, which the struct/union body /// walkers above don't match. fn findReturnTypeExpr(body: *const Node) ?*const Node { if (body.data == .block) { for (body.data.block.stmts) |stmt| { if (stmt.data == .return_stmt) return stmt.data.return_stmt.value; } return null; } return body; } /// Instantiate a tagged enum from a type function body. fn instantiateTypeUnion(self: *Lowering, alias_name: []const u8, mangled_name: []const u8, ed: *const ast.EnumDecl) ?TypeId { const table = &self.module.types; // Build variant fields (tagged enum variants stored as StructInfo.Field) var variant_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (ed.variant_names, 0..) |vname, i| { const payload_ty: TypeId = if (i < ed.variant_types.len and ed.variant_types[i] != null) self.resolveTypeWithBindings(ed.variant_types[i].?) else .void; variant_fields.append(self.alloc, .{ .name = table.internString(vname), .ty = payload_ty, }) catch {}; } const alias_name_id = table.internString(alias_name); const info: types.TypeInfo = .{ .tagged_union = .{ .name = alias_name_id, .fields = variant_fields.items, .tag_type = .s64, } }; const id = if (table.findByName(alias_name_id)) |existing| existing else table.intern(info); table.updatePreservingKey(id, info); // Also register under mangled name if (!std.mem.eql(u8, alias_name, mangled_name)) { const mangled_name_id = table.internString(mangled_name); const mangled_info: types.TypeInfo = .{ .tagged_union = .{ .name = mangled_name_id, .fields = variant_fields.items, .tag_type = .s64, } }; const mid = if (table.findByName(mangled_name_id)) |existing| existing else table.intern(mangled_info); table.updatePreservingKey(mid, mangled_info); } return id; } /// Walk an AST body to find a struct declaration (from `return struct { ... }` or bare struct expr). fn findStructInBody(body: *const Node) ?ast.StructDecl { if (body.data == .struct_decl) return body.data.struct_decl; if (body.data == .block) { for (body.data.block.stmts) |stmt| { if (stmt.data == .return_stmt) { if (stmt.data.return_stmt.value) |val| { if (val.data == .struct_decl) return val.data.struct_decl; } } if (stmt.data == .struct_decl) return stmt.data.struct_decl; } } return null; } /// Walk an AST body to find a tagged enum declaration. fn findUnionInBody(body: *const Node) ?ast.EnumDecl { const isTaggedEnum = struct { fn check(node: *const Node) ?ast.EnumDecl { if (node.data == .enum_decl and node.data.enum_decl.variant_types.len > 0) { return node.data.enum_decl; } return null; } }; if (isTaggedEnum.check(body)) |ed| return ed; const stmts = if (body.data == .block) body.data.block.stmts else return null; for (stmts) |stmt| { if (stmt.data == .return_stmt) { if (stmt.data.return_stmt.value) |val| { if (isTaggedEnum.check(val)) |ed| return ed; } } if (isTaggedEnum.check(stmt)) |ed| return ed; } return null; } // ── Type registration ─────────────────────────────────────────── /// Register a struct declaration's fields and methods in the IR type table. /// Register a `Foo :: error { A, B }` declaration as an error-set type. /// Rejects an empty set here (sema gate) since type_bridge has no /// diagnostics; non-empty sets are interned via type_bridge. pub fn registerErrorSetDecl(self: *Lowering, node: *const Node) void { const esd = node.data.error_set_decl; if (esd.tag_names.len == 0) { if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "error set '{s}' must declare at least one tag", .{esd.name}); } return; } _ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } /// The `nominal_id` stamped on a nominal `TypeInfo` (0 for non-nominal / /// structural). Reading it back lets a re-registration preserve the slot's /// existing key when refreshing a forward-stubbed body. fn nominalIdOf(info: types.TypeInfo) u32 { return switch (info) { .@"struct" => |s| s.nominal_id, .@"enum" => |e| e.nominal_id, .@"union" => |u| u.nominal_id, .tagged_union => |u| u.nominal_id, .error_set => |e| e.nominal_id, else => 0, }; } /// Return `info` with its nominal arm's `nominal_id` set to `nid` (a no-op for /// non-nominal infos). Used to build the key-matching body for /// `updatePreservingKey` after a shadow author interned at a nonzero id. fn stampNominalId(info: types.TypeInfo, nid: u32) types.TypeInfo { var out = info; switch (out) { .@"struct" => |*s| s.nominal_id = nid, .@"enum" => |*e| e.nominal_id = nid, .@"union" => |*u| u.nominal_id = nid, .tagged_union => |*u| u.nominal_id = nid, .error_set => |*e| e.nominal_id = nid, else => {}, } return out; } /// Reserve a GENUINE same-name STRUCT shadow author's DISTINCT nominal slot /// BEFORE any field resolves, so a self / forward / mutual reference to a shadow /// name (`next: *Box`; `peer: *Node` where Node is a shadow declared later) /// binds to ITS nominal TypeId via `type_decl_tids` instead of the global /// findByName first-author fallback (issue 0105 / F1). Called only from the /// `scanDecls` genuine-shadow pass, which has already established that ≥2 /// distinct struct decls author this name; ALL of them reserve — the FIRST at /// id 0, the rest at fresh nonzero ids — so none falls through to the name-only /// `findByName` (which, once a shadow is interned, no longer uniquely identifies /// the first author). Idempotent per decl key: an already-reserved decl returns /// before re-invoking `shadowNominalId`, so the shadow id is computed once. /// Generic templates resolve lazily on instantiation and are skipped. fn reserveShadowStructSlot(self: *Lowering, sd: *const ast.StructDecl) void { if (sd.type_params.len > 0) return; const table = &self.module.types; const decl_key: *const anyopaque = @ptrCast(sd); if (table.type_decl_tids.contains(decl_key)) return; const name_id = table.internString(sd.name); const nominal_id = self.shadowNominalId(name_id); // 0 for the first author, nonzero for the rest const reserved = table.internNominal(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }, nominal_id); table.type_decl_tids.put(decl_key, reserved) catch {}; } /// Reserve a GENUINE same-name ENUM shadow author's DISTINCT nominal slot /// up-front — the enum twin of `reserveShadowStructSlot` (E6a). The reserved /// slot's KIND MUST match what `buildEnumInfo` will produce (a payload enum → /// `.tagged_union`, a payload-less enum → `.enum`), because `internNamedTypeDecl` /// later refreshes the body via `updatePreservingKey`, whose key-stability /// assert compares the FULL info tag — a struct/enum/tagged_union mismatch would /// trip it. The empty body and placeholder `tag_type` are not part of the intern /// key (name + nominal id only), so the real body fills in freely. fn reserveShadowEnumSlot(self: *Lowering, ed: *const ast.EnumDecl) void { const table = &self.module.types; const decl_key: *const anyopaque = @ptrCast(ed); if (table.type_decl_tids.contains(decl_key)) return; const name_id = table.internString(ed.name); const nominal_id = self.shadowNominalId(name_id); const empty: types.TypeInfo = if (ed.variant_types.len > 0) .{ .tagged_union = .{ .name = name_id, .fields = &.{}, .tag_type = .s64 } } else .{ .@"enum" = .{ .name = name_id, .variants = &.{} } }; const reserved = table.internNominal(empty, nominal_id); table.type_decl_tids.put(decl_key, reserved) catch {}; } /// Reserve a GENUINE same-name UNION shadow author's DISTINCT nominal slot /// up-front — the union twin of `reserveShadowStructSlot` (E6a). fn reserveShadowUnionSlot(self: *Lowering, ud: *const ast.UnionDecl) void { const table = &self.module.types; const decl_key: *const anyopaque = @ptrCast(ud); if (table.type_decl_tids.contains(decl_key)) return; const name_id = table.internString(ud.name); const nominal_id = self.shadowNominalId(name_id); const reserved = table.internNominal(.{ .@"union" = .{ .name = name_id, .fields = &.{} } }, nominal_id); table.type_decl_tids.put(decl_key, reserved) catch {}; } /// A top-level NAMED type decl the genuine-shadow scan tracks, KIND-tagged so /// same-name authors of DIFFERENT kinds (a `struct Foo` and an `enum Foo`) are /// NOT mistaken for one shadow group. Carries the stable decl pointer (the /// `decl_key` / raw-facts identity) so the scan de-dups by decl identity, and /// dispatches the per-kind reservation. Later E6 sub-steps add their kind here. const ShadowTypeDecl = union(enum) { @"struct": *const ast.StructDecl, @"enum": *const ast.EnumDecl, @"union": *const ast.UnionDecl, pub fn key(self: ShadowTypeDecl) *const anyopaque { return switch (self) { inline else => |p| @ptrCast(p), }; } pub fn name(self: ShadowTypeDecl) []const u8 { return switch (self) { inline else => |p| p.name, }; } pub fn isGeneric(self: ShadowTypeDecl) bool { return switch (self) { .@"struct" => |p| p.type_params.len > 0, else => false, }; } }; /// Classify a top-level node as the NAMED type decl it authors — a bare /// `struct`/`enum`/`union` node, or a `const_decl` whose value is one — so the /// genuine-shadow scan enumerates all three kinds uniformly. Null when the node /// is not a struct/enum/union author. The shared infra E6b/E6c extend by adding /// their kind here. pub fn topLevelTypeDecl(decl: *const Node) ?ShadowTypeDecl { return switch (decl.data) { .struct_decl => .{ .@"struct" = &decl.data.struct_decl }, .enum_decl => .{ .@"enum" = &decl.data.enum_decl }, .union_decl => .{ .@"union" = &decl.data.union_decl }, .const_decl => |cd| switch (cd.value.data) { .struct_decl => .{ .@"struct" = &cd.value.data.struct_decl }, .enum_decl => .{ .@"enum" = &cd.value.data.enum_decl }, .union_decl => .{ .@"union" = &cd.value.data.union_decl }, else => null, }, else => null, }; } /// Dispatch a genuine-shadow reservation to the matching per-kind reserver. pub fn reserveShadowSlot(self: *Lowering, td: ShadowTypeDecl) void { switch (td) { .@"struct" => |sd| self.reserveShadowStructSlot(sd), .@"enum" => |ed| self.reserveShadowEnumSlot(ed), .@"union" => |ud| self.reserveShadowUnionSlot(ud), } } /// Register (or re-register) a top-level NAMED type decl under a per-source /// nominal identity (E2), returning its TypeId. `decl_key` is the decl's /// stable pointer (the import raw-facts identity); `info` carries the full /// body; `nominal_id` is the slot's identity (0 for a single / first author, /// nonzero for a later same-name shadow) — computed once by the caller /// (`registerStructDecl`), which reuses the id reserved up-front in `scanDecls` /// for a genuine shadow (so its fields' self / forward / mutual refs already /// resolved against it). This stamps the id and records the `decl_key → TypeId` /// map (`type_decl_tids`, the `fn_decl_fids` analogue). /// /// A `nominal_id == 0` author adopts any forward-reference stub (`findByName` /// orelse intern) — BYTE-IDENTICAL to pre-E2 registration. For a genuinely /// multi-authored name, the FIRST source keeps id 0 and later sources get /// fresh ids → DISTINCT TypeIds, so the authors no longer collapse last-wins /// (issue 0105). Idempotent per `decl_key`: a re-registration — OR an up-front /// shadow reservation — reuses the recorded slot, refreshing its body via /// `updatePreservingKey` (key-stable because a struct's intern key is its /// name + nominal id, not its fields). fn internNamedTypeDecl(self: *Lowering, decl_key: *const anyopaque, name_id: types.StringId, info: types.TypeInfo, nominal_id: u32) TypeId { const table = &self.module.types; // Slot already recorded (re-registration, or a reserve-before-fields shadow // reservation) → reuse its slot + nominal id, refresh the body. if (table.type_decl_tids.get(decl_key)) |existing_id| { table.updatePreservingKey(existing_id, stampNominalId(info, nominalIdOf(table.get(existing_id)))); return existing_id; } const id = if (nominal_id == 0) (table.findByName(name_id) orelse table.internNominal(info, 0)) else table.internNominal(info, nominal_id); const stamped = stampNominalId(info, nominal_id); // A self / mutual `*Name` field in an enum/union body forward-creates a // STRUCT placeholder under `Name` (the stateless resolver has no kind // context — `type_resolver.resolveNamed` always stubs a struct), which the // `findByName` above then returns. Adopting a wrong-kind stub needs a // re-key, NOT the in-place `updatePreservingKey` body-fill — whose // kind-stability assert trips on struct→enum/union. if (adoptsForwardStructStub(table.get(id), stamped)) table.replaceKeyedInfo(id, stamped) else table.updatePreservingKey(id, stamped); table.type_decl_tids.put(decl_key, id) catch {}; return id; } /// TRUE when `existing` is a forward-reference STRUCT placeholder (empty /// fields — the stateless resolver's stub for an as-yet-unregistered name) and /// `incoming` is a NON-struct nominal (enum / union / tagged_union): the one /// case where `internNamedTypeDecl` must re-key the slot rather than fill its /// body in place. A struct adopting its own struct stub is same-kind and stays /// on `updatePreservingKey`; a fresh-interned slot has no stub to adopt. fn adoptsForwardStructStub(existing: types.TypeInfo, incoming: types.TypeInfo) bool { if (existing != .@"struct" or existing.@"struct".fields.len != 0) return false; return switch (incoming) { .@"enum", .@"union", .tagged_union => true, else => false, }; } /// The `nominal_id` to register a NAMED type author of `name_id` under. 0 /// unless `name_id` is authored as a named type by ≥2 distinct modules (a real /// same-name shadow per the import facts): the FIRST source to register keeps /// 0, each later source gets a fresh monotonic id. Gating on the import facts /// keeps the single-author path at id 0 (byte-identical) even when one logical /// type is re-registered from several `current_source_file` contexts. fn shadowNominalId(self: *Lowering, name_id: types.StringId) u32 { if (!self.nameHasMultipleTypeAuthors(self.module.types.getString(name_id))) return 0; const src = self.current_source_file orelse self.main_file orelse ""; const gop = self.nominal_name_authors.getOrPut(name_id) catch return 0; if (!gop.found_existing) { gop.value_ptr.* = src; return 0; } if (std.mem.eql(u8, gop.value_ptr.*, src)) return 0; self.next_nominal_id += 1; return self.next_nominal_id; } /// TRUE iff `name` is authored AS A NAMED TYPE (struct / enum / union / /// error-set / protocol / foreign class) by ≥2 DISTINCT modules in the import /// raw facts — the authoritative same-name-shadow signal (the only case where /// distinct `nominal_id`s are needed). Module distinctness is by LEXICALLY /// NORMALIZED path: one logical file reached through several spellings /// (`testpkg/../allocators.sx` vs `allocators.sx`) is cached — and so parsed — /// twice, landing two `module_decls` entries with two decl pointers for the /// SAME source; normalizing collapses them to one author, NOT a false shadow. /// False when the facts are unwired (comptime / registration host with no /// `module_decls`): the single-author path applies, correct there. fn nameHasMultipleTypeAuthors(self: *Lowering, name: []const u8) bool { const decls = self.program_index.module_decls orelse return false; var first_norm: ?[]const u8 = null; defer if (first_norm) |f| self.alloc.free(f); var it = decls.iterator(); while (it.next()) |entry| { const m = entry.value_ptr; const ref = m.names.get(name) orelse continue; if (rawNamedTypePtr(ref) == null) continue; const norm = std.fs.path.resolvePosix(self.alloc, &.{entry.key_ptr.*}) catch continue; if (first_norm) |f| { defer self.alloc.free(norm); if (!std.mem.eql(u8, f, norm)) return true; } else { first_norm = norm; } } return false; } /// The opaque decl-pointer identity of a NAMED-type `RawDeclRef`, or null when /// the ref is not a named type (fn / value-const / namespace alias). Used to /// de-dup same-name authors by decl identity. fn rawNamedTypePtr(ref: resolver_mod.RawDeclRef) ?*const anyopaque { return switch (ref) { .struct_decl => |d| @ptrCast(d), .enum_decl => |d| @ptrCast(d), .union_decl => |d| @ptrCast(d), .error_set_decl => |d| @ptrCast(d), .protocol_decl => |d| @ptrCast(d), .foreign_class_decl => |d| @ptrCast(d), .fn_decl, .const_decl, .namespace_decl => null, }; } /// Build an owned generic-struct template (type params, field names, field /// type nodes) for `sd`, pinned to its declaring `source_file`. The returned /// template is heap-owned via `self.alloc`; callers register it under a bare /// or namespace-qualified key. Null on OOM. fn buildGenericStructTemplate(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) ?StructTemplate { const owned_name = self.alloc.dupe(u8, sd.name) catch return null; const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return null; for (sd.type_params, 0..) |tp, i| { const is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: { const cname = tp.constraint.data.type_expr.name; // "Type" or a protocol name → type param break :blk std.mem.eql(u8, cname, "Type") or self.program_index.protocol_decl_map.contains(cname) or self.program_index.protocol_ast_map.contains(cname); } else false); tps[i] = .{ .name = self.alloc.dupe(u8, tp.name) catch return null, // $T: Type, $T: Lerpable, $T: Type/Eq — all are type params. // `..$Ts: []Type` (variadic) is a type-pack param. Only value // params like $N: u32 are non-type. .is_type_param = is_type_param, .is_variadic = tp.is_variadic, // Capture a value param's declared type name (`$K: u32` → // "u32") so instantiation can range-check the folded arg. .value_type = if (!is_type_param and tp.constraint.data == .type_expr) (self.alloc.dupe(u8, tp.constraint.data.type_expr.name) catch null) else null, }; } const fnames = self.alloc.alloc([]const u8, sd.field_names.len) catch return null; for (sd.field_names, 0..) |fn_str, i| { fnames[i] = self.alloc.dupe(u8, fn_str) catch return null; } // Field type nodes are *Node pointers into the AST; copy the slice of // pointers (the nodes themselves are heap-allocated). const ftype_nodes = self.alloc.dupe(*const Node, sd.field_types) catch return null; return .{ .name = owned_name, .type_params = tps, .field_names = fnames, .field_type_nodes = ftype_nodes, .source_file = source_file, .decl = sd, }; } /// Select the generic struct template AUTHORED by namespace `alias`'s target /// module (the `importer → alias → NamespaceTarget` edge), not the bare /// last-wins `struct_template_map`. A qualified head `ns.Box(..)` must /// instantiate ns's OWN `Box`, even when another module's same-name `Box` won /// the bare map. Null when the alias is unknown in the current source or its /// module authors no such generic struct — the caller then falls back to the /// legacy bare lookup. fn qualifiedStructTemplate(self: *Lowering, alias: []const u8, member: []const u8) ?StructTemplate { const edges = self.program_index.namespace_edges orelse return null; const from = self.current_source_file orelse return null; const alias_map = edges.getPtr(from) orelse return null; const target = alias_map.get(alias) orelse return null; for (target.own_decls) |decl| { // A top-level struct is authored either as a bare `struct_decl` node // or a `const_decl` whose value is one (`Box :: struct($T){...}`). const sd: *const ast.StructDecl = switch (decl.data) { .struct_decl => |*s| s, .const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else continue, else => continue, }; if (!std.mem.eql(u8, sd.name, member)) continue; if (sd.type_params.len == 0) continue; return self.buildGenericStructTemplate(sd, decl.source_file orelse target.target_module_path); } return null; } /// TRUE iff `alias` is a KNOWN namespace in the current source but its target /// module authors NO member named `member` at all. A qualified generic head /// `a.Box(..)` whose namespace lacks `Box` must diagnose the missing member — /// never silently fall back to the bare last-wins `struct_template_map` (which /// would instantiate an unrelated module's same-name `Box`, E4 finding #2). /// FALSE when `alias` is not a namespace at all (leave the caller's existing /// non-namespace handling), or when the namespace DOES author `member` (a /// generic struct → `qualifiedStructTemplate` already selected it; any other /// kind → the type-fn / named-type arms handle it). fn qualifiedMemberMissing(self: *Lowering, alias: []const u8, member: []const u8) bool { const edges = self.program_index.namespace_edges orelse return false; const from = self.current_source_file orelse return false; const alias_map = edges.getPtr(from) orelse return false; const target = alias_map.get(alias) orelse return false; for (target.own_decls) |decl| { const dn = decl.data.declName() orelse continue; if (std.mem.eql(u8, dn, member)) return false; } return true; } /// The bare-VISIBLE single generic-struct author of `name` (its `StructDecl` + /// defining source) when that author is NOT the one the global last-wins /// `struct_template_map` already holds — the E4 non-transitive selection for a /// bare generic head / alias / static-method head whose visible author (own or /// a single 1-hop flat import) is shadowed in the global map by a NON-visible /// (≥2-flat-hop) same-name template (finding #1). Exposing the decl (not just a /// rebuilt template) lets a static-method head source-pin the METHOD body too, /// not only the type layout. Null — caller uses the global map unchanged /// (byte-identical) — when: no source context; the single visible author IS the /// canonical map author (the common single-author case, matched by source /// file); or the visible picture is not a clean single generic-struct author /// (own non-generic shadow, or ≥2 flat authors whose ambiguity `headTypeLeak` /// has already diagnosed + poisoned before this is consulted). fn bareVisibleStructDecl(self: *Lowering, name: []const u8) ?VisibleStructAuthor { if (self.emitting_default_context) return null; const from = self.current_source_file orelse return null; const canon = self.program_index.struct_template_map.get(name) orelse return null; const canon_src = canon.source_file orelse ""; var res_walk = self.resolver(); const set = res_walk.collectVisibleAuthors(name, from, .user_bare_flat); defer if (set.flat.len > 0) self.alloc.free(set.flat); // Own author wins — must be a generic struct to count. if (set.own) |own| { const sd = structDeclOfRaw(own.raw) orelse return null; // alias / fn / other → skip if (sd.type_params.len == 0) return null; if (std.mem.eql(u8, from, canon_src)) return null; return .{ .sd = sd, .source = from }; } // Single flat-import generic-struct author. var picked: ?*const ast.StructDecl = null; var picked_src: []const u8 = ""; for (set.flat) |fa| { const sd = structDeclOfRaw(fa.raw) orelse continue; if (sd.type_params.len == 0) continue; if (picked != null) return null; // ≥2 visible authors picked = sd; picked_src = fa.source; } const sd = picked orelse return null; if (std.mem.eql(u8, picked_src, canon_src)) return null; return .{ .sd = sd, .source = picked_src }; } /// The rebuilt, source-pinned generic struct TEMPLATE of the single bare-VISIBLE /// author (`bareVisibleStructDecl`) — instantiate this INSTEAD of the global /// last-wins map entry. Null under the same conditions `bareVisibleStructDecl` /// returns null (caller keeps the global map, byte-identical). fn bareVisibleStructTemplate(self: *Lowering, name: []const u8) ?StructTemplate { const v = self.bareVisibleStructDecl(name) orelse return null; return self.buildGenericStructTemplate(v.sd, v.source); } /// Instantiate a generic struct template and register the result under an /// alias name (`Vec3 :: Vec(3, f32)` / `ABox :: a.Box(s64)`). Shared by the /// `.call` and `.parameterized_type_expr` const-decl alias branches and the /// qualified-head selection that precedes the bare `struct_template_map` /// fallback in each. pub fn registerGenericStructAlias(self: *Lowering, alias_name: []const u8, tmpl: *const StructTemplate, args: []const *const Node) void { const inst_id = self.instantiateGenericStruct(tmpl, args); const alias_name_id = self.module.types.internString(alias_name); const inst_info = self.module.types.get(inst_id); if (inst_info != .@"struct") return; const alias_info: types.TypeInfo = .{ .@"struct" = .{ .name = alias_name_id, .fields = inst_info.@"struct".fields, } }; const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info); self.module.types.updatePreservingKey(alias_id, alias_info); // A generic-struct instantiation alias IS a type author: route it through // the unified writer so it lands in `type_aliases_by_source` and the // bare-TYPE gate treats it like any other alias. self.putTypeAlias(self.current_source_file, alias_name, alias_id); // CP-3: the alias display name (`ABox`) is the struct type name a receiver // typed `x: ABox` reports, so method dispatch on it looks up the instance // maps under `ABox`. Mirror the mangled instance's template/bindings/author // onto the alias name so an alias-typed receiver is a first-class dispatch // instance (runs the selected author's body + bindings), not a dead end. const inst_name = self.formatTypeName(inst_id); if (self.struct_instance_author.get(inst_name)) |author_decl| { const tmpl_name = self.struct_instance_template.get(inst_name) orelse return; const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse return; self.struct_instance_template.put(self.alloc.dupe(u8, alias_name) catch return, tmpl_name) catch {}; self.struct_instance_bindings.put(self.alloc.dupe(u8, alias_name) catch return, bindings.*) catch {}; self.struct_instance_author.put(self.alloc.dupe(u8, alias_name) catch return, author_decl) catch {}; } } pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void { const table = &self.module.types; const name_id = table.internString(sd.name); // Generic structs: store as owned template, don't resolve fields yet if (sd.type_params.len > 0) { const tmpl = self.buildGenericStructTemplate(sd, source_file) orelse return; self.program_index.struct_template_map.put(tmpl.name, tmpl) catch {}; // S1.1 (additive): key the template by DeclId in parallel. Nothing // reads this for selection yet; `struct_template_map` stays the live // consumer. A template whose decl is not in the table (comptime / // block-local registration with facts unwired) keeps only the // name-keyed entry. if (self.program_index.decl_table) |dt| { if (dt.declIdForStructDecl(sd)) |id| { self.program_index.struct_template_by_decl.put(id, tmpl) catch {}; } } // Register methods under "TemplateName.method" in fn_ast_map for (sd.methods) |method_node| { if (method_node.data == .fn_decl) { const method_fd = &method_node.data.fn_decl; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue; self.program_index.fn_ast_map.put(qualified, method_fd) catch {}; } } return; } // Per-decl nominal identity (E2). EACH author of a GENUINE same-name STRUCT // shadow already reserved its distinct slot up-front in `scanDecls` (the // first at id 0, the rest at nonzero ids), so a self / forward / mutual // reference to the shadow name bound to ITS nominal TypeId via // `type_decl_tids`, not the global findByName first-author fallback (issue // 0105 / F1): reuse that reserved id. A single-author name (or a phantom // over-counted by the raw import facts) was NOT reserved — it keeps id 0 and // the legacy post-field registration, byte-identical to pre-F1. // `shadowNominalId` here only fires for the non-scanDecls registration paths // (comptime `lowerDecls`, block-local), where module facts are unwired so it // returns 0. const decl_key: *const anyopaque = @ptrCast(sd); const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id); // Build field list, expanding #using entries var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; var field_idx: usize = 0; var using_idx: usize = 0; const total_explicit = sd.field_names.len; while (field_idx < total_explicit or using_idx < sd.using_entries.len) { // Insert #using fields at their declared positions while (using_idx < sd.using_entries.len and sd.using_entries[using_idx].insert_index == fields.items.len) { const ue = sd.using_entries[using_idx]; const used_name_id = table.internString(ue.type_name); if (table.findByName(used_name_id)) |used_ty| { const used_info = table.get(used_ty); if (used_info == .@"struct") { for (used_info.@"struct".fields) |f| { fields.append(self.alloc, f) catch unreachable; } } } using_idx += 1; } if (field_idx < total_explicit) { const field_ty = self.resolveType(sd.field_types[field_idx]); fields.append(self.alloc, .{ .name = table.internString(sd.field_names[field_idx]), .ty = field_ty, }) catch unreachable; field_idx += 1; } else break; } // Append remaining #using entries after all explicit fields while (using_idx < sd.using_entries.len) { const ue = sd.using_entries[using_idx]; const used_name_id = table.internString(ue.type_name); if (table.findByName(used_name_id)) |used_ty| { const used_info = table.get(used_ty); if (used_info == .@"struct") { for (used_info.@"struct".fields) |f| { fields.append(self.alloc, f) catch unreachable; } } } using_idx += 1; } // Qualify inline __anon type names: __anon → StructName.field_name for (sd.field_names, 0..) |fname, fi| { if (fi < fields.items.len) { const field_ty = fields.items[fi].ty; if (!field_ty.isBuiltin()) { self.qualifyAnonType(table, field_ty, sd.name, fname); } } } // Register under the per-decl nominal identity computed above. A non-first // shadow author's slot was already reserved before fields resolved, so this // fills it (key-stable updatePreservingKey); a first / single author adopts // any forward-reference stub. Same-name structs in DIFFERENT sources get // distinct TypeIds instead of last-wins clobbering the first (issue 0105). const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); // Store field defaults for struct literal lowering if (sd.field_defaults.len > 0) { var has_any_default = false; for (sd.field_defaults) |d| { if (d != null) { has_any_default = true; break; } } if (has_any_default) { self.struct_defaults_map.put(sd.name, sd.field_defaults) catch {}; } } // Register struct methods as StructName.method in fn_ast_map for (sd.methods) |method_node| { if (method_node.data == .fn_decl) { const method_fd = &method_node.data.fn_decl; // Build qualified name: StructName.method const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue; self.program_index.fn_ast_map.put(qualified, method_fd) catch {}; // Declare extern stub (body is lowered lazily on demand) self.declareFunction(method_fd, qualified); } } // Register struct-level constants (e.g., GRAVITY :f32: 9.81) for (sd.constants) |const_node| { if (const_node.data == .const_decl) { const cd = const_node.data.const_decl; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, cd.name }) catch continue; const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table, &self.program_index.type_alias_map, &self.program_index.module_const_map) else null; self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {}; } } } /// Register a top-level ENUM decl under a per-decl nominal identity (E6a) — /// the enum twin of `registerStructDecl`. A GENUINE same-name shadow already /// reserved its DISTINCT slot up-front in `scanDecls` (the first at id 0, the /// rest at nonzero ids), so a forward / self / mutual reference to the shadow /// name already bound to ITS nominal TypeId via `type_decl_tids`: reuse that /// reserved id. A single-author name (or one over-counted by the raw facts but /// not a genuine scanned shadow) was NOT reserved — it keeps id 0 and the legacy /// post-build registration, byte-identical to pre-E6a. The body is built once by /// the shared `type_bridge.buildEnumInfo`; `internNamedTypeDecl` interns it under /// the computed nominal id and records `decl_key → TypeId` so `namedRefTid` /// resolves bare references to this exact author. pub fn registerEnumDecl(self: *Lowering, ed: *const ast.EnumDecl) void { const table = &self.module.types; const name_id = table.internString(ed.name); const decl_key: *const anyopaque = @ptrCast(ed); const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id); const info = type_bridge.buildEnumInfo(ed, table, &self.program_index.type_alias_map, &self.program_index.module_const_map); _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); } /// Register a top-level UNION decl under a per-decl nominal identity (E6a) — /// the union twin of `registerEnumDecl` / `registerStructDecl`. pub fn registerUnionDecl(self: *Lowering, ud: *const ast.UnionDecl) void { const table = &self.module.types; const name_id = table.internString(ud.name); const decl_key: *const anyopaque = @ptrCast(ud); const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id); const info = type_bridge.buildUnionInfo(ud, table, &self.program_index.type_alias_map, &self.program_index.module_const_map); _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); } /// Rename an __anon type to a qualified name: ParentStruct.field_name /// Also renames variant payload struct types from __anon.X to ParentStruct.field_name.X fn qualifyAnonType(self: *Lowering, table: *types.TypeTable, ty: TypeId, parent_name: []const u8, field_name: []const u8) void { const ti = table.get(ty); switch (ti) { .@"union" => |u| { const old_name = table.getString(u.name); if (!std.mem.eql(u8, old_name, "__anon")) return; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; const qname_id = table.internString(qualified); table.replaceKeyedInfo(ty, .{ .@"union" = .{ .name = qname_id, .fields = u.fields } }); }, .tagged_union => |u| { const old_name = table.getString(u.name); if (!std.mem.eql(u8, old_name, "__anon")) return; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; const qname_id = table.internString(qualified); // Rename variant payload structs: __anon.X → ParentStruct.field.X for (u.fields) |f| { if (!f.ty.isBuiltin()) { const finfo = table.get(f.ty); if (finfo == .@"struct") { const sname = table.getString(finfo.@"struct".name); if (std.mem.startsWith(u8, sname, "__anon.")) { const suffix = sname["__anon".len..]; // .VariantName const sq = std.fmt.allocPrint(self.alloc, "{s}{s}", .{ qualified, suffix }) catch continue; const sq_id = table.internString(sq); table.replaceKeyedInfo(f.ty, .{ .@"struct" = .{ .name = sq_id, .fields = finfo.@"struct".fields } }); } } } } table.replaceKeyedInfo(ty, .{ .tagged_union = .{ .name = qname_id, .fields = u.fields, .tag_type = u.tag_type, .backing_type = u.backing_type, .explicit_tag_values = u.explicit_tag_values } }); }, .@"enum" => |e| { const old_name = table.getString(e.name); if (!std.mem.eql(u8, old_name, "__anon")) return; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; const qname_id = table.internString(qualified); table.replaceKeyedInfo(ty, .{ .@"enum" = .{ .name = qname_id, .variants = e.variants, .explicit_values = e.explicit_values } }); }, .@"struct" => |s| { const old_name = table.getString(s.name); if (!std.mem.eql(u8, old_name, "__anon")) return; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; const qname_id = table.internString(qualified); table.replaceKeyedInfo(ty, .{ .@"struct" = .{ .name = qname_id, .fields = s.fields } }); }, else => {}, } } /// Register a protocol declaration as a struct type in the IR type table. /// Inline protocols: { ctx: *void, method1: *void, method2: *void, ... } /// Non-inline protocols: { ctx: *void, __vtable: *void } /// Also stores protocol info for dispatch and vtable struct type for vtable protocols. /// Register a protocol declaration. Thin delegation to the canonical owner /// (`ProtocolResolver`, `protocols.zig`); kept on `Lowering` as a `pub` /// entry point because the scan pass + several unit tests reach it here. pub fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void { return self.protocolResolver().registerProtocolDecl(pd); } /// Instantiate a parameterized protocol as a runtime VALUE type: /// `VL(s64)` → a 16-byte `{ctx, __vtable}` protocol value (`is_protocol`), /// with method infos resolved under the type-arg binding (so `get -> T` /// becomes `get -> s64`) and the binding recorded for projection. Cached by /// the mangled name `VL__s64`. Mirrors the non-parameterized path in /// `registerProtocolDecl`. fn instantiateParamProtocol(self: *Lowering, pd: *const ast.ProtocolDecl, args: []const *const Node) TypeId { const table = &self.module.types; const void_ptr_ty = table.ptrTo(.void); var np = std.ArrayList(u8).empty; np.appendSlice(self.alloc, pd.name) catch {}; var tb = std.StringHashMap(TypeId).init(self.alloc); for (pd.type_params, 0..) |tp, i| { if (i >= args.len) break; const ty = self.resolveTypeWithBindings(args[i]); tb.put(tp.name, ty) catch {}; np.appendSlice(self.alloc, "__") catch {}; np.appendSlice(self.alloc, self.formatTypeName(ty)) catch {}; } const mangled = np.items; const name_id = table.internString(mangled); if (table.findByName(name_id)) |existing| { const info = table.get(existing); if (info == .@"struct" and info.@"struct".is_protocol) return existing; } // Value struct: {ctx, __vtable} (or ctx + fn-ptrs for an inline protocol). var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; fields.append(self.alloc, .{ .name = table.internString("ctx"), .ty = void_ptr_ty }) catch unreachable; if (pd.is_inline) { for (pd.methods) |m| fields.append(self.alloc, .{ .name = table.internString(m.name), .ty = void_ptr_ty }) catch unreachable; } else { fields.append(self.alloc, .{ .name = table.internString("__vtable"), .ty = void_ptr_ty }) catch unreachable; } const struct_info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items, .is_protocol = true } }; const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info); table.updatePreservingKey(id, struct_info); // Method infos resolved with the type-arg binding (T → s64), pinned to // the protocol's OWN module (E4) so a method-signature type visible only // there resolves correctly when instantiated cross-module. `Self` and the // bound type-args short-circuit before the leaf; a concrete library type // in a signature is the case this pin protects. const saved_tb = self.type_bindings; self.type_bindings = tb; const saved_pp_src = self.current_source_file; defer self.setCurrentSourceFile(saved_pp_src); if (pd.source_file) |src| self.setCurrentSourceFile(src); var method_infos = std.ArrayList(ProtocolMethodInfo).empty; for (pd.methods) |method| { var ptypes = std.ArrayList(TypeId).empty; for (method.params) |p| { const pty = blk: { if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) break :blk void_ptr_ty; break :blk self.resolveTypeWithBindings(p); }; ptypes.append(self.alloc, pty) catch unreachable; } var ret_is_self = false; const ret = if (method.return_type) |rt| blk: { if (rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Self")) { ret_is_self = true; break :blk void_ptr_ty; } break :blk self.resolveTypeWithBindings(rt); } else .void; method_infos.append(self.alloc, .{ .name = method.name, .param_types = self.alloc.dupe(TypeId, ptypes.items) catch unreachable, .ret_type = ret, .ret_is_self = ret_is_self, }) catch unreachable; } self.type_bindings = saved_tb; const owned = self.alloc.dupe(u8, mangled) catch return id; self.program_index.protocol_decl_map.put(owned, .{ .name = owned, .is_inline = pd.is_inline, .methods = self.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable, }) catch {}; // Record the type-arg binding so projection (`xs.T`, `.value`) and // method-arg resolution on this instance can recover it. self.struct_instance_bindings.put(owned, tb) catch {}; if (!pd.is_inline) { var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (pd.methods) |m| vtable_fields.append(self.alloc, .{ .name = table.internString(m.name), .ty = void_ptr_ty }) catch unreachable; var vtable_name_buf: [192]u8 = undefined; const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{mangled}) catch "__Vtable"; const vtable_ty = table.intern(.{ .@"struct" = .{ .name = table.internString(vtable_name), .fields = vtable_fields.items } }); self.protocol_vtable_type_map.put(owned, vtable_ty) catch {}; } return id; } // ── Pack projection name resolution (Feature 1, Decision 4) ────────── // // A `..pack.` projection can target two protocol namespaces: // - type-arg namespace: the `protocol($T, ...)` params. // - runtime-accessor namespace: the protocol's methods (protocols have // no fields; a zero-arg method like `value` is the accessor). // Resolution is POSITION-driven, not precedence-driven: type position // consults type-args, value position consults methods, with NO // cross-namespace fallback. pub const ProjectionPosition = enum { type_position, value_position }; pub const PackProjection = union(enum) { type_arg: u32, // index into the protocol's `type_params` method: u32, // index into the protocol's `methods` not_found, // `name` absent from the position-selected namespace }; /// Find `name` in `protocol_name`'s type-arg namespace (`protocol($T,...)`). /// Returns the `type_params` index, or null (also for unknown protocols). pub fn lookupProtocolArg(self: *Lowering, protocol_name: []const u8, name: []const u8) ?u32 { const pd = self.program_index.protocol_ast_map.get(protocol_name) orelse return null; for (pd.type_params, 0..) |tp, i| { if (std.mem.eql(u8, tp.name, name)) return @intCast(i); } return null; } /// Find `name` in `protocol_name`'s runtime-accessor namespace (its methods /// — protocols have no fields). Returns the `methods` index, or null. pub fn lookupProtocolField(self: *Lowering, protocol_name: []const u8, name: []const u8) ?u32 { const pd = self.program_index.protocol_ast_map.get(protocol_name) orelse return null; for (pd.methods, 0..) |m, i| { if (std.mem.eql(u8, m.name, name)) return @intCast(i); } return null; } /// Resolve `..pack.` against `protocol_name` by position (Decision 4). /// No cross-namespace fallback: a value-position name that exists only as a /// type-arg (or vice versa) is `.not_found`, letting the caller emit a /// position-specific diagnostic (G3, Step 2.7). pub fn resolvePackProjection( self: *Lowering, protocol_name: []const u8, name: []const u8, pos: ProjectionPosition, ) PackProjection { return switch (pos) { .type_position => if (self.lookupProtocolArg(protocol_name, name)) |i| .{ .type_arg = i } else .not_found, .value_position => if (self.lookupProtocolField(protocol_name, name)) |i| .{ .method = i } else .not_found, }; } /// Register a foreign-class declaration. The alias goes into /// `foreign_class_map` for method-dispatch lookup. The underlying /// type (e.g. `*Activity`) is resolved via the existing struct /// fallback in `type_bridge.resolveTypeName` (which interns unknown /// named types as 0-field structs). /// /// sx-defined Obj-C classes (no `#foreign`, runtime == .objc_class) /// also land in `module.objc_defined_class_cache` in declaration /// order AND have their bodied methods registered into `fn_ast_map` /// under qualified names `.`. Lazy lowering /// then handles the body via the standard path; `*Self` is /// substituted to `*State` during body lowering (M1.2 A.2b). pub fn registerForeignClassDecl(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { self.program_index.foreign_class_map.put(fcd.name, fcd) catch {}; if (!fcd.is_foreign and fcd.runtime == .objc_class) { if (self.module.lookupObjcDefinedClass(fcd.name) == null) { self.module.appendObjcDefinedClass(fcd.name, fcd); // M2.3 — resolve the `#extends` alias to the actual // Obj-C runtime class name. `#extends NSObjectBase` // where NSObjectBase is aliased to "NSObject" must // pass "NSObject" to objc_allocateClassPair, otherwise // the runtime's class-hierarchy link is broken and // inherited-method dispatch fails. self.module.setObjcDefinedClassParent(fcd.name, self.resolveObjcParentName(fcd)); // M1.2 A.4b.i: per-class ivar handle global. The class-pair // init constructor (emit_llvm) populates it via // class_getInstanceVariable after the class is registered; // IMP trampolines read it to find the __sx_state ivar. self.declareObjcDefinedStateIvarGlobal(fcd.name); // M1.2 A.6: per-class class-object global. -dealloc reads // it to build an `objc_super` struct for `[super dealloc]` // dispatch via `objc_msgSendSuper2`. self.declareObjcDefinedClassGlobal(fcd.name); } self.registerObjcDefinedClassMethods(fcd); } } /// Resolve the `#extends ParentAlias` declaration on a sx-defined /// `#objc_class` to the actual Obj-C runtime class name. Falls /// back to "NSObject" when no `#extends` is declared. /// Aliases that resolve to foreign Obj-C classes use the /// foreign_path; aliases for OTHER sx-defined classes use the /// alias name directly (which equals the Obj-C class name for /// sx-defined classes). fn resolveObjcParentName(self: *Lowering, fcd: *const ast.ForeignClassDecl) []const u8 { for (fcd.members) |m| switch (m) { .extends => |alias| { if (self.program_index.foreign_class_map.get(alias)) |parent_fcd| { if (parent_fcd.is_foreign) return parent_fcd.foreign_path; // Sx-defined parent — its alias IS its Obj-C name. return parent_fcd.name; } // Unknown alias — pass through as-is and let the // runtime diagnose if it's genuinely wrong. return alias; }, else => {}, }; return "NSObject"; } /// Declare a per-class global `___state_ivar : *void = null`. /// emit_llvm's `emitObjcDefinedClassInit` constructor fills it in via /// `class_getInstanceVariable(cls, "__sx_state")` once per module load. fn declareObjcDefinedStateIvarGlobal(self: *Lowering, class_name: []const u8) void { const gname = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{class_name}) catch return; const name_id = self.module.types.internString(gname); _ = self.module.addGlobal(.{ .name = name_id, .ty = self.module.types.ptrTo(.void), .init_val = .null_val, .is_extern = false, .is_const = false, }); } /// Declare a per-class global `___class : *void = null`. /// emit_llvm's `emitObjcDefinedClassInit` constructor stores the /// freshly-allocated Class pointer into it after objc_registerClassPair. /// The synthesized `-dealloc` IMP reads it to construct an `objc_super` /// for `[super dealloc]` dispatch. fn declareObjcDefinedClassGlobal(self: *Lowering, class_name: []const u8) void { const gname = std.fmt.allocPrint(self.alloc, "__{s}_class", .{class_name}) catch return; const name_id = self.module.types.internString(gname); _ = self.module.addGlobal(.{ .name = name_id, .ty = self.module.types.ptrTo(.void), .init_val = .null_val, .is_extern = false, .is_const = false, }); } /// For each bodied instance method on an sx-defined `#objc_class`, /// synthesize an `FnDecl` from the `ForeignMethodDecl`, register it /// in `fn_ast_map` under `.`, declare the IR /// function, AND collect per-method registration data (selector /// mangling + type encoding + IMP symbol name) into the class's /// cache entry so emit_llvm can wire up `class_addMethod` calls /// (M1.2 A.4b.iii). Bodyless declarations are skipped — they /// reference inherited / external methods, not sx-side bodies. fn registerObjcDefinedClassMethods(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { // Set current_foreign_class so `*Self` substitutions in // declareFunction's type resolution find the state struct. const saved = self.current_foreign_class; self.current_foreign_class = fcd; defer self.current_foreign_class = saved; var method_infos = std.ArrayList(Module.ObjcDefinedMethodEntry).empty; for (fcd.members) |m| { const method = switch (m) { .method => |md| md, else => continue, }; const body = method.body orelse continue; const fd = self.synthesizeFnDeclFromObjcMethod(method, body) orelse continue; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, method.name }) catch continue; self.program_index.fn_ast_map.put(qualified, fd) catch {}; self.declareFunction(fd, qualified); // Selector mangling — A.1's deriveObjcSelector handles // `#selector("...")` override + the default rule. Static // methods use the same mangling rule (their first param // ISN'T *Self, so no offset). // // ABI for the IMP signature (both instance + class methods): // `(recv: id|Class, _cmd: SEL, ...user_args) -> ret` // For instance methods the user-declared self is at param[0] // (skipped); class methods have no self in the AST. const user_param_start: usize = if (method.is_static) 0 else 1; const user_arg_count = if (method.params.len > user_param_start) method.params.len - user_param_start else 0; const sel_info = self.objc().deriveObjcSelector(method, user_arg_count); const ret_ty: TypeId = if (method.return_type) |rt| self.resolveType(rt) else .void; var arg_tys = std.ArrayList(TypeId).empty; defer arg_tys.deinit(self.alloc); if (method.params.len > user_param_start) { for (method.params[user_param_start..]) |p_node| { arg_tys.append(self.alloc, self.resolveType(p_node)) catch unreachable; } } const encoding = self.objc().objcTypeEncodingFromSignature(ret_ty, arg_tys.items, null) catch continue; const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, method.name }) catch continue; method_infos.append(self.alloc, .{ .sel = sel_info.sel, .encoding = encoding, .imp_name = imp_name, .is_class = method.is_static, }) catch unreachable; } if (method_infos.items.len > 0) { const methods_slice = method_infos.toOwnedSlice(self.alloc) catch return; self.module.setObjcDefinedClassMethods(fcd.name, methods_slice); } } /// Build an `FnDecl` whose params are zipped from the /// `ForeignMethodDecl.params` (type nodes) and `param_names`. Used /// to feed sx-defined class methods through the standard /// fn-lowering pipeline. Allocator-owned; lives for the duration /// of the Lowering pass. fn synthesizeFnDeclFromObjcMethod(self: *Lowering, method: ast.ForeignMethodDecl, body: *ast.Node) ?*ast.FnDecl { if (method.params.len != method.param_names.len) return null; var params = std.ArrayList(ast.Param).empty; for (method.params, method.param_names) |type_node, p_name| { params.append(self.alloc, .{ .name = p_name, .name_span = .{ .start = 0, .end = 0 }, .type_expr = type_node, }) catch unreachable; } const fd = self.alloc.create(ast.FnDecl) catch return null; fd.* = .{ .name = method.name, .params = params.toOwnedSlice(self.alloc) catch unreachable, .return_type = method.return_type, .body = body, }; return fd; } /// If `name` matches an sx-defined `#objc_class`'s qualified-method /// pattern (`.`), return the class's /// ForeignClassDecl. Used by `lowerFunction` to set /// `current_foreign_class` so `*Self` resolves to the state struct /// during body lowering. pub fn lookupObjcDefinedClassForMethod(self: *Lowering, name: []const u8) ?*const ast.ForeignClassDecl { const dot = std.mem.indexOf(u8, name, ".") orelse return null; return self.module.lookupObjcDefinedClass(name[0..dot]); } /// Lazily declare the `sx_jni_env_tl_get` / `sx_jni_env_tl_set` /// runtime externs (step 2.16c). The storage lives in /// `library/vendors/sx_jni_runtime/sx_jni_env_tl.c` as a /// `_Thread_local` slot — keeping it OUT of the user's IR module /// is what lets the LLVM ORC JIT load the module cleanly without /// orc_rt platform support. AOT targets get the same .c file /// linked in via `needs_jni_env_tl_runtime`, which Compilation /// reads to append a synthetic c_import alongside the user's. pub fn getJniEnvTlFids(self: *Lowering) struct { get: FuncId, set: FuncId } { self.needs_jni_env_tl_runtime = true; const ptr_ty = self.module.types.ptrTo(.void); if (self.jni_env_tl_get_fid == null) { const name = self.module.types.internString("sx_jni_env_tl_get"); const fid = self.builder.declareExtern(name, &.{}, ptr_ty); const func = self.module.getFunctionMut(fid); func.call_conv = .c; self.jni_env_tl_get_fid = fid; } if (self.jni_env_tl_set_fid == null) { const name = self.module.types.internString("sx_jni_env_tl_set"); const env_param = self.module.types.internString("env"); var params = std.ArrayList(inst_mod.Function.Param).empty; params.append(self.alloc, .{ .name = env_param, .ty = ptr_ty }) catch unreachable; const fid = self.builder.declareExtern(name, params.toOwnedSlice(self.alloc) catch unreachable, .void); const func = self.module.getFunctionMut(fid); func.call_conv = .c; self.jni_env_tl_set_fid = fid; } return .{ .get = self.jni_env_tl_get_fid.?, .set = self.jni_env_tl_set_fid.? }; } /// When a namespaced import (`Ns :: #import "..."`) contains foreign-class /// declarations, ALSO register them under their qualified name `Ns.Class` /// so receiver types like `*Ns.Class` can find the fcd. The recursive /// scan/lower already handles bare-name registration; this only adds the /// qualified-name entry, so cross-class refs in method signatures /// (`*View` → bare lookup) still work. pub fn registerNamespacedForeignClasses(self: *Lowering, ns: ast.NamespaceDecl) void { for (ns.decls) |inner| { if (inner.data == .foreign_class_decl) { const fcd = &inner.data.foreign_class_decl; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ns.name, fcd.name }) catch fcd.name; self.program_index.foreign_class_map.put(qualified, fcd) catch {}; } else if (inner.data == .namespace_decl) { // Nested namespaces — qualify with both prefixes. self.registerNamespacedForeignClasses(inner.data.namespace_decl); } } } // ── Protocol dispatch ────────────────────────────────────────── /// Check if a type name is a registered protocol. fn isProtocolType(self: *Lowering, type_name: []const u8) bool { return self.program_index.protocol_decl_map.contains(type_name); } /// Get protocol info for a TypeId (if it's a protocol type). /// Protocol lookup. Thin delegation to the canonical owner /// (`ProtocolResolver`, `protocols.zig`); kept on `Lowering` because ~9 /// callers (dispatch sites here + `calls.zig`) reach it. pub fn getProtocolInfo(self: *Lowering, ty: TypeId) ?ProtocolDeclInfo { return self.protocolResolver().getProtocolInfo(ty); } /// Get or create thunks for a (protocol, concrete_type) pair. /// Returns a slice of FuncIds, one per protocol method. fn getOrCreateThunks(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8) []const FuncId { // Key: "Proto\x00Type" const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ proto_name, concrete_type_name }) catch return &.{}; if (self.protocol_thunk_map.get(key)) |thunks| return thunks; // PLANNING: which methods need a thunk (owned by the registry). const methods = self.protocolResolver().protocolMethodInfos(proto_name) orelse return &.{}; var thunk_ids = std.ArrayList(FuncId).empty; defer thunk_ids.deinit(self.alloc); // EMISSION: materialize one thunk per method (stays in Lowering). for (methods) |method| { const thunk_id = self.createProtocolThunk(proto_name, concrete_type_name, method); thunk_ids.append(self.alloc, thunk_id) catch unreachable; } const owned = self.alloc.dupe(FuncId, thunk_ids.items) catch unreachable; self.protocol_thunk_map.put(key, owned) catch {}; return owned; } /// Emit the process-wide default Context as an LLVM static constant. /// /// @__sx_default_context = internal constant %Context { /// %Allocator { ptr null, /// ptr @__thunk_CAllocator_Allocator_alloc, /// ptr @__thunk_CAllocator_Allocator_dealloc }, /// ptr null /// } /// /// Used by FFI inbound wrappers (Step 4) and the interp's default- /// context call entry (Step 7). Only emitted when the program imports /// `std.sx` — without that, Context / Allocator / CAllocator aren't /// registered and the global has no purpose. pub fn emitDefaultContextGlobal(self: *Lowering) void { const saved_edc = self.emitting_default_context; self.emitting_default_context = true; defer self.emitting_default_context = saved_edc; const tbl = &self.module.types; const ctx_name_id = tbl.internString("Context"); const ctx_ty = tbl.findByName(ctx_name_id) orelse return; if (tbl.findByName(tbl.internString("Allocator")) == null) return; if (tbl.findByName(tbl.internString("CAllocator")) == null) return; // Force the CAllocator → Allocator thunks to exist so we can // reference them by FuncId in the static initializer. const thunks = self.getOrCreateThunks("Allocator", "CAllocator"); if (thunks.len < 2) return; // Inline Allocator value: { ctx: *void, alloc_fn: *void, dealloc_fn: *void } // CAllocator is stateless, so ctx is null. const alloc_fields = self.alloc.alloc(inst_mod.ConstantValue, 3) catch return; alloc_fields[0] = .null_val; alloc_fields[1] = .{ .func_ref = thunks[0] }; alloc_fields[2] = .{ .func_ref = thunks[1] }; // Context value: { allocator: Allocator, data: *void } const ctx_fields = self.alloc.alloc(inst_mod.ConstantValue, 2) catch return; ctx_fields[0] = .{ .aggregate = alloc_fields }; ctx_fields[1] = .null_val; const global_name = "__sx_default_context"; const global_name_id = tbl.internString(global_name); const gid = self.module.addGlobal(.{ .name = global_name_id, .ty = ctx_ty, .init_val = .{ .aggregate = ctx_fields }, .is_const = true, }); self.putGlobal(self.current_source_file, global_name, .{ .id = gid, .ty = ctx_ty }); } /// Create a thunk function: __thunk_ConcreteType_Protocol_method(ctx: *void, args...) -> ret /// The thunk calls ConcreteType.method(ctx, args...). fn createProtocolThunk(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8, method: ProtocolMethodInfo) FuncId { // Build params: [__sx_ctx]? + ctx: *void + method params. // Thunks are sx-side functions, so they get the implicit __sx_ctx // at slot 0 when it's enabled program-wide. The concrete protocol // receiver (ctx) follows at slot 1; user method args at slot 2+. var params = std.ArrayList(inst_mod.Function.Param).empty; defer params.deinit(self.alloc); const void_ptr = self.module.types.ptrTo(.void); const thunk_has_ctx = self.implicit_ctx_enabled; if (thunk_has_ctx) { params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr }) catch unreachable; } params.append(self.alloc, .{ .name = self.module.types.internString("ctx"), .ty = void_ptr }) catch unreachable; for (method.param_types, 0..) |pty, i| { var buf: [32]u8 = undefined; const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg"; params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable; } // Generate unique name var name_buf: [192]u8 = undefined; const thunk_name = std.fmt.bufPrint(&name_buf, "__thunk_{s}_{s}_{s}", .{ concrete_type_name, proto_name, method.name }) catch "__thunk"; const thunk_name_id = self.module.types.internString(thunk_name); // Save builder state const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const saved_ctx_ref_thunk = self.current_ctx_ref; defer self.current_ctx_ref = saved_ctx_ref_thunk; const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable; var func = inst_mod.Function.init(thunk_name_id, owned_params, method.ret_type); func.has_implicit_ctx = thunk_has_ctx; const func_id = self.module.addFunction(func); self.builder.func = func_id; self.builder.inst_counter = @intCast(owned_params.len); if (thunk_has_ctx) self.current_ctx_ref = Ref.fromIndex(0); const entry_block = self.builder.appendBlock(self.module.types.internString("entry"), &.{}); self.builder.switchToBlock(entry_block); // Ensure the concrete method is lowered const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ concrete_type_name, method.name }) catch method.name; if (!self.lowered_functions.contains(qualified)) { if (self.program_index.fn_ast_map.contains(qualified)) { self.lazyLowerFunction(qualified); } else if (self.genericInstanceMethod(concrete_type_name, method.name)) |gm| { // Generic-struct instance (`Combined__s64_s64`): the impl method is // authored on the instance's STAMPED decl (CP-4). Monomorphize it // for this instance's bindings so the thunk has a concrete // `Combined__s64_s64.get` to call. self.monomorphizeFunction(gm.fd, qualified, gm.bindings); } } // Call the concrete method: ConcreteType.method(__sx_ctx?, ctx, args...). // The concrete method is itself an sx function that takes the // implicit __sx_ctx at slot 0 (when implicit_ctx is enabled); we // forward the thunk's own __sx_ctx. if (self.resolveFuncByName(qualified)) |concrete_fid| { const concrete_func = &self.module.functions.items[@intFromEnum(concrete_fid)]; var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); // Slot offsets inside the thunk: __sx_ctx at 0 (if present), // protocol receiver (ctx) at slot user_base, user args at +1, +2... const user_base: u32 = if (thunk_has_ctx) 1 else 0; // Forward our __sx_ctx to the concrete method's __sx_ctx slot. if (concrete_func.has_implicit_ctx) { call_args.append(self.alloc, self.current_ctx_ref) catch unreachable; } // Pass ctx as the next arg (it's the concrete *Type disguised as *void). // If the concrete method expects a value (e.g., f32) not a pointer, load from ctx. const ctx_ref = Ref.fromIndex(user_base); const concrete_receiver_idx: usize = if (concrete_func.has_implicit_ctx) 1 else 0; if (concrete_receiver_idx < concrete_func.params.len) { const first_concrete_ty = concrete_func.params[concrete_receiver_idx].ty; const first_info = self.module.types.get(first_concrete_ty); if (first_info != .pointer) { // Concrete expects value — load from ctx pointer call_args.append(self.alloc, self.builder.load(ctx_ref, first_concrete_ty)) catch unreachable; } else { call_args.append(self.alloc, ctx_ref) catch unreachable; } } else { call_args.append(self.alloc, ctx_ref) catch unreachable; } for (method.param_types, 0..) |proto_pty, i| { var arg_ref = Ref.fromIndex(@intCast(user_base + 1 + i)); // If protocol param is a pointer (Self→*void) but concrete method // expects a value type, load the value from the pointer. const concrete_idx = concrete_receiver_idx + 1 + i; if (concrete_idx < concrete_func.params.len) { const concrete_pty = concrete_func.params[concrete_idx].ty; const proto_info = self.module.types.get(proto_pty); const concrete_info = self.module.types.get(concrete_pty); if (proto_info == .pointer and concrete_info != .pointer) { arg_ref = self.builder.load(arg_ref, concrete_pty); } } call_args.append(self.alloc, arg_ref) catch unreachable; } const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; const concrete_ret = concrete_func.ret; const result = self.builder.call(concrete_fid, owned_args, concrete_ret); if (method.ret_type != .void) { // If protocol returns *void (Self) but concrete returns a value type, // box the value: alloca+store and return the pointer const ret_info = self.module.types.get(method.ret_type); const concrete_ret_info = self.module.types.get(concrete_ret); if (ret_info == .pointer and concrete_ret_info != .pointer) { const slot = self.builder.alloca(concrete_ret); self.builder.store(slot, result); self.builder.ret(slot, method.ret_type); } else { self.builder.ret(result, method.ret_type); } } else { self.builder.retVoid(); } } else { // Can't resolve concrete method — emit unreachable _ = self.builder.emit(.{ .@"unreachable" = {} }, .void); } self.builder.finalize(); // Restore builder state self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return func_id; } /// Build a protocol value from a concrete pointer. /// For inline protocols: struct_init { ctx, thunk1, thunk2, ... } /// For vtable protocols: struct_init { ctx, vtable_ptr } where vtable is stack-allocated /// When `heap_copy` is true, the concrete data is heap-copied so the protocol value /// outlives the current stack frame (used when source is a value, not an explicit pointer). /// When false, the pointer is used directly (user manages the pointee's lifetime). fn buildProtocolValue(self: *Lowering, concrete_ptr: Ref, proto_name: []const u8, concrete_type_name: []const u8, proto_ty: TypeId, concrete_ty: TypeId, heap_copy: bool) Ref { const pd = self.program_index.protocol_decl_map.get(proto_name) orelse return concrete_ptr; const thunks = self.getOrCreateThunks(proto_name, concrete_type_name); if (thunks.len != pd.methods.len) return concrete_ptr; const void_ptr_ty = self.module.types.ptrTo(.void); // When source is a value (not an explicit pointer), heap-allocate // so the protocol value outlives the current stack frame. // When source is an explicit pointer (xx @obj), use it directly — // the user is responsible for the pointee's lifetime. var ctx_ptr = concrete_ptr; if (heap_copy) { const concrete_size = self.module.types.typeSizeBytes(concrete_ty); const size_ref = self.builder.constInt(@intCast(concrete_size), .s64); const heap_ptr = self.allocViaContext(size_ref, void_ptr_ty); _ = self.callForeign("memcpy", &.{ heap_ptr, concrete_ptr, size_ref }, void_ptr_ty); ctx_ptr = heap_ptr; } if (pd.is_inline) { // Inline: { ctx, fn1, fn2, ... } var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); field_vals.append(self.alloc, ctx_ptr) catch unreachable; for (thunks) |thunk_id| { const fn_ref = self.builder.emit(.{ .func_ref = thunk_id }, void_ptr_ty); field_vals.append(self.alloc, fn_ref) catch unreachable; } const owned = self.alloc.dupe(Ref, field_vals.items) catch unreachable; return self.builder.emit(.{ .struct_init = .{ .fields = owned } }, proto_ty); } else { // Vtable: { ctx, vtable_ptr } // Vtable is a global constant (same function pointers for every instance // of the same Protocol+ConcreteType pair). Cached per pair. const vtable_ty = self.protocol_vtable_type_map.get(proto_name) orelse return concrete_ptr; // Build cache key: "Proto\x00Type" const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ proto_name, concrete_type_name }) catch unreachable; const vtable_global_id = self.protocol_vtable_global_map.get(key) orelse blk: { // Create vtable global with function pointer initializer const global_name = std.fmt.allocPrint(self.alloc, "__{s}__{s}__vtable", .{ proto_name, concrete_type_name }) catch unreachable; const global_name_id = self.module.types.strings.intern(self.alloc, global_name); const thunk_ids = self.alloc.dupe(FuncId, thunks) catch unreachable; const gid = self.module.addGlobal(.{ .name = global_name_id, .ty = vtable_ty, .init_val = .{ .vtable = thunk_ids }, .is_const = true, }); self.protocol_vtable_global_map.put(key, gid) catch {}; break :blk gid; }; // Reference the vtable global's address const vtable_ptr_ty = self.module.types.ptrTo(vtable_ty); const vtable_addr = self.builder.emit(.{ .global_addr = vtable_global_id }, vtable_ptr_ty); // Build protocol struct: { ctx, &vtable } var proto_fields = std.ArrayList(Ref).empty; defer proto_fields.deinit(self.alloc); proto_fields.append(self.alloc, ctx_ptr) catch unreachable; proto_fields.append(self.alloc, vtable_addr) catch unreachable; const proto_owned = self.alloc.dupe(Ref, proto_fields.items) catch unreachable; return self.builder.emit(.{ .struct_init = .{ .fields = proto_owned } }, proto_ty); } } /// Emit protocol method dispatch for a protocol-typed receiver. /// Returns the call result ref. fn emitProtocolDispatch(self: *Lowering, receiver: Ref, proto_info: ProtocolDeclInfo, method_name: []const u8, args: []const Ref, proto_ty: TypeId) Ref { // Find method index var method_idx: ?usize = null; var method_info: ?ProtocolMethodInfo = null; for (proto_info.methods, 0..) |m, i| { if (std.mem.eql(u8, m.name, method_name)) { method_idx = i; method_info = m; break; } } const mi = method_info orelse return self.emitError(method_name, null); const midx = method_idx orelse 0; // Extract ctx from protocol struct (field 0) const void_ptr = self.module.types.ptrTo(.void); const ctx = self.builder.structGet(receiver, 0, void_ptr); // Extract fn_ptr const fn_ptr = if (proto_info.is_inline) blk: { // Inline: fn_ptr at field 1+method_idx break :blk self.builder.structGet(receiver, @intCast(1 + midx), void_ptr); } else blk: { // Vtable: load vtable struct, extract fn_ptr at method_idx const vtable_ptr = self.builder.structGet(receiver, 1, void_ptr); const vtable_ty = self.protocol_vtable_type_map.get(proto_info.name) orelse return self.emitError("vtable", null); const vtable = self.builder.emit(.{ .deref = .{ .operand = vtable_ptr } }, vtable_ty); break :blk self.builder.structGet(vtable, @intCast(midx), void_ptr); }; _ = proto_ty; // Build call args: [__sx_ctx]? + receiver_ctx + user args. // Protocol thunks are sx-side, so they carry the implicit __sx_ctx // at slot 0 when the program uses Context — forward our caller's // ctx so the thunk's body (and the concrete method it forwards to) // sees the same Context as the dispatching code. var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); if (self.implicit_ctx_enabled) { call_args.append(self.alloc, self.current_ctx_ref) catch unreachable; } call_args.append(self.alloc, ctx) catch unreachable; for (args, 0..) |a, i| { const expected_ty = if (i < mi.param_types.len) mi.param_types[i] else void_ptr; const arg_ty = self.builder.getRefType(a); // Untargeted `null` lowers as const_null with type .void. Re-emit it // as a null of the expected pointer type instead of alloca'ing void. if (arg_ty == .void and expected_ty == void_ptr) { call_args.append(self.alloc, self.builder.constNull(void_ptr)) catch unreachable; continue; } // A protocol method that expects `*void` accepts any single-pointer // value directly (`*T`, `[*]T`). Only wrap non-pointer values in an // alloca-slot — wrapping a pointer would pass the stack slot's // address instead of the actual pointer, and the callee would read // 8 bytes of pointer plus garbage from beyond the stack. const is_pointer_ty = if (!arg_ty.isBuiltin()) blk: { const info = self.module.types.get(arg_ty); break :blk info == .pointer or info == .many_pointer; } else false; if (expected_ty == void_ptr and arg_ty != void_ptr and !is_pointer_ty) { const slot = self.builder.alloca(arg_ty); self.builder.store(slot, a); call_args.append(self.alloc, slot) catch unreachable; } else { // Coerce to match declared parameter type (critical for WASM strict signatures) const coerced = self.coerceToType(a, arg_ty, expected_ty); call_args.append(self.alloc, coerced) catch unreachable; } } const owned = self.alloc.dupe(Ref, call_args.items) catch unreachable; const raw_result = self.builder.emit(.{ .call_indirect = .{ .callee = fn_ptr, .args = owned } }, mi.ret_type); // If the protocol method was declared `-> Self` (encoded here as *void) // and the caller expects a value type, unbox: load the concrete value // from the returned pointer. A literal `-> *void` return is NOT // auto-loaded — it's a real pointer whose pointee size we don't know. if (mi.ret_is_self) { if (self.target_type) |target| { const target_info = self.module.types.get(target); if (target_info != .pointer) { return self.builder.load(raw_result, target); } } } return raw_result; } /// Resolve the concrete type name for protocol erasure. /// Handles both direct types and pointer-to-types. pub fn resolveConcreteTypeName(self: *Lowering, ty: TypeId) ?[]const u8 { if (ty.isBuiltin()) { // Primitive types like s64 — check if they have toName() return self.module.types.typeName(ty); } const info = self.module.types.get(ty); if (info == .pointer) { // *ConcreteType → resolve pointee const pointee = info.pointer.pointee; if (pointee.isBuiltin()) return self.module.types.typeName(pointee); const pi = self.module.types.get(pointee); if (pi == .@"struct") return self.module.types.getString(pi.@"struct".name); return null; } if (info == .@"struct") return self.module.types.getString(info.@"struct".name); return null; } // ── Helpers ───────────────────────────────────────────────────── /// Infer the type of an expression from its AST node (used for untyped var decls). pub fn inferExprType(self: *Lowering, node: *const Node) TypeId { return switch (node.data) { .call => |*c| self.callResolver().resultType(c), else => self.exprTyper().inferType(node), }; } fn exprTyper(self: *Lowering) ExprTyper { return .{ .l = self }; } fn callResolver(self: *Lowering) CallResolver { return .{ .l = self }; } /// A `Resolver` facade over the borrowed Phase A import facts (Phase B). Cheap /// by-value; `collectVisibleAuthors`'s `AuthorSet.flat` slice is backed by /// `self.alloc` and owned by the caller (`selectPlainCallableAuthor` frees it). pub fn resolver(self: *Lowering) resolver_mod.Resolver { return resolver_mod.Resolver.init(&self.program_index, self.alloc); } pub fn genericResolver(self: *Lowering) GenericResolver { return .{ .l = self }; } pub fn protocolResolver(self: *Lowering) ProtocolResolver { return .{ .l = self }; } pub fn coercionResolver(self: *Lowering) CoercionResolver { return .{ .l = self }; } pub fn errorAnalysis(self: *Lowering) ErrorAnalysis { return .{ .l = self }; } pub fn errorFlow(self: *Lowering) ErrorFlow { return .{ .l = self }; } pub fn objc(self: *Lowering) ObjcLowering { return .{ .l = self }; } /// Lower the `xx` operator (type coercion). /// Uses self.target_type for context when available. Handles: /// - Any → concrete type: unbox_any /// - int → int: widen/narrow /// - int ↔ float: int_to_float/float_to_int fn lowerXX(self: *Lowering, operand: Ref, operand_node: *const Node) Ref { // Use the operand's *actual* lowered Ref type rather than reaching // back through inferExprType — the latter doesn't cover every // expression shape (notably lambdas), and a wrong src_ty here can // route the cast through coerceToType (e.g. a bogus s64→ptr bitcast) // and silently skip the user-space Into fallback. const src_ty = self.builder.getRefType(operand); const target_explicit = self.target_type != null; const dst_ty = self.target_type orelse .unresolved; // PLANNING: the `xx`-head decision (conversions.zig). `.coerce` falls // through to the built-in ladder + the user-`Into` fallback below. switch (self.coercionResolver().classifyXX(src_ty, dst_ty)) { // Any → concrete type: unbox. .unbox_any => { // When inside a float match arm covering both f32 and f64, // and target is f64, we need a mini-dispatch to unbox correctly. // f32 values are stored as zext(bitcast(f32→i32), i64) in Any, // so bitcasting i64→f64 directly gives wrong results for f32. if (dst_ty == .f64) { if (self.current_match_tags) |tags| { var has_f32 = false; var has_f64 = false; for (tags) |t| { const tid = TypeId.fromIndex(@intCast(t)); if (tid == .f32) has_f32 = true; if (tid == .f64) has_f64 = true; } if (has_f32 and has_f64) { return self.lowerAnyToF64Dispatch(operand); } if (has_f32 and !has_f64) { // Only f32 values: unbox as f32, then widen const f32_val = self.builder.emit(.{ .unbox_any = .{ .operand = operand, } }, .f32); return self.builder.emit(.{ .widen = .{ .operand = f32_val, .from = .f32, .to = .f64 } }, .f64); } } } return self.builder.emit(.{ .unbox_any = .{ .operand = operand, } }, dst_ty); }, // Same type: no-op. .no_op => return operand, // Concrete → Protocol: build protocol value. .erase_protocol => return self.buildProtocolErasure(operand, operand_node, src_ty, dst_ty), // Protocol → pointer: recover the typed ctx pointer (field 0). // The protocol value is `{ ctx, fn1, fn2, ... }` (inline) or // `{ ctx, vtable_ptr }` — either way, ctx lives at field 0. .protocol_to_pointer => { const void_ptr_ty = self.module.types.ptrTo(.void); const ctx_ref = self.builder.emit(.{ .struct_get = .{ .base = operand, .field_index = 0 } }, void_ptr_ty); if (dst_ty == void_ptr_ty) return ctx_ref; return self.builder.emit(.{ .bitcast = .{ .operand = ctx_ref, .from = void_ptr_ty, .to = dst_ty } }, dst_ty); }, .coerce => {}, } const result = self.coerceExplicit(operand, src_ty, dst_ty); // User-space fallback via `impl Into(Target) for Source`. Only fires // when the target was explicitly named (not the .s64 default), src and // dst differ, and the built-in ladder made no progress. Built-ins // always win. if (target_explicit and src_ty != dst_ty and result == operand) { if (self.tryUserConversion(operand, operand_node, src_ty, dst_ty)) |converted| { return converted; } // Pointer-target fallback: `xx ` whose surrounding context // expects `*T` (a fn arg slot, a var typed as a pointer-to-aggregate) // can be satisfied by `impl Into(T) for src` plus an implicit // alloca+store on the result. Lets users write // `fn(xx () => { ... })` instead of materialising a named Block local // just to take its address. if (!dst_ty.isBuiltin()) { const dst_info = self.module.types.get(dst_ty); if (dst_info == .pointer) { const pointee = dst_info.pointer.pointee; if (pointee != src_ty) { if (self.tryUserConversion(operand, operand_node, src_ty, pointee)) |converted| { const slot = self.builder.alloca(pointee); self.builder.store(slot, converted); return slot; } } } } } return result; } /// Detect the `xx closure : Block` cast pattern so `tryUserConversion` /// can emit a focused diagnostic when no `Into(Block) for Closure(...)` /// impl is reachable. Replaces what was briefly a compiler-synthesised /// trampoline path with a "declare an impl" requirement — the stdlib /// covers common signatures (see modules/std/objc_block.sx), users /// add their own for unusual ones. fn isClosureToBlockCast(self: *Lowering, src_ty: TypeId, dst_ty: TypeId) bool { if (src_ty.isBuiltin()) return false; const src_info = self.module.types.get(src_ty); if (src_info != .closure) return false; if (dst_ty.isBuiltin()) return false; const dst_info = self.module.types.get(dst_ty); if (dst_info != .@"struct") return false; const block_name = self.module.types.internString("Block"); return dst_info.@"struct".name == block_name; } /// Pack-variadic impl matching. Walks `param_impl_pack_map[pack_key]` /// and returns a call ref when a single pack impl matches `src_ty`'s /// shape (concrete src closure / fn with the same fixed prefix as /// the impl's source pack closure). Binds the pack-var to the source's /// tail param types and the return-var (when generic) to the source's /// return type, then monomorphises the convert method. /// Returns null if no pack impls registered for this (proto, dst) or /// none of them match `src_ty`'s shape. fn tryPackImplMatch( self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId, proto_name: []const u8, pack_key: []const u8, guard_key: u64, ) ?Ref { _ = operand_node; // PLANNING: select the matching pack impl + its `convert` (registry). const match = self.protocolResolver().matchPackImpl(src_ty, pack_key) orelse return null; const entry = match.entry; const fd = match.convert_fd; const src_params = match.src_params; const src_ret = match.src_ret; const table = &self.module.types; // EMISSION: bind the pack tail + ret-var, monomorphise, call (Lowering). // Build bindings. Target → dst_ty (already in the protocol's type // params), pack-var → src tail TypeIds, ret-var (when generic) → // src ret. const ent_pack_start = table.get(entry.source_pack_ty).closure.pack_start.?; const tail = src_params[ent_pack_start..]; const tail_owned = self.alloc.dupe(TypeId, tail) catch return null; var bindings = std.StringHashMap(TypeId).init(self.alloc); defer bindings.deinit(); const pd = self.program_index.protocol_ast_map.get(proto_name) orelse return null; bindings.put(pd.type_params[0].name, dst_ty) catch return null; if (entry.ret_var_name) |rv| bindings.put(rv, src_ret) catch return null; var pack_bindings = std.StringHashMap([]const TypeId).init(self.alloc); defer pack_bindings.deinit(); pack_bindings.put(entry.pack_var_name, tail_owned) catch return null; // Mangled name keyed on the CONCRETE source so distinct shapes // monomorphise separately. Same scheme as the concrete path: // ".convert__". const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }) catch return null; self.xx_reentrancy.put(guard_key, {}) catch {}; defer _ = self.xx_reentrancy.remove(guard_key); if (!self.lowered_functions.contains(mangled)) { const saved_pack = self.pack_bindings; self.pack_bindings = pack_bindings; defer self.pack_bindings = saved_pack; self.monomorphizeFunction(fd, mangled, &bindings); } const fid = self.resolveFuncByName(mangled) orelse return null; const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; var single = [_]Ref{operand}; const final_args = self.prependCtxIfNeeded(func, single[0..]); self.coerceCallArgs(final_args, params); return self.builder.call(fid, final_args, ret_ty); } /// Look up `Into(dst_ty)` impl for `src_ty` and, if found, monomorphise /// the impl's `convert` method and emit a direct call. Returns null when /// no impl matches (caller falls back to the built-in result, which is /// the unchanged operand — Phase 3 emits no diagnostic for v0). fn tryUserConversion(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) ?Ref { // Reentrancy guard — pack (src, dst) into a u64. const guard_key: u64 = (@as(u64, src_ty.index()) << 32) | @as(u64, dst_ty.index()); if (self.xx_reentrancy.contains(guard_key)) { if (self.diagnostics) |diags| { diags.addFmt(.err, operand_node.span, "recursive xx conversion from '{s}' to '{s}'", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }); } return operand; } // Build lookup key: "Into\x00\x00". // Hardcoded to the "Into" protocol for v1. Generalising to other // parameterised protocols would walk protocol_decl_map looking for // protocols that take a single type-param and have a `convert` method. const proto_name = "Into"; const pd = self.program_index.protocol_ast_map.get(proto_name) orelse return null; if (pd.type_params.len != 1) return null; var key_buf = std.ArrayList(u8).empty; key_buf.appendSlice(self.alloc, proto_name) catch return null; key_buf.append(self.alloc, 0) catch return null; key_buf.appendSlice(self.alloc, self.mangleTypeName(dst_ty)) catch return null; key_buf.append(self.alloc, 0) catch return null; key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return null; const key = key_buf.items; // Pack-only key (proto + dst) — used if the concrete lookup misses. // Same prefix as the concrete key, minus the `\x00` tail. const dst_mangled_len = self.mangleTypeName(dst_ty).len; const pack_key = key_buf.items[0 .. proto_name.len + 1 + dst_mangled_len]; const entries_opt = self.param_impl_map.get(key); const has_concrete = entries_opt != null and entries_opt.?.items.len > 0; if (!has_concrete) { // Concrete miss — try the pack map before emitting a diagnostic. if (self.tryPackImplMatch(operand, operand_node, src_ty, dst_ty, proto_name, pack_key, guard_key)) |result| { return result; } if (self.isClosureToBlockCast(src_ty, dst_ty)) { if (self.diagnostics) |diags| { const saved = diags.current_source_file; diags.current_source_file = operand_node.source_file orelse self.current_source_file; defer diags.current_source_file = saved; diags.addFmt(.err, operand_node.span, "no `Into(Block) for {s}` impl — add a per-signature `__block_invoke_` trampoline + Into impl alongside the existing ones in modules/std/objc_block.sx, or declare it in your own code", .{self.mangleTypeName(src_ty)}); } return operand; } return null; } const entries = entries_opt.?; // Filter by import visibility: only impls in modules that the current // file transitively imports (or the current file itself) are reachable. // Falls open when import_graph isn't wired (e.g. comptime callers). var visible_impls = std.ArrayList(ParamImplEntry).empty; defer visible_impls.deinit(self.alloc); self.protocolResolver().findVisibleImpls(entries.items, &visible_impls); if (visible_impls.items.len == 0) { if (self.diagnostics) |diags| { const saved = diags.current_source_file; diags.current_source_file = operand_node.source_file orelse self.current_source_file; defer diags.current_source_file = saved; diags.addFmt(.err, operand_node.span, "no visible xx conversion from '{s}' to '{s}' — impl exists in another module but is not imported", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }); } return operand; } if (visible_impls.items.len > 1) { if (self.diagnostics) |diags| { const saved = diags.current_source_file; diags.current_source_file = operand_node.source_file orelse self.current_source_file; defer diags.current_source_file = saved; diags.addFmt(.err, operand_node.span, "duplicate xx conversion from '{s}' to '{s}': impls in {s} and {s}", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), visible_impls.items[0].defining_module, visible_impls.items[1].defining_module, }); } return operand; } const entry = visible_impls.items[0]; // Find the `convert` method on this impl. var convert_fd: ?*const ast.FnDecl = null; for (entry.methods) |m| { if (std.mem.eql(u8, m.name, "convert")) { convert_fd = m; break; } } const fd = convert_fd orelse return null; // Bind Target → dst_ty. var bindings = std.StringHashMap(TypeId).init(self.alloc); defer bindings.deinit(); bindings.put(pd.type_params[0].name, dst_ty) catch return null; // Mangled name: ".convert__". const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }) catch return null; self.xx_reentrancy.put(guard_key, {}) catch {}; defer _ = self.xx_reentrancy.remove(guard_key); if (!self.lowered_functions.contains(mangled)) { self.monomorphizeFunction(fd, mangled, &bindings); } const fid = self.resolveFuncByName(mangled) orelse return null; const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; var single = [_]Ref{operand}; const final_args = self.prependCtxIfNeeded(func, single[0..]); self.coerceCallArgs(final_args, params); return self.builder.call(fid, final_args, ret_ty); } /// True for expression shapes that name an addressable storage location /// (variables, fields, array elements, dereferenced pointers). Used by /// `xx ` to decide between borrow (lvalue → take the /// address) and heap-copy (rvalue → allocate a fresh copy). fn isLvalueExpr(self: *Lowering, node: *const Node) bool { _ = self; return switch (node.data) { .identifier, .field_access, .index_expr, .deref_expr => true, else => false, }; } /// Build a protocol value from a concrete value via xx conversion. /// Coerce `val` (type `src`) to `dst`: if `dst` is a protocol, `xx`-erase /// the concrete value into it; otherwise fall back to numeric/struct /// coercion. Used to materialize a pack into a protocol-typed tuple field. fn coerceOrErase(self: *Lowering, val: Ref, src: TypeId, dst: TypeId, node: *const Node) Ref { if (src == dst) return val; if (!dst.isBuiltin()) { const di = self.module.types.get(dst); if (di == .@"struct" and di.@"struct".is_protocol) { return self.buildProtocolErasure(val, node, src, dst); } } return self.coerceToType(val, src, dst); } pub fn buildProtocolErasure(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) Ref { const dst_info = self.module.types.get(dst_ty); if (dst_info != .@"struct") return operand; const proto_name = self.module.types.getString(dst_info.@"struct".name); // Determine concrete type name and type — resolve through pointer if needed var concrete_ptr = operand; var concrete_type_name: ?[]const u8 = null; var concrete_ty: TypeId = src_ty; var heap_copy = false; if (!src_ty.isBuiltin()) { const src_info = self.module.types.get(src_ty); if (src_info == .pointer) { // xx @acc — operand is already a pointer (user manages lifetime) const pointee = src_info.pointer.pointee; concrete_type_name = self.resolveConcreteTypeName(pointee); concrete_ty = pointee; heap_copy = false; } else if (src_info == .@"struct") { // Struct-typed operand. Split on lvalue-ness: // - lvalue (identifier, field, index, deref): borrow the // storage the operand already names. No heap copy; the // protocol value's ctx points at the caller's slot, and // mutations through the protocol are visible to the // original. Lifetime is the caller's responsibility. // - rvalue (struct literal, call result, etc.): heap-copy // into a fresh allocation so the protocol value is // self-contained and outlives this expression. concrete_type_name = self.module.types.getString(src_info.@"struct".name); concrete_ty = src_ty; if (self.isLvalueExpr(operand_node)) { concrete_ptr = self.lowerExprAsPtr(operand_node); heap_copy = false; } else { heap_copy = true; const slot = self.builder.alloca(src_ty); self.builder.store(slot, operand); concrete_ptr = slot; } } } // Also try from the operand node for struct literals: xx Accumulator.{ total = 0 } if (concrete_type_name == null) { concrete_type_name = self.inferConcreteTypeName(operand_node); if (concrete_type_name != null) heap_copy = true; } if (concrete_type_name) |ctn| { return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy); } return operand; } /// Try to infer the concrete type name from an AST node (for struct literals etc.) fn inferConcreteTypeName(self: *Lowering, node: *const Node) ?[]const u8 { return switch (node.data) { .struct_literal => |sl| if (sl.struct_name) |n| n else null, .unary_op => |uop| if (uop.op == .address_of) self.inferConcreteTypeName(uop.operand) else null, .identifier => |id| blk: { // Check if identifier's type resolves to a struct if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (!binding.ty.isBuiltin()) { const bi = self.module.types.get(binding.ty); if (bi == .@"struct") break :blk self.module.types.getString(bi.@"struct".name); if (bi == .pointer) { const pointee = bi.pointer.pointee; if (!pointee.isBuiltin()) { const pi = self.module.types.get(pointee); if (pi == .@"struct") break :blk self.module.types.getString(pi.@"struct".name); } } } } } break :blk null; }, else => null, }; } /// Generate a mini-dispatch for unboxing Any to f64 when the value might be f32 or f64. /// Uses alloca-based merge: create result slot, branch, store in each arm, load after merge. fn lowerAnyToF64Dispatch(self: *Lowering, any_val: Ref) Ref { // Create result alloca BEFORE the branch const result_slot = self.builder.alloca(.f64); // Extract type tag from Any const tag = self.builder.structGet(any_val, 0, .s64); const f32_bb = self.freshBlock("f32.unbox"); const f64_bb = self.freshBlock("f64.unbox"); const merge_bb = self.freshBlock("float.merge"); // Branch: tag == f32_tag ? f32_bb : f64_bb const f32_tag = self.builder.constInt(TypeId.f32.index(), .s64); const cond = self.builder.emit(.{ .cmp_eq = .{ .lhs = tag, .rhs = f32_tag } }, .bool); self.builder.condBr(cond, f32_bb, &.{}, f64_bb, &.{}); // f32 block: unbox as f32, fpext to f64, store self.builder.switchToBlock(f32_bb); const f32_val = self.builder.emit(.{ .unbox_any = .{ .operand = any_val, } }, .f32); const f64_from_f32 = self.builder.emit(.{ .widen = .{ .operand = f32_val, .from = .f32, .to = .f64 } }, .f64); self.builder.store(result_slot, f64_from_f32); self.builder.br(merge_bb, &.{}); // f64 block: unbox as f64 directly, store self.builder.switchToBlock(f64_bb); const f64_val = self.builder.emit(.{ .unbox_any = .{ .operand = any_val, } }, .f64); self.builder.store(result_slot, f64_val); self.builder.br(merge_bb, &.{}); // Merge block: load result self.builder.switchToBlock(merge_bb); return self.builder.load(result_slot, .f64); } /// Produce a default value for a type, applying struct field defaults. /// For structs with defaults (e.g., `b: s32 = 99`), creates a struct_literal with defaults applied. /// For other types, returns a zero value. pub fn buildDefaultValue(self: *Lowering, ty: TypeId) Ref { if (ty.isBuiltin()) return self.builder.constInt(0, ty); const info = self.module.types.get(ty); if (info != .@"struct" and info != .tuple) return self.zeroValue(ty); // For tuples, build a zero-initialized tuple if (info == .tuple) { var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); for (info.tuple.fields) |f| { field_vals.append(self.alloc, self.zeroValue(f)) catch unreachable; } return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable }, }, ty); } // Check for struct defaults const struct_name_str = self.module.types.getString(info.@"struct".name); const field_defaults = self.struct_defaults_map.get(struct_name_str) orelse return self.builder.constUndef(ty); const fields = info.@"struct".fields; var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); for (fields, 0..) |f, i| { if (i < field_defaults.len) { if (field_defaults[i]) |default_expr| { field_vals.append(self.alloc, self.lowerCoercedDefault(default_expr, f.ty)) catch unreachable; } else { field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable; } } else { field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable; } } return self.builder.emit(.{ .struct_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable }, }, ty); } /// Wrap ty in ?ty, but flatten: if ty is already ?U, return ?U (not ??U) pub fn optionalOfFlattened(self: *Lowering, ty: TypeId) TypeId { if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .optional) return ty; } return self.module.types.optionalOf(ty); } /// Produce a zero/default value for any type — constInt(0) for integers, /// constNull for pointers, constUndef for structs/complex types. pub fn zeroValue(self: *Lowering, ty: TypeId) Ref { if (ty.isBuiltin()) return self.builder.constInt(0, ty); const info = self.module.types.get(ty); return switch (info) { // Arbitrary-width integer types (u1, u2, s4, ...) interned as // `.signed`/`.unsigned` variants — fall through `isBuiltin()`. .signed, .unsigned => self.builder.constInt(0, ty), .pointer, .tuple, .optional => self.builder.constNull(ty), .@"struct", .array, .slice, .many_pointer => self.builder.constNull(ty), else => self.builder.constUndef(ty), }; } /// Check if a name refers to a known type (primitive or registered struct/enum/union). /// Used to distinguish type-as-value (silent placeholder) from genuinely unresolved names. pub fn isKnownTypeName(self: *Lowering, name: []const u8) bool { if (type_bridge.resolveTypePrimitive(name) != null) return true; if (self.type_bindings) |bindings| { if (bindings.get(name) != null) return true; } if (self.program_index.type_alias_map.get(name) != null) return true; const name_id = self.module.types.internString(name); return self.module.types.findByName(name_id) != null; } /// Update `self.current_source_file` and mirror it onto `diags.current_source_file`, /// so any diagnostic emitted from inside a function lowered from another module is /// attributed to that module — not whichever file the diagnostics list was init'd with. pub fn setCurrentSourceFile(self: *Lowering, source_file: ?[]const u8) void { self.current_source_file = source_file; if (self.diagnostics) |d| d.current_source_file = source_file; } /// Stamp a caller-provided comptime `$`-arg node with the caller's source /// file. When the node is later substituted into the (defining-module-pinned) /// metaprogram body and lowered, lowerExpr's per-node source switch resolves /// its bare names in the CALLER's visibility context — not the callee's — so /// a caller-owned helper passed to an imported metaprogram stays visible. /// Only stamps a node with no source yet, and only when the caller context /// is known; an unknown caller source leaves the node's fall-open intact. pub fn stampCallerSource(self: *Lowering, node: *Node) void { if (node.source_file != null) return; if (self.current_source_file) |src| node.source_file = src; } pub fn emitError(self: *Lowering, name: []const u8, span: ?ast.Span) Ref { if (self.diagnostics) |diags| { // The literal message carries the lowering's `current_source_file` // and enclosing function name. The diagnostic renderer's // `source_file` -> `file:line:col` prefix can drift when a span is // offset into one source but the diagnostic falls back to another // (e.g. synthetic AST nodes inserted from `#insert` take their // span from the call site, not from the string being inserted). // Embedding the file + function in the message means a // misattributed span can never hide WHERE the lookup actually // failed. Setting SX_TRACE_UNRESOLVED=1 also dumps a Zig stack // trace at the emit site to surface the calling lowering path. const sf = self.current_source_file orelse ""; const fn_name: []const u8 = if (self.builder.func) |fid| self.module.types.getString(self.module.functions.items[@intFromEnum(fid)].name) else ""; if (std.c.getenv("SX_TRACE_UNRESOLVED") != null) { std.debug.print("\n== unresolved '{s}' (in {s} fn {s}) ==\n", .{ name, sf, fn_name }); std.debug.dumpCurrentStackTrace(.{ .first_address = @returnAddress() }); } diags.addFmt(.err, span, "unresolved '{s}' (in {s} fn {s})", .{ name, sf, fn_name }); } return self.emitPlaceholder(name); } pub fn emitFieldError(self: *Lowering, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { // A field access on an already-`.unresolved` object is a cascade from an // upstream type-resolution failure that was ALREADY diagnosed (e.g. an // unresolvable / oversized array dimension — issue 0083). The // `.unresolved` sentinel never exists without an accompanying error, so // piling a second "field not found on unresolved" onto the real one is // pure noise; stay silent and return a placeholder so lowering finishes // and `hasErrors()` aborts the build on the genuine diagnostic. if (obj_ty != .unresolved) { if (self.diagnostics) |diags| { const ty_name = self.formatTypeName(obj_ty); diags.addFmt(.err, span, "field '{s}' not found on type '{s}'", .{ field, ty_name }); } } return self.emitPlaceholder(field); } /// Emit the unified non-integral float→int narrowing diagnostic (F0.11 / /// issue 0095). ONE wording, ONE place: every site that rejects an implicit /// narrowing of a non-integral compile-time float to an integer type calls /// this, so the message + fix-it stay identical across the typed-binding /// coerce arm, the field/param-default sites, the typed-const path, and the /// global-initializer path. pub fn diagNonIntegralNarrow(self: *Lowering, span: ast.Span, value: f64, dst_ty: TypeId) void { if (self.diagnostics) |d| d.addFmt(.err, span, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ value, self.formatTypeName(dst_ty) }); } /// Lower a struct field default `default_expr`, coerced to the field type /// `field_ty`. A compile-time float default narrowing into an integer field /// follows the unified rule via `foldComptimeFloatInit`; everything else /// lowers under the field type as target and coerces at the IR level. fn lowerCoercedDefault(self: *Lowering, default_expr: *const Node, field_ty: TypeId) Ref { if (self.foldComptimeFloatInit(default_expr, field_ty)) |folded| return folded; const saved_tt = self.target_type; self.target_type = field_ty; const raw = self.lowerExpr(default_expr); self.target_type = saved_tt; return self.coerceToType(raw, self.builder.getRefType(raw), field_ty); } /// How a float→int conversion is treated. An IMPLICIT coercion (a typed /// binding initializer) folds an integral compile-time float to its int and /// REJECTS a non-integral one; an EXPLICIT `xx` / `cast` always truncates. const CoerceMode = enum { implicit, explicit }; /// Insert a conversion if src_ty and dst_ty differ. /// Handles int widening/narrowing, float widening/narrowing, and int↔float. /// IMPLICIT coercion — the typed-binding initializer path. A compile-time /// float narrowing to an integer folds when integral, errors when not. pub fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { return self.coerceMode(val, src_ty, dst_ty, .implicit); } /// EXPLICIT coercion — the `xx` / `cast(T)` escape hatch. A float→int here /// always truncates, bypassing the integral-fold / non-integral-error rule. fn coerceExplicit(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { return self.coerceMode(val, src_ty, dst_ty, .explicit); } fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mode: CoerceMode) Ref { // PLANNING: classify the built-in coercion (conversions.zig). // EMISSION: each arm below reproduces the original lowering. switch (self.coercionResolver().classify(src_ty, dst_ty)) { .no_op, .none => return val, // Unbox Any → concrete type .unbox_any => return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty), // Box concrete → Any .box_any => return self.builder.boxAny(val, src_ty), // Closure VALUE → bare function-pointer slot: not soundly representable. // A bare `(T) -> U` slot is called as `fn_ptr(ctx, args)` with NO env // arg, but a closure's underlying fn takes an env slot — so passing a // closure value's fn_ptr drops the env and shifts the args (UB for a // matching ABI, a wrong-tuple read for ∅-widening, a segfault when the // closure captures). Only a closure LITERAL can cross this boundary, // via the static adapter `lowerLambda` emits (so a literal arrives here // already typed `.function`). Reject the variable case loudly. .closure_to_fn_reject => { if (self.diagnostics) |d| { const cs = self.builder.current_span; d.addFmt(.err, ast.Span{ .start = cs.start, .end = cs.end }, "a closure value cannot be passed as a bare function-pointer `(...) -> ...` — its environment can't be carried across the bare ABI; pass the closure literal directly at the call site, or declare the parameter type as `Closure(...)`", .{}); } return val; }, // Tuple → Tuple element-wise coercion (e.g. a `(s64, s64)` literal // flowing into a `(s32, s32)` slot — the multi-value failable success // tuple). Same arity: extract each slot, coerce it, rebuild. .tuple_elementwise => { const si = self.module.types.get(src_ty); const di = self.module.types.get(dst_ty); var elems = std.ArrayList(Ref).empty; defer elems.deinit(self.alloc); for (si.tuple.fields, di.tuple.fields, 0..) |sf, df, i| { const fv = self.builder.emit(.{ .tuple_get = .{ .base = val, .field_index = @intCast(i), .base_type = src_ty } }, sf); elems.append(self.alloc, self.coerceMode(fv, sf, df, mode)) catch unreachable; } return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, elems.items) catch unreachable } }, dst_ty); }, // Optional → Concrete unwrapping (flow-sensitive narrowing coercion) .optional_unwrap => { const child_ty = self.module.types.get(src_ty).optional.child; const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); return self.coerceMode(unwrapped, child_ty, dst_ty, mode); }, // void → Optional: produce null (void is the type of null_literal) .void_to_optional => return self.builder.constNull(dst_ty), // Concrete → Optional wrapping (coerce to the inner type first) .optional_wrap => { const child_ty = self.module.types.get(dst_ty).optional.child; const coerced = self.coerceMode(val, src_ty, child_ty, mode); return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty); }, // Concrete → Protocol (auto type erasure) .erase_protocol => { const proto_name = self.module.types.getString(self.module.types.get(dst_ty).@"struct".name); const ctn = self.resolveConcreteTypeName(src_ty).?; // If src is a pointer, use directly; otherwise alloca+store + heap-copy var concrete_ptr = val; var concrete_ty = src_ty; var heap_copy = false; if (!src_ty.isBuiltin()) { const si = self.module.types.get(src_ty); if (si == .pointer) { concrete_ty = si.pointer.pointee; heap_copy = false; } else { const slot = self.builder.alloca(src_ty); self.builder.store(slot, val); concrete_ptr = slot; heap_copy = true; } } return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy); }, .int_to_float => return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .float_to_int => { // Implicit float→int narrowing follows the unified rule (the // same `floatToIntExact` the array-dim / `$K: Count` paths use): // a compile-time INTEGRAL float folds to its int, a NON-integral // one is a compile error. Explicit `xx` / `cast` (mode // `.explicit`) skips this and truncates. A runtime float has no // compile-time value to fold — it truncates as before. if (mode == .implicit) { if (self.builder.constFloatInfo(val)) |info| { if (program_index_mod.floatToIntExact(info.value)) |iv| { return self.builder.constInt(iv, dst_ty); } // Non-integral: diagnose, then fall through to the // truncating op below so lowering finishes and // `hasErrors()` aborts the build. self.diagNonIntegralNarrow(.{ .start = info.span.start, .end = info.span.end }, info.value, dst_ty); } } return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); }, // Ptr ↔ Int — explicit `xx ptr` to/from an integer-typed slot. // Emits a `bitcast` IR op; emit_llvm.zig's bitcast arm dispatches // to LLVMBuildPtrToInt / LLVMBuildIntToPtr at the LLVM level // since LLVMBuildBitCast itself doesn't accept ptr↔int. .ptr_int_bitcast => return self.builder.emit(.{ .bitcast = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .narrow => return self.builder.emit(.{ .narrow = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .widen => return self.builder.emit(.{ .widen = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .array_to_slice => return self.builder.emit(.{ .array_to_slice = .{ .operand = val } }, dst_ty), } } /// Get the alloca Ref for an expression, if it's a simple variable reference. /// Returns null for complex expressions (field access, function calls, etc.) pub fn getExprAlloca(self: *Lowering, node: *const Node) ?Ref { const name = switch (node.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => return null, }; if (self.scope) |scope| { if (scope.lookup(name)) |binding| { if (binding.is_alloca) return binding.ref; } } return null; } /// Get the element type for a slice/array/string type. A non-collection /// type has no element type — return `.unresolved` (asking for it is a bug) /// rather than a plausible `.s64`. pub fn getElementType(self: *Lowering, ty: TypeId) TypeId { if (ty == .string) return .u8; if (ty.isBuiltin()) return .unresolved; const info = self.module.types.get(ty); return switch (info) { .slice => |s| s.element, .array => |a| a.element, .vector => |v| v.element, .many_pointer => |p| p.element, else => .unresolved, }; } pub fn isFloat(ty: TypeId) bool { return ty == .f32 or ty == .f64; } /// Result type of an arithmetic / bitwise / shift binary op over two /// scalar operand types. This is the single promotion rule shared by the /// value path (`lowerBinaryOp`) and AST-level inference /// (`ExprTyper.inferType`'s binary-op arm), so static typing reports /// exactly the type the lowered value carries. An integer LHS with a /// floating-point RHS promotes to the float (`s64 + f64` → `f64`); every /// other pairing — including vectors / structs, whose `isInt` is false — /// takes the LHS type. Comparison / logical ops never reach here (they /// are `.bool` at both sites). pub fn arithResultType(lhs_ty: TypeId, rhs_ty: TypeId) TypeId { if (isInt(lhs_ty) and isFloat(rhs_ty)) return rhs_ty; return lhs_ty; } fn isInt(ty: TypeId) bool { return switch (ty) { .s8, .s16, .s32, .s64, .u8, .u16, .u32, .u64, .usize, .isize => true, else => false, }; } pub fn isIntEx(self: *Lowering, ty: TypeId) bool { if (isInt(ty)) return true; if (!ty.isBuiltin()) { const info = self.module.types.get(ty); return switch (info) { .signed, .unsigned => true, else => false, }; } return false; } /// Operands valid for a scalar numeric op (`+ - * / %`): ints (incl. /// custom widths), floats, SIMD vectors, and pointers (pointer /// arithmetic). `.unresolved` returns true so a type we couldn't infer /// is never diagnosed — the check only fires on a concretely /// incompatible operand (e.g. `string`, a struct, an enum). fn isArithOperand(self: *Lowering, ty: TypeId) bool { if (ty == .unresolved) return true; if (isInt(ty) or isFloat(ty)) return true; if (ty.isBuiltin()) return false; return switch (self.module.types.get(ty)) { .signed, .unsigned, .vector, .pointer, .many_pointer => true, else => false, }; } /// Operands valid for ordering comparisons (`< <= > >=`): numbers /// (incl. custom int widths), enums (ordinal), pointers (address /// order), bool, and SIMD vectors. NOT strings (no lexicographic `<` /// lowering exists) or any other aggregate. `.unresolved` passes so an /// un-inferable operand is never falsely diagnosed. fn isOrderingOperand(self: *Lowering, ty: TypeId) bool { if (ty == .unresolved) return true; if (isInt(ty) or isFloat(ty) or ty == .bool) return true; if (ty.isBuiltin()) return false; return switch (self.module.types.get(ty)) { .signed, .unsigned, .@"enum", .pointer, .many_pointer, .vector => true, else => false, }; } /// Operands valid for bitwise/shift ops (`& | ^ << >>`): integers /// (incl. custom widths), enums (flags are int-backed), bool, and SIMD /// vectors. NOT floats, strings, pointers, or aggregates. `.unresolved` /// passes (see `isOrderingOperand`). fn isBitwiseOperand(self: *Lowering, ty: TypeId) bool { if (ty == .unresolved) return true; if (isInt(ty) or ty == .bool) return true; if (ty.isBuiltin()) return false; return switch (self.module.types.get(ty)) { .signed, .unsigned, .@"enum", .vector => true, else => false, }; } /// Human-readable description of a typed module-const initializer, used in /// the issue-0088 type-mismatch diagnostic. A literal names its kind; a /// const-expression is described by its inferred type category, so the /// message is accurate for `N : string : M + 2` ("an integer expression") /// as well as for `N : string : 4` ("an integer literal"). pub fn initializerDescription(self: *Lowering, node: *const Node) []const u8 { return switch (node.data) { .int_literal => "an integer literal", .float_literal => "a float literal", .bool_literal => "a boolean literal", .string_literal => "a string literal", .null_literal => "null", .undef_literal => "'---'", else => self.constExprDescription(self.inferExprType(node)), }; } fn constExprDescription(self: *Lowering, init_ty: TypeId) []const u8 { if (self.isIntEx(init_ty)) return "an integer expression"; if (isFloat(init_ty)) return "a floating-point expression"; if (init_ty == .bool) return "a boolean expression"; if (init_ty == .string) return "a string expression"; return "an expression of an incompatible type"; } fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 { return switch (op) { .add => "+", .sub => "-", .mul => "*", .div => "/", .mod => "%", .eq => "==", .neq => "!=", .lt => "<", .lte => "<=", .gt => ">", .gte => ">=", .and_op => "and", .or_op => "or", .bit_and => "&", .bit_or => "|", .bit_xor => "^", .shl => "<<", .shr => ">>", .in_op => "in", }; } fn typeBits(ty: TypeId) u32 { return switch (ty) { .bool => 1, .s8, .u8 => 8, .s16, .u16 => 16, .s32, .u32 => 32, .s64, .u64 => 64, .usize, .isize => 0, // target-dependent — use typeBitsEx .f32 => 32, .f64 => 64, else => 0, }; } pub fn typeBitsEx(self: *Lowering, ty: TypeId) u32 { if (ty == .usize or ty == .isize) return @as(u32, self.module.types.pointer_size) * 8; const b = typeBits(ty); if (b > 0) return b; if (!ty.isBuiltin()) { const info = self.module.types.get(ty); return switch (info) { .signed => |w| @as(u32, w), .unsigned => |w| @as(u32, w), else => 0, }; } return 0; } /// Apply C default argument promotion to variadic-tail args. These rules /// (bool/s8/s16/u8/u16 → s32, f32 → f64) match the C calling convention's /// implicit promotions when an argument is passed through `...`. fn promoteCVariadicArgs(self: *Lowering, args: []Ref, fixed_count: usize) void { if (args.len <= fixed_count) return; for (args[fixed_count..]) |*arg| { const src_ty = self.builder.getRefType(arg.*); const promoted: TypeId = switch (src_ty) { .bool, .s8, .s16, .u8, .u16 => .s32, .f32 => .f64, else => continue, }; arg.* = self.coerceToType(arg.*, src_ty, promoted); } } /// Coerce call arguments in-place to match function parameter types. fn coerceCallArgs(self: *Lowering, args: []Ref, params: []const Function.Param) void { for (0..@min(args.len, params.len)) |i| { const src_ty = self.builder.getRefType(args[i]); const dst_ty = params[i].ty; if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) { const src_info = self.module.types.get(src_ty); const dst_info = self.module.types.get(dst_ty); // Array → many_pointer decay: alloca the array, GEP to first element if (src_info == .array and dst_info == .many_pointer) { const slot = self.builder.alloca(src_ty); self.builder.store(slot, args[i]); const zero = self.builder.constInt(0, .s64); args[i] = self.builder.emit(.{ .index_gep = .{ .lhs = slot, .rhs = zero } }, dst_ty); continue; } // Implicit address-of: passing T value where *T is expected → alloca + store // Only when the pointee type matches the source type. if (dst_info == .pointer and src_info != .pointer and dst_info.pointer.pointee == src_ty) { const slot = self.builder.alloca(src_ty); self.builder.store(slot, args[i]); args[i] = slot; continue; } } args[i] = self.coerceToType(args[i], src_ty, dst_ty); } } /// Emit a C-ABI exported function for every bodied method on a /// `#jni_main #jni_class("...")` declaration. The symbol name follows /// JNI's name-mangling convention so Android's JNI runtime can resolve /// `private native sx_(...)` (declared in the bundled /// classes.dex by `jni_java_emit`) without an explicit `RegisterNatives` /// call — i.e. `Java___sx_1`. /// /// Param ABI: prepended `(env: *void, self: *void)` (JNIEnv* + jobject /// receiver), followed by the user-declared params with pointer types /// type-erased to `*void` (JNI carries jobjects, not sx-typed handles — /// future work can keep richer typing inside the body when needed). /// Eagerly lower bodied instance methods on every sx-defined /// `#objc_class`. The Obj-C runtime invokes these via the IMP /// pointers wired up in M1.2 A.4 — no sx-side call path triggers /// lazy lowering, so we walk the cache and force-lower here. /// `lowerFunction` sets `current_foreign_class` automatically based /// on the qualified name, so `*Self` substitutions in the body /// resolve correctly (M1.2 A.2b). After the bodies are lowered, /// `emitObjcDefinedClassImps` wraps each with a C-ABI trampoline /// (M1.2 A.4b.ii). pub fn lowerObjcDefinedClassMethods(self: *Lowering) void { for (self.module.objc_defined_class_cache.items) |entry| { const fcd = entry.decl; for (fcd.members) |m| { const method = switch (m) { .method => |md| md, else => continue, }; if (method.body == null) continue; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, method.name }) catch continue; self.lazyLowerFunction(qualified); } } // Now the bodies are lowered — emit the C-ABI IMP trampolines // that bridge `objc_msgSend` invocations to them. self.emitObjcDefinedClassImps(); } /// If `obj_expr` is typed as a pointer to a foreign Obj-C class /// and that class (or any of its `#extends` ancestors) declares a /// `#property` field with the given name, return the /// `ForeignFieldDecl`. M2.2 + M2.3. pub fn lookupObjcPropertyOnPointer(self: *Lowering, obj_expr: *const ast.Node, field_name: []const u8) ?ast.ForeignFieldDecl { const obj_ty = self.inferExprType(obj_expr); if (obj_ty.isBuiltin()) return null; const ptr_info = self.module.types.get(obj_ty); if (ptr_info != .pointer) return null; const pointee_info = self.module.types.get(ptr_info.pointer.pointee); if (pointee_info != .@"struct") return null; const struct_name = self.module.types.getString(pointee_info.@"struct".name); const fcd = self.program_index.foreign_class_map.get(struct_name) orelse return null; if (fcd.runtime != .objc_class and fcd.runtime != .objc_protocol) return null; return self.findForeignPropertyInChain(fcd, field_name); } /// Walk the `#extends` chain looking for a method by name. M2.3. /// Returns the owning fcd + the method decl, or null if no ancestor /// declares it. Depth-capped at 16 to break accidental cycles /// (real Obj-C class chains rarely exceed 6 levels). fn findForeignMethodInChain(self: *Lowering, fcd: *const ast.ForeignClassDecl, method_name: []const u8) ?struct { fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl } { var current: *const ast.ForeignClassDecl = fcd; var depth: u32 = 0; while (depth < 16) : (depth += 1) { for (current.members) |m| switch (m) { .method => |md| if (std.mem.eql(u8, md.name, method_name)) return .{ .fcd = current, .method = md }, else => {}, }; // Not on this level — follow `#extends ParentName`. const parent = blk: { for (current.members) |m| switch (m) { .extends => |p| break :blk p, else => {}, }; break :blk null; } orelse return null; current = self.program_index.foreign_class_map.get(parent) orelse return null; } return null; } /// Walk the `#extends` chain looking for a `#property` field by /// name. M2.3 companion to findForeignMethodInChain. fn findForeignPropertyInChain(self: *Lowering, fcd: *const ast.ForeignClassDecl, field_name: []const u8) ?ast.ForeignFieldDecl { var current: *const ast.ForeignClassDecl = fcd; var depth: u32 = 0; while (depth < 16) : (depth += 1) { for (current.members) |m| switch (m) { .field => |f| if (f.is_property and std.mem.eql(u8, f.name, field_name)) return f, else => {}, }; const parent = blk: { for (current.members) |m| switch (m) { .extends => |p| break :blk p, else => {}, }; break :blk null; } orelse return null; current = self.program_index.foreign_class_map.get(parent) orelse return null; } return null; } const ObjcDefinedStateField = struct { field_ty: TypeId, state_ty: TypeId, field_idx: u32, fcd: *const ast.ForeignClassDecl, }; /// State-field-access info: if obj_expr is * /// and `field_name` is in the state struct (not a property), /// returns the field's TypeId, the state struct's TypeId, and /// the field's index. M1.2 A.3 supports. pub fn lookupObjcDefinedStateFieldOnPointer(self: *Lowering, obj_expr: *const ast.Node, field_name: []const u8) ?ObjcDefinedStateField { const obj_ty = self.inferExprType(obj_expr); if (obj_ty.isBuiltin()) return null; const ptr_info = self.module.types.get(obj_ty); if (ptr_info != .pointer) return null; const pointee_info = self.module.types.get(ptr_info.pointer.pointee); if (pointee_info != .@"struct") return null; const struct_name = self.module.types.getString(pointee_info.@"struct".name); const fcd = self.program_index.foreign_class_map.get(struct_name) orelse return null; // Only sx-defined Obj-C classes have a state struct. Foreign // classes' fields are purely declaration metadata (no state). if (fcd.is_foreign or fcd.runtime != .objc_class) return null; // Skip property fields — those dispatch via the M2.2 getter/setter // path. Plain instance fields take the ivar+gep path. for (fcd.members) |m| switch (m) { .field => |f| { if (std.mem.eql(u8, f.name, field_name)) { if (f.is_property) return null; const state_ty = self.objc().objcDefinedStateStructType(fcd); const state_info = self.module.types.get(state_ty); if (state_info != .@"struct") return null; const fname_id = self.module.types.internString(f.name); for (state_info.@"struct".fields, 0..) |sf, idx| { if (sf.name == fname_id) { return .{ .field_ty = sf.ty, .state_ty = state_ty, .field_idx = @intCast(idx), .fcd = fcd, }; } } return null; } }, else => {}, }; return null; } /// Lower a read of `self.field` (or `obj.field`) on a sx-defined /// Obj-C class: `state = object_getIvar(self, load(ivar_global))` /// then `struct_gep(state, idx)` + load. M1.2 A.3 — the runtime /// hop through the hidden ivar. fn lowerObjcDefinedStateFieldRead( self: *Lowering, obj_expr: *const ast.Node, info: ObjcDefinedStateField, ) Ref { const obj_ref = self.lowerExpr(obj_expr); const state_ptr = self.lowerObjcDefinedStateForObj(obj_ref, info.fcd) orelse return Ref.none; const ptr_void = self.module.types.ptrTo(.void); const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = info.field_idx, .base_type = info.state_ty, } }, ptr_void); return self.builder.load(field_addr, info.field_ty); } /// `state = object_getIvar(obj, load(___state_ivar))`. Shared /// helper for state-field read + write (M1.2 A.3). pub fn lowerObjcDefinedStateForObj(self: *Lowering, obj_ref: Ref, fcd: *const ast.ForeignClassDecl) ?Ref { const ptr_void = self.module.types.ptrTo(.void); const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return null; defer self.alloc.free(ivar_global_name); const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return null; const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void); const ivar_handle = self.builder.load(ivar_addr, ptr_void); const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void); const args = self.alloc.alloc(Ref, 2) catch return null; args[0] = obj_ref; args[1] = ivar_handle; return self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = args } }, ptr_void); } /// Lower `obj.field` for an Obj-C `#property` field as /// `objc_msg_send(obj, sel_)`. M2.2 — getter side. /// The setter side lives in the assignment-statement lowering. fn lowerObjcPropertyGetter(self: *Lowering, obj_expr: *const ast.Node, field: ast.ForeignFieldDecl, _: []const u8, _: ast.Span) Ref { const obj_ref = self.lowerExpr(obj_expr); const ret_ty = self.resolveType(field.field_type); const vptr_ty = self.module.types.ptrTo(.void); // The selector for a property getter is the field name verbatim // (Obj-C convention; the override hook is for niche cases like // `isHidden` and lands with M2.2's modifier handling). const sel_slot_gid = self.internObjcSelector(field.name); const slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty)); const sel = self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty); return self.builder.emit(.{ .objc_msg_send = .{ .recv = obj_ref, .sel = sel, .args = &.{}, } }, ret_ty); } /// Lower `obj.field = val` for an Obj-C `#property` field as /// `objc_msg_send(obj, sel_set:, val)`. M2.2 — setter side. /// Selector: prepend "set", capitalize the first letter of the /// field name, append ":". `backgroundColor` → `setBackgroundColor:`. pub fn lowerObjcPropertySetter(self: *Lowering, obj_expr: *const ast.Node, field: ast.ForeignFieldDecl, val: Ref) void { const obj_ref = self.lowerExpr(obj_expr); const vptr_ty = self.module.types.ptrTo(.void); // Build the setter selector. var sel_buf = std.ArrayList(u8).empty; defer sel_buf.deinit(self.alloc); sel_buf.appendSlice(self.alloc, "set") catch unreachable; if (field.name.len > 0) { sel_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable; sel_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable; } sel_buf.append(self.alloc, ':') catch unreachable; const sel_str = self.alloc.dupe(u8, sel_buf.items) catch unreachable; const sel_slot_gid = self.internObjcSelector(sel_str); const slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty)); const sel = self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty); const args = self.alloc.alloc(Ref, 1) catch unreachable; args[0] = val; _ = self.builder.emit(.{ .objc_msg_send = .{ .recv = obj_ref, .sel = sel, .args = args, } }, .void); } /// Get a FuncId for an external C-callconv function. If a function /// with this exported name already exists in the module (e.g. /// declared by stdlib `#foreign` decl), return it; otherwise /// declare it fresh with the given signature. /// /// One helper instead of a `getFid` per runtime function — /// avoids per-function cache fields and per-function boilerplate. fn ensureCRuntimeDecl(self: *Lowering, name: []const u8, param_tys: []const TypeId, ret_ty: TypeId) FuncId { const name_id = self.module.types.internString(name); for (self.module.functions.items, 0..) |f, i| { if (f.name == name_id) return FuncId.fromIndex(@intCast(i)); } var params = std.ArrayList(inst_mod.Function.Param).empty; for (param_tys, 0..) |pty, i| { // Param names don't matter at the LLVM ABI boundary — // synthesize generic ones (`a0`, `a1`, ...) so we don't // need a parallel name list per call site. const synth = std.fmt.allocPrint(self.alloc, "a{d}", .{i}) catch unreachable; params.append(self.alloc, .{ .name = self.module.types.internString(synth), .ty = pty, }) catch unreachable; } const fid = self.builder.declareExtern(name_id, params.toOwnedSlice(self.alloc) catch unreachable, ret_ty); self.module.getFunctionMut(fid).call_conv = .c; return fid; } /// For each bodied instance method on a sx-defined `#objc_class`, /// emit a C-ABI IMP trampoline that the Obj-C runtime calls (after /// the dispatch path from `objc_msgSend`). The trampoline: /// 1. Loads the cached ivar handle from `@___state_ivar`. /// 2. Calls `object_getIvar(obj, ivar)` to get the `*State` /// state pointer. /// 3. Calls the sx body `@.(__sx_default_context, /// state, ...user_args)` (default sx-callconv). /// 4. Returns the result (or `ret void`). /// /// IMP name: `____imp`. emit_llvm's /// constructor (A.4b.ii companion) registers this via /// `class_addMethod` with a derived selector + type encoding. fn emitObjcDefinedClassImps(self: *Lowering) void { for (self.module.objc_defined_class_cache.items) |entry| { const fcd = entry.decl; // Pin to the class's defining module (E4) so the IMP trampolines' // method-signature types (`-> BOOL`, param types) resolve where they // are visible, not at whatever lowering site triggered emission. const saved_src = self.current_source_file; defer self.setCurrentSourceFile(saved_src); if (fcd.source_file) |src| self.setCurrentSourceFile(src); // Synthesize +alloc (M1.2 A.5) and -dealloc (M1.2 A.6). emit_llvm // registers +alloc on the metaclass and -dealloc on the class // itself after objc_registerClassPair. self.emitObjcDefinedClassAllocImp(fcd); self.emitObjcDefinedClassDeallocImp(fcd); for (fcd.members) |m| { switch (m) { .method => |method| { if (method.body == null) continue; self.emitObjcDefinedClassImp(fcd, method); }, .field => |field| { // M2.2 second pass — sx-defined property fields // synthesize getter (+ setter unless `readonly`) // IMPs that GEP into the state struct. if (field.is_property) { self.emitObjcDefinedClassPropertyImps(fcd, field); } }, else => {}, } } } } /// Lazily declare libobjc's ARC runtime helpers. Idempotent — uses /// `ensureCRuntimeDecl` which skips already-declared symbols. Called /// from the property setter/getter and -dealloc emission paths when /// they need to emit a retain/release/storeWeak/etc. fn ensureArcRuntimeDecls(self: *Lowering) void { const ptr_void = self.module.types.ptrTo(.void); _ = self.ensureCRuntimeDecl("objc_retain", &.{ptr_void}, ptr_void); _ = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void); _ = self.ensureCRuntimeDecl("objc_storeWeak", &.{ ptr_void, ptr_void }, ptr_void); _ = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void); _ = self.ensureCRuntimeDecl("objc_initWeak", &.{ ptr_void, ptr_void }, ptr_void); _ = self.ensureCRuntimeDecl("objc_destroyWeak", &.{ptr_void}, .void); } /// M2.2 second pass — emit synthesized getter/setter IMPs for a /// property field on a sx-defined `#objc_class`. The state struct /// already holds the field (via objcDefinedStateStructType); the /// IMPs just dispatch a load/store through the `__sx_state` ivar. /// /// Getter IMP: `____imp(self, _cmd) -> T` /// state = object_getIvar(self, load(___state_ivar)) /// return state. /// /// Setter IMP (skipped if `readonly` in modifiers): /// `___set_imp(self, _cmd, val) -> void` /// state = object_getIvar(self, load(___state_ivar)) /// state. = val /// /// Both IMPs land in the cache's methods slice with appropriate /// selectors + encodings; emit_llvm's class_addMethod loop wires /// them up like any other instance method. fn emitObjcDefinedClassPropertyImps(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl) void { const state_ty = self.objc().objcDefinedStateStructType(fcd); const state_info = self.module.types.get(state_ty); if (state_info != .@"struct") return; // Find the field's index in the state struct. const field_name_id = self.module.types.internString(field.name); var field_idx: ?u32 = null; for (state_info.@"struct".fields, 0..) |sf, i| { if (sf.name == field_name_id) { field_idx = @intCast(i); break; } } const fidx = field_idx orelse return; const field_ty = self.resolveType(field.field_type); // M4.B: validate modifiers + resolve ARC kind. Side-effect: emits // diagnostics for typos, weak-on-non-object, ambiguous *void, etc. // For now the setter/getter still emit bare load/store; subsequent // M4.B commits wire the actual ARC ops keyed on this kind. _ = self.objc().objcPropertyKind(field); // (1) Getter: ____imp self.emitObjcDefinedPropertyGetter(fcd, field, state_ty, fidx, field_ty); // (2) Setter — skipped for `readonly`. var is_readonly = false; for (field.property_modifiers) |mod| { if (std.mem.eql(u8, mod, "readonly")) { is_readonly = true; break; } } if (!is_readonly) { self.emitObjcDefinedPropertySetter(fcd, field, state_ty, fidx, field_ty); } // (3) Register in the cache's methods slice. Both IMPs use the // method-registration pipeline that lands in class_addMethod // calls from emit_llvm. self.registerObjcDefinedPropertyMethodEntries(fcd, field, field_ty, is_readonly); } fn emitObjcDefinedPropertyGetter(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl, state_ty: TypeId, fidx: u32, field_ty: TypeId) void { const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; defer { self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, field.name }) catch return; const name_id = self.module.types.internString(imp_name); const ptr_void = self.module.types.ptrTo(.void); var params = std.ArrayList(inst_mod.Function.Param).empty; params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return; params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return; const params_slice = params.toOwnedSlice(self.alloc) catch return; _ = self.builder.beginFunction(name_id, params_slice, field_ty); const func = self.builder.currentFunc(); func.linkage = .external; func.call_conv = .c; func.has_implicit_ctx = false; const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // state = object_getIvar(self, load @___state_ivar) const self_ref = Ref.fromIndex(0); const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return; defer self.alloc.free(ivar_global_name); const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return; const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void); const ivar_handle = self.builder.load(ivar_addr, ptr_void); const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void); const get_args = self.alloc.alloc(Ref, 2) catch return; get_args[0] = self_ref; get_args[1] = ivar_handle; const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void); const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void); // M4.B getter — weak fields go through objc_loadWeakRetained + // objc_autorelease for race-safe reads. The bare-load path // (strong/copy/assign) is the common case and reads the slot // directly. const kind = self.objc().objcPropertyKind(field); if (kind == .weak) { self.ensureArcRuntimeDecls(); const load_weak_fid = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void); const autorelease_fid = self.ensureCRuntimeDecl("objc_autorelease", &.{ptr_void}, ptr_void); // retained = objc_loadWeakRetained(field_addr) // - atomic upgrade-to-strong via libobjc's side-table; if the // target deinitialised, returns null. The caller gets a // +1 retained reference (or null). const load_args = self.alloc.alloc(Ref, 1) catch return; load_args[0] = field_addr; const retained = self.builder.emit(.{ .call = .{ .callee = load_weak_fid, .args = load_args } }, ptr_void); // autoreleased = objc_autorelease(retained) // - drops it into the current pool so the caller doesn't need // to manually release. Returns the same pointer (typed). const ar_args = self.alloc.alloc(Ref, 1) catch return; ar_args[0] = retained; const autoreleased = self.builder.emit(.{ .call = .{ .callee = autorelease_fid, .args = ar_args } }, ptr_void); self.builder.ret(autoreleased, field_ty); self.builder.finalize(); return; } // strong / copy / assign — bare load. const val = self.builder.load(field_addr, field_ty); self.builder.ret(val, field_ty); self.builder.finalize(); } fn emitObjcDefinedPropertySetter(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl, state_ty: TypeId, fidx: u32, field_ty: TypeId) void { const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; defer { self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } // Setter selector: set: → imp name: ___set_imp var setter_field_buf = std.ArrayList(u8).empty; defer setter_field_buf.deinit(self.alloc); setter_field_buf.appendSlice(self.alloc, "set") catch unreachable; if (field.name.len > 0) { setter_field_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable; setter_field_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable; } const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, setter_field_buf.items }) catch return; const name_id = self.module.types.internString(imp_name); const ptr_void = self.module.types.ptrTo(.void); var params = std.ArrayList(inst_mod.Function.Param).empty; params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return; params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return; params.append(self.alloc, .{ .name = self.module.types.internString("val"), .ty = field_ty }) catch return; const params_slice = params.toOwnedSlice(self.alloc) catch return; _ = self.builder.beginFunction(name_id, params_slice, .void); const func = self.builder.currentFunc(); func.linkage = .external; func.call_conv = .c; func.has_implicit_ctx = false; const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); const self_ref = Ref.fromIndex(0); const val_ref = Ref.fromIndex(2); const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return; defer self.alloc.free(ivar_global_name); const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return; const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void); const ivar_handle = self.builder.load(ivar_addr, ptr_void); const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void); const get_args = self.alloc.alloc(Ref, 2) catch return; get_args[0] = self_ref; get_args[1] = ivar_handle; const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void); const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void); // M4.B setter — emit ARC ops based on the property's modifier kind. const kind = self.objc().objcPropertyKind(field); switch (kind) { .assign => { // Primitives or explicit assign: bare store, no ARC. self.builder.store(field_addr, val_ref); }, .strong => { // Retain new, release old. Order matters: retain first // (in case val == old, we don't release before retain). self.ensureArcRuntimeDecls(); const retain_fid = self.ensureCRuntimeDecl("objc_retain", &.{ptr_void}, ptr_void); const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void); // old = load field_addr const old_val = self.builder.load(field_addr, field_ty); // new = objc_retain(val) const retain_args = self.alloc.alloc(Ref, 1) catch return; retain_args[0] = val_ref; _ = self.builder.emit(.{ .call = .{ .callee = retain_fid, .args = retain_args } }, ptr_void); // store field_addr, val self.builder.store(field_addr, val_ref); // objc_release(old) — Apple's runtime treats release(NULL) as a no-op, // so we skip an explicit null-check (saves a branch on every assign). const release_args = self.alloc.alloc(Ref, 1) catch return; release_args[0] = old_val; _ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = release_args } }, .void); }, .weak => { // objc_storeWeak(field_addr, val) handles first-store // (init) and re-store (destroy old + init new) atomically. self.ensureArcRuntimeDecls(); const store_weak_fid = self.ensureCRuntimeDecl("objc_storeWeak", &.{ ptr_void, ptr_void }, ptr_void); const store_args = self.alloc.alloc(Ref, 2) catch return; store_args[0] = field_addr; store_args[1] = val_ref; _ = self.builder.emit(.{ .call = .{ .callee = store_weak_fid, .args = store_args } }, ptr_void); }, .copy => { // copy = objc_msgSend(val, sel_copy) — returns retained // (NSCopying contract). // Release old, then store the copy. self.ensureArcRuntimeDecls(); const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void); // Load + cache the `copy` selector slot. const sel_copy_gid = self.internObjcSelector("copy"); const sel_slot_ptr = self.builder.emit(.{ .global_addr = sel_copy_gid }, self.module.types.ptrTo(ptr_void)); const sel_copy = self.builder.emit(.{ .load = .{ .operand = sel_slot_ptr } }, ptr_void); // copy = [val copy] const copy_args = self.alloc.alloc(Ref, 0) catch return; const copied = self.builder.emit(.{ .objc_msg_send = .{ .recv = val_ref, .sel = sel_copy, .args = copy_args, } }, ptr_void); const old_val = self.builder.load(field_addr, field_ty); self.builder.store(field_addr, copied); const release_args = self.alloc.alloc(Ref, 1) catch return; release_args[0] = old_val; _ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = release_args } }, .void); }, } self.builder.retVoid(); self.builder.finalize(); } /// Append the property's getter (and setter, unless readonly) /// entries to the class's method-registration slice so emit_llvm /// calls class_addMethod on each. Selectors + encodings derived /// from the field type. fn registerObjcDefinedPropertyMethodEntries(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl, field_ty: TypeId, is_readonly: bool) void { const cur = self.module.lookupObjcDefinedClass(fcd.name) orelse return; _ = cur; // Find the existing entry and grow its methods slice. var new_methods = std.ArrayList(Module.ObjcDefinedMethodEntry).empty; for (self.module.objc_defined_class_cache.items) |entry| { if (!std.mem.eql(u8, entry.name, fcd.name)) continue; for (entry.methods) |m| new_methods.append(self.alloc, m) catch unreachable; // Getter entry — selector = field name, encoding = "@:". const getter_enc = self.objc().objcTypeEncodingFromSignature(field_ty, &.{}, null) catch return; const getter_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, field.name }) catch return; new_methods.append(self.alloc, .{ .sel = field.name, .encoding = getter_enc, .imp_name = getter_imp_name, .is_class = false, }) catch unreachable; // Setter entry — selector = set:, encoding = "v@:". if (!is_readonly) { var sel_buf = std.ArrayList(u8).empty; defer sel_buf.deinit(self.alloc); sel_buf.appendSlice(self.alloc, "set") catch unreachable; if (field.name.len > 0) { sel_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable; sel_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable; } sel_buf.append(self.alloc, ':') catch unreachable; const setter_sel = self.alloc.dupe(u8, sel_buf.items) catch return; const setter_enc = self.objc().objcTypeEncodingFromSignature(.void, &.{field_ty}, null) catch return; var setter_imp_field_buf = std.ArrayList(u8).empty; defer setter_imp_field_buf.deinit(self.alloc); setter_imp_field_buf.appendSlice(self.alloc, "set") catch unreachable; if (field.name.len > 0) { setter_imp_field_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable; setter_imp_field_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable; } const setter_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, setter_imp_field_buf.items }) catch return; new_methods.append(self.alloc, .{ .sel = setter_sel, .encoding = setter_enc, .imp_name = setter_imp_name, .is_class = false, }) catch unreachable; } break; } const slice = new_methods.toOwnedSlice(self.alloc) catch return; self.module.setObjcDefinedClassMethods(fcd.name, slice); } fn emitObjcDefinedClassImp(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void { // Class methods (no `*Self` first param) skip the ivar read — // they have no instance state to thread through. if (md.is_static) { self.emitObjcDefinedClassStaticImp(fcd, md); return; } // Save+restore builder state — we're switching into a new fn // mid-pass and need to restore for the next emit_llvm steps. const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; defer { self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, md.name }) catch return; const name_id = self.module.types.internString(imp_name); const ptr_void = self.module.types.ptrTo(.void); // C-ABI signature: (obj: *void, _cmd: *void, ...user_args) -> ret. // User params skip index 0 (which is *Self). var params = std.ArrayList(inst_mod.Function.Param).empty; params.append(self.alloc, .{ .name = self.module.types.internString("obj"), .ty = ptr_void }) catch return; params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return; // Set current_foreign_class so *Self in user-param resolution // resolves to *State (M1.2 A.2b). Save+restore. const saved_fc = self.current_foreign_class; self.current_foreign_class = fcd; defer self.current_foreign_class = saved_fc; const param_start: usize = 1; for (md.params[param_start..], 0..) |p_node, i| { // User params are reflected at the C-ABI boundary AS-IS — // the runtime trampoline forwards them through to the body. // *Self here would be a programming error (only the implicit // self at index 0 is *Self), but we use resolveType to handle // pointer types correctly. const pty = self.resolveType(p_node); params.append(self.alloc, .{ .name = self.module.types.internString(md.param_names[param_start + i]), .ty = pty, }) catch return; } const ret_ty: TypeId = if (md.return_type) |rt| self.resolveType(rt) else .void; const params_slice = params.toOwnedSlice(self.alloc) catch return; _ = self.builder.beginFunction(name_id, params_slice, ret_ty); const func = self.builder.currentFunc(); func.linkage = .external; func.call_conv = .c; func.has_implicit_ctx = false; const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // Pass the Obj-C receiver pointer through to the sx body as // `self`. The body's `self: *Self` type resolves to the // foreign-class stub (the opaque Obj-C type), matching Apple's // Obj-C semantics where `self` IS the object. `self.field` // access on a sx-defined class is rewritten by lowerFieldAccess // to go through `object_getIvar(self, __sx_state_ivar)` and // a struct_gep on the state struct — see M1.2 A.3. const obj_ref = Ref.fromIndex(0); // Call sx body `@.(default_ctx, self, ...user_args)`. const body_name = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, md.name }) catch return; defer self.alloc.free(body_name); const body_fid = self.resolveFuncByName(body_name) orelse return; const ctx_ref: ?Ref = blk: { if (!self.implicit_ctx_enabled) break :blk null; const dctx_gi = self.program_index.global_names.get("__sx_default_context") orelse break :blk null; break :blk self.builder.emit(.{ .global_addr = dctx_gi.id }, ptr_void); }; // Build arg list: [ctx?] + self + user_args. const num_user_args = params_slice.len - 2; // minus obj + _cmd const num_call_args = (if (ctx_ref != null) @as(usize, 1) else 0) + 1 + num_user_args; const call_args = self.alloc.alloc(Ref, num_call_args) catch return; var idx: usize = 0; if (ctx_ref) |c_ref| { call_args[idx] = c_ref; idx += 1; } call_args[idx] = obj_ref; idx += 1; var ip: usize = 2; while (ip < params_slice.len) : (ip += 1) { call_args[idx] = Ref.fromIndex(@intCast(ip)); idx += 1; } const call_ref = self.builder.emit(.{ .call = .{ .callee = body_fid, .args = call_args, } }, ret_ty); // (4) Return. if (ret_ty == .void) { self.builder.retVoid(); } else { self.builder.ret(call_ref, ret_ty); } self.builder.finalize(); } /// Synthesize the `+alloc` IMP for an sx-defined `#objc_class`. /// Class method registered on the metaclass — when `[SxFoo alloc]` /// runs from Apple's runtime (Info.plist principal class, /// NSCoder unarchive, UIKit reflection), this IMP fires. /// /// C-ABI: `(cls: id, _cmd: SEL) -> id`. No implicit ctx. /// /// Body (M4.0): /// %instance = class_createInstance(cls, 0) /// %ctx_addr = &__sx_default_context /// %state = ctx_addr.allocator.alloc(STATE_SIZE) /// memset(state, 0, STATE_SIZE) /// state[0] = allocator ← capture for -dealloc /// object_setIvar(instance, __sx_state_ivar, state) /// ret instance /// /// Sx-side `Cls.alloc()` is intercepted at the call site (see /// `lowerObjcStaticCall`) and emits the same sequence inline with /// `current_ctx_ref` as the ctx — so `push Context.{ allocator = ... }` /// flows through to per-instance allocator capture without going via /// the IMP. fn emitObjcDefinedClassAllocImp(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; defer { self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_alloc_imp", .{fcd.name}) catch return; const name_id = self.module.types.internString(imp_name); const ptr_void = self.module.types.ptrTo(.void); var params = std.ArrayList(inst_mod.Function.Param).empty; params.append(self.alloc, .{ .name = self.module.types.internString("cls"), .ty = ptr_void }) catch return; params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return; const params_slice = params.toOwnedSlice(self.alloc) catch return; _ = self.builder.beginFunction(name_id, params_slice, ptr_void); const func = self.builder.currentFunc(); func.linkage = .external; func.call_conv = .c; func.has_implicit_ctx = false; const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // ctx_addr = &__sx_default_context — IMP runs in Apple's runtime // context, no implicit sx ctx to inherit, so use the process-wide // default allocator. Sx-side callers bypass this IMP entirely // (compiler intercepts Cls.alloc()) and use their own // `context.allocator`. const default_ctx_gi = self.program_index.global_names.get("__sx_default_context") orelse { if (self.diagnostics) |d| { d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassAllocImp: __sx_default_context global missing for class '{s}' (compiler bug — scan pass did not register the default context)", .{fcd.name}); } return; }; const ctx_addr = self.builder.emit(.{ .global_addr = default_ctx_gi.id }, ptr_void); const cls_ref = Ref.fromIndex(0); const instance = self.emitObjcDefinedAllocAndInit(fcd, cls_ref, ctx_addr) orelse return; self.builder.ret(instance, ptr_void); self.builder.finalize(); } /// Shared inline sequence: allocate Obj-C instance + sx state struct, /// capture the allocator, bind to the `__sx_state` ivar. Used by both /// the `+alloc` IMP (ctx_addr = &__sx_default_context) and the sx-side /// `Cls.alloc()` interception (ctx_addr = current_ctx_ref). /// /// Returns the new instance pointer, or `null` if a required global is /// missing (compiler bug — should be impossible after scan pass). fn emitObjcDefinedAllocAndInit( self: *Lowering, fcd: *const ast.ForeignClassDecl, cls_ref: Ref, ctx_addr: Ref, ) ?Ref { const ptr_void = self.module.types.ptrTo(.void); // (1) instance = class_createInstance(cls, 0) const create_fid = self.ensureCRuntimeDecl("class_createInstance", &.{ ptr_void, .u64 }, ptr_void); const create_args = self.alloc.alloc(Ref, 2) catch return null; create_args[0] = cls_ref; create_args[1] = self.builder.constInt(0, .u64); const instance = self.builder.emit(.{ .call = .{ .callee = create_fid, .args = create_args } }, ptr_void); // STATE_SIZE = max(typeSizeBytes(__State), 1). const state_struct_ty = self.objc().objcDefinedStateStructType(fcd); const raw_size = self.module.types.typeSizeBytes(state_struct_ty); const state_size: u64 = if (raw_size == 0) 1 else @intCast(raw_size); const size_const = self.builder.constInt(@intCast(state_size), .u64); // (2) Dispatch through Context.allocator at ctx_addr: // allocator = (*ctx_addr).field[0] // state = allocator.alloc(size) (via inline-protocol fn-ptr) const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse { if (self.diagnostics) |d| { d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: Context type not found in module for class '{s}' (compiler bug)", .{fcd.name}); } return null; }; const ctx_info = self.module.types.get(ctx_ty); if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) { if (self.diagnostics) |d| { d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: Context has unexpected shape for class '{s}' (compiler bug)", .{fcd.name}); } return null; } const allocator_ty = ctx_info.@"struct".fields[0].ty; const ctx_val = self.builder.load(ctx_addr, ctx_ty); const allocator = self.builder.structGet(ctx_val, 0, allocator_ty); const alloc_ctx = self.builder.structGet(allocator, 0, ptr_void); const alloc_fn_ptr = self.builder.structGet(allocator, 1, ptr_void); const call_args = self.alloc.dupe(Ref, &.{ ctx_addr, alloc_ctx, size_const }) catch return null; const state = self.builder.emit(.{ .call_indirect = .{ .callee = alloc_fn_ptr, .args = call_args, } }, ptr_void); // (3) memset(state, 0, STATE_SIZE) — zero everything including the // allocator slot; the next store re-writes the allocator slot. const memset_fid = self.ensureCRuntimeDecl("memset", &.{ ptr_void, .s32, .u64 }, ptr_void); const memset_args = self.alloc.alloc(Ref, 3) catch return null; memset_args[0] = state; memset_args[1] = self.builder.constInt(0, .s32); memset_args[2] = size_const; _ = self.builder.emit(.{ .call = .{ .callee = memset_fid, .args = memset_args } }, ptr_void); // (4) Capture allocator at state[0] — `-dealloc` reads it back. const state_alloc_addr = self.builder.emit(.{ .struct_gep = .{ .base = state, .field_index = 0, .base_type = state_struct_ty, } }, ptr_void); self.builder.store(state_alloc_addr, allocator); // (5) object_setIvar(instance, load(@___state_ivar), state) const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return null; defer self.alloc.free(ivar_global_name); const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse { if (self.diagnostics) |d| { d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: ivar global '{s}' missing (scan-pass bug)", .{ivar_global_name}); } return null; }; const ivar_addr_v = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void); const ivar_handle = self.builder.load(ivar_addr_v, ptr_void); const set_ivar_fid = self.ensureCRuntimeDecl("object_setIvar", &.{ ptr_void, ptr_void, ptr_void }, .void); const set_args = self.alloc.alloc(Ref, 3) catch return null; set_args[0] = instance; set_args[1] = ivar_handle; set_args[2] = state; _ = self.builder.emit(.{ .call = .{ .callee = set_ivar_fid, .args = set_args } }, .void); return instance; } /// Emit a C-ABI IMP trampoline for a CLASS method (no `*Self` /// first param) on a sx-defined `#objc_class`. M2.1(b). /// Registered on the metaclass by emit_llvm. /// /// C-ABI: `(cls: Class, _cmd: SEL, ...user_args) -> ret` /// /// Body: /// call @.(__sx_default_context, ...user_args) /// ret /// /// No ivar read — class methods have no per-instance state. fn emitObjcDefinedClassStaticImp(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void { const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; defer { self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, md.name }) catch return; const name_id = self.module.types.internString(imp_name); const ptr_void = self.module.types.ptrTo(.void); var params = std.ArrayList(inst_mod.Function.Param).empty; params.append(self.alloc, .{ .name = self.module.types.internString("cls"), .ty = ptr_void }) catch return; params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return; // current_foreign_class lets `*Self` (if it appears in // user-arg types — rare for class methods) resolve to the // state-struct type. Save+restore. const saved_fc = self.current_foreign_class; self.current_foreign_class = fcd; defer self.current_foreign_class = saved_fc; for (md.params, 0..) |p_node, i| { const pty = self.resolveType(p_node); params.append(self.alloc, .{ .name = self.module.types.internString(md.param_names[i]), .ty = pty, }) catch return; } const ret_ty: TypeId = if (md.return_type) |rt| self.resolveType(rt) else .void; const params_slice = params.toOwnedSlice(self.alloc) catch return; _ = self.builder.beginFunction(name_id, params_slice, ret_ty); const func = self.builder.currentFunc(); func.linkage = .external; func.call_conv = .c; func.has_implicit_ctx = false; const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // Call @.(default_ctx, ...user_args). const body_name = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, md.name }) catch return; defer self.alloc.free(body_name); const body_fid = self.resolveFuncByName(body_name) orelse return; const ctx_ref: ?Ref = blk: { if (!self.implicit_ctx_enabled) break :blk null; const dctx_gi = self.program_index.global_names.get("__sx_default_context") orelse break :blk null; break :blk self.builder.emit(.{ .global_addr = dctx_gi.id }, ptr_void); }; const num_user_args = params_slice.len - 2; // minus cls + _cmd const num_call_args = (if (ctx_ref != null) @as(usize, 1) else 0) + num_user_args; const call_args = self.alloc.alloc(Ref, num_call_args) catch return; var idx: usize = 0; if (ctx_ref) |c_ref| { call_args[idx] = c_ref; idx += 1; } var ip: usize = 2; while (ip < params_slice.len) : (ip += 1) { call_args[idx] = Ref.fromIndex(@intCast(ip)); idx += 1; } const call_ref = self.builder.emit(.{ .call = .{ .callee = body_fid, .args = call_args, } }, ret_ty); if (ret_ty == .void) self.builder.retVoid() else self.builder.ret(call_ref, ret_ty); self.builder.finalize(); } /// Synthesize the `-dealloc` IMP for an sx-defined `#objc_class`. /// Runs when the Obj-C runtime drops the last retain on an instance. /// /// C-ABI: `(self: id, _cmd: SEL) -> void`. No implicit sx ctx. /// /// Body (M4.0c): /// %state = object_getIvar(self, load @___state_ivar) /// %allocator = load struct_gep(state, 0) ← __sx_allocator (M4.0a) /// allocator.dealloc(state) ← via inline-protocol fn-ptr /// object_setIvar(self, ivar, null) /// [super dealloc] // objc_msgSendSuper2(&super, sel_dealloc) /// ret void /// /// The state struct's first field is the allocator captured at /// +alloc time (M4.0a + M4.0b). Reading it back lets -dealloc free /// through the same allocator the instance was constructed with — /// the per-instance allocator design from M1.2 A.5, now realised. fn emitObjcDefinedClassDeallocImp(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; defer { self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_dealloc_imp", .{fcd.name}) catch return; const name_id = self.module.types.internString(imp_name); const ptr_void = self.module.types.ptrTo(.void); var params = std.ArrayList(inst_mod.Function.Param).empty; params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return; params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return; const params_slice = params.toOwnedSlice(self.alloc) catch return; _ = self.builder.beginFunction(name_id, params_slice, .void); const func = self.builder.currentFunc(); func.linkage = .external; func.call_conv = .c; func.has_implicit_ctx = false; const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); const self_ref = Ref.fromIndex(0); // (1) state = object_getIvar(self, load @___state_ivar) const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return; defer self.alloc.free(ivar_global_name); const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return; const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void); const ivar_handle = self.builder.load(ivar_addr, ptr_void); const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void); const get_args = self.alloc.alloc(Ref, 2) catch return; get_args[0] = self_ref; get_args[1] = ivar_handle; const state = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void); // (2) M4.B dealloc — release strong/copy property ivars and // destroyWeak weak property ivars BEFORE freeing the state struct // (which would invalidate the pointers we need to read). Property // metadata is re-derived from `fcd.members`; the state struct is // already interned via objcDefinedStateStructType. const state_struct_ty = self.objc().objcDefinedStateStructType(fcd); const state_info_check = self.module.types.get(state_struct_ty); if (state_info_check == .@"struct") { const state_fields = state_info_check.@"struct".fields; for (fcd.members) |m| switch (m) { .field => |f| { if (!f.is_property) continue; // Find the field index in the state struct (by name — // M4.0a's prepended __sx_allocator shifted user fields). const field_name_id = self.module.types.internString(f.name); var pfidx: ?u32 = null; for (state_fields, 0..) |sf, i| { if (sf.name == field_name_id) { pfidx = @intCast(i); break; } } const fidx = pfidx orelse continue; const field_ty = self.resolveType(f.field_type); const kind = self.objc().objcPropertyKind(f); switch (kind) { .assign => {}, // no ARC ops .strong, .copy => { // val = load field; objc_release(val) — release(NULL) is a no-op. self.ensureArcRuntimeDecls(); const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void); const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state, .field_index = fidx, .base_type = state_struct_ty, } }, ptr_void); const val = self.builder.load(field_addr, field_ty); const args = self.alloc.alloc(Ref, 1) catch continue; args[0] = val; _ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = args } }, .void); }, .weak => { // objc_destroyWeak(&field) — unregisters the slot // from libobjc's side-table. self.ensureArcRuntimeDecls(); const destroy_weak_fid = self.ensureCRuntimeDecl("objc_destroyWeak", &.{ptr_void}, .void); const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state, .field_index = fidx, .base_type = state_struct_ty, } }, ptr_void); const args = self.alloc.alloc(Ref, 1) catch continue; args[0] = field_addr; _ = self.builder.emit(.{ .call = .{ .callee = destroy_weak_fid, .args = args } }, .void); }, } }, else => {}, }; } // (3) Free state through the captured allocator (M4.0a + M4.0b): // allocator = load struct_gep(state, 0) ← __sx_allocator field // allocator.dealloc(state) ← inline-protocol fn-ptr at field 2 // Compare to the old `free(state)` — that ignored the per-instance // allocator and went straight to libc. Now `push Context.{ allocator = arena }` // round-trips correctly: arena.alloc on construction, arena.dealloc here. const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse { if (self.diagnostics) |d| { d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: Context type not found for class '{s}' (compiler bug)", .{fcd.name}); } return; }; const ctx_info = self.module.types.get(ctx_ty); if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) { if (self.diagnostics) |d| { d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: Context has unexpected shape for class '{s}'", .{fcd.name}); } return; } const allocator_ty = ctx_info.@"struct".fields[0].ty; const state_alloc_addr = self.builder.emit(.{ .struct_gep = .{ .base = state, .field_index = 0, .base_type = state_struct_ty, } }, ptr_void); const allocator = self.builder.load(state_alloc_addr, allocator_ty); // Default-context address for the implicit __sx_ctx the dealloc // fn-ptr takes as its first arg (the dealloc body might allocate // internally; default GPA is the safe baseline). const default_ctx_gi = self.program_index.global_names.get("__sx_default_context") orelse { if (self.diagnostics) |d| { d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: __sx_default_context global missing for class '{s}'", .{fcd.name}); } return; }; const default_ctx_addr = self.builder.emit(.{ .global_addr = default_ctx_gi.id }, ptr_void); const alloc_ctx = self.builder.structGet(allocator, 0, ptr_void); const dealloc_fn_ptr = self.builder.structGet(allocator, 2, ptr_void); const dealloc_args = self.alloc.dupe(Ref, &.{ default_ctx_addr, alloc_ctx, state }) catch return; _ = self.builder.emit(.{ .call_indirect = .{ .callee = dealloc_fn_ptr, .args = dealloc_args, } }, .void); // (3) object_setIvar(self, ivar, null) const set_ivar_fid = self.ensureCRuntimeDecl("object_setIvar", &.{ ptr_void, ptr_void, ptr_void }, .void); const null_ptr = self.builder.constInt(0, ptr_void); const set_args = self.alloc.alloc(Ref, 3) catch return; set_args[0] = self_ref; set_args[1] = ivar_handle; set_args[2] = null_ptr; _ = self.builder.emit(.{ .call = .{ .callee = set_ivar_fid, .args = set_args } }, .void); // (4) [super dealloc] // // objc_super = struct { receiver: id, super_class: Class } const super_struct_ty = self.module.types.intern(.{ .@"struct" = .{ .name = self.module.types.internString("__sx_objc_super"), .fields = blk: { var f = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; f.append(self.alloc, .{ .name = self.module.types.internString("receiver"), .ty = ptr_void }) catch unreachable; f.append(self.alloc, .{ .name = self.module.types.internString("super_class"), .ty = ptr_void }) catch unreachable; break :blk f.toOwnedSlice(self.alloc) catch unreachable; }, } }); const super_alloca = self.builder.alloca(super_struct_ty); // store receiver const recv_gep = self.builder.emit(.{ .struct_gep = .{ .base = super_alloca, .field_index = 0, .base_type = super_struct_ty } }, ptr_void); self.builder.store(recv_gep, self_ref); // store super_class = load @___class const class_global_name = std.fmt.allocPrint(self.alloc, "__{s}_class", .{fcd.name}) catch return; defer self.alloc.free(class_global_name); const class_global_id = self.lookupGlobalIdByName(class_global_name) orelse return; const class_addr = self.builder.emit(.{ .global_addr = class_global_id }, ptr_void); const class_val = self.builder.load(class_addr, ptr_void); const cls_gep = self.builder.emit(.{ .struct_gep = .{ .base = super_alloca, .field_index = 1, .base_type = super_struct_ty } }, ptr_void); self.builder.store(cls_gep, class_val); // sel_dealloc = sel_registerName("dealloc") const sel_reg_fid = self.ensureCRuntimeDecl("sel_registerName", &.{ptr_void}, ptr_void); const sel_str_gid = self.internStringConstantGlobal("dealloc"); const sel_str_addr = self.builder.emit(.{ .global_addr = sel_str_gid }, ptr_void); const sel_args = self.alloc.alloc(Ref, 1) catch return; sel_args[0] = sel_str_addr; const sel_dealloc = self.builder.emit(.{ .call = .{ .callee = sel_reg_fid, .args = sel_args } }, ptr_void); // objc_msgSendSuper2(&super, sel_dealloc) const send_super_fid = self.ensureCRuntimeDecl("objc_msgSendSuper2", &.{ ptr_void, ptr_void }, .void); const send_args = self.alloc.alloc(Ref, 2) catch return; send_args[0] = super_alloca; send_args[1] = sel_dealloc; _ = self.builder.emit(.{ .call = .{ .callee = send_super_fid, .args = send_args } }, .void); self.builder.retVoid(); self.builder.finalize(); } /// Intern a C-string constant as a `[N:0]u8` global and return /// its GlobalId. Used by IMP trampolines that need to pass a /// literal string to runtime helpers (e.g. selector names). fn internStringConstantGlobal(self: *Lowering, s: []const u8) inst_mod.GlobalId { const z = self.alloc.allocSentinel(u8, s.len, 0) catch unreachable; @memcpy(z[0..s.len], s); const arr_ty = self.module.types.arrayOf(.u8, @intCast(s.len + 1)); const slot_name = std.fmt.allocPrint(self.alloc, "__sx_objc_cstr_{s}", .{s}) catch unreachable; const name_id = self.module.types.internString(slot_name); if (self.lookupGlobalIdByName(slot_name)) |existing| { self.alloc.free(z); return existing; } var bytes_vec = std.ArrayList(inst_mod.ConstantValue).empty; for (z[0 .. s.len + 1]) |b| { bytes_vec.append(self.alloc, .{ .int = b }) catch unreachable; } const init_val: inst_mod.ConstantValue = .{ .aggregate = bytes_vec.toOwnedSlice(self.alloc) catch unreachable }; return self.module.addGlobal(.{ .name = name_id, .ty = arr_ty, .init_val = init_val, .is_extern = false, .is_const = true, }); } /// Linear scan over module globals for a given name. Used for /// looking up the per-class ivar handle global from inside IMP /// trampoline emission. fn lookupGlobalIdByName(self: *Lowering, name: []const u8) ?inst_mod.GlobalId { const name_id = self.module.types.internString(name); for (self.module.globals.items, 0..) |g, i| { if (g.name == name_id) return inst_mod.GlobalId.fromIndex(@intCast(i)); } return null; } pub fn synthesizeJniMainStubs(self: *Lowering) void { var seen = std.StringHashMap(void).init(self.alloc); defer seen.deinit(); var it = self.program_index.foreign_class_map.iterator(); while (it.next()) |entry| { const fcd = entry.value_ptr.*; if (!fcd.is_main) continue; if (fcd.is_foreign) continue; if (fcd.runtime != .jni_class) continue; if (seen.contains(fcd.foreign_path)) continue; seen.put(fcd.foreign_path, {}) catch continue; for (fcd.members) |m| switch (m) { .method => |md| { if (md.body == null) continue; if (md.is_static) continue; // future: emit static native ABI without `self` self.synthesizeJniMainStub(fcd, md); }, else => {}, }; } } fn synthesizeJniMainStub(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void { const mangled = jni_descriptor.jniMangleNativeName(self.alloc, fcd.foreign_path, md.name) catch return; const name_id = self.module.types.internString(mangled); const ptr_void = self.module.types.ptrTo(.void); var params = std.ArrayList(inst_mod.Function.Param).empty; params.append(self.alloc, .{ .name = self.module.types.internString("env"), .ty = ptr_void, }) catch return; params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void, }) catch return; // User's declared params (skip the implicit `*Self` at index 0 for // instance methods — we synthesized `self` above as the jobject). const param_start: usize = 1; for (md.params[param_start..], 0..) |p_node, i| { const pty = jniMapParamType(self, p_node); params.append(self.alloc, .{ .name = self.module.types.internString(md.param_names[param_start + i]), .ty = pty, }) catch return; } const ret_ty = if (md.return_type) |rt| jniMapParamType(self, rt) else .void; const params_slice = params.toOwnedSlice(self.alloc) catch return; _ = self.builder.beginFunction(name_id, params_slice, ret_ty); self.builder.currentFunc().linkage = .external; self.builder.currentFunc().call_conv = .c; const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); var scope = Scope.init(self.alloc, self.scope); defer scope.deinit(); const saved_scope = self.scope; self.scope = &scope; defer self.scope = saved_scope; for (params_slice, 0..) |p, i| { const slot = self.builder.alloca(p.ty); const param_ref = Ref.fromIndex(@intCast(i)); self.builder.store(slot, param_ref); scope.put(self.module.types.getString(p.name), .{ .ref = slot, .ty = p.ty, .is_alloca = true }); } // Push the JNIEnv* arg onto the lexical `#jni_env` stack so the // method body's `#jni_call(...)` / `super.method(...)` sites pick // it up without an explicit `#jni_env(env) { ... }` wrapper. The // JNI runtime guarantees the env passed to a native method is // valid for the calling thread. const env_slot = scope.lookup("env").?.ref; const env_loaded = self.builder.load(env_slot, ptr_void); const env_stack_base = self.jni_env_stack_base; self.jni_env_stack_base = self.jni_env_stack.items.len; self.jni_env_stack.append(self.alloc, env_loaded) catch {}; defer { _ = self.jni_env_stack.pop(); self.jni_env_stack_base = env_stack_base; } // Record method context so `super.method(args)` inside the body // can find the parent class (via `#extends`) and the method's // signature. const saved_fcd = self.current_foreign_class; const saved_method = self.current_foreign_method; self.current_foreign_class = fcd; self.current_foreign_method = md; defer { self.current_foreign_class = saved_fcd; self.current_foreign_method = saved_method; } // JNI native methods are C-callable entry points — install the // static default Context so `context.X` reads in the method body // resolve through `current_ctx_ref`. Mirror the same binding // `lowerFunction` does for callconv(.c) / isExportedEntryName. const saved_ctx_ref_jni = self.current_ctx_ref; defer self.current_ctx_ref = saved_ctx_ref_jni; if (self.implicit_ctx_enabled) { if (self.program_index.global_names.get("__sx_default_context")) |dctx_gi| { self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, ptr_void); } } const saved_target = self.target_type; self.target_type = if (ret_ty != .void) ret_ty else null; if (ret_ty != .void) { const body_val = self.lowerBlockValue(md.body.?); if (!self.currentBlockHasTerminator()) { if (body_val) |val| { const val_ty = self.builder.getRefType(val); if (val_ty == .void) { self.ensureTerminator(ret_ty); } else { const coerced = self.coerceToType(val, val_ty, ret_ty); self.builder.ret(coerced, ret_ty); } } else { self.ensureTerminator(ret_ty); } } } else { self.lowerBlock(md.body.?); self.ensureTerminator(ret_ty); } self.target_type = saved_target; self.builder.finalize(); } // --- moved to lower/error.zig (lower_error) --- pub const getTraceFids = lower_error.getTraceFids; pub const tracesEnabled = lower_error.tracesEnabled; pub const emitTracePush = lower_error.emitTracePush; pub const emitTraceClear = lower_error.emitTraceClear; pub const placeholderTraceFrame = lower_error.placeholderTraceFrame; pub const errorSetTypeOf = lower_error.errorSetTypeOf; pub const isErrorTagLiteralNode = lower_error.isErrorTagLiteralNode; pub const tryLowerErrorSetEquality = lower_error.tryLowerErrorSetEquality; pub const effectiveReturnType = lower_error.effectiveReturnType; pub const errorChannelOf = lower_error.errorChannelOf; pub const isInferredErrorSet = lower_error.isInferredErrorSet; pub const checkErrorSetSubset = lower_error.checkErrorSetSubset; pub const diagTagsNotInSet = lower_error.diagTagsNotInSet; pub const lowerRaise = lower_error.lowerRaise; pub const lowerFailableSuccessReturn = lower_error.lowerFailableSuccessReturn; pub const buildFailableTuple = lower_error.buildFailableTuple; pub const failableSuccessType = lower_error.failableSuccessType; pub const failableReturnTarget = lower_error.failableReturnTarget; pub const extractSuccessValue = lower_error.extractSuccessValue; pub const extractErrorSlot = lower_error.extractErrorSlot; pub const emitTupleRet = lower_error.emitTupleRet; pub const diagRaiseNotFailable = lower_error.diagRaiseNotFailable; pub const exprIsFailable = lower_error.exprIsFailable; pub const lowerCallerLocation = lower_error.lowerCallerLocation; pub const sourceForFile = lower_error.sourceForFile; pub const currentFunctionName = lower_error.currentFunctionName; pub const lowerTry = lower_error.lowerTry; pub const emitErrorReturn = lower_error.emitErrorReturn; pub const diagTryNotFailable = lower_error.diagTryNotFailable; pub const lowerCatch = lower_error.lowerCatch; pub const lowerCatchOverChain = lower_error.lowerCatchOverChain; pub const finishCatchHandler = lower_error.finishCatchHandler; pub const runCatchBody = lower_error.runCatchBody; pub const checkEscapeWidening = lower_error.checkEscapeWidening; pub const orIsFailableChain = lower_error.orIsFailableChain; pub const operandIsFailableLike = lower_error.operandIsFailableLike; pub const orChainSuccessType = lower_error.orChainSuccessType; pub const unwrapTryNode = lower_error.unwrapTryNode; pub const flattenOrChain = lower_error.flattenOrChain; pub const lowerFailableOr = lower_error.lowerFailableOr; pub const callTargetName = lower_error.callTargetName; pub const astIsPureBareInferred = lower_error.astIsPureBareInferred; pub const astPureNamedSet = lower_error.astPureNamedSet; pub const namedSetTags = lower_error.namedSetTags; pub const convergeInferredErrorSets = lower_error.convergeInferredErrorSets; pub const containsTag = lower_error.containsTag; pub const convergeClosureShapeSets = lower_error.convergeClosureShapeSets; pub const recordClosureShape = lower_error.recordClosureShape; pub const calleeEscapeTags = lower_error.calleeEscapeTags; pub const unionShapeTags = lower_error.unionShapeTags; pub const closureShapeKey = lower_error.closureShapeKey; pub const returnValuePart = lower_error.returnValuePart; pub const shapeKeyOfCallee = lower_error.shapeKeyOfCallee; // --- moved to lower/comptime.zig (lower_comptime) --- pub const SelectedConst = lower_comptime.SelectedConst; pub const evalComptimeCondition = lower_comptime.evalComptimeCondition; pub const evalComptimeMatch = lower_comptime.evalComptimeMatch; pub const evalComptimeInt = lower_comptime.evalComptimeInt; pub const evalComptimeString = lower_comptime.evalComptimeString; pub const lowerComptimeGlobal = lower_comptime.lowerComptimeGlobal; pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect; pub const lowerComptimeCall = lower_comptime.lowerComptimeCall; pub const lowerInlineComptime = lower_comptime.lowerInlineComptime; pub const lowerInsertExpr = lower_comptime.lowerInsertExpr; pub const lowerInsertExprValue = lower_comptime.lowerInsertExprValue; pub const lowerComptimeDeps = lower_comptime.lowerComptimeDeps; pub const substituteComptimeNodes = lower_comptime.substituteComptimeNodes; pub const fnBodyHasReturn = lower_comptime.fnBodyHasReturn; pub const createComptimeFunction = lower_comptime.createComptimeFunction; pub const constExprValue = lower_comptime.constExprValue; pub const constArrayLiteral = lower_comptime.constArrayLiteral; pub const constStructLiteral = lower_comptime.constStructLiteral; pub const constEnumLiteral = lower_comptime.constEnumLiteral; pub const foldSourceConstInt = lower_comptime.foldSourceConstInt; pub const foldSourceConstFloat = lower_comptime.foldSourceConstFloat; pub const sourceConstIsFloatTyped = lower_comptime.sourceConstIsFloatTyped; pub const comptimeIntNamed = lower_comptime.comptimeIntNamed; pub const selectModuleConst = lower_comptime.selectModuleConst; pub const sourceModuleConst = lower_comptime.sourceModuleConst; pub const pinConstAuthorSource = lower_comptime.pinConstAuthorSource; pub const foldComptimeFloatInit = lower_comptime.foldComptimeFloatInit; // --- moved to lower/stmt.zig (lower_stmt) --- pub const lowerBlock = lower_stmt.lowerBlock; pub const lowerInlineBranch = lower_stmt.lowerInlineBranch; pub const lowerBlockValue = lower_stmt.lowerBlockValue; pub const lowerValueBody = lower_stmt.lowerValueBody; pub const tryLowerAsExpr = lower_stmt.tryLowerAsExpr; pub const lowerStmt = lower_stmt.lowerStmt; pub const lowerVarDecl = lower_stmt.lowerVarDecl; pub const lowerLocalFnDecl = lower_stmt.lowerLocalFnDecl; pub const lowerConstDecl = lower_stmt.lowerConstDecl; pub const lowerReturn = lower_stmt.lowerReturn; pub const lowerAssignment = lower_stmt.lowerAssignment; pub const fieldLvaluePtr = lower_stmt.fieldLvaluePtr; pub const lowerExprAsPtr = lower_stmt.lowerExprAsPtr; pub const storeOrCompound = lower_stmt.storeOrCompound; pub const emitCompoundOp = lower_stmt.emitCompoundOp; pub const lowerMultiAssign = lower_stmt.lowerMultiAssign; pub const lowerDestructureDecl = lower_stmt.lowerDestructureDecl; pub const lowerPush = lower_stmt.lowerPush; pub const lowerDefer = lower_stmt.lowerDefer; pub const lowerOnFail = lower_stmt.lowerOnFail; pub const diagOnFailNotFailable = lower_stmt.diagOnFailNotFailable; pub const emitBlockDefers = lower_stmt.emitBlockDefers; pub const lowerCleanupBody = lower_stmt.lowerCleanupBody; pub const emitErrorCleanup = lower_stmt.emitErrorCleanup; // --- moved to lower/control_flow.zig (lower_control_flow) --- pub const lowerIfExpr = lower_control_flow.lowerIfExpr; pub const tryConstBoolCondition = lower_control_flow.tryConstBoolCondition; pub const lowerWhile = lower_control_flow.lowerWhile; pub const listView = lower_control_flow.listView; pub const lowerFor = lower_control_flow.lowerFor; pub const lowerRuntimeRangeFor = lower_control_flow.lowerRuntimeRangeFor; pub const lowerInlineRangeFor = lower_control_flow.lowerInlineRangeFor; pub const lowerMatch = lower_control_flow.lowerMatch; pub const lowerBreak = lower_control_flow.lowerBreak; pub const lowerContinue = lower_control_flow.lowerContinue; pub const freshBlock = lower_control_flow.freshBlock; pub const freshBlockWithParams = lower_control_flow.freshBlockWithParams; pub const currentBlockHasTerminator = lower_control_flow.currentBlockHasTerminator; pub const ensureTerminator = lower_control_flow.ensureTerminator; // --- moved to lower/decl.zig (lower_decl) --- pub const SelectedFunc = lower_decl.SelectedFunc; pub const BareCallee = lower_decl.BareCallee; pub const VisibleStructAuthor = lower_decl.VisibleStructAuthor; pub const lowerRoot = lower_decl.lowerRoot; pub const validateMainSignature = lower_decl.validateMainSignature; pub const checkRequiredEntryPoints = lower_decl.checkRequiredEntryPoints; pub const injectComptimeConstants = lower_decl.injectComptimeConstants; pub const findVariantIndex = lower_decl.findVariantIndex; pub const lowerDeferredTypeFns = lower_decl.lowerDeferredTypeFns; pub const lowerDecls = lower_decl.lowerDecls; pub const detectContextDecl = lower_decl.detectContextDecl; pub const funcWantsImplicitCtx = lower_decl.funcWantsImplicitCtx; pub const fnPtrTypeWantsCtx = lower_decl.fnPtrTypeWantsCtx; pub const scanDecls = lower_decl.scanDecls; pub const registerTypedModuleConst = lower_decl.registerTypedModuleConst; pub const typedConstInitFits = lower_decl.typedConstInitFits; pub const constExprInitFits = lower_decl.constExprInitFits; pub const registerTopLevelGlobal = lower_decl.registerTopLevelGlobal; pub const globalInitValue = lower_decl.globalInitValue; pub const diagnoseNonConstGlobal = lower_decl.diagnoseNonConstGlobal; pub const resolveForwardIdentifierAliases = lower_decl.resolveForwardIdentifierAliases; pub const aliasResolvedInSource = lower_decl.aliasResolvedInSource; pub const declareFunction = lower_decl.declareFunction; pub const registerNamespaceQualifiedFns = lower_decl.registerNamespaceQualifiedFns; pub const registerQualifiedFn = lower_decl.registerQualifiedFn; pub const isVisible = lower_decl.isVisible; pub const visibleOverEdges = lower_decl.visibleOverEdges; pub const isCImportVisible = lower_decl.isCImportVisible; pub const isNameVisible = lower_decl.isNameVisible; pub const lazyLowerFunction = lower_decl.lazyLowerFunction; pub const lowerFunctionBodyInto = lower_decl.lowerFunctionBodyInto; pub const lowerFunction = lower_decl.lowerFunction; pub const lowerMainAndComptime = lower_decl.lowerMainAndComptime; pub const lowerRetainedSameNameAuthors = lower_decl.lowerRetainedSameNameAuthors; pub const selectPlainCallableAuthor = lower_decl.selectPlainCallableAuthor; pub const selectNominalLeaf = lower_decl.selectNominalLeaf; pub const isNamedTypeKind = lower_decl.isNamedTypeKind; pub const namedRefTid = lower_decl.namedRefTid; pub const nameAuthoredAsTypeAnywhere = lower_decl.nameAuthoredAsTypeAnywhere; pub const recordLocalTypeName = lower_decl.recordLocalTypeName; pub const localTypeInSource = lower_decl.localTypeInSource; pub const localTypeInAnySource = lower_decl.localTypeInAnySource; pub const resolveNominalLeaf = lower_decl.resolveNominalLeaf; pub const fnDeclOfRaw = lower_decl.fnDeclOfRaw; pub const structDeclOfRaw = lower_decl.structDeclOfRaw; pub const structMethodFn = lower_decl.structMethodFn; pub const typeFnAuthor = lower_decl.typeFnAuthor; pub const selectedFuncId = lower_decl.selectedFuncId; pub const bareAuthorFuncId = lower_decl.bareAuthorFuncId; pub const putTypeAlias = lower_decl.putTypeAlias; pub const putModuleConst = lower_decl.putModuleConst; pub const putGlobal = lower_decl.putGlobal; pub const dropModuleConst = lower_decl.dropModuleConst; pub const emitModuleConst = lower_decl.emitModuleConst; pub const emitPlaceholder = lower_decl.emitPlaceholder; }; /// JNI param/return type resolution: user-declared types pass through /// `resolveType` so the method body can dispatch on richer foreign-class /// types (`holder.getSurface()` etc.). At LLVM level both `*SurfaceHolder` /// and `*void` lower to the same `ptr`, so the C ABI shape Java sees is /// unchanged — only sx-side method resolution benefits. fn jniMapParamType(self: *Lowering, type_node: *ast.Node) TypeId { return self.resolveType(type_node); }