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 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; /// Names that must keep external LLVM linkage because the OS loader (not /// sx code) is the caller. Without this they'd default to internal and /// either DCE away or stay hidden from the dynamic symbol table. /// Anything starting with `Java_` is a JNI native method that Android's /// runtime resolves by name mangling — same rule. fn isExportedEntryName(name: []const u8) bool { return std.mem.eql(u8, name, "main") or std.mem.eql(u8, name, "JNI_OnLoad") or std.mem.startsWith(u8, name, "Java_"); } // ── Scope ─────────────────────────────────────────────────────────────── const Binding = struct { ref: Ref, ty: TypeId, is_alloca: bool, // true if ref is a pointer that needs load }; const Scope = struct { map: std.StringHashMap(Binding), fn_names: std.StringHashMap([]const u8), // bare name → mangled name for local functions parent: ?*Scope, fn init(alloc: Allocator, parent: ?*Scope) Scope { return .{ .map = std.StringHashMap(Binding).init(alloc), .fn_names = std.StringHashMap([]const u8).init(alloc), .parent = parent, }; } fn deinit(self: *Scope) void { self.map.deinit(); self.fn_names.deinit(); } fn put(self: *Scope, name: []const u8, binding: Binding) void { self.map.put(name, binding) catch unreachable; } 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; } 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; } }; // ── 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 fn_ast_map: std.StringHashMap(*const ast.FnDecl), 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 local_fn_counter: u32 = 0, // unique counter for mangling local function names import_flags: std.StringHashMap(bool), // tracks whether each function is imported module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null, // per-module visible names (from import resolution) import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, // module path → set of directly imported paths (used by param_impl_map visibility filter) 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 foreign_class_map: std.StringHashMap(*const ast.ForeignClassDecl) = std.StringHashMap(*const ast.ForeignClassDecl).init(std.heap.page_allocator), // sx alias → ForeignClassDecl (jni_class / objc_class / swift_class / ... — registered in scan pass) 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 defer_stack: std.ArrayList(*const Node) = std.ArrayList(*const Node).empty, // block-scoped defer stack func_defer_base: usize = 0, // defer stack base for current function (lowerReturn drains to this) global_names: std.StringHashMap(GlobalInfo) = std.StringHashMap(GlobalInfo).init(std.heap.page_allocator), // #run global name → GlobalId 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) struct_template_map: std.StringHashMap(StructTemplate) = std.StringHashMap(StructTemplate).init(std.heap.page_allocator), // generic struct name → template 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 comptime_value_bindings: ?std.StringHashMap(i64) = null, // comptime value bindings ($N → integer value) protocol_decl_map: std.StringHashMap(ProtocolDeclInfo) = std.StringHashMap(ProtocolDeclInfo).init(std.heap.page_allocator), // protocol name → protocol info protocol_ast_map: std.StringHashMap(*const ast.ProtocolDecl) = std.StringHashMap(*const ast.ProtocolDecl).init(std.heap.page_allocator), // protocol name → AST node 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, struct_const_map: std.StringHashMap(StructConstInfo) = std.StringHashMap(StructConstInfo).init(std.heap.page_allocator), // "Struct.CONST" → value info module_const_map: std.StringHashMap(ModuleConstInfo) = std.StringHashMap(ModuleConstInfo).init(std.heap.page_allocator), // module-level value constants (e.g. AF_INET :s32: 2) foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames type_alias_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // type alias name → target TypeId ufcs_alias_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // UFCS alias name → target function name 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 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) }; const ModuleConstInfo = struct { value: *const Node, ty: TypeId, }; const ProtocolDeclInfo = struct { name: []const u8, is_inline: bool, methods: []const ProtocolMethodInfo, }; const ProtocolMethodInfo = struct { name: []const u8, param_types: []const TypeId, // excluding self ret_type: TypeId, // True when the AST return type was `Self` (encoded here as *void). // Lets the dispatcher distinguish Self-disguised-as-*void (auto-unbox // on the caller side) from a literal `-> *void` (return as-is). ret_is_self: bool = false, }; /// 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. 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 }; /// 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. 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, }; /// Owned copy of a generic struct template (AST pointers are copied/interned to survive imports) const StructTemplate = struct { name: []const u8, type_params: []const TemplateParam, field_names: []const []const u8, field_type_nodes: []const *const Node, // raw AST pointers — must be copied from heap nodes }; const TemplateParam = struct { name: []const u8, is_type_param: bool, // true for $T: Type, false for $N: u32 }; const GlobalInfo = struct { id: inst_mod.GlobalId, ty: TypeId }; pub fn init(module: *Module) Lowering { return .{ .module = module, .builder = Builder.init(module), .alloc = module.alloc, .fn_ast_map = std.StringHashMap(*const ast.FnDecl).init(module.alloc), .lowered_functions = std.StringHashMap(void).init(module.alloc), .import_flags = std.StringHashMap(bool).init(module.alloc), .global_names = std.StringHashMap(GlobalInfo).init(module.alloc), }; } // ── Public entry point ────────────────────────────────────────── /// Lower all top-level declarations from a root node. /// Pass 1: Scan all declarations (register ASTs, types, extern stubs). /// Pass 2: Lower only `main` (everything else is lowered lazily on demand). pub fn lowerRoot(self: *Lowering, root: *const Node) void { const decls = switch (root.data) { .root => |r| r.decls, else => return, }; // Pass 0: pre-scan for `Context :: struct {...}`. If the program // imports `std.sx` it has Context, and every default-conv sx // function gets the implicit `__sx_ctx` param. Otherwise the // implicit-ctx machinery stays fully disabled — programs that // call only libc directly keep their bare C ABI. self.implicit_ctx_enabled = detectContextDecl(decls); self.module.has_implicit_ctx = self.implicit_ctx_enabled; // Pass 1: scan — register all function ASTs, struct types, extern stubs self.scanDecls(decls); // Pass 1b: inject compile-time constants (OS, ARCH, POINTER_SIZE) from target config self.injectComptimeConstants(); // Pass 1c: emit the process-wide default Context global, statically // initialised to a CAllocator-backed Allocator value. Used by FFI // wrappers in Step 4 and by the interp's `callWithDefaultContext` // entry. Only fires when the program imports `std.sx` (so Context + // Allocator + CAllocator are all registered). self.emitDefaultContextGlobal(); // Pass 2: lower main (and comptime side-effects) self.lowerMainAndComptime(decls); // Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered self.lowerDeferredTypeFns(); // Pass 4: target-specific entry-point sanity checks self.checkRequiredEntryPoints(); // Pass 4b: eagerly lower bodied methods on sx-defined `#objc_class` // declarations. The Obj-C runtime calls these via IMP pointers // registered in M1.2 A.4 — no sx-side call path drives lazy // lowering, so we trigger it here. Mirrors the JNI eager-lower // pattern in Pass 5. self.lowerObjcDefinedClassMethods(); // Pass 5: synthesize JNI-mangled exports for `#jni_main` bodied methods. // Android's JNI runtime resolves `private native sx_(...)` declared in // the bundled classes.dex by looking up the symbol // `Java___sx_1` in the loaded .so. Each // bodied method on a `#jni_main #jni_class` decl becomes an exported // C-ABI fn with that name; the JNIEnv* / jobject params are prepended, // then the user-declared params (with type-erased pointers since JNI // doesn't carry sx-side types across the binding). self.synthesizeJniMainStubs(); } /// On Android, the OS loads the .so via a Java-side Activity declared /// with `#jni_main #jni_class("...")`. The Java class drives the /// lifecycle (onCreate / onPause / etc.) and sx provides the native /// delegates bound via JNI name mangling. Without a `#jni_main` decl /// there's no entry point — the .so would load but Android has nothing /// to call into. fn checkRequiredEntryPoints(self: *Lowering) void { const tc = self.target_config orelse return; if (!tc.isAndroid()) return; var it = self.foreign_class_map.iterator(); while (it.next()) |entry| { const fcd = entry.value_ptr.*; if (fcd.is_main and !fcd.is_foreign and fcd.runtime == .jni_class) return; } if (self.diagnostics) |diags| { diags.addFmt(.err, null, "target is Android but no `#jni_main` Activity declared. " ++ "The OS launches a Java-side Activity that delegates lifecycle " ++ "callbacks into sx — declare one like:\n\n" ++ " Bundle :: #foreign #jni_class(\"android/os/Bundle\") {{ }}\n\n" ++ " MyApp :: #jni_main #jni_class(\"co/example/MyApp\") {{\n" ++ " onCreate :: (self: *Self, b: *Bundle) {{ /* ... */ }}\n" ++ " }}", .{}); } } /// Inject compile-time constants from target_config into comptime_constants. /// Called after scanDecls so that enum types (OperatingSystem, Architecture) are registered. fn injectComptimeConstants(self: *Lowering) void { const tc = self.target_config orelse return; // OS: OperatingSystem enum { macos; linux; windows; wasm; unknown; } const os_name_id = self.module.types.internString("OperatingSystem"); if (self.module.types.findByName(os_name_id)) |os_ty| { const os_info = self.module.types.get(os_ty); if (os_info == .@"enum") { const tag: u32 = if (tc.isWasm()) self.findVariantIndex(os_info.@"enum".variants, "wasm") else if (tc.isWindows()) self.findVariantIndex(os_info.@"enum".variants, "windows") else if (tc.isAndroid()) self.findVariantIndex(os_info.@"enum".variants, "android") else if (tc.isLinux()) self.findVariantIndex(os_info.@"enum".variants, "linux") else if (tc.isIOS()) self.findVariantIndex(os_info.@"enum".variants, "ios") else if (tc.isMacOS()) self.findVariantIndex(os_info.@"enum".variants, "macos") else self.findVariantIndex(os_info.@"enum".variants, "unknown"); self.comptime_constants.put("OS", .{ .enum_tag = .{ .ty = os_ty, .tag = tag } }) catch {}; } } // ARCH: Architecture enum { aarch64; x86_64; wasm32; wasm64; unknown; } const arch_name_id = self.module.types.internString("Architecture"); if (self.module.types.findByName(arch_name_id)) |arch_ty| { const arch_info = self.module.types.get(arch_ty); if (arch_info == .@"enum") { const tag: u32 = if (tc.isWasm32()) self.findVariantIndex(arch_info.@"enum".variants, "wasm32") else if (tc.isWasm64()) self.findVariantIndex(arch_info.@"enum".variants, "wasm64") else if (tc.isAarch64()) self.findVariantIndex(arch_info.@"enum".variants, "aarch64") else if (tc.isX86_64()) self.findVariantIndex(arch_info.@"enum".variants, "x86_64") else self.findVariantIndex(arch_info.@"enum".variants, "unknown"); self.comptime_constants.put("ARCH", .{ .enum_tag = .{ .ty = arch_ty, .tag = tag } }) catch {}; } } // POINTER_SIZE: s64 (4 for wasm32, 8 for wasm64 and other 64-bit targets) const ptr_size: i64 = if (tc.isWasm32()) 4 else 8; self.comptime_constants.put("POINTER_SIZE", .{ .int_val = ptr_size }) catch {}; } fn findVariantIndex(self: *Lowering, variants: []const types.StringId, name: []const u8) u32 { const name_id = self.module.types.internString(name); for (variants, 0..) |v, i| { if (v == name_id) return @intCast(i); } return 0; // fallback to first variant } /// Lower functions that were deferred because they use type-category matching. /// At this point, main is fully lowered and all types are in the TypeTable. fn lowerDeferredTypeFns(self: *Lowering) void { if (self.deferred_type_fns.items.len == 0) return; self.processing_deferred = true; for (self.deferred_type_fns.items) |name| { self.lazyLowerFunction(name); } self.processing_deferred = false; } /// Lower a list of top-level declarations (used by irComptimeEval — non-lazy path). /// This preserves the old behavior for comptime evaluation contexts. pub fn lowerDecls(self: *Lowering, decls: []const *const Node) void { for (decls) |decl| { self.setCurrentSourceFile(decl.source_file); const is_imported = if (self.main_file) |mf| (if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false) else false; switch (decl.data) { .fn_decl => |fd| { self.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {}; self.lowerFunction(&fd, fd.name, is_imported); }, .const_decl => |cd| { if (cd.value.data == .fn_decl) { self.fn_ast_map.put(cd.name, &cd.value.data.fn_decl) catch {}; self.lowerFunction(&cd.value.data.fn_decl, cd.name, is_imported); } else if (cd.value.data == .struct_decl) { self.registerStructDecl(&cd.value.data.struct_decl); } else if (cd.value.data == .enum_decl) { _ = type_bridge.resolveAstType(cd.value, &self.module.types); } else if (cd.value.data == .union_decl) { _ = type_bridge.resolveAstType(cd.value, &self.module.types); } else if (cd.value.data == .comptime_expr) { self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation); } }, .comptime_expr => |ct| { self.lowerComptimeSideEffect(ct.expr); }, .struct_decl => |sd| { self.registerStructDecl(&sd); }, .enum_decl => { _ = type_bridge.resolveAstType(decl, &self.module.types); }, .union_decl => { _ = type_bridge.resolveAstType(decl, &self.module.types); }, .protocol_decl => { self.registerProtocolDecl(&decl.data.protocol_decl); }, .impl_block => { self.registerImplBlock(&decl.data.impl_block, is_imported, decl); }, .foreign_class_decl => { self.registerForeignClassDecl(&decl.data.foreign_class_decl); }, .namespace_decl => |ns| { self.registerNamespacedForeignClasses(ns); if (self.main_file != null) { self.lowerDecls(ns.decls); } }, else => {}, } } } /// Detect whether `Context :: struct {...}` is declared anywhere in the /// program. Used to gate the implicit `__sx_ctx` param machinery: when /// `std.sx` is in the dep graph, `Context` is declared and every sx /// function gets the implicit param. Otherwise the program runs with a /// bare C ABI (no global Context, no implicit param, no FFI wrappers). fn detectContextDecl(decls: []const *const Node) bool { for (decls) |decl| { const found = switch (decl.data) { .struct_decl => |sd| std.mem.eql(u8, sd.name, "Context"), .const_decl => |cd| std.mem.eql(u8, cd.name, "Context") and cd.value.data == .struct_decl, .namespace_decl => |ns| detectContextDecl(ns.decls), else => false, }; if (found) return true; } return false; } /// Returns true if a sx function declaration should receive the /// implicit `__sx_ctx` parameter. False for foreign-libc bindings, /// #builtin / #compiler bodies, and C-conv functions (which keep /// their literal C ABI). Also false for OS-called entry points /// (`isExportedEntryName`): main and JNI hooks are invoked by the /// dyld / JVM with no `__sx_ctx` arg, so the visible signature must /// not include one. Their bodies are still sx code — they /// synthesise `&__sx_default_context` at entry and use it as their /// own `current_ctx_ref`. Full FFI-wrapper split (a separate /// `__sx__impl` with the ctx param) lands in Step 4 proper. fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool { if (!self.implicit_ctx_enabled) return false; if (fd.call_conv == .c) return false; return switch (fd.body.data) { .foreign_expr, .builtin_expr, .compiler_expr => false, else => !isExportedEntryName(fd.name), }; } /// Returns true if a fn-pointer of the given type carries an implicit /// `__sx_ctx` at LLVM slot 0. Default-conv sx fn-pointers do; C-conv /// (and any non-function type) does not. fn fnPtrTypeWantsCtx(self: *const Lowering, ty: TypeId) bool { if (!self.implicit_ctx_enabled) return false; if (ty.isBuiltin()) return false; const ti = self.module.types.get(ty); if (ti != .function) return false; return ti.function.call_conv != .c; } /// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies. fn scanDecls(self: *Lowering, decls: []const *const Node) void { for (decls) |decl| { self.setCurrentSourceFile(decl.source_file); const is_imported = if (self.main_file) |mf| (if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false) else false; switch (decl.data) { .fn_decl => |fd| { self.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {}; self.import_flags.put(fd.name, is_imported) catch {}; // Declare extern stub for all functions (bodies lowered lazily) self.declareFunction(&fd, fd.name); }, .const_decl => |cd| { if (cd.value.data == .fn_decl) { self.fn_ast_map.put(cd.name, &cd.value.data.fn_decl) catch {}; self.import_flags.put(cd.name, is_imported) catch {}; self.declareFunction(&cd.value.data.fn_decl, cd.name); } else if (cd.value.data == .struct_decl) { self.registerStructDecl(&cd.value.data.struct_decl); } else if (cd.value.data == .enum_decl) { // Register enum/tagged-union types in the type table _ = type_bridge.resolveAstType(cd.value, &self.module.types); } else if (cd.value.data == .union_decl) { // Register plain union types in the type table _ = type_bridge.resolveAstType(cd.value, &self.module.types); } else if (cd.value.data == .type_expr or cd.value.data == .pointer_type_expr or cd.value.data == .many_pointer_type_expr or cd.value.data == .array_type_expr or cd.value.data == .slice_type_expr or cd.value.data == .optional_type_expr or cd.value.data == .function_type_expr) { // Type alias: MyFloat :: f64; Ptr :: *u8; Cb :: (s32) -> s32; const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types); self.type_alias_map.put(cd.name, target_ty) catch {}; } else if (cd.value.data == .identifier) { // Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide; // Chase through type_alias_map, then look up named types // in the table. Forward references resolve lazily because // the .identifier branch of resolveTypeArg also consults // type_alias_map at use time. const rhs_name = cd.value.data.identifier.name; if (self.type_alias_map.get(rhs_name)) |chained| { self.type_alias_map.put(cd.name, chained) catch {}; } else { const name_id = self.module.types.internString(rhs_name); if (self.module.types.findByName(name_id)) |tid| { self.type_alias_map.put(cd.name, tid) catch {}; } } } // Handle generic struct instantiation: Vec3 :: Vec(3, f32) // Parser produces a .call node for these (not parameterized_type_expr) if (cd.value.data == .call) { const call_data = &cd.value.data.call; const callee_name = switch (call_data.callee.data) { .identifier => |id| id.name, .field_access => |fa| fa.field, else => "", }; if (callee_name.len > 0) { if (self.struct_template_map.getPtr(callee_name)) |tmpl| { const inst_id = self.instantiateGenericStruct(tmpl, call_data.args); // Register under the alias name const alias_name_id = self.module.types.internString(cd.name); const inst_info = self.module.types.get(inst_id); if (inst_info == .@"struct") { 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.update(alias_id, alias_info); } } else if (self.fn_ast_map.get(callee_name)) |fd| { // Type-returning function: Foo :: Complex(u32) if (fd.type_params.len > 0) { if (self.instantiateTypeFunction(cd.name, callee_name, fd, call_data.args)) |result_ty| { self.type_alias_map.put(cd.name, result_ty) catch {}; } } } } } else if (cd.value.data == .parameterized_type_expr) { // Type alias for generic struct (from type_bridge path) const pt = &cd.value.data.parameterized_type_expr; const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name; if (self.struct_template_map.getPtr(base_name)) |tmpl| { const inst_id = self.instantiateGenericStruct(tmpl, pt.args); const alias_name_id = self.module.types.internString(cd.name); const inst_info = self.module.types.get(inst_id); if (inst_info == .@"struct") { 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.update(alias_id, alias_info); } } } // comptime_expr handled in Pass 2 // Simple value constants with type annotation (e.g. AF_INET :s32: 2) if (cd.type_annotation) |ta| { switch (cd.value.data) { .int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal, .null_literal => { const ty = self.resolveType(ta); self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = ty }) catch {}; }, else => {}, } } else { // Untyped literal constants (e.g. UI_VERT_SRC :: #string GLSL...GLSL;) switch (cd.value.data) { .string_literal => self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .string }) catch {}, .int_literal => self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {}, .float_literal => self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .f64 }) catch {}, .bool_literal => self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .bool }) catch {}, // Complex constant expressions (e.g. COLOR_WHITE :: Color.{ r = 255, ... }) .struct_literal => { const inferred_ty = self.inferExprType(cd.value); self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = inferred_ty }) catch {}; }, else => {}, } } }, .struct_decl => |sd| { self.registerStructDecl(&sd); }, .enum_decl => { // Register enum/tagged-union types in the type table _ = type_bridge.resolveAstType(decl, &self.module.types); }, .union_decl => { // Register plain union types in the type table _ = type_bridge.resolveAstType(decl, &self.module.types); }, .protocol_decl => { self.registerProtocolDecl(&decl.data.protocol_decl); }, .impl_block => { self.registerImplBlock(&decl.data.impl_block, is_imported, decl); }, .foreign_class_decl => { self.registerForeignClassDecl(&decl.data.foreign_class_decl); }, .namespace_decl => |ns| { self.registerNamespacedForeignClasses(ns); if (self.main_file != null) { self.scanDecls(ns.decls); } }, .ufcs_alias => |ua| { self.ufcs_alias_map.put(ua.name, ua.target) catch {}; }, .var_decl => |vd| { // Top-level mutable global (e.g., `context : Context = ---;`) // Use self.resolveType so type aliases like `Handle :: u32;` resolve // to their target type (not a synthetic empty struct). When the // user omitted the annotation, infer from the initializer // expression; foreign globals with no annotation are diagnosed // because their type can't be inferred without an initializer. const var_ty: TypeId = if (vd.type_annotation) |ta| self.resolveType(ta) else if (vd.value) |val| self.inferExprType(val) else blk: { if (self.diagnostics) |d| d.addFmt(.err, null, "top-level var '{s}' has no type annotation and no initializer to infer from", .{vd.name}); break :blk .void; }; // Foreign globals reference a symbol defined in libSystem etc. // (`_NSConcreteStackBlock : *void #foreign;`). The C symbol // name is the optional override or the sx name itself. const sym_name = vd.foreign_name orelse vd.name; const name_id = self.module.types.internString(sym_name); const init_val: ?inst_mod.ConstantValue = if (vd.is_foreign) null else if (vd.value) |v| switch (v.data) { .undef_literal => .zeroinit, .int_literal => |il| .{ .int = il.value }, .bool_literal => |bl| .{ .boolean = bl.value }, .float_literal => |fl| .{ .float = fl.value }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, .array_literal => |al| self.constArrayLiteral(al.elements), .struct_literal => |sl| self.constStructLiteral(&sl, var_ty), else => null, } else null; const gid = self.module.addGlobal(.{ .name = name_id, .ty = var_ty, .init_val = init_val, .is_const = false, .is_extern = vd.is_foreign, }); self.global_names.put(vd.name, .{ .id = gid, .ty = var_ty }) catch {}; }, else => {}, } } } /// Try to convert an array literal's elements into a compile-time ConstantValue.aggregate. /// Returns null if any element is not a compile-time constant. fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue { const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null; for (elements, 0..) |elem, i| { vals[i] = self.constExprValue(elem) orelse return null; } return .{ .aggregate = vals }; } /// Try to convert a single AST expression into a compile-time ConstantValue. /// Returns null if the expression is not constant-foldable here. fn constExprValue(self: *Lowering, expr: *const Node) ?inst_mod.ConstantValue { return switch (expr.data) { .int_literal => |il| .{ .int = il.value }, .bool_literal => |bl| .{ .boolean = bl.value }, .float_literal => |fl| .{ .float = fl.value }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, .undef_literal => .zeroinit, .unary_op => |uo| switch (uo.op) { .negate => switch (uo.operand.data) { .int_literal => |il| .{ .int = -il.value }, .float_literal => |fl| .{ .float = -fl.value }, else => null, }, else => null, }, .array_literal => |al| self.constArrayLiteral(al.elements), else => null, }; } /// Try to convert a struct literal into a compile-time ConstantValue.aggregate of the /// struct's fields in declaration order, filling missing fields from the struct's /// field defaults. Returns null if any value is not constant-foldable. fn constStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, ty: TypeId) ?inst_mod.ConstantValue { if (ty.isBuiltin()) return null; const ti = self.module.types.get(ty); if (ti != .@"struct") return null; const struct_fields = ti.@"struct".fields; const struct_name = self.module.types.getString(ti.@"struct".name); const field_defaults: []const ?*const Node = self.struct_defaults_map.get(struct_name) orelse &.{}; const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; const vals = self.alloc.alloc(inst_mod.ConstantValue, struct_fields.len) catch return null; for (struct_fields, 0..) |sf, fi| { const sf_name = self.module.types.getString(sf.name); const init_expr: ?*const Node = blk: { if (has_names) { for (sl.field_inits) |init_pair| { if (init_pair.name) |n| { if (std.mem.eql(u8, n, sf_name)) break :blk init_pair.value; } } } else if (fi < sl.field_inits.len) { break :blk sl.field_inits[fi].value; } if (fi < field_defaults.len) break :blk field_defaults[fi]; break :blk null; }; if (init_expr) |e| { vals[fi] = self.constExprValue(e) orelse return null; } else { vals[fi] = .zeroinit; } } return .{ .aggregate = vals }; } /// Pass 2: Lower main function body and comptime side-effects. fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void { for (decls) |decl| { switch (decl.data) { .const_decl => |cd| { if (cd.value.data == .fn_decl) { if (isExportedEntryName(cd.name)) { self.lazyLowerFunction(cd.name); } } else if (cd.value.data == .comptime_expr) { self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation); } }, .fn_decl => |fd| { if (isExportedEntryName(fd.name)) { self.lazyLowerFunction(fd.name); } }, .comptime_expr => |ct| { self.lowerComptimeSideEffect(ct.expr); }, .namespace_decl => |ns| { if (self.main_file != null) { self.lowerMainAndComptime(ns.decls); } }, else => {}, } } } /// Declare a function as an extern stub (signature only, no body). fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void { // Skip generic templates — they're monomorphized on demand, not declared as extern if (fd.type_params.len > 0) return; const ret_ty = self.resolveReturnType(fd); // Foreign declarations with a trailing variadic param map to the C // calling convention's `...` tail. Drop the variadic param from the // IR signature (it has no C-level slot) and set is_variadic. const is_foreign = fd.body.data == .foreign_expr; var is_variadic = false; var effective_params = fd.params; if (is_foreign and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) { is_variadic = true; effective_params = fd.params[0 .. fd.params.len - 1]; } const wants_ctx = self.funcWantsImplicitCtx(fd); 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 (effective_params) |p| { const pty = self.resolveParamType(&p); params.append(self.alloc, .{ .name = self.module.types.internString(p.name), .ty = pty, }) catch unreachable; } // `#foreign` declarations are external C symbols by definition — // promote them to callconv(.c) when the user didn't write it // explicitly. This keeps fn-ptr coercion type-safe: anything // typed by name as `(args) -> ret` of a `#foreign` decl can be // assigned to / passed as a `callconv(.c)` fn-pointer without a // call-convention mismatch. const cc: Function.CallingConvention = if (fd.call_conv == .c or is_foreign) .c else .default; // For #foreign with C name override, declare under C name and map sx name → C name if (is_foreign) { const fe = fd.body.data.foreign_expr; if (fe.c_name) |c_name| { const c_name_id = self.module.types.internString(c_name); const fid = self.builder.declareExtern(c_name_id, params.items, ret_ty); const func = self.module.getFunctionMut(fid); func.call_conv = cc; func.source_file = self.current_source_file; func.is_variadic = is_variadic; func.has_implicit_ctx = wants_ctx; self.foreign_name_map.put(name, c_name) catch {}; return; } } const name_id = self.module.types.internString(name); const fid = self.builder.declareExtern(name_id, params.items, ret_ty); const func = self.module.getFunctionMut(fid); func.call_conv = cc; func.source_file = self.current_source_file; func.is_variadic = is_variadic; func.has_implicit_ctx = wants_ctx; } /// Check if a C-imported function is visible from the current source file. /// Returns true for non-C functions (always visible) or if no scoping info available. fn isCImportVisible(self: *Lowering, fn_name: []const u8) bool { const fd = self.fn_ast_map.get(fn_name) orelse return true; // Only restrict C import fn_decls: foreign_expr with no library_ref if (fd.body.data != .foreign_expr) return true; if (fd.body.data.foreign_expr.library_ref != null) return true; return self.isNameVisible(fn_name); } /// Non-transitive `#import` visibility check for top-level decls. /// /// `module_scopes[F]` holds ONLY the names authored in file F (plus its /// namespace aliases). Cross-module visibility is joined here at query /// time by walking each direct flat-import edge in `import_graph` — a /// name is visible from F when it's authored in F or in any module F /// directly `#import`s. Doing the join here (instead of pre-merging in /// `resolveImports`) lets cyclic imports like std.sx ↔ allocators.sx /// still resolve, since the cycle's skipped edge is still recorded in /// `import_graph` and the partner's scope is filled in by the time /// lowering queries it. /// /// Falls open when the scoping infrastructure isn't wired (comptime /// callers, directory imports without main_file, etc.). The caller is /// responsible for restricting the call to names that ARE known /// top-level decls; otherwise every local variable would be policed. fn isNameVisible(self: *Lowering, name: []const u8) bool { const scopes = self.module_scopes orelse return true; const source = self.current_source_file orelse return true; const own_scope = scopes.get(source) orelse return true; if (own_scope.contains(name)) return true; const graph = self.import_graph orelse return true; const direct = graph.get(source) orelse return true; var it = direct.iterator(); while (it.next()) |kv| { const dep = scopes.get(kv.key_ptr.*) orelse continue; if (dep.contains(name)) return true; } return false; } /// Lazily lower a function body on demand. Called when lowerCall can't find /// the function and it exists in fn_ast_map. fn lazyLowerFunction(self: *Lowering, name: []const u8) void { // Already lowered? if (self.lowered_functions.contains(name)) return; // For sx-defined `#objc_class` methods, pin current_foreign_class // so `*Self` substitutions in resolveTypeWithBindings find the // state-struct type (M1.2 A.2b). The inline body-lowering path // below re-resolves param types, so the context must be set // BEFORE any resolveReturnType / resolveParamType call. const saved_fc_lazy = self.current_foreign_class; defer self.current_foreign_class = saved_fc_lazy; if (self.lookupObjcDefinedClassForMethod(name)) |fcd| { self.current_foreign_class = fcd; } // No AST? (builtins, foreign functions, or imported functions not in this file) const fd = self.fn_ast_map.get(name) orelse return; // Foreign declarations stay as extern stubs but need to be REGISTERED // in the current module so callers get a real FuncId. Without this, // a comptime-lowered function (e.g. `concat` from std.sx pulled into // a fresh ct_module via `evalComptimeString`) emits `.call` against a // FuncId that doesn't exist locally; the interp can't find the // foreign target and silently no-ops instead of dispatching to libc. if (fd.body.data == .foreign_expr) { if (self.resolveFuncByName(name) == null) { self.declareFunction(fd, name); self.lowered_functions.put(name, {}) catch {}; } return; } // Builtins / #compiler bodies stay as compiler-handled — no extern stub needed. if (fd.body.data == .builtin_expr or fd.body.data == .compiler_expr) return; if (fd.type_params.len > 0) return; // generics handled by monomorphization (Step 3.13) // Defer functions with type-category matches until all types are registered. // any_to_string uses `if type == { case slice: ... }` which compiles a switch // with type tags from resolveTypeCategoryTags. This must happen AFTER main is // fully lowered so all types ([]s32, List__s32, etc.) are in the TypeTable. if (!self.processing_deferred and std.mem.eql(u8, name, "any_to_string")) { self.deferred_type_fns.append(self.alloc, name) catch {}; return; } // Mark as lowered before lowering (prevents infinite recursion) self.lowered_functions.put(name, {}) catch {}; // Save builder state (same pattern as lambda lowering) 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_force_block_value = self.force_block_value; const saved_source_file = self.current_source_file; // The `#jni_env` Ref stack is lexical within ONE function's instruction // stream — Refs from the caller don't dereference correctly in this // callee's body. Move the visible base to the current top so // omitted-env `#jni_call` in this fn doesn't accidentally pick up the // caller's Refs. Defer covers all the early-return paths below. const saved_jni_env_base = self.jni_env_stack_base; self.jni_env_stack_base = self.jni_env_stack.items.len; defer self.jni_env_stack_base = saved_jni_env_base; // Pack-fn mono state is lexical to the pack-fn body. A lazily // lowered callee may share a param NAME with the active pack // (e.g. `walk(args: []Any)` called from `probe(..$args)`); without // isolation, `lowerFieldAccess`'s `.len` intercept // folds the callee's `args.len` to the outer mono's arity and // bakes the constant into the IR. Same shape for the AST-node // and per-element-type maps. Null out for the duration of the // body lowering and restore on exit. 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; self.force_block_value = false; // Find the existing extern stub and replace it with a full body const name_id = self.module.types.internString(name); const ret_ty = self.resolveReturnType(fd); // Look up the existing function declaration (from scanDecls) var func_id: ?FuncId = null; for (self.module.functions.items, 0..) |func, i| { if (func.name == name_id) { func_id = FuncId.fromIndex(@intCast(i)); break; } } if (func_id == null) { // Function not yet declared — create it fresh via lowerFunction self.lowerFunction(fd, name, false); // Restore builder state self.setCurrentSourceFile(saved_source_file); self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.force_block_value = saved_force_block_value; self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return; } if (func_id) |fid| { // Re-use the existing function slot — switch builder to it self.builder.func = fid; const func = &self.module.functions.items[@intFromEnum(fid)]; self.setCurrentSourceFile(func.source_file); if (!func.is_extern) { // Already promoted (e.g., via lowerComptimeDeps) — skip self.setCurrentSourceFile(saved_source_file); self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.block_terminated = saved_block_terminated; self.force_block_value = saved_force_block_value; self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return; } func.is_extern = false; // promote from extern stub to real function func.linkage = if (isExportedEntryName(name)) .external else .internal; if (fd.call_conv == .c) func.call_conv = .c; // Set inst_counter to param count (params occupy refs 0..N-1). // IR params = AST params + 1 if the function carries `__sx_ctx` // at slot 0. const ctx_slots: usize = if (func.has_implicit_ctx) 1 else 0; std.debug.assert(func.params.len == fd.params.len + ctx_slots); self.builder.inst_counter = @intCast(func.params.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 and bind params var scope = Scope.init(self.alloc, null); defer scope.deinit(); self.scope = &scope; // The implicit `__sx_ctx` param (when present) lives at slot 0; // user params shift by one. `current_ctx_ref` is bound to slot 0 // so call-site lowering can prepend it to every sx-to-sx call. // For OS-called entry points (main / JNI hooks), there's no // ctx param at all — we synthesise `&__sx_default_context` and // bind `current_ctx_ref` to its address so the body's sx-to-sx // calls have a sensible Context to forward. const wants_ctx = self.funcWantsImplicitCtx(fd); const saved_ctx_ref = self.current_ctx_ref; defer self.current_ctx_ref = saved_ctx_ref; const user_param_base: u32 = if (wants_ctx) 1 else 0; if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); for (fd.params, 0..) |p, i| { const pty = self.resolveParamType(&p); const slot = self.builder.alloca(pty); const param_ref = Ref.fromIndex(@intCast(i + user_param_base)); self.builder.store(slot, param_ref); scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); } // Inbound entry points and callconv(.c) sx functions: bind // current_ctx_ref to the static default before any user code // runs. C-callable sx functions don't get a __sx_ctx param, // but their bodies may call ctx-aware sx functions / fn-ptrs // and need a real Context to forward. if (!wants_ctx and self.implicit_ctx_enabled) { if (self.global_names.get("__sx_default_context")) |dctx_gi| { self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void)); } } // Lower the function body (set target_type to return type for implicit returns) 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(fd.body); if (!self.currentBlockHasTerminator()) { if (body_val) |val| { // Check if the body value is void (e.g., last stmt is a void call) 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(fd.body); self.ensureTerminator(ret_ty); } self.target_type = saved_target; self.builder.finalize(); } // Restore builder state self.setCurrentSourceFile(saved_source_file); self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.block_terminated = saved_block_terminated; self.force_block_value = saved_force_block_value; self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } /// Lower a single function declaration. pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, is_imported: bool) void { // For sx-defined `#objc_class` methods (qualified `.`), // set `current_foreign_class` so `*Self` substitutions through // `resolveTypeWithBindings` find the state-struct type (M1.2 A.2b). // Save+restore — function lowering can re-enter. const saved_fc = self.current_foreign_class; defer self.current_foreign_class = saved_fc; if (self.lookupObjcDefinedClassForMethod(name)) |fcd| { self.current_foreign_class = fcd; } const name_id = self.module.types.internString(name); const ret_ty = self.resolveReturnType(fd); const wants_ctx = self.funcWantsImplicitCtx(fd); // Build param list 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| { const pty = self.resolveParamType(&p); params.append(self.alloc, .{ .name = self.module.types.internString(p.name), .ty = pty, }) catch unreachable; } // Check if the function body is a builtin or foreign declaration (no body needed) if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr) { // Already declared by scanDecls/declareFunction (which handles #foreign renames) return; } // Skip generic functions (they have type parameters and are templates, not concrete) if (fd.type_params.len > 0) { const fid = self.builder.declareExtern(name_id, params.items, ret_ty); self.module.getFunctionMut(fid).has_implicit_ctx = wants_ctx; return; } // Imported functions: declare as extern (don't lower bodies from other files) if (is_imported) { const fid = self.builder.declareExtern(name_id, params.items, ret_ty); self.module.getFunctionMut(fid).has_implicit_ctx = wants_ctx; return; } const func_id = self.builder.beginFunction( name_id, params.items, ret_ty, ); _ = func_id; self.builder.currentFunc().has_implicit_ctx = wants_ctx; // Set linkage. Default for fn defs is `internal` (LLVM DCE-friendly, // matches C `static`). isExportedEntryName lists the names the OS // loader calls — `main`, Android NativeActivity hooks — which must // stay externally visible. if (isExportedEntryName(name)) { self.builder.currentFunc().linkage = .external; } // Set calling convention if (fd.call_conv == .c) { self.builder.currentFunc().call_conv = .c; } // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // Create scope and bind params var scope = Scope.init(self.alloc, self.scope); defer scope.deinit(); self.scope = &scope; defer self.scope = scope.parent; // Implicit `__sx_ctx` at slot 0 when funcWantsImplicitCtx is true; // user params shift by one. Bind `current_ctx_ref` for call-site // forwarding inside the body. const wants_ctx_lf = self.funcWantsImplicitCtx(fd); const saved_ctx_ref_lf = self.current_ctx_ref; defer self.current_ctx_ref = saved_ctx_ref_lf; const user_param_base_lf: u32 = if (wants_ctx_lf) 1 else 0; if (wants_ctx_lf) self.current_ctx_ref = Ref.fromIndex(0); for (fd.params, 0..) |p, i| { const pty = self.resolveParamType(&p); // Allocate stack slot for param, store initial value. // Refs 0..N-1 are reserved for function parameters by beginFunction. const slot = self.builder.alloca(pty); const param_ref = Ref.fromIndex(@intCast(i + user_param_base_lf)); self.builder.store(slot, param_ref); scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); } // Inbound entry points + callconv(.c) sx functions: bind // current_ctx_ref to &__sx_default_context. See companion comment // in `lowerFunction` for the same case. if (!wants_ctx_lf and self.implicit_ctx_enabled) { if (self.global_names.get("__sx_default_context")) |dctx_gi| { self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void)); } } // Lower the function body, capturing the last expression's value for implicit return 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(fd.body); if (!self.currentBlockHasTerminator()) { if (body_val) |val| { // Check if body value is void (e.g., last stmt is a void call) 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(fd.body); self.ensureTerminator(ret_ty); } self.target_type = saved_target; self.builder.finalize(); } // ── Statement lowering ────────────────────────────────────────── fn lowerBlock(self: *Lowering, node: *const Node) void { switch (node.data) { .block => |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(); } for (blk.stmts) |stmt| { if (self.block_terminated) break; self.lowerStmt(stmt); } }, else => { // Single expression as body (arrow functions) self.lowerStmt(node); }, } } /// Lower an `inline if` branch — block body emits statements, expression returns value. fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref { if (node.data == .block) { self.lowerBlock(node); // A `return` inside the branch terminates the current LLVM block; propagate // that up so the enclosing block lowering stops emitting fall-through. if (self.currentBlockHasTerminator()) { self.block_terminated = true; return .none; } return self.builder.constInt(0, .void); } return self.lowerExpr(node); } /// Lower a block and return the last expression's value (for implicit returns). fn lowerBlockValue(self: *Lowering, node: *const Node) ?Ref { // Set force_block_value so nested if-else expressions produce values const saved = self.force_block_value; self.force_block_value = true; defer self.force_block_value = saved; switch (node.data) { .block => |blk| { if (blk.stmts.len == 0) return null; // 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(); } // Lower all statements except the last normally self.force_block_value = false; // don't force for non-last statements for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| { if (self.block_terminated) return null; self.lowerStmt(stmt); } if (self.block_terminated) return null; // Last statement: if it's an expression, return its value self.force_block_value = true; const last = blk.stmts[blk.stmts.len - 1]; return self.tryLowerAsExpr(last); }, else => { // Single expression as body (arrow functions) return self.tryLowerAsExpr(node); }, } } /// Try to lower a node as an expression, returning its value. /// Statement nodes are lowered as statements (returning null). fn tryLowerAsExpr(self: *Lowering, node: *const Node) ?Ref { return switch (node.data) { .var_decl, .const_decl, .fn_decl, .return_stmt, .assignment, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl => { self.lowerStmt(node); return null; }, else => self.lowerExpr(node), }; } fn lowerStmt(self: *Lowering, node: *const Node) void { switch (node.data) { .var_decl => |vd| self.lowerVarDecl(&vd), .const_decl => |cd| self.lowerConstDecl(&cd), .fn_decl => |fd| self.lowerLocalFnDecl(&fd), .return_stmt => |rs| self.lowerReturn(&rs), .assignment => |asgn| self.lowerAssignment(&asgn), .defer_stmt => |ds| self.lowerDefer(&ds), .push_stmt => |ps| self.lowerPush(&ps), .multi_assign => |ma| self.lowerMultiAssign(&ma), .destructure_decl => |dd| self.lowerDestructureDecl(&dd), .insert_expr => |ins| self.lowerInsertExpr(ins.expr), .block => self.lowerBlock(node), .jni_env_block => |eb| { // Compile-time stack push for lexical-direct env resolution // (2.16b — `#jni_call` in the same fn picks up env from // jni_env_stack directly, no TL read). // // Runtime TL save/set/restore (2.16c) for cross-function // helpers: callees in OTHER fns invoked from inside the // body read the slot via `sx_jni_env_tl_get`. Storage // lives in a separately-linked C helper (see // library/vendors/sx_jni_runtime/sx_jni_env_tl.c) so the // JIT doesn't need orc_rt for TLS. 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; self.lowerBlock(eb.body); _ = 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); }, // Block-local type declarations .struct_decl => |sd| self.registerStructDecl(&sd), .enum_decl, .union_decl => { _ = type_bridge.resolveAstType(node, &self.module.types); }, .ufcs_alias => |ua| { self.ufcs_alias_map.put(ua.name, ua.target) catch {}; }, // Expression statement else => { _ = self.lowerExpr(node); }, } } fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void { if (vd.type_annotation) |ta| { // Explicit type annotation — resolve type first, then lower value const ty = self.resolveType(ta); const slot = self.builder.alloca(ty); if (vd.value) |val| { // = --- (undef_literal) on tuple types: zero-initialize if (val.data == .undef_literal and !ty.isBuiltin()) { const ti = self.module.types.get(ty); if (ti == .tuple) { var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); for (ti.tuple.fields) |f| { field_vals.append(self.alloc, self.builder.constInt(0, f)) catch unreachable; } const zero = self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable }, }, ty); self.builder.store(slot, zero); if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } return; } } const saved_target = self.target_type; const saved_fbv = self.force_block_value; self.target_type = ty; self.force_block_value = true; var ref = self.lowerExpr(val); self.target_type = saved_target; self.force_block_value = saved_fbv; // If target is optional and value isn't null, wrap with optional_wrap if (!ty.isBuiltin()) { const ty_info = self.module.types.get(ty); if (ty_info == .optional and val.data != .null_literal) { ref = self.builder.optionalWrap(ref, ty); } else if (ty_info == .slice) { // Array → slice promotion: if value is an array, convert to slice const ref_ty = self.builder.getRefType(ref); if (!ref_ty.isBuiltin()) { const ref_info = self.module.types.get(ref_ty); if (ref_info == .array) { ref = self.builder.emit(.{ .array_to_slice = .{ .operand = ref } }, ty); } } } else if (self.getProtocolInfo(ty) != null) { // Auto type erasure: concrete → protocol const ref_ty = self.builder.getRefType(ref); if (ref_ty != ty) { ref = self.buildProtocolErasure(ref, val, ref_ty, ty); } } } // Coerce value to match target type (e.g. u8 → s64 widening) { const ref_ty = self.builder.getRefType(ref); if (ref_ty != ty and ref_ty != .void and ty != .void) { ref = self.coerceToType(ref, ref_ty, ty); } } self.builder.store(slot, ref); } else { // No value: zero-initialize or apply struct defaults const zero = self.buildDefaultValue(ty); self.builder.store(slot, zero); } if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } } else if (vd.value) |val| { // No type annotation — lower expr first, then get type from result. // This is critical for generic calls where the return type is only // known after monomorphization. const saved_fbv = self.force_block_value; self.force_block_value = true; const ref = self.lowerExpr(val); self.force_block_value = saved_fbv; const ty = self.builder.getRefType(ref); const slot = self.builder.alloca(ty); self.builder.store(slot, ref); if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } } else { const ty = TypeId.s64; const slot = self.builder.alloca(ty); self.builder.store(slot, self.zeroValue(ty)); if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } } } /// Handle a bare fn_decl node as a local function declaration. /// The parser produces `fn_decl` (not `const_decl`) for `name :: (params) -> T { body }`. fn lowerLocalFnDecl(self: *Lowering, fd: *const ast.FnDecl) void { // Use mangled name for local functions to support block-scoped shadowing const name = if (self.scope) |scope| blk: { const mangled = std.fmt.allocPrint(self.alloc, "{s}__{d}", .{ fd.name, self.local_fn_counter }) catch fd.name; self.local_fn_counter += 1; scope.fn_names.put(fd.name, mangled) catch {}; break :blk mangled; } else fd.name; self.fn_ast_map.put(name, fd) catch {}; self.lazyLowerFunction(name); } fn lowerConstDecl(self: *Lowering, cd: *const ast.ConstDecl) void { // Handle local function declarations: fx :: (s:s3) -> s3 { ... } if (cd.value.data == .fn_decl) { const fd = &cd.value.data.fn_decl; // Use mangled name for local functions to support block-scoped shadowing const name = if (self.scope != null) blk: { const mangled = std.fmt.allocPrint(self.alloc, "{s}__{d}", .{ cd.name, self.local_fn_counter }) catch cd.name; self.local_fn_counter += 1; // Register the bare→mangled mapping in the current scope if (self.scope) |scope| { scope.fn_names.put(cd.name, mangled) catch {}; } break :blk mangled; } else cd.name; // Register in fn_ast_map so it can be resolved by lowerCall self.fn_ast_map.put(name, fd) catch {}; // Lower the function body (saves/restores builder state) self.lazyLowerFunction(name); return; } // Handle local type declarations: MyType :: struct/union/enum { ... } if (cd.value.data == .struct_decl) { self.registerStructDecl(&cd.value.data.struct_decl); return; } if (cd.value.data == .enum_decl or cd.value.data == .union_decl) { _ = type_bridge.resolveAstType(cd.value, &self.module.types); return; } const ref = self.lowerExpr(cd.value); // If there's an explicit type annotation, use it. Otherwise, infer from the expression. const ty = if (cd.type_annotation) |ta| self.resolveType(ta) else self.builder.getRefType(ref); if (self.scope) |scope| { scope.put(cd.name, .{ .ref = ref, .ty = ty, .is_alloca = false }); } } fn lowerReturn(self: *Lowering, rs: *const ast.ReturnStmt) void { // Set target_type to function return type so null_literal etc. get the right type. // When inlining a comptime body, the *inlined* fn's declared return type wins // over the caller's — otherwise `return 42` inside a `-> s64` body lowered into // a `-> s32` caller would coerce 42 to s32 before storing into the s64 slot. const old_target = self.target_type; const ret_ty_for_target: TypeId = if (self.inline_return_target) |iri| iri.ret_ty else if (self.builder.func) |fid| self.module.functions.items[@intFromEnum(fid)].ret else TypeId.s64; if (ret_ty_for_target != .void) self.target_type = ret_ty_for_target; // Evaluate return value first (before defers) const ret_val = if (rs.value) |val| self.lowerExpr(val) else null; self.target_type = old_target; // Inlined-comptime-body return: store into the slot the inliner // gave us and branch to the inliner's "return-done" basic block. // The branch is the basic block's terminator — so subsequent // dead code in the same block trips the LLVM verifier (the // SAME behaviour as a regular `return X;` followed by code). // // We DO NOT set `block_terminated = true`: that flag would // leak past structured control flow (e.g. an `if cond { return // X; }` whose merge block continues to subsequent statements) // and incorrectly skip the trailing statements. CFG-level // termination is what we actually want — let the basic-block // terminator do its job. if (self.inline_return_target) |iri| { if (ret_val) |ref| { const val_ty = self.builder.getRefType(ref); const coerced = if (val_ty != iri.ret_ty) self.coerceToType(ref, val_ty, iri.ret_ty) else ref; self.builder.store(iri.slot, coerced); } // Drain block-scoped defers up to the inlined-body base so // they fire on this return path the same as a real fn return. self.emitBlockDefers(self.func_defer_base); self.builder.br(iri.done_bb, &.{}); return; } // Emit ALL pending defers for THIS function in LIFO order before the return self.emitBlockDefers(self.func_defer_base); if (ret_val) |ref| { const ret_ty = if (self.builder.func) |fid| self.module.functions.items[@intFromEnum(fid)].ret else TypeId.s64; if (ret_ty == .void) { // Void function — just return void (the value expression was evaluated for side effects) self.builder.retVoid(); } else { // Coerce return value to match function return type (e.g., ?s32 → s32) const val_ty = self.builder.getRefType(ref); const coerced = self.coerceToType(ref, val_ty, ret_ty); self.builder.ret(coerced, ret_ty); } } else { self.builder.retVoid(); } } fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void { // Set target_type from LHS for RHS lowering (enum literals, struct literals, etc.) const old_target = self.target_type; if (asgn.target.data == .identifier) { var found_local = false; if (self.scope) |scope| { if (scope.lookup(asgn.target.data.identifier.name)) |binding| { self.target_type = binding.ty; found_local = true; } } if (!found_local) { if (self.global_names.get(asgn.target.data.identifier.name)) |gi| { self.target_type = gi.ty; } } } else if (asgn.target.data == .index_expr) { // For array[i] = val, set target_type to the element type const elem_ty = self.getElementType(self.inferExprType(asgn.target.data.index_expr.object)); if (elem_ty != .void) self.target_type = elem_ty; } else if (asgn.target.data == .field_access) { // For obj.field = val, set target_type to the field's type so RHS // sub-expressions (enum/struct literals, branch arms, xx casts) can // resolve against it. Skipped for forms that would forward the type // unchanged into method-call arg slots (`resolveCallParamTypes` can't // override target_type per-arg). const needs_target = switch (asgn.value.data) { .enum_literal, .struct_literal, .if_expr, .match_expr, .block, .unary_op, .binary_op => true, .call => |vc| vc.callee.data == .enum_literal, else => false, }; if (needs_target) { const fa = asgn.target.data.field_access; const obj_ty_raw = self.inferExprType(fa.object); const obj_ty = if (!obj_ty_raw.isBuiltin()) blk: { const pinfo = self.module.types.get(obj_ty_raw); break :blk if (pinfo == .pointer) pinfo.pointer.pointee else obj_ty_raw; } else obj_ty_raw; 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) { self.target_type = f.ty; break; } } } } } const val = self.lowerExpr(asgn.value); self.target_type = old_target; switch (asgn.target.data) { .identifier => |id| { var handled = false; if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { handled = true; if (asgn.op == .assign) { // Coerce value to match binding type (e.g., f32 → ?f32, concrete → protocol) var store_val = val; const val_ty = self.builder.getRefType(val); if (val_ty != binding.ty and val_ty != .void and binding.ty != .void) { store_val = self.coerceToType(val, val_ty, binding.ty); } self.builder.store(binding.ref, store_val); } else { // Compound assignment: load, op, store const loaded = self.builder.load(binding.ref, binding.ty); const result = self.emitCompoundOp(loaded, val, asgn.op, binding.ty); self.builder.store(binding.ref, result); } } } } // Fallback: global variable assignment if (!handled) { if (self.global_names.get(id.name)) |gi| { if (asgn.op == .assign) { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != gi.ty and val_ty != .void and gi.ty != .void) self.coerceToType(val, val_ty, gi.ty) else val; self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = store_val } }, .void); } else { // Compound assignment: load current value, apply op, store back const loaded = self.builder.emit(.{ .global_get = gi.id }, gi.ty); const result = self.emitCompoundOp(loaded, val, asgn.op, gi.ty); self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = result } }, .void); } } } }, .field_access => |fa| { // M2.2 — `obj.field = val` for an Obj-C `#property` field // dispatches via objc_msgSend `setField:`. Skip struct- // pointer / GEP entirely; receivers are opaque Obj-C ids. // Compound ops on properties are deferred (need load-via- // getter + op + store-via-setter — Month 4 ARC territory). if (asgn.op == .assign) { if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| { self.lowerObjcPropertySetter(fa.object, prop, val); return; } } // M1.2 A.3 — `self.field [op]= val` on a sx-defined Obj-C // class instance field (NOT a #property): write through // the __sx_state ivar. Handles plain assignment AND // compound ops (+=, -=, etc.) via storeOrCompound. if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| { const obj_ref = self.lowerExpr(fa.object); const state_ptr = self.lowerObjcDefinedStateForObj(obj_ref, info.fcd) orelse return; 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); self.storeOrCompound(field_addr, val, asgn.op, info.field_ty); return; } var obj_ptr = self.lowerExprAsPtr(fa.object); var obj_ty = self.inferExprType(fa.object); // Auto-deref: if the object is a pointer field from a non-identifier // (i.e., result of structGep on a pointer slot), load the pointer value. if (fa.object.data != .identifier and !obj_ty.isBuiltin()) { const pinfo = self.module.types.get(obj_ty); if (pinfo == .pointer) { obj_ptr = self.builder.load(obj_ptr, obj_ty); obj_ty = pinfo.pointer.pointee; } } // Special .len/.ptr handling only for slices, strings, arrays — NOT structs const is_special_container = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: { const obj_info = self.module.types.get(obj_ty); break :blk obj_info == .slice or obj_info == .array or obj_info == .vector; } else false); if (is_special_container and std.mem.eql(u8, fa.field, "len")) { const gep = self.builder.structGepTyped(obj_ptr, 1, .s64, obj_ty); self.storeOrCompound(gep, val, asgn.op, .s64); } else if (is_special_container and std.mem.eql(u8, fa.field, "ptr")) { const gep = self.builder.structGepTyped(obj_ptr, 0, .s64, obj_ty); self.storeOrCompound(gep, val, asgn.op, .s64); } else { const field_name_id = self.module.types.internString(fa.field); // Check if this is a union field assignment if (!obj_ty.isBuiltin()) { const type_info = self.module.types.get(obj_ty); const union_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (type_info) { .@"union" => |u| u.fields, .tagged_union => |u| u.fields, else => null, }; if (union_fields) |fields| { for (fields, 0..) |f, i| { if (f.name == field_name_id) { const gep = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty)); const src_ty = self.builder.getRefType(val); const coerced = self.coerceToType(val, src_ty, f.ty); self.storeOrCompound(gep, coerced, asgn.op, f.ty); return; } // 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, 0..) |sf, si| { if (sf.name == field_name_id) { // GEP into union payload area, then into the struct field const union_gep = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty)); const field_gep = self.builder.structGepTyped(union_gep, @intCast(si), sf.ty, f.ty); const src_ty = self.builder.getRefType(val); const coerced = self.coerceToType(val, src_ty, sf.ty); self.storeOrCompound(field_gep, coerced, asgn.op, sf.ty); return; } } } } } } } const struct_fields = self.getStructFields(obj_ty); var field_idx: u32 = 0; var field_ty: TypeId = .s64; for (struct_fields, 0..) |f, i| { if (f.name == field_name_id) { field_idx = @intCast(i); field_ty = f.ty; break; } } // Wrap in ptrTo so the store handler sees *field_ty (consistent // with index_gep which uses ptrTo(elem_ty)). Without this, a // [*]BigNode field makes the store handler extract BigNode as the // target type, storing element-sized bytes instead of a pointer. const gep_ty = self.module.types.ptrTo(field_ty); const gep = self.builder.structGepTyped(obj_ptr, field_idx, gep_ty, obj_ty); // Coerce value to field type — use the lowered value's actual type // (not inferExprType, which can re-read target_type after restore). const src_ty = self.builder.getRefType(val); const coerced = self.coerceToType(val, src_ty, field_ty); self.storeOrCompound(gep, coerced, asgn.op, field_ty); } }, .index_expr => |ie| { 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 fixed-size array assignment targets, use the alloca pointer directly // so that the store modifies the original variable (not a loaded copy). const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null; if (obj_alloca) |alloca_ref| { // Array alloca: single-index GEP with element stride const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty); self.storeOrCompound(gep, val, asgn.op, elem_ty); } else if (is_array) { // Array in a struct field or other composite: get pointer to array in-place const obj_ptr = self.lowerExprAsPtr(ie.object); const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj_ptr, .rhs = idx } }, ptr_ty); self.storeOrCompound(gep, val, asgn.op, elem_ty); } else { // Pointer/slice: load the pointer value and GEP const obj = self.lowerExpr(ie.object); const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty); self.storeOrCompound(gep, val, asgn.op, elem_ty); } }, .deref_expr => |de| { const ptr = self.lowerExpr(de.operand); if (asgn.op == .assign) { const pointee_ty = blk: { const ptr_ty = self.inferExprType(de.operand); if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) break :blk info.pointer.pointee; } break :blk ptr_ty; }; const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void) self.coerceToType(val, val_ty, pointee_ty) else val; self.builder.store(ptr, store_val); } else { const pointee_ty = self.inferExprType(de.operand); const elem_ty = blk: { if (!pointee_ty.isBuiltin()) { const info = self.module.types.get(pointee_ty); if (info == .pointer) break :blk info.pointer.pointee; } break :blk pointee_ty; }; self.storeOrCompound(ptr, val, asgn.op, elem_ty); } }, else => { _ = self.emitError("assignment_target", asgn.target.span); }, } } /// Get the pointer (alloca ref) for an lvalue expression, without loading. fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref { switch (node.data) { .identifier => |id| { if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { // If the variable IS a pointer (e.g., p: *Vec2), load it // to get the actual pointer value for GEP/store operations if (!binding.ty.isBuiltin()) { const info = self.module.types.get(binding.ty); if (info == .pointer) { return self.builder.load(binding.ref, binding.ty); } } return binding.ref; } } } }, .field_access => |fa| { var obj_ptr = self.lowerExprAsPtr(fa.object); var obj_ty = self.inferExprType(fa.object); // Auto-deref for chained pointer field access: // When fa.object is a field_access or index_expr, lowerExprAsPtr returns // a structGep/pointer to the slot. If the slot holds a pointer type, // we need to load the pointer value before GEPing into the pointee struct. // (Identifiers are already loaded by the identifier handler in lowerExprAsPtr.) if (fa.object.data != .identifier and !obj_ty.isBuiltin()) { const info = self.module.types.get(obj_ty); if (info == .pointer) { obj_ptr = self.builder.load(obj_ptr, obj_ty); obj_ty = info.pointer.pointee; } } const struct_fields = self.getStructFields(obj_ty); const field_name_id = self.module.types.internString(fa.field); for (struct_fields, 0..) |f, i| { if (f.name == field_name_id) { return self.builder.structGepTyped(obj_ptr, @intCast(i), f.ty, obj_ty); } } return self.builder.structGepTyped(obj_ptr, 0, .s64, obj_ty); }, .index_expr => |ie| { 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 fixed-size arrays, use the alloca so GEP addresses the original memory 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); return self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty); }, .deref_expr => |de| { return self.lowerExpr(de.operand); }, else => {}, } // Fallback: lower as expression (may produce a value, not pointer) return self.lowerExpr(node); } /// Store a value to a GEP, handling both plain and compound assignment. fn storeOrCompound(self: *Lowering, gep: Ref, val: Ref, op: ast.Assignment.Op, ty: TypeId) void { if (op == .assign) { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != ty and val_ty != .void and ty != .void) self.coerceToType(val, val_ty, ty) else val; self.builder.store(gep, store_val); } else { const loaded = self.builder.load(gep, ty); const result = self.emitCompoundOp(loaded, val, op, ty); self.builder.store(gep, result); } } fn emitCompoundOp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.Assignment.Op, ty: TypeId) Ref { return switch (op) { .add_assign => self.builder.add(lhs, rhs, ty), .sub_assign => self.builder.sub(lhs, rhs, ty), .mul_assign => self.builder.mul(lhs, rhs, ty), .div_assign => self.builder.div(lhs, rhs, ty), .mod_assign => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty), .and_assign => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty), .or_assign => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty), .xor_assign => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty), .shl_assign => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty), .shr_assign => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty), else => self.emitError("compound_assign", null), }; } // ── Expression lowering ───────────────────────────────────────── fn lowerExpr(self: *Lowering, node: *const Node) Ref { 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: { const pat = self.pack_arg_types orelse { 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)); }; const arg_tys = pat.get(cpr.pack_name) orelse { if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack reference ${s} has no active binding", .{cpr.pack_name}); } break :blk self.builder.constNull(self.module.types.sliceOf(.any)); }; break :blk self.buildPackSliceValue(arg_tys); }, // 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); }, .null_literal => self.builder.constNull(self.target_type orelse .void), .undef_literal => self.builder.constUndef(self.target_type orelse .void), .identifier => |id| blk: { 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.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.module_const_map.get(id.name)) |ci| { 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); } break :blk self.emitModuleConst(ci); } // 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.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.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.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); } if (!self.lowered_functions.contains(eff_fn_name)) { self.lazyLowerFunction(eff_fn_name); } if (self.resolveFuncByName(eff_fn_name)) |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: if target is Any (Type context), produce a type name string if (self.target_type == .any) { const sid = self.module.types.internString(id.name); const str = self.builder.constString(sid); break :blk self.builder.boxAny(str, .string); } // Type-as-value: known type name used where a value is expected (e.g. cast(s64, val)) // The type arg is lowered but unused — the caller resolves from AST. if (self.isKnownTypeName(id.name)) { break :blk self.emitPlaceholder(id.name); } // Unknown identifier break :blk self.emitError(id.name, node.span); }, .binary_op => |bop| self.lowerBinaryOp(&bop), .unary_op => |uop| blk: { // 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 alloca directly so the pointer is persistent 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.lowerExpr(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.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(); } if (self.force_block_value and blk.stmts.len > 0) { // Extract last expression value (for if-else branch blocks) 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.global_names.get(te.name)) |gi| { break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); } // Type-as-value: if target is Any (Type variable), produce a boxed string if (self.target_type == .any) { const sid = self.module.types.internString(te.name); const str = self.builder.constString(sid); break :blk self.builder.boxAny(str, .string); } // Type expressions are always type names from the parser — they appear // in value position when used as args to cast() etc. Silent placeholder. if (self.isKnownTypeName(te.name)) { break :blk self.emitPlaceholder(te.name); } break :blk self.emitError(te.name, node.span); }, else => self.emitError("unknown_expr", node.span), }; } 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) { 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); } // 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; } } } } // 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) { const other_ty = if (null_on_rhs) self.inferExprType(bop.lhs) else self.inferExprType(bop.rhs); if (other_ty != .void) { const saved_tt = self.target_type; self.target_type = other_ty; const lv = self.lowerExpr(bop.lhs); const rv = self.lowerExpr(bop.rhs); 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); // Set target_type from LHS so enum literals on RHS resolve correctly const lhs_ty = self.inferExprType(bop.lhs); 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); self.target_type = saved_tt; // Infer result type from LHS operand (covers float, bool, etc.) var ty = lhs_ty; // Promote int×float → float (e.g., s64 * f32 → f32) // Only for scalar int LHS — don't affect vectors or structs. { const rhs_inferred = self.inferExprType(bop.rhs); const l_int = isInt(ty); const r_float = (rhs_inferred == .f32 or rhs_inferred == .f64); if (l_int and r_float) { 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 = 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); } } } 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 ──────────────────────────────────────────────── fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref { // inline if: evaluate condition at compile time, only lower taken branch if (ie.is_comptime) { if (self.evalComptimeCondition(ie.condition)) |is_true| { if (is_true) { return self.lowerInlineBranch(ie.then_branch); } else if (ie.else_branch) |eb| { return self.lowerInlineBranch(eb); } return self.builder.constInt(0, .void); } // Condition couldn't be evaluated — fall through to runtime } // Check for constant-bool conditions (e.g., is_flags(T) → false) to avoid dead-code LLVM errors if (self.tryConstBoolCondition(ie.condition)) |is_true| { if (is_true) { // Condition always true: only lower then-branch if ((ie.is_inline or self.force_block_value) and ie.else_branch != null) { return self.lowerExpr(ie.then_branch); } self.lowerBlock(ie.then_branch); // If then-branch terminated (return/break), mark block as dead if (self.currentBlockHasTerminator()) { self.block_terminated = true; return .none; } return self.builder.constInt(0, .void); } else { // Condition always false: only lower else-branch (if any) if (ie.else_branch) |eb| { if (ie.is_inline or self.force_block_value) { return self.lowerExpr(eb); } self.lowerBlock(eb); if (self.currentBlockHasTerminator()) { self.block_terminated = true; return .none; } } return self.builder.constInt(0, .void); } } // Optional binding: `if val := expr { ... }` // Clear target_type so the ternary's result type doesn't leak into the condition // (e.g., `if x != 0 then 1.0 else 2.0` — the `0` must be s64, not f32) const saved_cond_target = self.target_type; self.target_type = null; const opt_val = self.lowerExpr(ie.condition); self.target_type = saved_cond_target; const cond = if (ie.binding_name != null) blk: { // The condition is an optional — emit has_value check break :blk self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool); } else opt_val; const has_else = ie.else_branch != null; // If-else produces a value when inline OR when then-branch has a non-void type const is_value = (ie.is_inline or self.force_block_value) and has_else; // Infer result type from then branch for value if-exprs // If then_branch is null/void, try else_branch (e.g., `if cond then null else val`) const result_type: TypeId = if (is_value) blk: { const then_ty = self.inferExprType(ie.then_branch); if (then_ty == .void and ie.else_branch != null) { break :blk self.inferExprType(ie.else_branch.?); } break :blk then_ty; } else .void; const then_bb = self.freshBlock("if.then"); const else_bb: ?BlockId = if (has_else) self.freshBlock("if.else") else null; const merge_params: []const TypeId = if (is_value) &.{result_type} else &.{}; const merge_bb = self.freshBlockWithParams("if.merge", merge_params); // Conditional branch self.builder.condBr( cond, then_bb, &.{}, if (else_bb) |eb| eb else merge_bb, &.{}, ); // Then branch self.builder.switchToBlock(then_bb); // If binding: unwrap the optional and bind to the name if (ie.binding_name) |bind_name| { const opt_ty = self.inferExprType(ie.condition); const inner_ty = if (!opt_ty.isBuiltin()) blk: { const info = self.module.types.get(opt_ty); break :blk if (info == .optional) info.optional.child else opt_ty; } else opt_ty; const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = opt_val } }, inner_ty); const slot = self.builder.alloca(inner_ty); self.builder.store(slot, unwrapped); if (self.scope) |scope| { scope.put(bind_name, .{ .ref = slot, .ty = inner_ty, .is_alloca = true }); } } // Set target_type so null/undef in branches get the right type const saved_target = self.target_type; if (is_value and result_type != .void) self.target_type = result_type; if (is_value) { var v = self.lowerExpr(ie.then_branch); if (!self.currentBlockHasTerminator()) { const v_ty = self.builder.getRefType(v); if (v_ty != result_type and v_ty != .void and result_type != .void) { v = self.coerceToType(v, v_ty, result_type); } self.builder.br(merge_bb, &.{v}); } } else { self.lowerBlock(ie.then_branch); if (!self.currentBlockHasTerminator()) { self.builder.br(merge_bb, &.{}); } } // Else branch if (has_else) { self.builder.switchToBlock(else_bb.?); if (is_value) { var v = self.lowerExpr(ie.else_branch.?); if (!self.currentBlockHasTerminator()) { const v_ty = self.builder.getRefType(v); if (v_ty != result_type and v_ty != .void and result_type != .void) { v = self.coerceToType(v, v_ty, result_type); } self.builder.br(merge_bb, &.{v}); } } else { self.lowerBlock(ie.else_branch.?); if (!self.currentBlockHasTerminator()) { self.builder.br(merge_bb, &.{}); } } } self.target_type = saved_target; // Continue at merge self.builder.switchToBlock(merge_bb); if (is_value) { return self.builder.blockParam(merge_bb, 0, result_type); } return self.builder.constInt(0, .void); } /// Try to evaluate an AST condition as a compile-time constant bool. /// Returns true/false if the condition is known at compile time, null otherwise. fn tryConstBoolCondition(self: *Lowering, node: *const Node) ?bool { switch (node.data) { .bool_literal => |bl| return bl.value, .call => |c| { if (c.callee.data == .identifier) { const cname = c.callee.data.identifier.name; if (std.mem.eql(u8, cname, "is_flags")) { // Resolve the type arg to check if it's actually a flags enum if (c.args.len > 0) { const ty = self.resolveTypeArg(c.args[0]); if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .@"enum") return info.@"enum".is_flags; } } return false; } if (std.mem.eql(u8, cname, "type_eq") and c.args.len >= 2) { const a = self.resolveTypeArg(c.args[0]); const b = self.resolveTypeArg(c.args[1]); return a == b; } if (std.mem.eql(u8, cname, "has_impl") and c.args.len >= 2) { const ty = self.resolveTypeArg(c.args[1]); return self.computeHasImpl(c.args[0], ty); } } }, else => {}, } return null; } /// 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). fn computeHasImpl(self: *Lowering, proto_node: *const Node, ty: TypeId) bool { switch (proto_node.data) { .identifier => |id| return self.hasImplPlain(id.name, ty), .type_expr => |te| return self.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 hasImplPlain(self: *Lowering, p_name: []const u8, ty: TypeId) bool { const ty_name = self.formatTypeName(ty); const thunk_key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ p_name, ty_name }) catch return false; return self.protocol_thunk_map.contains(thunk_key); } /// Evaluate a compile-time condition for `inline if`. /// Handles: `ident == .variant`, `ident != .variant`, `ident == int`, `ident != int`. fn evalComptimeCondition(self: *Lowering, node: *const Node) ?bool { if (node.data != .binary_op) return null; const bo = &node.data.binary_op; if (bo.op != .eq and bo.op != .neq) return null; // LHS must be an identifier that's in comptime_constants const name = switch (bo.lhs.data) { .identifier => |id| id.name, else => return null, }; const cv = self.comptime_constants.get(name) orelse return null; switch (cv) { .enum_tag => |et| { // RHS must be an enum literal (.variant) const variant_name = switch (bo.rhs.data) { .enum_literal => |el| el.name, else => return null, }; // Look up variant index in the enum type const enum_info = self.module.types.get(et.ty); if (enum_info != .@"enum") return null; const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name); const result = et.tag == variant_idx; return if (bo.op == .eq) result else !result; }, .int_val => |iv| { // RHS must be an integer literal const rhs_val: i64 = switch (bo.rhs.data) { .int_literal => |il| il.value, else => return null, }; const result = iv == rhs_val; return if (bo.op == .eq) result else !result; }, } } /// Evaluate a compile-time match expression for `inline if ... == { case ... }`. /// Returns the body of the matching arm, or null if the match can't be resolved. fn evalComptimeMatch(self: *Lowering, me: *const ast.MatchExpr) ?*const Node { // Subject must be a comptime constant identifier const name = switch (me.subject.data) { .identifier => |id| id.name, else => return null, }; const cv = self.comptime_constants.get(name) orelse return null; switch (cv) { .enum_tag => |et| { const enum_info = self.module.types.get(et.ty); if (enum_info != .@"enum") return null; for (me.arms) |arm| { if (arm.pattern == null) continue; // default arm const variant_name = switch (arm.pattern.?.data) { .enum_literal => |el| el.name, else => continue, }; const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name); if (et.tag == variant_idx) return arm.body; } // No match — try default arm for (me.arms) |arm| { if (arm.pattern == null) return arm.body; } return null; }, .int_val => |iv| { for (me.arms) |arm| { if (arm.pattern == null) continue; const rhs_val: i64 = switch (arm.pattern.?.data) { .int_literal => |il| il.value, else => continue, }; if (iv == rhs_val) return arm.body; } for (me.arms) |arm| { if (arm.pattern == null) return arm.body; } return null; }, } } fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref { const header_bb = self.freshBlock("while.hdr"); const body_bb = self.freshBlock("while.body"); const exit_bb = self.freshBlock("while.exit"); // Branch to header self.builder.br(header_bb, &.{}); // Header: evaluate condition self.builder.switchToBlock(header_bb); const cond = self.lowerExpr(we.condition); self.builder.condBr(cond, body_bb, &.{}, exit_bb, &.{}); // Body self.builder.switchToBlock(body_bb); // Save and set loop targets const old_break = self.break_target; const old_continue = self.continue_target; self.break_target = exit_bb; self.continue_target = header_bb; defer { self.break_target = old_break; self.continue_target = old_continue; } self.lowerBlock(we.body); if (!self.currentBlockHasTerminator()) { self.builder.br(header_bb, &.{}); } // Continue at exit self.builder.switchToBlock(exit_bb); return self.builder.constInt(0, .void); } fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref { // Lower iterable const iterable = self.lowerExpr(fe.iterable); // Get length const len = self.builder.emit(.{ .length = .{ .operand = iterable } }, .s64); // Create index variable const idx_slot = self.builder.alloca(.s64); const zero = self.builder.constInt(0, .s64); self.builder.store(idx_slot, zero); const header_bb = self.freshBlock("for.hdr"); const body_bb = self.freshBlock("for.body"); const inc_bb = self.freshBlock("for.inc"); const exit_bb = self.freshBlock("for.exit"); self.builder.br(header_bb, &.{}); // Header: compare index < length self.builder.switchToBlock(header_bb); const idx_val = self.builder.load(idx_slot, .s64); const cmp = self.builder.cmpLt(idx_val, len); self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{}); // Body self.builder.switchToBlock(body_bb); // Bind element — resolve element type from iterable const iterable_ty = self.inferExprType(fe.iterable); const elem_ty = self.getElementType(iterable_ty); const elem = self.builder.emit(.{ .index_get = .{ .lhs = iterable, .rhs = idx_val } }, elem_ty); var body_scope = Scope.init(self.alloc, self.scope); const old_scope = self.scope; self.scope = &body_scope; body_scope.put(fe.capture_name, .{ .ref = elem, .ty = elem_ty, .is_alloca = false }); // Bind index if requested if (fe.index_name) |iname| { body_scope.put(iname, .{ .ref = idx_val, .ty = .s64, .is_alloca = false }); } // Save and set loop targets const old_break = self.break_target; const old_continue = self.continue_target; self.break_target = exit_bb; self.continue_target = inc_bb; // continue → increment, not header self.lowerBlock(fe.body); self.break_target = old_break; self.continue_target = old_continue; self.scope = old_scope; body_scope.deinit(); // Fall through to increment block if (!self.currentBlockHasTerminator()) { self.builder.br(inc_bb, &.{}); } // Increment block: increment index and jump back to header self.builder.switchToBlock(inc_bb); { const cur_idx = self.builder.load(idx_slot, .s64); const one = self.builder.constInt(1, .s64); const next_idx = self.builder.add(cur_idx, one, .s64); self.builder.store(idx_slot, next_idx); self.builder.br(header_bb, &.{}); } // Continue at exit self.builder.switchToBlock(exit_bb); return self.builder.constInt(0, .void); } fn lowerMatch(self: *Lowering, me: *const ast.MatchExpr) Ref { // inline if match: evaluate at compile time, only lower the matching arm if (me.is_comptime) { if (self.evalComptimeMatch(me)) |arm_body| { return self.lowerInlineBranch(arm_body); } // Couldn't evaluate — fall through to runtime } const is_type_match = isTypeCategoryMatch(me); const subject = self.lowerExpr(me.subject); // Detect optional subject type const subject_ty = self.inferExprType(me.subject); const is_optional_match = blk: { if (!subject_ty.isBuiltin()) { const info = self.module.types.get(subject_ty); break :blk info == .optional; } break :blk false; }; // Determine if the match produces a value (has non-void arms) // For type-category matches (inside any_to_string), only produce value when force_block_value // For regular enum/optional matches, always produce value if arms are non-void const inferred_result = self.inferMatchResultType(me); const is_value = if (is_type_match) self.force_block_value else (self.force_block_value or inferred_result != .void); const result_type: TypeId = if (is_value) inferred_result else .void; const merge_params: []const TypeId = if (is_value and result_type != .void) &.{result_type} else &.{}; const merge_bb = self.freshBlockWithParams("match.merge", merge_params); // Build arm blocks var default_bb: ?BlockId = null; var arm_blocks = std.ArrayList(BlockId).empty; defer arm_blocks.deinit(self.alloc); for (me.arms) |_| { arm_blocks.append(self.alloc, self.freshBlock("match.arm")) catch unreachable; } // Build case list and pre-collect type tags per arm var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty; defer cases.deinit(self.alloc); var arm_tag_values = std.ArrayList([]const u64).empty; defer arm_tag_values.deinit(self.alloc); for (me.arms, 0..) |arm, i| { if (arm.pattern == null) { default_bb = arm_blocks.items[i]; arm_tag_values.append(self.alloc, &.{}) catch unreachable; continue; } const pat = arm.pattern.?; if (is_type_match) { // Type-category match: resolve category name to tag values const name = switch (pat.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => "", }; const tag_values = self.resolveTypeCategoryTags(name); arm_tag_values.append(self.alloc, tag_values) catch unreachable; for (tag_values) |tag| { cases.append(self.alloc, .{ .value = @intCast(tag), .target = arm_blocks.items[i], .args = &.{}, }) catch unreachable; } } else if (is_optional_match) { // Optional match: .some → 1 (has_value=true), .none → 0 arm_tag_values.append(self.alloc, &.{}) catch unreachable; const pat_name = switch (pat.data) { .enum_literal => |el| el.name, .identifier => |id| id.name, else => "", }; const case_val: u64 = if (std.mem.eql(u8, pat_name, "some")) 1 else 0; cases.append(self.alloc, .{ .value = @intCast(case_val), .target = arm_blocks.items[i], .args = &.{}, }) catch unreachable; } else { // Enum/value match: resolve variant name to actual tag value arm_tag_values.append(self.alloc, &.{}) catch unreachable; const case_val: u64 = blk: { const pat_name = switch (pat.data) { .enum_literal => |el| el.name, .identifier => |id| id.name, .int_literal => |il| break :blk @intCast(il.value), .bool_literal => |bl| break :blk @as(u64, if (bl.value) 1 else 0), else => break :blk @as(u64, @intCast(i)), }; // Look up variant value in the subject's type if (!subject_ty.isBuiltin()) { const ty_info = self.module.types.get(subject_ty); if (ty_info == .tagged_union) { for (ty_info.tagged_union.fields, 0..) |f, vi| { const vname = self.module.types.strings.get(f.name); if (std.mem.eql(u8, vname, pat_name)) { if (ty_info.tagged_union.explicit_tag_values) |vals| { if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi]))); } break :blk @intCast(vi); } } if (self.diagnostics) |diags| { const ty_name = self.formatTypeName(subject_ty); diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name }); } } else if (ty_info == .@"enum") { for (ty_info.@"enum".variants, 0..) |v, vi| { const vname = self.module.types.strings.get(v); if (std.mem.eql(u8, vname, pat_name)) { if (ty_info.@"enum".explicit_values) |vals| { if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi]))); } break :blk @intCast(vi); } } if (self.diagnostics) |diags| { const ty_name = self.formatTypeName(subject_ty); diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name }); } } } break :blk @intCast(i); }; cases.append(self.alloc, .{ .value = @intCast(case_val), .target = arm_blocks.items[i], .args = &.{}, }) catch unreachable; } } // If no default arm, create an unreachable default if (default_bb == null) { default_bb = self.freshBlock("match.unr"); } // Switch on the subject (for type match, subject IS the tag; for enum match, extract tag) const tag = if (is_type_match) subject else if (is_optional_match) self.builder.emit(.{ .optional_has_value = .{ .operand = subject } }, .bool) else blk: { // Determine actual tag type from union info (e.g. u32 for SDL_Event) const tag_ty: TypeId = tt: { if (!subject_ty.isBuiltin()) { const ty_info = self.module.types.get(subject_ty); if (ty_info == .tagged_union) break :tt ty_info.tagged_union.tag_type; } break :tt .s32; }; break :blk self.builder.enumTag(subject, tag_ty); }; self.builder.switchBr(tag, cases.items, default_bb.?, &.{}); // Lower each arm's body for (me.arms, 0..) |arm, i| { self.builder.switchToBlock(arm_blocks.items[i]); // For type-match arms with empty tag lists, the arm is unreachable // (no switch case targets it). Skip lowering to avoid invalid IR // from runtime cast/dispatch with no matching types. if (is_type_match and arm.pattern != null and arm_tag_values.items[i].len == 0) { self.builder.emitUnreachable(); continue; } var arm_scope = Scope.init(self.alloc, self.scope); const old_scope = self.scope; self.scope = &arm_scope; if (arm.capture) |capture_name| { if (is_optional_match) { // For optional match, unwrap the optional value const opt_info = self.module.types.get(subject_ty); const child_ty = if (opt_info == .optional) opt_info.optional.child else .s64; const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = subject } }, child_ty); arm_scope.put(capture_name, .{ .ref = unwrapped, .ty = child_ty, .is_alloca = false }); } else { // Resolve actual variant index and payload type from the subject's type var variant_idx: u32 = @intCast(i); var payload_ty: TypeId = .s64; if (arm.pattern) |arm_pat| { const pat_name = switch (arm_pat.data) { .enum_literal => |el| el.name, .identifier => |id| id.name, else => "", }; if (!subject_ty.isBuiltin()) { const ty_info = self.module.types.get(subject_ty); if (ty_info == .tagged_union) { for (ty_info.tagged_union.fields, 0..) |f, vi| { const vname = self.module.types.strings.get(f.name); if (std.mem.eql(u8, vname, pat_name)) { variant_idx = @intCast(vi); payload_ty = f.ty; break; } } } } } const payload = self.builder.emit(.{ .enum_payload = .{ .base = subject, .field_index = variant_idx, } }, payload_ty); arm_scope.put(capture_name, .{ .ref = payload, .ty = payload_ty, .is_alloca = false }); } } // Set match arm context for runtime type dispatch const saved_match_tags = self.current_match_tags; if (is_type_match) { self.current_match_tags = arm_tag_values.items[i]; } if (is_value and result_type != .void) { var v = self.lowerBlockValue(arm.body) orelse if (result_type == .string or !result_type.isBuiltin()) self.builder.constUndef(result_type) else self.builder.constInt(0, result_type); self.current_match_tags = saved_match_tags; self.scope = old_scope; arm_scope.deinit(); if (!self.currentBlockHasTerminator()) { // Coerce arm value to match result type const v_ty = self.builder.getRefType(v); v = self.coerceToType(v, v_ty, result_type); self.builder.br(merge_bb, &.{v}); } } else { self.lowerBlock(arm.body); self.current_match_tags = saved_match_tags; self.scope = old_scope; arm_scope.deinit(); if (!self.currentBlockHasTerminator()) { self.builder.br(merge_bb, &.{}); } } } // Emit default block if no explicit else arm if (default_bb != null) { var found_default = false; for (me.arms) |arm| { if (arm.pattern == null) { found_default = true; break; } } if (!found_default) { self.builder.switchToBlock(default_bb.?); if (is_type_match) { // For type-category matches, unrecognized tags should skip to merge // (e.g., optional types not covered by any_to_string categories) if (is_value and result_type != .void) { const default_val = self.builder.constUndef(result_type); self.builder.br(merge_bb, &.{default_val}); } else { self.builder.br(merge_bb, &.{}); } } else { // For non-exhaustive matches (union/enum with unhandled variants), // fall through to merge instead of unreachable const is_exhaustive = blk: { if (!subject_ty.isBuiltin()) { const ty_info = self.module.types.get(subject_ty); if (ty_info == .tagged_union) { break :blk cases.items.len >= ty_info.tagged_union.fields.len; } else if (ty_info == .@"enum") { break :blk cases.items.len >= ty_info.@"enum".variants.len; } } break :blk false; }; if (is_exhaustive) { self.builder.emitUnreachable(); } else if (is_value and result_type != .void) { const default_val = self.builder.constUndef(result_type); self.builder.br(merge_bb, &.{default_val}); } else { self.builder.br(merge_bb, &.{}); } } } } self.builder.switchToBlock(merge_bb); if (is_value and result_type != .void) { return self.builder.blockParam(merge_bb, 0, result_type); } return self.builder.constInt(0, .void); } fn lowerBreak(self: *Lowering) Ref { if (self.break_target) |target| { self.builder.br(target, &.{}); } return Ref.none; } fn lowerContinue(self: *Lowering) Ref { if (self.continue_target) |target| { self.builder.br(target, &.{}); } return Ref.none; } // ── Struct/enum/union ops ─────────────────────────────────────── 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 .s64; 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 .s64; 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| blk: { const name_id = self.module.types.internString(name); break :blk self.module.types.findByName(name_id) orelse self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); } else if (sl.type_expr) |te| // Generic struct literal: Pair(s32).{ ... } — resolve type from type_expr self.resolveTypeWithBindings(te) else self.target_type orelse .s64; // 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| { const saved_tt = self.target_type; self.target_type = sf.ty; const val = self.lowerExpr(default_expr); self.target_type = saved_tt; fields.append(self.alloc, val) 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| { const saved_tt = self.target_type; self.target_type = sf.ty; const val = self.lowerExpr(default_expr); self.target_type = saved_tt; fields.append(self.alloc, val) 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. 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; } } } } /// 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 .s64; }; if (idx < tuple.fields.len) return tuple.fields[idx]; return .s64; } } const struct_fields = self.getStructFields(ty); for (struct_fields) |f| { if (f.name == field_name_id) return f.ty; } return .s64; } fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref { // 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); } } } // 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); } } // 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.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); } /// 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) 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 field access: .x/.y/.z/.w → index 0/1/2/3 if (!obj_ty.isBuiltin()) { const vinfo = self.module.types.get(obj_ty); if (vinfo == .vector) { const vidx: u32 = if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) 0 else if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) 1 else if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) 2 else if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) 3 else 0; 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 .s64; const tag = self.resolveVariantValue(target, el.name); return self.builder.enumInit(tag, Ref.none, target); } /// 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. 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 = .s64; 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 != .s64) { 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; const val = self.lowerExpr(elem); self.target_type = old_tt; 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 .s64, }; if (std.mem.eql(u8, callee_name, "Vector")) { if (cl.args.len == 2) { const length: u32 = switch (cl.args[0].data) { .int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))), else => 0, }; const elem = self.resolveTypeWithBindings(cl.args[1]); if (length > 0) return self.module.types.vectorOf(elem, length); } } // Try as generic struct if (self.struct_template_map.getPtr(callee_name)) |tmpl| { return self.instantiateGenericStruct(tmpl, cl.args); } return .s64; }, .parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt), .identifier => |id| { const name_id = self.module.types.internString(id.name); return self.module.types.findByName(name_id) orelse .s64; }, .type_expr => return type_bridge.resolveAstType(te, &self.module.types), .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 .s64; }, else => return .s64, } } 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); } 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; if (ie.index.data != .int_literal) return false; const raw: i64 = ie.index.data.int_literal.value; 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. 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; if (ie.index.data != .int_literal) return null; const raw: i64 = ie.index.data.int_literal.value; if (raw < 0) return null; const i: usize = @intCast(raw); if (i >= arg_nodes.len) return null; return arg_nodes[i]; } 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; for (tl.elements) |elem| { const val = self.lowerExpr(elem.value); elems.append(self.alloc, val) catch unreachable; const ety = self.inferExprType(elem.value); field_type_ids.append(self.alloc, ety) 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; } } // Create a tuple type const tuple_ty = 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); var pointee_ty: TypeId = .s64; if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) { pointee_ty = info.pointer.pointee; } } return self.builder.emit(.{ .deref = .{ .operand = ptr } }, pointee_ty); } 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 .s64; } // ── 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.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 (!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); } /// Resolve the Obj-C selector for a foreign-class method, honoring /// any `#selector("...")` override on the declaration. When an /// override is present the selector string is the user's literal; /// `keyword_count` is the `:` count in the literal (so callers can /// still cross-check arity, downgrading the diagnostic to a /// warning). When no override exists, the default mangling rule /// runs: /// - niladic: name verbatim (`length` → `length`). /// - arity ≥ 1: split the sx name on `_`; each piece becomes a /// keyword with a trailing `:` (`addObject` → `addObject:`, /// `combine_and` → `combine:and:`). fn deriveObjcSelector(self: *Lowering, method: ast.ForeignMethodDecl, arity: usize) struct { sel: []const u8, keyword_count: usize, is_override: bool } { if (method.selector_override) |sel| { var colons: usize = 0; for (sel) |ch| { if (ch == ':') colons += 1; } return .{ .sel = sel, .keyword_count = colons, .is_override = true }; } if (arity == 0) { return .{ .sel = method.name, .keyword_count = 0, .is_override = false }; } // Each `_` in the sx name becomes a `:` (one-byte-for-one), plus // one trailing `:` regardless of how many pieces. Piece count // = (number of `_`) + 1. var pieces: usize = 1; for (method.name) |ch| { if (ch == '_') pieces += 1; } const out = self.alloc.alloc(u8, method.name.len + 1) catch unreachable; for (method.name, 0..) |ch, i| { out[i] = if (ch == '_') ':' else ch; } out[method.name.len] = ':'; return .{ .sel = out, .keyword_count = pieces, .is_override = false }; } /// Derive an Obj-C type-encoding string for a synthesized IMP /// signature (M1.2 A.1). Apple's runtime accepts these strings on /// `class_addMethod(cls, sel, imp, types)`; the encoding tells the /// runtime the IMP's argument layout for KVC, NSCoder, and reflective /// dispatch. /// /// Layout: ` @ : ...`. The `@` slot is the /// receiver (self); `:` is `_cmd`. Caller passes user-declared params /// AFTER stripping `self`. /// /// Single-character encodings (the common case): /// v=void B=bool c=s8/BOOL s=s16 i=s32 q=s64 /// C=u8 S=u16 I=u32 Q=u64 f=f32 d=f64 /// @=id #=Class :=SEL *=C string ^v=void* / generic ptr /// /// Foreign-class pointers (`*UIView` etc.) encode as `@` (object /// pointer). Other pointers fall to `^v` — the encoding is metadata, /// not ABI, so being conservative here is safe. Struct returns and /// other complex shapes BAIL loudly via diagnostics rather than /// silently mis-encoding (per CLAUDE.md rejected-patterns rule). /// /// Returns an allocator-owned slice; caller frees via `self.alloc`. fn objcTypeEncodingFromSignature( self: *Lowering, return_ty: TypeId, param_tys: []const TypeId, span: ?ast.Span, ) ![]const u8 { var out = std.ArrayList(u8).empty; errdefer out.deinit(self.alloc); try self.appendObjcEncoding(&out, return_ty, span); try out.append(self.alloc, '@'); // self try out.append(self.alloc, ':'); // _cmd for (param_tys) |pty| { try self.appendObjcEncoding(&out, pty, span); } return try out.toOwnedSlice(self.alloc); } fn appendObjcEncoding(self: *Lowering, out: *std.ArrayList(u8), ty: TypeId, span: ?ast.Span) !void { const info = self.module.types.get(ty); switch (info) { .void => try out.append(self.alloc, 'v'), .bool => try out.append(self.alloc, 'B'), .signed => |bits| { const ch: u8 = switch (bits) { 8 => 'c', 16 => 's', 32 => 'i', 64 => 'q', else => return self.bailObjcEncoding(span, "signed integer with non-standard bit width", bits), }; try out.append(self.alloc, ch); }, .unsigned => |bits| { const ch: u8 = switch (bits) { 8 => 'C', 16 => 'S', 32 => 'I', 64 => 'Q', else => return self.bailObjcEncoding(span, "unsigned integer with non-standard bit width", bits), }; try out.append(self.alloc, ch); }, .f32 => try out.append(self.alloc, 'f'), .f64 => try out.append(self.alloc, 'd'), // sx-target arm64 — pointer-sized aliases match s64/u64. .isize => try out.append(self.alloc, 'q'), .usize => try out.append(self.alloc, 'Q'), .pointer => |p| { // Pointer to a foreign Obj-C class (or sx-defined #objc_class) // encodes as `@`. Anything else falls to `^v` — generic // pointer; the runtime treats it as opaque. const pointee_info = self.module.types.get(p.pointee); const is_objc_obj = blk: { if (pointee_info != .@"struct") break :blk false; const name = self.module.types.getString(pointee_info.@"struct".name); break :blk self.foreign_class_map.get(name) != null; }; if (is_objc_obj) { try out.append(self.alloc, '@'); } else { try out.appendSlice(self.alloc, "^v"); } }, .many_pointer => |mp| { // `[*]u8` is the canonical C-string carrier — encode as `*`. // Other element types fall to generic `^v`. const el = self.module.types.get(mp.element); if (el == .unsigned and el.unsigned == 8) { try out.append(self.alloc, '*'); } else { try out.appendSlice(self.alloc, "^v"); } }, .optional => |o| { // sx's `?T` is a nullable T. At the Obj-C ABI boundary // nullability is just "this pointer may be null" — the // wire-level encoding is the same as T. Unwrap and // recurse. (Same goes for `?*UIView` etc. — the // underlying pointer kind drives the encoding char.) return self.appendObjcEncoding(out, o.child, span); }, else => return self.bailObjcEncoding(span, "type kind not yet supported by Obj-C encoding", @intFromEnum(std.meta.activeTag(info))), } } fn bailObjcEncoding(self: *Lowering, span: ?ast.Span, reason: []const u8, detail: anytype) anyerror { if (self.diagnostics) |d| { d.addFmt(.err, span, "cannot derive Obj-C type encoding: {s} (detail={any})", .{ reason, detail }); } return error.ObjcEncodingUnsupported; } /// 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); } 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 = &.{} } }); } /// Build (and cache) the hidden sx-state struct type for an sx-defined /// `#objc_class`. The state struct is what the runtime's `__sx_state` /// ivar points at — separate from the Obj-C object itself, which stays /// opaque. Layout (M1.2 A.2): /// /// __State { /// user_field_0, /// user_field_1, /// ... /// } /// /// M1.2 A.5 will prepend `__sx_allocator: Allocator` so `-dealloc` /// can free through the per-instance allocator and method bodies can /// access `self.allocator`. For A.2 the struct holds only the /// user-declared fields — sufficient for the body lowering + /// `self.field` access work in A.2/A.3. Field-by-name resolution /// stays correct across the future repositioning. /// /// Foreign-class members other than `.field` are ignored here — /// methods / `#extends` / `#implements` don't contribute to the /// state layout. fn objcDefinedStateStructType(self: *Lowering, fcd: *const ast.ForeignClassDecl) TypeId { const state_name = std.fmt.allocPrint(self.alloc, "__{s}State", .{fcd.name}) catch unreachable; const name_id = self.module.types.internString(state_name); if (self.module.types.findByName(name_id)) |existing| return existing; var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; // M4.0: prepend __sx_allocator at field index 0 — captured at +alloc // time, read at -dealloc time to free the state struct through the // same allocator. Lookup by name (the existing by-name resolution in // emitObjcDefinedClassPropertyImps + lookupObjcDefinedStateFieldOnPointer) // naturally finds user fields at their post-shift indices. if (self.objcStateAllocatorType()) |allocator_ty| { fields.append(self.alloc, .{ .name = self.module.types.internString("__sx_allocator"), .ty = allocator_ty, }) catch unreachable; } for (fcd.members) |m| { switch (m) { .field => |f| { const f_name_id = self.module.types.internString(f.name); const f_ty = self.resolveType(f.field_type); fields.append(self.alloc, .{ .name = f_name_id, .ty = f_ty }) catch unreachable; }, else => {}, } } return self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = fields.toOwnedSlice(self.alloc) catch unreachable, } }); } /// Return the `Allocator` protocol TypeId (the value-shape used in /// Context.allocator). Falls back to null if Context isn't registered /// yet (early-init paths); callers omit the field in that case. fn objcStateAllocatorType(self: *Lowering) ?TypeId { const ctx_name = self.module.types.internString("Context"); const ctx_ty = self.module.types.findByName(ctx_name) orelse return null; const ctx_info = self.module.types.get(ctx_ty); if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) return null; return ctx_info.@"struct".fields[0].ty; } /// 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.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.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.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.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.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.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.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 { // 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. var c = c_in; if (self.expandCallDefaults(c)) |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.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.ufcs_alias_map.get(id_name) == null and self.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.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; if (!self.lowered_functions.contains(fn_name)) { self.lazyLowerFunction(fn_name); } if (self.resolveFuncByName(fn_name)) |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.ufcs_alias_map.get(id_name)) |target| { break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; } break :blk scoped; }; if (self.fn_ast_map.get(early_name)) |fd| { if (hasComptimeParams(fd)) { if (isPackFn(fd)) { return self.lowerPackFnCall(fd, c); } 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.) 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); // 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 .s64; 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| { // Skip spread expressions — they'll be handled by packVariadicCallArgs from AST if (arg.data == .spread_expr) { 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 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; } } } } } } const val = self.lowerExpr(arg); self.target_type = saved_target; 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.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.coerceToType(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); } } } } // Check for comptime-expanded or generic functions if (self.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.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) 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.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.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.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.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; 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; // Generic struct static method: Animated(Size).make(...) if (self.struct_template_map.getPtr(resolved)) |tmpl| { const inst_ty = self.instantiateGenericStruct(tmpl, inner_call.args); const inst_name = self.formatTypeName(inst_ty); // Look up template method, monomorphize, and call if (self.struct_instance_template.get(inst_name)) |tmpl_name| { const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch fa.field; if (self.fn_ast_map.get(tmpl_qualified)) |fd| { if (self.struct_instance_bindings.getPtr(inst_name)) |bindings| { const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ inst_name, fa.field }) catch fa.field; if (!self.lowered_functions.contains(mangled)) { self.monomorphizeFunction(fd, mangled, bindings); } if (self.resolveFuncByName(mangled)) |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); } } } } } if (self.fn_ast_map.get(resolved)) |fd| { if (fd.type_params.len > 0) { // 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); } } } } } } // Check if this is a namespace-qualified call (e.g., std.print) // If the object is an identifier/type_expr not in scope, treat as namespace prefix const is_namespace = blk: { const obj_name: ?[]const u8 = switch (fa.object.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => null, }; if (obj_name) |name| { // Check local scope first if (self.scope) |scope| { if (scope.lookup(name) != null) break :blk false; } // Check global variables (e.g., g_font : *FontAtlas) if (self.global_names.contains(name)) break :blk false; // Not a local or global variable → namespace prefix break :blk true; } break :blk false; }; 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.fn_ast_map.get(qualified_name) != null) qualified_name else func_name; if (self.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.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.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.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.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) else .void; return self.builder.compilerCall(qualified, method_args.items, ret_ty); } } // Check for generic struct template method if (self.struct_instance_template.get(sname)) |tmpl_name| { // This is an instantiated generic struct — look up template method const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch fa.field; if (self.fn_ast_map.get(tmpl_qualified)) |fd| { // Get the stored type bindings for this instance if (self.struct_instance_bindings.getPtr(sname)) |bindings| { // Monomorphize the method with the struct's type bindings const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field; if (!self.lowered_functions.contains(mangled)) { self.monomorphizeFunction(fd, mangled, bindings); } if (self.resolveFuncByName(mangled)) |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(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.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.buildTypeBindings(gen_fd, eff_args.items); defer gbindings.deinit(); const gmangled = self.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.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 (method) if (self.resolveFuncByName(fa.field)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const final_args = self.prependCtxIfNeeded(func, method_args.items); 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.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. 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; } 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; } 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; for (lam.params, 0..) |p, pi| { var pty = self.resolveParamType(&p); // Infer param type from target closure type if no annotation if (p.type_expr.data == .inferred_type and target_closure_params != null) { if (pi < target_closure_params.?.len) { pty = target_closure_params.?[pi]; } } 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); } // Use target closure return type if available if (self.target_type) |tt| { if (!tt.isBuiltin()) { const tti = self.module.types.get(tt); if (tti == .closure) 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) 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) |p| { const pty = self.resolveParamType(&p); 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); // 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) for (lam.params, 0..) |p, i| { const pty = self.resolveParamType(&p); 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 if (ret_ty != .void) { if (self.lowerBlockValue(lam.body)) |val| { if (!self.currentBlockHasTerminator()) { 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.lowerBlock(lam.body); } 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; // 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; } /// 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.fn_ast_map.contains(id.name)) return; // Skip type names if (self.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); 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 ────────────────────────────────────── fn lowerDefer(self: *Lowering, ds: *const ast.DeferStmt) void { // Push deferred expression onto the stack — will be emitted at block exit in LIFO order self.defer_stack.append(self.alloc, ds.expr) catch {}; } /// Emit deferred expressions from saved_len..current in reverse (LIFO) order, /// then truncate the defer stack back to saved_len. fn emitBlockDefers(self: *Lowering, saved_len: usize) void { // Guard: if stack was already drained (e.g., by a return that emitted all defers) if (saved_len > self.defer_stack.items.len) return; if (self.currentBlockHasTerminator()) { // Block already terminated (e.g., by return) — defers were already emitted self.defer_stack.shrinkRetainingCapacity(saved_len); return; } const stack = self.defer_stack.items; var i = stack.len; while (i > saved_len) { i -= 1; _ = self.lowerExpr(stack[i]); } self.defer_stack.shrinkRetainingCapacity(saved_len); } fn lowerPush(self: *Lowering, ps: *const ast.PushStmt) void { // push Context.{...} { body } — allocates a fresh Context on the // stack frame, rebinds the lowering's `current_ctx_ref` to it for // the body's lexical scope, then restores. No global, no walk. if (!self.implicit_ctx_enabled) { _ = self.diagnoseMissingContext("`push Context.{...}`"); self.lowerBlock(ps.body); return; } const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse { _ = self.diagnoseMissingContext("`push Context.{...}`"); self.lowerBlock(ps.body); return; }; const saved_ctx_ref = self.current_ctx_ref; defer self.current_ctx_ref = saved_ctx_ref; const saved_target = self.target_type; self.target_type = ctx_ty; const ctx_val = self.lowerExpr(ps.context_expr); self.target_type = saved_target; const slot = self.builder.alloca(ctx_ty); self.builder.store(slot, ctx_val); self.current_ctx_ref = slot; self.lowerBlock(ps.body); } fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void { // Evaluate all RHS values first, then assign to LHS targets var vals = std.ArrayList(Ref).empty; defer vals.deinit(self.alloc); for (ma.values) |v| { vals.append(self.alloc, self.lowerExpr(v)) catch unreachable; } for (ma.targets, 0..) |target, i| { if (i >= vals.items.len) break; const val = vals.items[i]; switch (target.data) { .identifier => |id| { if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != binding.ty and val_ty != .void and binding.ty != .void) self.coerceToType(val, val_ty, binding.ty) else val; self.builder.store(binding.ref, store_val); } } } }, .index_expr => |ie| { 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); const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != elem_ty and val_ty != .void and elem_ty != .void) self.coerceToType(val, val_ty, elem_ty) else val; // For fixed-size arrays, use the alloca pointer directly const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null; if (obj_alloca) |alloca_ref| { const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty); self.builder.store(gep, store_val); } else { const obj = self.lowerExpr(ie.object); const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty); self.builder.store(gep, store_val); } }, .field_access => |fa| { const obj_ptr = self.lowerExprAsPtr(fa.object); const obj_ty = self.inferExprType(fa.object); const field_name_id = self.module.types.internString(fa.field); const struct_fields = self.getStructFields(obj_ty); var field_idx: u32 = 0; var field_ty: TypeId = .s64; for (struct_fields, 0..) |f, fi| { if (f.name == field_name_id) { field_idx = @intCast(fi); field_ty = f.ty; break; } } const gep = self.builder.structGepTyped(obj_ptr, field_idx, field_ty, obj_ty); const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != field_ty and val_ty != .void and field_ty != .void) self.coerceToType(val, val_ty, field_ty) else val; self.builder.store(gep, store_val); }, .deref_expr => |de| { const ptr = self.lowerExpr(de.operand); const pointee_ty = blk: { const ptr_ty = self.inferExprType(de.operand); if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) break :blk info.pointer.pointee; } break :blk ptr_ty; }; const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void) self.coerceToType(val, val_ty, pointee_ty) else val; self.builder.store(ptr, store_val); }, else => { _ = self.emitError("multi_assign_target", target.span); }, } } } fn lowerDestructureDecl(self: *Lowering, dd: *const ast.DestructureDecl) void { // Lower the RHS expression (must produce a tuple) const saved_fbv = self.force_block_value; self.force_block_value = true; const ref = self.lowerExpr(dd.value); self.force_block_value = saved_fbv; const ty = self.builder.getRefType(ref); // Get tuple field info if (ty.isBuiltin()) return; const ti = self.module.types.get(ty); if (ti != .tuple) return; const tuple = ti.tuple; if (dd.names.len > tuple.fields.len) return; // Extract each field and bind to a new variable for (dd.names, 0..) |name, i| { if (std.mem.eql(u8, name, "_")) continue; // discard const field_ty = tuple.fields[i]; const field_val = self.builder.emit(.{ .tuple_get = .{ .base = ref, .field_index = @intCast(i), .base_type = ty, } }, field_ty); const slot = self.builder.alloca(field_ty); self.builder.store(slot, field_val); if (self.scope) |scope| { scope.put(name, .{ .ref = slot, .ty = field_ty, .is_alloca = true }); } } } // ── Comptime lowering ──────────────────────────────────────────── /// Lower a `#run expr` that appears as a top-level constant binding: /// NAME :: #run expr; /// Creates a comptime function wrapping the expression (for later /// interpretation), plus a global constant to hold the result. fn lowerComptimeGlobal(self: *Lowering, name: []const u8, expr: *const Node, type_ann: ?*const Node) void { // When the user writes `NAME :: #run expr;` with no type annotation, // infer the global's type from the comptime expression's return // shape. `resolveType(null)` returns `.s64` for legacy reasons — // good for primitive helpers, silently wrong for anything else. const ret_ty: TypeId = if (type_ann) |n| self.resolveTypeWithBindings(n) else self.inferExprType(expr); const func_id = self.createComptimeFunction(name, expr, ret_ty); // Add a global constant whose initializer will be filled by the interpreter. const name_id = self.module.types.internString(name); const gid = self.module.addGlobal(.{ .name = name_id, .ty = ret_ty, .init_val = null, // will be filled by interpreter at emit time .is_const = true, .comptime_func = func_id, }); // Register for runtime lookup: identifier resolution emits global_get self.global_names.put(name, .{ .id = gid, .ty = ret_ty }) catch {}; } /// Lower a standalone `#run expr;` at the top level (side-effect only). /// Creates a comptime function that the interpreter should execute. fn lowerComptimeSideEffect(self: *Lowering, expr: *const Node) void { _ = self.createComptimeFunction("__run", expr, .void); } /// Lower a `#run expr` that appears inline within an expression. /// Creates a comptime function and emits a `call` to it, so the /// interpreter can evaluate it and replace with the constant result. fn lowerInlineComptime(self: *Lowering, expr: *const Node) Ref { const ret_ty: TypeId = self.target_type orelse self.inferExprType(expr); const func_id = self.createComptimeFunction("__ct", expr, ret_ty); // Emit a call to the comptime function. At interpretation time, // this will be evaluated and the result inlined as a constant. const func = &self.module.functions.items[@intFromEnum(func_id)]; const final_args: []const Ref = if (func.has_implicit_ctx) self.alloc.dupe(Ref, &.{self.current_ctx_ref}) catch &.{} else &.{}; return self.builder.call(func_id, final_args, ret_ty); } /// Lower a `#insert expr` statement. Evaluates `expr` at compile time to get /// a string, parses it as sx code, and lowers each statement inline. fn lowerInsertExpr(self: *Lowering, expr: *const Node) void { _ = self.lowerInsertExprValue(expr); } /// Like lowerInsertExpr but returns the value of the last parsed expression. fn lowerInsertExprValue(self: *Lowering, expr: *const Node) Ref { // Step 1: Substitute comptime param nodes (e.g., replace $fmt with its literal) const substituted = if (self.comptime_param_nodes) |cpn| self.substituteComptimeNodes(expr, cpn) catch expr else expr; // Step 2: Evaluate the expression to get a string const code_str = self.evalComptimeString(substituted) orelse return self.builder.constInt(0, .void); // Step 3: Parse the string as sx code and lower each statement // The last expression's value is captured as the return value var p = parser_mod.Parser.init(self.alloc, code_str); var last_val: Ref = self.builder.constInt(0, .void); while (p.current.tag != .eof) { const stmt = p.parseStmt() catch break; if (p.current.tag == .eof) { // Last statement — try to capture as expression value // Note: tryLowerAsExpr internally calls lowerStmt for statement nodes, // so we must NOT call lowerStmt again in the else branch. if (self.tryLowerAsExpr(stmt)) |val| { last_val = val; } } else { self.lowerStmt(stmt); } } return last_val; } /// Evaluate an expression at compile time, returning its string value. /// Returns null if evaluation fails. fn evalComptimeString(self: *Lowering, expr: *const Node) ?[:0]const u8 { // Case 1: String literal — return it directly (no need for interpreter) if (expr.data == .string_literal) { const lit = expr.data.string_literal; const str = if (lit.is_raw) lit.raw else unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; return self.alloc.dupeZ(u8, str) catch null; } // Case 2: Evaluate via IR interpreter, reusing the parent module. // The parent's `scanDecls` pass has already registered every // type / protocol / impl / thunk the comptime call may need // (Allocator, CAllocator, Context, the per-impl thunks). A // fresh empty module would only lazy-lower function ASTs and // would miss the type/protocol registrations, which would break // `context.allocator.X` — the protocol dispatch chain needs // those types to resolve struct field layout and the alloc/ // dealloc thunks at the bottom of the dispatch. const ct_func_id = self.createComptimeFunction("__insert", expr, .string); var interp = interp_mod.Interpreter.init(self.module, self.alloc); defer interp.deinit(); const result = interp.call(ct_func_id, &.{}) catch return null; const str = result.asString(&interp) orelse switch (result) { .string => |s| s, else => return null, }; return self.alloc.dupeZ(u8, str) catch null; } /// Lower the direct callee of a comptime expression into the ct module. /// Transitive dependencies are resolved lazily via the shared fn_ast_map. fn lowerComptimeDeps(self: *Lowering, ct: *Lowering, expr: *const Node) void { if (expr.data != .call) return; if (expr.data.call.callee.data != .identifier) return; const name = expr.data.call.callee.data.identifier.name; if (resolveBuiltin(name) != null) return; if (self.fn_ast_map.get(name)) |fd| { if (ct.resolveFuncByName(name) == null) { ct.lowerFunction(fd, name, false); } } } /// Substitute comptime parameter identifiers with their actual AST nodes. fn substituteComptimeNodes(self: *Lowering, node: *const Node, cpn: std.StringHashMap(*const Node)) !*const Node { // Direct identifier match if (node.data == .identifier) { if (cpn.get(node.data.identifier.name)) |replacement| { return replacement; } } // Recurse into call arguments if (node.data == .call) { var changed = false; const new_args = try self.alloc.alloc(*Node, node.data.call.args.len); for (node.data.call.args, 0..) |arg, i| { const substituted = try self.substituteComptimeNodes(arg, cpn); new_args[i] = @constCast(substituted); if (substituted != arg) changed = true; } if (changed) { const new_node = try self.alloc.create(Node); new_node.* = .{ .span = node.span, .data = .{ .call = .{ .callee = node.data.call.callee, .args = new_args, } }, }; return new_node; } } return node; } /// Lower a call to a function with comptime params by inlining its body. /// Comptime params are substituted, `#insert` expressions are evaluated. fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref { // Build comptime param substitution map: param_name → call_site AST node var cpn = std.StringHashMap(*const Node).init(self.alloc); var call_arg_idx: usize = 0; // Pack-arg-node registration (step 2 of the variadic heterogeneous // type packs feature): when the fn declares a pack param, record // the slice of call-site arg nodes under the pack name so the // body's `args[$i]` lowering can substitute the i-th arg with // its concrete-typed value instead of the `[]Any` slice load. var pack_arg_name: ?[]const u8 = null; var pack_arg_slice: []const *const Node = &.{}; for (fd.params) |param| { if (param.is_variadic) { // Variadic param: pack remaining call args into []Any slice self.lowerVariadicArgs(param.name, call_node.args, call_arg_idx); // Only heterogeneous pack form `..$args` (is_comptime AND // is_variadic) registers for typed indexing. Plain // `args: ..Any` keeps the existing []Any path so stdlib's // `format`/`print` continue boxing through Any. if (param.is_comptime and call_arg_idx <= call_node.args.len) { pack_arg_name = param.name; pack_arg_slice = call_node.args[call_arg_idx..]; } break; // variadic is always the last param } if (call_arg_idx >= call_node.args.len) break; if (param.is_comptime) { cpn.put(param.name, call_node.args[call_arg_idx]) catch {}; call_arg_idx += 1; } else { const arg_val = self.lowerExpr(call_node.args[call_arg_idx]); const pty = self.resolveParamType(¶m); const slot = self.builder.alloca(pty); self.builder.store(slot, arg_val); if (self.scope) |scope| { scope.put(param.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); } call_arg_idx += 1; } } // Also bind comptime params as local string variables (for `fmt` used in runtime code) var cpn_iter = cpn.iterator(); while (cpn_iter.next()) |entry| { const param_name = entry.key_ptr.*; const param_node = entry.value_ptr.*; if (param_node.data == .string_literal) { // Create a local string variable with the literal value const str_ref = self.lowerExpr(param_node); const slot = self.builder.alloca(.string); self.builder.store(slot, str_ref); if (self.scope) |scope| { scope.put(param_name, .{ .ref = slot, .ty = .string, .is_alloca = true }); } } } // Install comptime param nodes and lower the function body inline const saved_cpn = self.comptime_param_nodes; self.comptime_param_nodes = cpn; defer self.comptime_param_nodes = saved_cpn; // Install pack-arg-node binding. Mirrors `comptime_param_nodes`: // each call owns its own map, nested calls shadow. `lowerIndexExpr` // reads the map for `args[]` substitution. const saved_pan = self.pack_arg_nodes; var pan_map: std.StringHashMap([]const *const Node) = undefined; var pan_installed = false; if (pack_arg_name) |pn| { pan_map = std.StringHashMap([]const *const Node).init(self.alloc); pan_map.put(pn, pack_arg_slice) catch {}; self.pack_arg_nodes = pan_map; pan_installed = true; } defer { if (pan_installed) pan_map.deinit(); self.pack_arg_nodes = saved_pan; } // Lower the body — capture return value for functions with return type const ret_ty = self.resolveReturnType(fd); if (ret_ty != .void) { // Detect whether the body might use `return X;` statements. // If so, set up the inline-return slot AND a dedicated // "return-done" basic block so each `return X;` stores to // the slot and branches to ret_done. After the body lowers, // we switch to ret_done and load. Pure tail-expression // bodies (arrow form, or a block whose last stmt is an // expression) skip the slot+block — keeps the common // `format`/`#insert`-style path unchanged. const has_return = fnBodyHasReturn(fd.body); if (has_return) { const ret_slot = self.builder.alloca(ret_ty); const ret_done_bb = self.freshBlock("ct.ret_done"); const saved_iri = self.inline_return_target; self.inline_return_target = .{ .slot = ret_slot, .ret_ty = ret_ty, .done_bb = ret_done_bb }; defer self.inline_return_target = saved_iri; // Lower body. Tail-expression bodies (rare here since // has_return == true) produce a tail value we still // route through the slot so the load in ret_done picks // it up. Block-statement bodies whose last stmt is // `return X;` already br to ret_done from inside // lowerReturn. if (self.lowerBlockValue(fd.body)) |val| { if (!self.currentBlockHasTerminator()) { const v_ty = self.builder.getRefType(val); const coerced = if (v_ty != ret_ty) self.coerceToType(val, v_ty, ret_ty) else val; self.builder.store(ret_slot, coerced); self.builder.br(ret_done_bb, &.{}); } } else if (!self.currentBlockHasTerminator()) { // Body fell through without producing a tail value // AND without branching to ret_done — this only // happens for bodies whose last stmt is a void // statement (e.g. side-effecting). Slot is // uninitialised on this path; safer to br anyway // so the CFG is well-formed. The load in ret_done // will read uninit, which is the same garbage // behaviour the regular fn-body lowering would // produce for a missing return. self.builder.br(ret_done_bb, &.{}); } self.builder.switchToBlock(ret_done_bb); return self.builder.load(ret_slot, ret_ty); } else { if (self.lowerBlockValue(fd.body)) |val| { return val; } } } else { self.lowerBlock(fd.body); } return self.builder.constInt(0, .void); } /// True if `node` (a fn body) contains any top-level `return` statement. /// Used by inline-comptime lowering to decide whether to allocate a /// result slot — pure tail-expression bodies skip the slot. Walks past /// `if`/`while`/`for`/`match` arms (early-return inside a conditional /// counts) but stops at nested fn/lambda bodies (those have their own /// return contexts). fn fnBodyHasReturn(node: *const Node) bool { return switch (node.data) { .return_stmt => true, .block => |b| blk: { for (b.stmts) |s| if (fnBodyHasReturn(s)) break :blk true; break :blk false; }, .if_expr => |ie| blk: { if (fnBodyHasReturn(ie.then_branch)) break :blk true; if (ie.else_branch) |eb| if (fnBodyHasReturn(eb)) break :blk true; break :blk false; }, .while_expr => |we| fnBodyHasReturn(we.body), .for_expr => |fe| fnBodyHasReturn(fe.body), .match_expr => |me| blk: { for (me.arms) |arm| if (fnBodyHasReturn(arm.body)) break :blk true; break :blk false; }, .defer_stmt => |ds| fnBodyHasReturn(ds.expr), else => false, }; } /// 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. 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 == .s64) { 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 var variadic_idx: ?usize = null; var elem_ty: TypeId = .any; for (fd.params, 0..) |p, i| { if (p.is_variadic) { variadic_idx = i; elem_ty = self.resolveTypeWithBindings(p.type_expr); 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); // 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 == .s64) { const ref_ty = self.builder.getRefType(val); if (ref_ty != .s64 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); } } 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()`. fn buildTypeBindings( self: *Lowering, fd: *const ast.FnDecl, args_ast: []const *const Node, ) std.StringHashMap(TypeId) { var bindings = std.StringHashMap(TypeId).init(self.alloc); const types_passed_explicitly = args_ast.len == fd.params.len; for (fd.type_params) |tp| { var found = false; // Strategy 1: explicit — the param whose name matches `tp.name` IS // the `$T: Type` declaration; the arg at that position is a type expression. if (types_passed_explicitly) { for (fd.params, 0..) |param, pi| { if (std.mem.eql(u8, param.name, tp.name)) { if (pi < args_ast.len and type_bridge.isTypeShapedAstNode(args_ast[pi], &self.module.types)) { const ty = self.resolveTypeArg(args_ast[pi]); bindings.put(tp.name, ty) catch {}; found = true; } break; } } } if (found) continue; // Strategy 2: infer from value params that USE the type param // (e.g. a: $T, b: T, items: []$T). Pick widest type across matches. var inferred_ty: ?TypeId = null; var s2_arg_idx: usize = 0; for (fd.params) |param| { const is_type_decl = isTypeParamDecl(¶m, fd.type_params); defer if (!is_type_decl) { s2_arg_idx += 1; }; if (is_type_decl) { if (types_passed_explicitly) s2_arg_idx += 1; continue; } const matched = self.matchTypeParam(param.type_expr, tp.name); if (matched) { if (s2_arg_idx < args_ast.len) { const arg_ty = self.inferExprType(args_ast[s2_arg_idx]); const extracted = self.extractTypeParam(param.type_expr, arg_ty, tp.name); if (extracted) |ety| { if (inferred_ty) |prev| { if (ety == .f64 and prev != .f64) { inferred_ty = ety; } else if (ety == .f32 and prev != .f64 and prev != .f32) { inferred_ty = ety; } } else { inferred_ty = ety; } } } } } if (inferred_ty) |ty| { bindings.put(tp.name, ty) catch {}; } } return bindings; } /// Mangle a generic call site into "base__Type1_Type2". /// Returns a heap-allocated string owned by self.alloc. fn mangleGenericName( self: *Lowering, base_name: []const u8, fd: *const ast.FnDecl, bindings: *const std.StringHashMap(TypeId), ) []const u8 { 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 ty = bindings.get(tp.name) orelse .s64; const type_name_str = self.mangleTypeName(ty); for (type_name_str) |ch| { if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } } } return self.alloc.dupe(u8, mangled_buf[0..mangled_len]) catch base_name; } /// 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.buildTypeBindings(fd, call_node.args); defer bindings.deinit(); const types_passed_explicitly = call_node.args.len == fd.params.len; const mangled_name = self.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 = self.lowerExpr(type_tag_node orelse return self.emitError("dispatch", call_node.callee.span)); 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) != .s64) { break :blk type_bridge.resolveAstType(rt, &self.module.types); } } } 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_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 runtime_arg_types = std.ArrayList(TypeId).empty; defer runtime_arg_types.deinit(self.alloc); var pack_arg_types = std.ArrayList(TypeId).empty; defer pack_arg_types.deinit(self.alloc); var pack_start: usize = call_node.args.len; var fi: usize = 0; for (fd.params) |p| { if (p.is_variadic and p.is_comptime) { pack_start = fi; break; } if (fi >= call_node.args.len) break; if (!p.is_comptime) { runtime_arg_types.append(self.alloc, self.inferExprType(call_node.args[fi])) catch return self.builder.constInt(0, .void); } // Comptime non-pack: consumed but not added to runtime types. fi += 1; } if (pack_start <= call_node.args.len) { for (call_node.args[pack_start..]) |a| { pack_arg_types.append(self.alloc, self.inferExprType(a)) catch return self.builder.constInt(0, .void); } } // 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 (p.is_variadic and p.is_comptime) 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.appendComptimeValueMangle(&name_buf, call_node.args[ct_fi]); } ct_fi += 1; } 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); } // Lower ONLY runtime args (skip comptime non-pack args; their // values are folded into the mangle, not passed at runtime). var args = std.ArrayList(Ref).empty; defer args.deinit(self.alloc); var ri: usize = 0; for (fd.params) |p| { if (p.is_variadic and p.is_comptime) break; if (ri >= call_node.args.len) break; if (!p.is_comptime) { args.append(self.alloc, self.lowerExpr(call_node.args[ri])) catch return self.builder.constInt(0, .void); } ri += 1; } for (call_node.args[pack_start..]) |a| { args.append(self.alloc, self.lowerExpr(a)) catch return self.builder.constInt(0, .void); } 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); } /// Append a stable mangle segment for a comptime call-arg literal. /// Supports int / bool / float / string literals; non-literals /// degrade to "?" (the mono is still cached but two different /// non-literal expressions sharing one call site would collide, /// which is acceptable since they'd lower the same body anyway). fn appendComptimeValueMangle(self: *Lowering, buf: *std.ArrayList(u8), node: *const Node) void { switch (node.data) { .int_literal => |lit| { var tmp: [32]u8 = undefined; const written = std.fmt.bufPrint(&tmp, "{d}", .{lit.value}) catch return; buf.appendSlice(self.alloc, written) catch return; }, .bool_literal => |lit| { buf.appendSlice(self.alloc, if (lit.value) "true" else "false") catch return; }, .float_literal => |lit| { var tmp: [64]u8 = undefined; const written = std.fmt.bufPrint(&tmp, "{d}", .{lit.value}) catch return; for (written) |c| { buf.append(self.alloc, if (c == '.') '_' else if (c == '-') 'n' else c) catch return; } }, .string_literal => |lit| { // Hash the string to a fixed-length tag — keeps the // mangle short and stable for arbitrary content. var h = std.hash.Wyhash.init(0); h.update(lit.raw); var tmp: [32]u8 = undefined; const written = std.fmt.bufPrint(&tmp, "s{x}", .{h.final()}) catch return; buf.appendSlice(self.alloc, written) catch return; }, else => buf.append(self.alloc, '?') catch return, } } /// 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, ) 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. var pack_name: []const u8 = ""; var pack_param_idx: usize = std.math.maxInt(usize); for (fd.params, 0..) |p, i| { if (p.is_variadic and p.is_comptime) { pack_name = p.name; pack_param_idx = i; 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_iri = self.inline_return_target; const saved_ctx_ref = self.current_ctx_ref; self.func_defer_base = self.defer_stack.items.len; self.block_terminated = false; self.inline_return_target = null; defer { 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.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; self.pack_arg_nodes = pre_pan; self.pack_param_count = pre_ppc; self.pack_arg_types = pre_pat; 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; } 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]; 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; } const pty = self.resolveParamType(&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); 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; self.func_defer_base = self.defer_stack.items.len; self.block_terminated = false; // Install type bindings self.type_bindings = bindings.*; // 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 { 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 (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, "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, "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, "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) — extract Any tag or produce compile-time constant if (c.args.len < 1) return self.builder.constInt(0, .s64); const arg_ty = self.inferExprType(c.args[0]); if (arg_ty == .any) { // Runtime: extract tag field (field 0 of Any {tag: s64, value: s64}) const val = self.lowerExpr(c.args[0]); return self.builder.structGet(val, 0, .s64); } else { // Static: emit type tag as constant return self.builder.constInt(@intCast(@intFromEnum(arg_ty)), .s64); } } 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; } /// 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(node: *const Node) bool { return switch (node.data) { .type_expr, .identifier, .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, => true, else => false, }; } 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 .void; } } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index ${s}[{}] used outside an active pack binding", .{ pi.pack_name, pi.index, }); } return .void; } 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; } if (self.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 .void; }, .type_expr => |te| { if (self.type_alias_map.get(te.name)) |alias_ty| return alias_ty; return type_bridge.resolveAstType(node, &self.module.types); }, .call => |cl| { // Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32)) return self.resolveTypeCallWithBindings(&cl); }, .pointer_type_expr, .many_pointer_type_expr, .array_type_expr, .slice_type_expr, .optional_type_expr, .function_type_expr, .tuple_literal, => return type_bridge.resolveAstType(node, &self.module.types), else => return .s64, } } /// Format a type name for display (e.g. "*Point", "[]s32", "[3]f64"). 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). 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), 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), 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. 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, }; }, else => null, }; } fn mangleTypeName(self: *Lowering, ty: TypeId) []const u8 { // Builtin types 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.mangleTypeName(p.pointee); break :blk std.fmt.allocPrint(self.alloc, "ptr_{s}", .{inner}) catch "pointer"; }, .many_pointer => |p| blk: { const inner = self.mangleTypeName(p.element); break :blk std.fmt.allocPrint(self.alloc, "mptr_{s}", .{inner}) catch "many_pointer"; }, .slice => |s| blk: { const inner = self.mangleTypeName(s.element); break :blk std.fmt.allocPrint(self.alloc, "SL_{s}", .{inner}) catch "slice"; }, .array => |a| blk: { const inner = self.mangleTypeName(a.element); break :blk std.fmt.allocPrint(self.alloc, "AR_{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.mangleTypeName(o.child); break :blk std.fmt.allocPrint(self.alloc, "opt_{s}", .{inner}) catch "optional"; }, .vector => |v| blk: { const inner = self.mangleTypeName(v.element); break :blk std.fmt.allocPrint(self.alloc, "vec_{d}_{s}", .{ v.length, inner }) catch "vector"; }, .closure => |c| self.mangleParamList("cl", c.params, c.ret), .function => |f| self.mangleParamList("fn", f.params, f.ret), .tuple => |t| blk: { var buf = std.ArrayList(u8).empty; buf.appendSlice(self.alloc, "tu") catch break :blk "tuple"; for (t.fields) |fid| { buf.append(self.alloc, '_') catch break :blk "tuple"; buf.appendSlice(self.alloc, self.mangleTypeName(fid)) catch break :blk "tuple"; } break :blk buf.items; }, else => @tagName(info), }; } /// Collect impl entries visible from `current_source_file` — defined in /// the current file or in any module the current file transitively /// imports. Falls open (returns all entries) when the source-file /// context or import graph isn't wired (e.g. comptime callers). fn findVisibleImpls(self: *Lowering, entries: []const ParamImplEntry, out: *std.ArrayList(ParamImplEntry)) void { const here = self.current_source_file orelse { out.appendSlice(self.alloc, entries) catch {}; return; }; const graph = self.import_graph orelse { out.appendSlice(self.alloc, entries) catch {}; return; }; // BFS over the import graph to compute the visible set. var visible = std.StringHashMap(void).init(self.alloc); defer visible.deinit(); visible.put(here, {}) catch {}; var queue = std.ArrayList([]const u8).empty; defer queue.deinit(self.alloc); queue.append(self.alloc, here) catch {}; var head: usize = 0; while (head < queue.items.len) : (head += 1) { const node = queue.items[head]; const direct = graph.get(node) orelse continue; var it = direct.iterator(); while (it.next()) |kv| { const next = kv.key_ptr.*; if (visible.contains(next)) continue; visible.put(next, {}) catch {}; queue.append(self.alloc, next) catch {}; } } for (entries) |e| { if (visible.contains(e.defining_module)) { out.append(self.alloc, e) catch {}; } } } fn mangleParamList(self: *Lowering, prefix: []const u8, params: []const TypeId, ret: TypeId) []const u8 { var buf = std.ArrayList(u8).empty; buf.appendSlice(self.alloc, prefix) catch return prefix; for (params) |p| { buf.append(self.alloc, '_') catch return prefix; buf.appendSlice(self.alloc, self.mangleTypeName(p)) catch return prefix; } buf.appendSlice(self.alloc, "__") catch return prefix; buf.appendSlice(self.alloc, self.mangleTypeName(ret)) catch return prefix; return buf.items; } /// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values. /// Returns a list of TypeId index values that match the category. 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 }; 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 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, }; 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). 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; 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 non-null arm determines the type (same as old behavior) const arm_ty = self.inferExprType(last_node); if (has_null and arm_ty != .void) { return self.module.types.optionalOf(arm_ty); } return arm_ty; } return .void; } 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) ?*ast.Call { if (c.callee.data != .identifier) return null; 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.ufcs_alias_map.get(id_name)) |target| { break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; } break :blk scoped; }; const fd = self.fn_ast_map.get(eff_name) orelse 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) { new_args[i] = fd.params[i].default_expr.?; } 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) []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.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.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.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.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.resolveParamType(&p)) catch unreachable; } return types_list.items; } } // Try generic struct template method: List__Container.append → List.append // with type bindings from the struct instantiation if (self.struct_instance_template.get(sname)) |tmpl_name| { const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch return &.{}; if (self.fn_ast_map.get(tmpl_qualified)) |fd| { if (fd.params.len > 0) { // Temporarily set type_bindings so resolveParamType can substitute T → concrete type const saved_bindings = self.type_bindings; if (self.struct_instance_bindings.getPtr(sname)) |bindings| { self.type_bindings = bindings.*; } var types_list = std.ArrayList(TypeId).empty; for (fd.params[1..]) |p| { types_list.append(self.alloc, self.resolveParamType(&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.ufcs_alias_map.get(bare_name)) |target| { break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; } break :blk scoped; }; // 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.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.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. 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. fn hasComptimeParams(fd: *const ast.FnDecl) bool { for (fd.params) |p| { if (p.is_comptime) return true; } return false; } /// 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). fn isPackFn(fd: *const ast.FnDecl) bool { for (fd.params) |p| { if (p.is_comptime and p.is_variadic) return true; } return false; } /// Creates a temporary function marked `is_comptime = true` that wraps /// the given expression as its return value. Returns the FuncId. pub fn createComptimeFunction(self: *Lowering, prefix: []const u8, expr: *const Node, ret_ty: TypeId) FuncId { var buf: [64]u8 = undefined; const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix; self.comptime_counter += 1; // Save current builder + lowering state. The wrapper fn we're // about to build runs the comptime expression in isolation — // it must NOT inherit the enclosing call's `inline_return_target` // (which would re-route a `return` inside the wrapper into a // slot belonging to a different basic block), pack bindings // (which would substitute caller's `args` inside the wrapper), // or comptime-param bindings (which would substitute caller's // `$fmt` inside the wrapper's #insert children). Without these // saves, nested comptime calls leak outer state into the // interp-executed wrapper, producing garbage stores (issue-0046 // face 1 — storeAtRawPtr null). const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const saved_scope = self.scope; const saved_ctx_ref = self.current_ctx_ref; const saved_iri = self.inline_return_target; const saved_pan = self.pack_arg_nodes; const saved_ppc = self.pack_param_count; const saved_pat = self.pack_arg_types; const saved_cpn = self.comptime_param_nodes; const saved_block_terminated = self.block_terminated; const saved_target_type = self.target_type; const saved_func_defer_base = self.func_defer_base; self.inline_return_target = null; self.pack_arg_nodes = null; self.pack_param_count = null; self.pack_arg_types = null; self.comptime_param_nodes = null; self.block_terminated = false; self.target_type = null; self.func_defer_base = self.defer_stack.items.len; defer { self.current_ctx_ref = saved_ctx_ref; self.inline_return_target = saved_iri; self.pack_arg_nodes = saved_pan; self.pack_param_count = saved_ppc; self.pack_arg_types = saved_pat; self.comptime_param_nodes = saved_cpn; self.block_terminated = saved_block_terminated; self.target_type = saved_target_type; self.func_defer_base = saved_func_defer_base; } // Build params: implicit `__sx_ctx` at slot 0 when the program // uses Context (so the body's `context.X` reads + transitive calls // resolve cleanly). The comptime function's top-level invocation // supplies `&__sx_default_context` (interp via callWithDefaultContext; // codegen via the comptime-eval glue in emit_llvm). const wants_ctx = self.implicit_ctx_enabled; const params_slice = blk: { if (!wants_ctx) break :blk &[_]Function.Param{}; const owned = self.alloc.alloc(Function.Param, 1) catch break :blk &[_]Function.Param{}; owned[0] = .{ .name = self.module.types.internString("__sx_ctx"), .ty = self.module.types.ptrTo(.void), }; break :blk owned; }; // Create the comptime function const name_id = self.module.types.internString(name); const func_id = self.builder.beginFunction(name_id, params_slice, ret_ty); // Mark as comptime + has_implicit_ctx const fn_mut = self.module.getFunctionMut(func_id); fn_mut.is_comptime = true; fn_mut.has_implicit_ctx = wants_ctx; // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); // Create a scope that chains to the enclosing scope (so the // expression can reference names visible at the #run site). var ct_scope = Scope.init(self.alloc, saved_scope); self.scope = &ct_scope; // Lower the expression and return it const result = self.lowerExpr(expr); if (ret_ty == .void) { self.builder.retVoid(); } else { self.builder.ret(result, ret_ty); } self.builder.finalize(); // Restore builder state self.scope = saved_scope; ct_scope.deinit(); self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return func_id; } // ── Block helpers ─────────────────────────────────────────────── fn freshBlock(self: *Lowering, prefix: []const u8) BlockId { return self.freshBlockWithParams(prefix, &.{}); } fn freshBlockWithParams(self: *Lowering, prefix: []const u8, params: []const TypeId) BlockId { var buf: [64]u8 = undefined; const name = std.fmt.bufPrint(&buf, "{s}.{d}", .{ prefix, self.block_counter }) catch prefix; self.block_counter += 1; const name_id = self.module.types.internString(name); return self.builder.appendBlock(name_id, params); } fn currentBlockHasTerminator(self: *Lowering) bool { const func = self.builder.module.getFunctionMut(self.builder.func.?); const block_idx = self.builder.current_block orelse return true; const block = &func.blocks.items[block_idx.index()]; if (block.insts.items.len > 0) { const last_op = block.insts.items[block.insts.items.len - 1].op; return switch (last_op) { .ret, .ret_void, .br, .cond_br, .switch_br, .@"unreachable" => true, else => false, }; } return false; } // ── Type resolution ───────────────────────────────────────────── // Delegates to type_bridge for full AST type node resolution. fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId { if (fd.return_type) |rt| { return self.resolveTypeWithBindings(rt); } // Arrow functions without explicit return type: infer from body expression if (fd.is_arrow) { return self.inferExprType(fd.body); } // No annotation, 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, }; } fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId { const elem_ty = self.resolveTypeWithBindings(p.type_expr); if (p.is_variadic) { // Variadic param (..T) → receives a []T slice return self.module.types.sliceOf(elem_ty); } return elem_ty; } fn resolveType(self: *Lowering, type_ann: *const Node) TypeId { return self.resolveTypeWithBindings(type_ann); } /// Resolve a type node, checking type_bindings first for generic type params. 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 emit a diagnostic; no // binding → bail with a diagnostic-friendly placeholder. 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 .s64; } } 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 .s64; } // `*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); } } } if (self.type_bindings) |tb| { switch (node.data) { .type_expr => |te| { // Check bindings for any type_expr name — not just those // marked is_generic. The return type `T` in `-> T` may // not have the `$` prefix, so is_generic is false, but // it still refers to the type param. if (tb.get(te.name)) |ty| return ty; }, .identifier => |id| { if (tb.get(id.name)) |ty| return ty; }, // Compound types: resolve inner types with bindings .slice_type_expr => |st| { const elem = self.resolveTypeWithBindings(st.element_type); return self.module.types.sliceOf(elem); }, .pointer_type_expr => |pt| { const pointee = self.resolveTypeWithBindings(pt.pointee_type); return self.module.types.ptrTo(pointee); }, .many_pointer_type_expr => |mp| { const elem = self.resolveTypeWithBindings(mp.element_type); return self.module.types.manyPtrTo(elem); }, .optional_type_expr => |ot| { const child = self.resolveTypeWithBindings(ot.inner_type); return self.module.types.optionalOf(child); }, .array_type_expr => |at| { const elem = self.resolveTypeWithBindings(at.element_type); const len: u32 = blk: { if (at.length.data == .int_literal) break :blk @intCast(at.length.data.int_literal.value); break :blk 0; }; return self.module.types.arrayOf(elem, len); }, .parameterized_type_expr => |pt| { return self.resolveParameterizedWithBindings(&pt); }, .call => |cl| { // Handle List(T), Vector(N, T) etc. as type constructor calls return self.resolveTypeCallWithBindings(&cl); }, .closure_type_expr => |ct| { return self.resolveClosureTypeWithBindings(&ct); }, .function_type_expr => |ft| { return self.resolveFunctionTypeWithBindings(&ft); }, else => {}, } } // 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); } if (node.data == .call) { return self.resolveTypeCallWithBindings(&node.data.call); } // Handle compound types that may contain generic structs (e.g., *List(ViewChild)) // These need the lowerer's resolveType to properly instantiate generics. switch (node.data) { .pointer_type_expr => |pt| { const pointee = self.resolveTypeWithBindings(pt.pointee_type); return self.module.types.ptrTo(pointee); }, .slice_type_expr => |st| { const elem = self.resolveTypeWithBindings(st.element_type); return self.module.types.sliceOf(elem); }, .many_pointer_type_expr => |mp| { const elem = self.resolveTypeWithBindings(mp.element_type); return self.module.types.manyPtrTo(elem); }, .optional_type_expr => |ot| { const child = self.resolveTypeWithBindings(ot.inner_type); return self.module.types.optionalOf(child); }, .array_type_expr => |at| { const elem = self.resolveTypeWithBindings(at.element_type); const len: u32 = if (at.length.data == .int_literal) @intCast(at.length.data.int_literal.value) else 0; return self.module.types.arrayOf(elem, len); }, .closure_type_expr => |ct| { return self.resolveClosureTypeWithBindings(&ct); }, .function_type_expr => |ft| { return self.resolveFunctionTypeWithBindings(&ft); }, else => {}, } // Check type aliases before falling through to type_bridge if (node.data == .type_expr) { if (self.type_alias_map.get(node.data.type_expr.name)) |alias_ty| return alias_ty; } return type_bridge.resolveAstType(node, &self.module.types); } /// Resolve a `Closure(...)` type expression with the active type/pack /// bindings applied. Pack-shaped closure exprs (`Closure(Prefix..., ..$pack)`) /// substitute `pack` from `self.pack_bindings`, producing a concrete /// closure type — used when monomorphising a pack-variadic impl body /// against a concrete source signature. fn resolveClosureTypeWithBindings(self: *Lowering, ct: *const ast.ClosureTypeExpr) TypeId { var param_ids = std.ArrayList(TypeId).empty; defer param_ids.deinit(self.alloc); for (ct.param_types) |pt| { param_ids.append(self.alloc, self.resolveTypeWithBindings(pt)) catch return .void; } if (ct.pack_name) |pn| { if (self.pack_bindings) |pb| { if (pb.get(pn)) |pack_tys| { for (pack_tys) |t| param_ids.append(self.alloc, t) catch return .void; // Fully bound — emit a concrete closure type, no pack_start. const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void; return self.module.types.closureType(param_ids.items, ret_ty); } } // Pack name in scope but no binding — preserve the pack-shape // so downstream code can still see it's variadic. (Hit during // impl-block parsing before any concrete monomorphisation.) const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void; return self.module.types.closureTypePack(param_ids.items, ret_ty, @intCast(param_ids.items.len)); } const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void; return self.module.types.closureType(param_ids.items, ret_ty); } /// Resolve a `(Params...) -> Ret` function type expression with the /// active type/pack bindings applied. Mirrors /// `resolveClosureTypeWithBindings` but for `function_type_expr`. /// Unlocks `$args[$i]` in fn-pointer type literals like /// `fp : (*void, $args[0]) -> $args[1] = ...` — used in step 5's /// generic trampoline body. fn resolveFunctionTypeWithBindings(self: *Lowering, ft: *const ast.FunctionTypeExpr) TypeId { var param_ids = std.ArrayList(TypeId).empty; defer param_ids.deinit(self.alloc); for (ft.param_types) |pt| { param_ids.append(self.alloc, self.resolveTypeWithBindings(pt)) catch return .void; } const ret_ty = if (ft.return_type) |rt| self.resolveTypeWithBindings(rt) else .void; const cc: types.TypeInfo.CallConv = switch (ft.call_conv) { .default => .default, .c => .c, }; return self.module.types.functionTypeCC(param_ids.items, ret_ty, cc); } /// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)). fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId { const callee_name: []const u8 = switch (cl.callee.data) { .identifier => |id| id.name, .field_access => |fa| fa.field, else => return .s64, }; // Built-in: Vector(N, T) if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) { const length: u32 = switch (cl.args[0].data) { .int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))), .identifier => |id| blk: { if (self.comptime_value_bindings) |cvb| { if (cvb.get(id.name)) |v| break :blk @intCast(@as(u64, @bitCast(v))); } break :blk 0; }, else => 0, }; const elem = self.resolveTypeWithBindings(cl.args[1]); return self.module.types.vectorOf(elem, length); } // User-defined generic struct if (self.struct_template_map.getPtr(callee_name)) |tmpl| { return self.instantiateGenericStruct(tmpl, cl.args); } // 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.fn_ast_map.get(resolved_name)) |fd| { if (fd.type_params.len > 0) { 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 .s64; } /// Resolve a parameterized type expr, substituting bindings for type/value params. /// Handles both built-in types (Vector) and user-defined generic structs. fn resolveParameterizedWithBindings(self: *Lowering, pt: *const ast.ParameterizedTypeExpr) TypeId { const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name; const table = &self.module.types; // Vector(N, T) — built-in parameterized type if (std.mem.eql(u8, base_name, "Vector")) { if (pt.args.len == 2) { // Resolve length: literal, or bound comptime value const length: u32 = switch (pt.args[0].data) { .int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))), .identifier => |id| blk: { if (self.comptime_value_bindings) |cvb| { if (cvb.get(id.name)) |v| break :blk @intCast(@as(u64, @bitCast(v))); } break :blk 0; }, .type_expr => |te| blk: { if (self.comptime_value_bindings) |cvb| { if (cvb.get(te.name)) |v| break :blk @intCast(@as(u64, @bitCast(v))); } break :blk 0; }, else => 0, }; // Resolve element type through bindings const elem = self.resolveTypeWithBindings(pt.args[1]); return table.vectorOf(elem, length); } } // User-defined generic struct: look up template and instantiate if (self.struct_template_map.getPtr(base_name)) |tmpl| { return self.instantiateGenericStruct(tmpl, pt.args); } // Fallback: register as named type placeholder const name_id = table.internString(pt.name); return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); } /// Instantiate a generic struct template with concrete args. /// E.g., Vec(3, f32) → struct Vec__3_f32 { data: Vector(3, f32) } 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 {}; // Bind type params to args and build name suffix 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); for (tmpl.type_params, 0..) |tp, i| { if (i >= args.len) break; 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) — extract integer const val: i64 = switch (args[i].data) { .int_literal => |lit| lit.value, else => 0, }; 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) { return existing; } } // Set up bindings and resolve fields self.type_bindings = tb; self.comptime_value_bindings = cvb; 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; } // Restore bindings self.type_bindings = saved_type_bindings; self.comptime_value_bindings = saved_value_bindings; // 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.update(id, info); // Store the type bindings and template name for method resolution 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 {}; 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. 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 { const val: i64 = switch (args[i].data) { .int_literal => |lit| lit.value, else => 0, }; 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; } // 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.update(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.update(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); } return null; } /// 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.update(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.update(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. fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl) 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 owned_name = self.alloc.dupe(u8, sd.name) catch return; // Build owned type_params const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return; for (sd.type_params, 0..) |tp, i| { tps[i] = .{ .name = self.alloc.dupe(u8, tp.name) catch return, // $T: Type, $T: Lerpable, $T: Type/Eq — all are type params // Only value params like $N: u32 are non-type .is_type_param = 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.protocol_decl_map.contains(cname) or self.protocol_ast_map.contains(cname); } else false, }; } // Copy field names const fnames = self.alloc.alloc([]const u8, sd.field_names.len) catch return; for (sd.field_names, 0..) |fn_str, i| { fnames[i] = self.alloc.dupe(u8, fn_str) catch return; } // Field type nodes: these 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; self.struct_template_map.put(owned_name, .{ .name = owned_name, .type_params = tps, .field_names = fnames, .field_type_nodes = ftype_nodes, }) 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.fn_ast_map.put(qualified, method_fd) catch {}; } } return; } // 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); } } } // Check if a forward-reference placeholder already exists (with empty fields) // If so, update it in-place rather than creating a duplicate 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.update(id, info); // 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.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) else null; self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {}; } } } /// 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.update(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.update(f.ty, .{ .@"struct" = .{ .name = sq_id, .fields = finfo.@"struct".fields } }); } } } } table.update(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.update(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.update(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. fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void { // Parameterised protocols are compile-time-only — no vtable, no boxed // instance struct. Methods reference unbound type params (e.g. // `convert :: () -> Target`) that only get a concrete TypeId per // (Source, Target) pair at xx resolution time. Stash the AST so // `param_impl_map` lookup can resolve method signatures lazily. if (pd.type_params.len > 0) { self.protocol_ast_map.put(pd.name, pd) catch {}; return; } const table = &self.module.types; const name_id = table.internString(pd.name); var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; // First field: ctx: *void const void_ptr_ty = table.ptrTo(.void); fields.append(self.alloc, .{ .name = table.internString("ctx"), .ty = void_ptr_ty, }) catch unreachable; if (pd.is_inline) { // One fn-ptr field per protocol method for (pd.methods) |method| { fields.append(self.alloc, .{ .name = table.internString(method.name), .ty = void_ptr_ty, // fn ptrs are opaque pointers }) catch unreachable; } } else { // Vtable pointer 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.update(id, struct_info); // Build protocol method info for dispatch var method_infos = std.ArrayList(ProtocolMethodInfo).empty; for (pd.methods) |method| { var ptypes = std.ArrayList(TypeId).empty; for (method.params) |p| { // Resolve param type; Self → *void for protocol context. // Type aliases (e.g. `ShaderHandle :: u32`) need to be // resolved through type_alias_map before falling through // to type_bridge — otherwise they're treated as named // empty structs and the LLVM call gets `{}` parameters. const pty = blk: { if (p.data == .type_expr) { if (std.mem.eql(u8, p.data.type_expr.name, "Self")) { break :blk void_ptr_ty; } if (self.type_alias_map.get(p.data.type_expr.name)) |aliased| { break :blk aliased; } } break :blk type_bridge.resolveAstType(p, table); }; 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) { if (std.mem.eql(u8, rt.data.type_expr.name, "Self")) { ret_is_self = true; break :blk void_ptr_ty; } if (self.type_alias_map.get(rt.data.type_expr.name)) |aliased| { break :blk aliased; } } break :blk type_bridge.resolveAstType(rt, table); } 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.protocol_decl_map.put(pd.name, .{ .name = pd.name, .is_inline = pd.is_inline, .methods = self.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable, }) catch {}; self.protocol_ast_map.put(pd.name, pd) catch {}; // For vtable protocols, create the vtable struct type if (!pd.is_inline) { var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (pd.methods) |method| { vtable_fields.append(self.alloc, .{ .name = table.internString(method.name), .ty = void_ptr_ty, }) catch unreachable; } var vtable_name_buf: [128]u8 = undefined; const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{pd.name}) catch "__Vtable"; const vtable_name_id = table.internString(vtable_name); const vtable_info: types.TypeInfo = .{ .@"struct" = .{ .name = vtable_name_id, .fields = vtable_fields.items } }; const vtable_ty = table.intern(vtable_info); self.protocol_vtable_type_map.put(pd.name, vtable_ty) catch {}; } } /// 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). fn registerForeignClassDecl(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { self.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.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.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.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.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. 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. 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. 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.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); } } } /// Register an impl block: register its methods as TypeName.method in fn_ast_map. fn registerImplBlock(self: *Lowering, ib: *const ast.ImplBlock, is_imported: bool, decl: *const Node) void { // Parameterised-protocol impl (e.g. `impl Into(Block) for Closure() -> void`): // record into `param_impl_map` for compile-time resolution by `lowerXX`. // Methods are NOT registered in fn_ast_map — they're monomorphised lazily // per (Source, Target) pair at the xx call site. if (ib.protocol_type_args.len > 0) { self.registerParamImpl(ib, decl); return; } // Collect explicitly implemented method names var impl_methods = std.StringHashMap(void).init(self.alloc); defer impl_methods.deinit(); for (ib.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}", .{ ib.target_type, method_fd.name }) catch continue; self.fn_ast_map.put(qualified, method_fd) catch {}; self.import_flags.put(qualified, is_imported) catch {}; self.declareFunction(method_fd, qualified); impl_methods.put(method_fd.name, {}) catch {}; } } // Synthesize default methods from protocol declaration if (self.protocol_ast_map.get(ib.protocol_name)) |pd| { for (pd.methods) |method| { if (method.default_body != null and !impl_methods.contains(method.name)) { // Create a synthesized fn_decl for the default method const synth_fd = self.synthesizeDefaultMethod(method, ib.target_type); const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ib.target_type, method.name }) catch continue; self.fn_ast_map.put(qualified, synth_fd) catch {}; self.import_flags.put(qualified, is_imported) catch {}; self.declareFunction(synth_fd, qualified); } } } } /// Register a parameterised-protocol impl into `param_impl_map`. /// Resolves the protocol's type args + the source type, mangles them, and /// stashes the impl's method fn_decls for later monomorphisation by /// `lowerXX`. Same-module duplicate impls produce a diagnostic here; /// cross-module duplicates are detected at the xx resolution site. /// /// Pack-shaped sources (`Closure(..$args) -> $R`, detected via /// `pack_start != null`) are additionally registered into /// `param_impl_pack_map` keyed without the source suffix — the matching /// site walks that map to bind packs against any concrete closure shape. fn registerParamImpl(self: *Lowering, ib: *const ast.ImplBlock, decl: *const Node) void { const table = &self.module.types; // Resolve the protocol's type-arg list to concrete TypeIds. var arg_tys = std.ArrayList(TypeId).empty; for (ib.protocol_type_args) |arg_node| { const t = type_bridge.resolveAstType(arg_node, table); arg_tys.append(self.alloc, t) catch return; } // Resolve the source type. Parser stores it on `target_type_expr` for // parameterised impls (back-compat `target_type` string is kept for // simple cases but the canonical form is the TypeExpr). const src_ty: TypeId = if (ib.target_type_expr) |te| type_bridge.resolveAstType(te, table) else if (ib.target_type.len > 0) type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table) else return; // Mangle into the lookup key. var key_buf = std.ArrayList(u8).empty; key_buf.appendSlice(self.alloc, ib.protocol_name) catch return; for (arg_tys.items) |t| { key_buf.append(self.alloc, 0) catch return; key_buf.appendSlice(self.alloc, self.mangleTypeName(t)) catch return; } const pack_key_len = key_buf.items.len; // proto + args, no src — used for pack map key_buf.append(self.alloc, 0) catch return; key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return; const key = key_buf.items; // Collect method fn_decl pointers. var methods = std.ArrayList(*const ast.FnDecl).empty; for (ib.methods) |method_node| { if (method_node.data == .fn_decl) { methods.append(self.alloc, &method_node.data.fn_decl) catch {}; } } const defining_module: []const u8 = self.current_source_file orelse ""; const entry: ParamImplEntry = .{ .methods = self.alloc.dupe(*const ast.FnDecl, methods.items) catch return, .source_ty = src_ty, .target_args = self.alloc.dupe(TypeId, arg_tys.items) catch return, .defining_module = defining_module, .span = decl.span, }; const gop = self.param_impl_map.getOrPut(key) catch return; if (!gop.found_existing) { gop.value_ptr.* = std.ArrayList(ParamImplEntry).empty; } else { // Same-file duplicate is an immediate error. Cross-file overlaps // are deferred to the xx resolution site (Phase 5) so the impl // surface can be richer than any one file's view. for (gop.value_ptr.items) |existing| { if (std.mem.eql(u8, existing.defining_module, defining_module)) { if (self.diagnostics) |diags| { diags.addFmt(.err, decl.span, "duplicate impl '{s}' for source '{s}' in {s}", .{ ib.protocol_name, self.mangleTypeName(src_ty), defining_module, }); } return; } } } gop.value_ptr.append(self.alloc, entry) catch return; // Pack-shaped source: also register in the pack map. The source // closure carries `pack_start` set; matching binds the source's // tail param types to the pack-name and the source's return to // the impl's return-type-var (when the return is generic). const src_info = table.get(src_ty); if (src_info == .closure and src_info.closure.pack_start != null) { const target_expr_node = ib.target_type_expr orelse return; if (target_expr_node.data != .closure_type_expr) return; const ct = target_expr_node.data.closure_type_expr; const pack_var = ct.pack_name orelse return; // Extract the return-type-var name if the impl's return is generic. // `Closure(...) -> $R` parses with the return-type node carrying // `is_generic = true`. Concrete returns leave it null. var ret_var: ?[]const u8 = null; if (ct.return_type) |rt| { if (rt.data == .type_expr and rt.data.type_expr.is_generic) { ret_var = rt.data.type_expr.name; } } const pack_entry: PackParamImplEntry = .{ .methods = self.alloc.dupe(*const ast.FnDecl, methods.items) catch return, .source_pack_ty = src_ty, .target_args = self.alloc.dupe(TypeId, arg_tys.items) catch return, .defining_module = defining_module, .span = decl.span, .pack_var_name = self.alloc.dupe(u8, pack_var) catch return, .ret_var_name = if (ret_var) |rv| (self.alloc.dupe(u8, rv) catch return) else null, }; const pack_key = key_buf.items[0..pack_key_len]; const pack_key_owned = self.alloc.dupe(u8, pack_key) catch return; const pgop = self.param_impl_pack_map.getOrPut(pack_key_owned) catch return; if (!pgop.found_existing) { pgop.value_ptr.* = std.ArrayList(PackParamImplEntry).empty; } else { for (pgop.value_ptr.items) |existing| { if (std.mem.eql(u8, existing.defining_module, defining_module)) { if (self.diagnostics) |diags| { diags.addFmt(.err, decl.span, "duplicate pack impl '{s}' for source '{s}' in {s}", .{ ib.protocol_name, self.mangleTypeName(src_ty), defining_module, }); } return; } } } pgop.value_ptr.append(self.alloc, pack_entry) catch return; } } /// Synthesize a fn_decl from a protocol default method for a concrete type. fn synthesizeDefaultMethod(self: *Lowering, method: ast.ProtocolMethodDecl, target_type: []const u8) *const ast.FnDecl { // Build parameter list: self: *TargetType, then the protocol method params var params_list = std.ArrayList(ast.Param).empty; defer params_list.deinit(self.alloc); // Add self parameter: self: *TargetType const self_type_node = self.alloc.create(ast.Node) catch unreachable; const pointee_node = self.alloc.create(ast.Node) catch unreachable; pointee_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = target_type } } }; self_type_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = pointee_node, } } }; params_list.append(self.alloc, .{ .name = "self", .name_span = .{ .start = 0, .end = 0 }, .type_expr = self_type_node, }) catch unreachable; // Add remaining params from the protocol method for (method.params, method.param_names) |pty, pname| { params_list.append(self.alloc, .{ .name = pname, .name_span = .{ .start = 0, .end = 0 }, .type_expr = pty, }) catch unreachable; } const fd = self.alloc.create(ast.FnDecl) catch unreachable; fd.* = .{ .name = method.name, .params = self.alloc.dupe(ast.Param, params_list.items) catch unreachable, .body = method.default_body.?, .return_type = method.return_type, }; return fd; } // ── Protocol dispatch ────────────────────────────────────────── /// Check if a type name is a registered protocol. fn isProtocolType(self: *Lowering, type_name: []const u8) bool { return self.protocol_decl_map.contains(type_name); } /// Get protocol info for a TypeId (if it's a protocol type). fn getProtocolInfo(self: *Lowering, ty: TypeId) ?ProtocolDeclInfo { if (ty.isBuiltin()) return null; const info = self.module.types.get(ty); if (info != .@"struct") return null; const name = self.module.types.getString(info.@"struct".name); return self.protocol_decl_map.get(name); } /// 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; const pd = self.protocol_decl_map.get(proto_name) orelse return &.{}; var thunk_ids = std.ArrayList(FuncId).empty; defer thunk_ids.deinit(self.alloc); for (pd.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. fn emitDefaultContextGlobal(self: *Lowering) void { 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.global_names.put(global_name, .{ .id = gid, .ty = ctx_ty }) catch {}; } /// 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.fn_ast_map.contains(qualified) and !self.lowered_functions.contains(qualified)) { self.lazyLowerFunction(qualified); } // 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.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. 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). fn inferExprType(self: *Lowering, node: *const Node) TypeId { return switch (node.data) { .string_literal => .string, .int_literal => .s64, .float_literal => .f64, .bool_literal => .bool, .null_literal => .void, .binary_op => |bop| switch (bop.op) { .eq, .neq, .lt, .lte, .gt, .gte, .and_op, .or_op => .bool, else => self.inferExprType(bop.lhs), }, .unary_op => |uop| switch (uop.op) { .not => .bool, .negate => self.inferExprType(uop.operand), .xx => self.target_type orelse .s64, .address_of => blk: { const inner = self.inferExprType(uop.operand); break :blk self.module.types.ptrTo(inner); }, else => .s64, }, .if_expr => |ie| { // If-else: infer from then branch if (ie.else_branch != null) { return self.inferExprType(ie.then_branch); } return .void; }, .block => |blk| { // Block type is the type of the last expression if (blk.stmts.len > 0) { return self.inferExprType(blk.stmts[blk.stmts.len - 1]); } return .void; }, .call => |c| { if (c.callee.data == .identifier) { const bare_name = c.callee.data.identifier.name; // Resolve local function name (bare → mangled) and UFCS aliases const name = blk: { const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name; if (self.ufcs_alias_map.get(bare_name)) |target| { break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; } break :blk scoped; }; if (resolveBuiltin(bare_name)) |bid| { return switch (bid) { .sqrt, .sin, .cos, .floor => blk: { 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; }, .size_of, .align_of => .s64, .cast => if (c.args.len > 0) self.resolveTypeArg(c.args[0]) else .s64, else => .s64, }; } // Check if it's a generic function — infer return type via type bindings if (self.fn_ast_map.get(name)) |fd| { if (fd.type_params.len > 0) { return self.inferGenericReturnType(fd, &c); } } // Check declared functions for return type if (self.resolveFuncByName(name)) |fid| { return self.module.functions.items[@intFromEnum(fid)].ret; } // Check if callee is a local closure variable — extract return type if (self.scope) |scope| { if (scope.lookup(bare_name)) |binding| { if (!binding.ty.isBuiltin()) { const ti = self.module.types.get(binding.ty); if (ti == .closure) return ti.closure.ret; } } } } else if (c.callee.data == .field_access) { const cfa = c.callee.data.field_access; // Check if receiver is a protocol type → return protocol method type const recv_ty = self.inferExprType(cfa.object); { if (self.getProtocolInfo(recv_ty)) |proto_info| { for (proto_info.methods) |m| { if (std.mem.eql(u8, m.name, cfa.field)) return m.ret_type; } } } // Foreign-class instance method: look up the method's // declared return type so chained calls (e.g. // `UIWindow.alloc().initWithWindowScene(scene)`) resolve. { var recv_inner = recv_ty; if (!recv_inner.isBuiltin()) { const ri = self.module.types.get(recv_inner); if (ri == .pointer) recv_inner = ri.pointer.pointee; } if (!recv_inner.isBuiltin()) { const inner_info = self.module.types.get(recv_inner); if (inner_info == .@"struct") { const sn = self.module.types.getString(inner_info.@"struct".name); if (self.foreign_class_map.get(sn)) |fcd| { for (fcd.members) |m| switch (m) { .method => |md| if (!md.is_static and std.mem.eql(u8, md.name, cfa.field)) { return self.resolveForeignMethodReturnType(fcd, md); }, else => {}, }; } } } } // Instance method call: obj.method(args) → look up StructName.method { var obj_ty = recv_ty; if (!obj_ty.isBuiltin()) { const oi = self.module.types.get(obj_ty); if (oi == .pointer) obj_ty = oi.pointer.pointee; } if (!obj_ty.isBuiltin()) { const oi = self.module.types.get(obj_ty); if (oi == .@"struct") { const struct_name = self.module.types.getString(oi.@"struct".name); const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field; // Generic #compiler method dispatch — return type from declaration if (self.fn_ast_map.get(qualified)) |method_fd| { if (method_fd.body.data == .compiler_expr) { if (method_fd.return_type) |rt| return type_bridge.resolveAstType(rt, &self.module.types); return .void; } } if (self.resolveFuncByName(qualified)) |fid| { return self.module.functions.items[@intFromEnum(fid)].ret; } } } } // Type.variant(args) — qualified enum construction const type_name = switch (cfa.object.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => null, }; if (type_name) |tn| { // Foreign-class static method: `Alias.static_method(args)`. if (self.foreign_class_map.get(tn)) |fcd| { for (fcd.members) |m| switch (m) { .method => |md| if (md.is_static and std.mem.eql(u8, md.name, cfa.field)) { return self.resolveForeignMethodReturnType(fcd, md); }, else => {}, }; } const type_name_id = self.module.types.internString(tn); if (self.module.types.findByName(type_name_id)) |ty| { const ti = self.module.types.get(ty); if (ti == .tagged_union or ti == .@"enum") return ty; } // Check for qualified function call const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tn, cfa.field }) catch cfa.field; if (self.resolveFuncByName(qualified)) |fid| { return self.module.functions.items[@intFromEnum(fid)].ret; } } } else if (c.callee.data == .enum_literal) { // .Variant(args) — dot-shorthand enum construction return self.target_type orelse .s64; } return .s64; }, .field_access => |fa| { // Pack-arity intercept: `.len` is s64. Mirrors // the lowerFieldAccess intercept so AST-level type // inference picks the same shape. if (self.pack_param_count) |ppc| { if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) { if (ppc.contains(fa.object.data.identifier.name)) return .s64; } } // M1.3 — `obj.class` on an Obj-C-class pointer returns Class (*void). if (std.mem.eql(u8, fa.field, "class")) { if (self.isObjcClassPointer(self.inferExprType(fa.object))) { return self.module.types.ptrTo(.void); } } // M2.2 — `obj.field` for an Obj-C `#property` field returns the field's type. if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| { return self.resolveType(prop.field_type); } // M1.2 A.3 — sx-defined class state field returns the field's type. if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| { return info.field_ty; } var obj_ty = self.inferExprType(fa.object); // Auto-deref: if object is a pointer, resolve through it (matches lowerFieldAccess behavior) if (!obj_ty.isBuiltin()) { const ptr_info = self.module.types.get(obj_ty); if (ptr_info == .pointer) { obj_ty = ptr_info.pointer.pointee; } } // Optional chaining: ?T.field → ?FieldType (flattened if field is already optional) const is_opt_chain = fa.is_optional; if (is_opt_chain and !obj_ty.isBuiltin()) { const opt_info = self.module.types.get(obj_ty); if (opt_info == .optional) { obj_ty = opt_info.optional.child; } } if (std.mem.eql(u8, fa.field, "len")) return if (is_opt_chain) self.module.types.optionalOf(.s64) else .s64; if (std.mem.eql(u8, fa.field, "ptr")) { // .ptr on slice/string → [*]element_type const elem_ty = self.getElementType(obj_ty); const mp_ty = self.module.types.manyPtrTo(elem_ty); return if (is_opt_chain) self.module.types.optionalOf(mp_ty) else mp_ty; } if (!obj_ty.isBuiltin()) { const field_name_id = self.module.types.internString(fa.field); // Check union fields (tagged enum payloads) + promoted struct fields const info = self.module.types.get(obj_ty); const u_fields2: ?[]const types.TypeInfo.StructInfo.Field = switch (info) { .@"union" => |u| u.fields, .tagged_union => |u| u.fields, else => null, }; if (u_fields2) |ufields| { for (ufields) |f| { if (f.name == field_name_id) return if (is_opt_chain) self.optionalOfFlattened(f.ty) else 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 if (is_opt_chain) self.optionalOfFlattened(sf.ty) else sf.ty; } } } } } // Check vector element access (.x/.y/.z/.w) if (info == .vector) { const elem = info.vector.element; return if (is_opt_chain) self.optionalOfFlattened(elem) else elem; } // Check struct fields const fields = self.getStructFields(obj_ty); for (fields) |f| { if (f.name == field_name_id) return if (is_opt_chain) self.optionalOfFlattened(f.ty) else f.ty; } } return .s64; }, .identifier => |id| { if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { return binding.ty; } } // `context` is the implicit-ctx identifier; type is Context // when the program has registered it (i.e. std.sx imported). if (self.implicit_ctx_enabled and std.mem.eql(u8, id.name, "context")) { if (self.module.types.findByName(self.module.types.internString("Context"))) |ty| return ty; } // Check global variables (e.g., `context : Context`) if (self.global_names.get(id.name)) |gi| { return gi.ty; } // Check module-level value constants (e.g., WIDTH :f32: 800) if (self.module_const_map.get(id.name)) |ci| { return ci.ty; } return .s64; }, .type_expr => |te| { // type_expr can also be a variable reference (e.g., "s1" matches builtin s1 type) if (self.scope) |scope| { if (scope.lookup(te.name)) |binding| { return binding.ty; } } return .s64; }, .enum_literal => { // Enum literals depend on context — use target_type if available return self.target_type orelse .s64; }, .struct_literal => |sl| { if (sl.struct_name) |name| { const name_id = self.module.types.internString(name); return self.module.types.findByName(name_id) orelse self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); } return self.target_type orelse .s64; }, .tuple_literal => |tl| { var field_types = std.ArrayList(TypeId).empty; defer field_types.deinit(self.alloc); for (tl.elements) |elem| { field_types.append(self.alloc, self.inferExprType(elem.value)) catch unreachable; } return self.module.types.intern(.{ .tuple = .{ .fields = self.alloc.dupe(TypeId, field_types.items) catch unreachable, .names = null, } }); }, .index_expr => |ie| { // Pack-arg type lookup: `[]`. // Read directly from `pack_arg_types` — bypasses the // synthesized-ident detour in `pack_arg_nodes` which // would otherwise lose the type when the mono's // scope isn't set up yet (generic-`$R` pre-inference). if (self.pack_arg_types) |pat| { if (ie.object.data == .identifier and ie.index.data == .int_literal) { if (pat.get(ie.object.data.identifier.name)) |arg_tys| { const raw: i64 = ie.index.data.int_literal.value; if (raw >= 0) { const i: usize = @intCast(raw); if (i < arg_tys.len) return arg_tys[i]; } } } } if (self.packArgNodeAt(&ie)) |arg_node| { return self.inferExprType(arg_node); } const obj_ty = self.inferExprType(ie.object); return self.getElementType(obj_ty); }, .slice_expr => |se| { const obj_ty = self.inferExprType(se.object); if (obj_ty == .string) return .string; return self.module.types.sliceOf(self.getElementType(obj_ty)); }, .deref_expr => |de| { const ptr_ty = self.inferExprType(de.operand); if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) return info.pointer.pointee; } return .s64; }, .chained_comparison => .bool, // Statements don't produce values .assignment, .var_decl, .const_decl, .fn_decl, .return_stmt, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl, => .void, else => .s64, }; } /// Infer the return type of a generic function call by resolving type bindings. fn inferGenericReturnType(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call) TypeId { if (fd.return_type == null) return .void; // Build ALL type bindings from call args before resolving return type var tmp_bindings = std.StringHashMap(TypeId).init(self.alloc); defer tmp_bindings.deinit(); for (fd.type_params) |tp| { // Strategy 1: direct type param decl ($T: Type) — param.name == tp.name. // Only fires when the caller actually supplied a type expression at // that position; otherwise fall through to value-based inference. var found = false; for (fd.params, 0..) |param, pi| { if (std.mem.eql(u8, param.name, tp.name)) { if (pi < c.args.len and type_bridge.isTypeShapedAstNode(c.args[pi], &self.module.types)) { const ty = self.resolveTypeArg(c.args[pi]); tmp_bindings.put(tp.name, ty) catch {}; found = true; } break; } } if (found) continue; // Strategy 2: inferred from usage (a: $T, b: T) — check ALL matching params, pick widest var inferred_ty: ?TypeId = null; for (fd.params, 0..) |param, pi| { if (param.type_expr.data == .type_expr) { const te = param.type_expr.data.type_expr; if (std.mem.eql(u8, te.name, tp.name)) { if (pi < c.args.len) { const arg_ty = self.inferExprType(c.args[pi]); if (inferred_ty) |prev| { if (arg_ty == .f64 and prev != .f64) { inferred_ty = arg_ty; } else if (arg_ty == .f32 and prev != .f64 and prev != .f32) { inferred_ty = arg_ty; } } else { inferred_ty = arg_ty; } } } } } if (inferred_ty) |ty| { tmp_bindings.put(tp.name, ty) catch {}; } } // Resolve return type with all bindings if (tmp_bindings.count() > 0) { const saved = self.type_bindings; self.type_bindings = tmp_bindings; const ret = self.resolveTypeWithBindings(fd.return_type.?); self.type_bindings = saved; return ret; } return .s64; } /// 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 .s64; // Any → concrete type: unbox if (src_ty == .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 if (src_ty == dst_ty) return operand; // Concrete → Protocol: build protocol value if (self.getProtocolInfo(dst_ty)) |_| { 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. if (self.getProtocolInfo(src_ty)) |_| { if (!dst_ty.isBuiltin()) { const dst_info = self.module.types.get(dst_ty); if (dst_info == .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); } } } const result = self.coerceToType(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; const pack_entries = self.param_impl_pack_map.get(pack_key) orelse return null; if (pack_entries.items.len == 0) return null; const table = &self.module.types; // Source must itself be a closure/function the pack can match. const src_info = table.get(src_ty); if (src_info != .closure and src_info != .function) return null; const src_params: []const TypeId = switch (src_info) { .closure => |c| c.params, .function => |f| f.params, else => unreachable, }; const src_ret: TypeId = switch (src_info) { .closure => |c| c.ret, .function => |f| f.ret, else => unreachable, }; // Find pack impls whose fixed prefix matches src's leading params. var matched_idx: ?usize = null; for (pack_entries.items, 0..) |entry, i| { const ent_info = table.get(entry.source_pack_ty); // Pack impls always wear a closure (resolveClosureType routes // both Closure and the future Fn pack forms through // closureTypePack); a function-typed pack impl is not produced // by current parser shapes. if (ent_info != .closure) continue; const ent_ci = ent_info.closure; const pack_start = ent_ci.pack_start orelse continue; // Fixed prefix must fit within the source's params. if (pack_start > src_params.len) continue; var prefix_ok = true; var i_fix: u32 = 0; while (i_fix < pack_start) : (i_fix += 1) { if (ent_ci.params[i_fix] != src_params[i_fix]) { prefix_ok = false; break; } } if (!prefix_ok) continue; // Return type: if the impl's return is a generic var // (ret_var_name set), any source return binds; otherwise it // must equal the source's return exactly. if (entry.ret_var_name == null and ent_ci.ret != src_ret) continue; // First match wins for v1; concrete-wins-over-pack already // happened by the caller checking concrete first. Multiple // overlapping pack impls would be a separate diagnostic // (deferred — same module duplicates are caught at registration). matched_idx = i; break; } const idx = matched_idx orelse return null; const entry = pack_entries.items[idx]; // Find the `convert` method. 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; // 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.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.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.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. 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. 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| { const saved_tt = self.target_type; self.target_type = f.ty; const val = self.lowerExpr(default_expr); self.target_type = saved_tt; field_vals.append(self.alloc, val) 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) 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. 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), }; } fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo) Ref { switch (ci.value.data) { .int_literal => |lit| { // If declared type is float, convert integer value to float constant if (ci.ty == .f32 or ci.ty == .f64) { return self.builder.constFloat(@floatFromInt(lit.value), ci.ty); } return self.builder.constInt(lit.value, ci.ty); }, .float_literal => |lit| return self.builder.constFloat(lit.value, ci.ty), .bool_literal => |lit| return self.builder.emit(.{ .const_bool = lit.value }, .bool), .string_literal => |lit| { 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); return self.builder.constString(sid); }, .undef_literal => return self.builder.constUndef(ci.ty), .null_literal => return self.builder.constNull(ci.ty), else => { // Complex expressions (struct_literal, call, etc.) — lower on demand const saved_target = self.target_type; self.target_type = ci.ty; const result = self.lowerExpr(ci.value); self.target_type = saved_target; return result; }, } } fn emitPlaceholder(self: *Lowering, name: []const u8) Ref { const sid = self.module.types.internString(name); return self.builder.emit(.{ .placeholder = sid }, .s64); } /// 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. 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; } 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. 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; } 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); } fn emitFieldError(self: *Lowering, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { 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); } /// Insert a conversion if src_ty and dst_ty differ. /// Handles int widening/narrowing, float widening/narrowing, and int↔float. fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { if (src_ty == dst_ty) return val; // Unbox Any → concrete type if (src_ty == .any and dst_ty != .any) { return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty); } // Box concrete → Any if (dst_ty == .any and src_ty != .any) { return self.builder.boxAny(val, src_ty); } // Optional → Concrete unwrapping (flow-sensitive narrowing coercion) if (!src_ty.isBuiltin()) { const src_info = self.module.types.get(src_ty); if (src_info == .optional) { const child_ty = src_info.optional.child; if (child_ty == dst_ty or (dst_ty.isBuiltin() and child_ty.isBuiltin())) { const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); return self.coerceToType(unwrapped, child_ty, dst_ty); } } } // void → Optional: produce null (void is the type of null_literal) if (src_ty == .void and !dst_ty.isBuiltin()) { const dst_info = self.module.types.get(dst_ty); if (dst_info == .optional) { return self.builder.constNull(dst_ty); } } // Concrete → Optional wrapping if (!dst_ty.isBuiltin()) { const dst_info = self.module.types.get(dst_ty); if (dst_info == .optional) { const child_ty = dst_info.optional.child; // Coerce the value to the inner type first const coerced = self.coerceToType(val, src_ty, child_ty); return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty); } } // Concrete → Protocol (auto type erasure) if (self.getProtocolInfo(dst_ty)) |_| { const dst_info = self.module.types.get(dst_ty); if (dst_info == .@"struct") { const proto_name = self.module.types.getString(dst_info.@"struct".name); if (self.resolveConcreteTypeName(src_ty)) |ctn| { // 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); } } } const src_float = isFloat(src_ty); const dst_float = isFloat(dst_ty); const src_int = self.isIntEx(src_ty); const dst_int = self.isIntEx(dst_ty); const src_ptr = !src_ty.isBuiltin() and self.module.types.get(src_ty) == .pointer; const dst_ptr = !dst_ty.isBuiltin() and self.module.types.get(dst_ty) == .pointer; // Int → Float if (src_int and dst_float) { return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); } // Float → Int if (src_float and dst_int) { 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. if ((src_ptr and dst_int) or (src_int and dst_ptr)) { return self.builder.emit(.{ .bitcast = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); } // Same kind — widen/narrow based on bit width const src_bits = self.typeBitsEx(src_ty); const dst_bits = self.typeBitsEx(dst_ty); if (src_bits > 0 and dst_bits > 0) { if (dst_bits < src_bits) { return self.builder.emit(.{ .narrow = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); } else if (dst_bits > src_bits) { return self.builder.emit(.{ .widen = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); } } return val; } /// Get the alloca Ref for an expression, if it's a simple variable reference. /// Returns null for complex expressions (field access, function calls, etc.) 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. Returns .s64 for unknown types. fn getElementType(self: *Lowering, ty: TypeId) TypeId { if (ty == .string) return .u8; if (ty.isBuiltin()) return .s64; 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 => .s64, }; } fn isFloat(ty: TypeId) bool { return ty == .f32 or ty == .f64; } fn isInt(ty: TypeId) bool { return switch (ty) { .s8, .s16, .s32, .s64, .u8, .u16, .u32, .u64, .usize, .isize => true, else => false, }; } 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; } 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, }; } 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); } } fn ensureTerminator(self: *Lowering, ret_ty: TypeId) void { if (self.currentBlockHasTerminator()) return; if (ret_ty == .void) { self.builder.retVoid(); } else { // Use const_undef for complex types (string, struct, etc.) const default_val = if (ret_ty == .string or !ret_ty.isBuiltin()) self.builder.constUndef(ret_ty) else self.builder.constInt(0, ret_ty); self.builder.ret(default_val, ret_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). 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(); } /// True if `ty` is a pointer to a struct whose name is registered /// in `foreign_class_map` under an Obj-C runtime. Used by the /// `obj.class` accessor (M1.3) to decide whether to lower the /// field access as a struct GEP or as `object_getClass(obj)`. fn isObjcClassPointer(self: *Lowering, ty: TypeId) bool { if (ty.isBuiltin()) return false; const ptr_info = self.module.types.get(ty); if (ptr_info != .pointer) return false; const pointee_info = self.module.types.get(ptr_info.pointer.pointee); if (pointee_info != .@"struct") return false; const struct_name = self.module.types.getString(pointee_info.@"struct".name); const fcd = self.foreign_class_map.get(struct_name) orelse return false; return fcd.runtime == .objc_class or fcd.runtime == .objc_protocol; } /// 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. 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.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.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.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. 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.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.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). 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:`. 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; // 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 => {}, } } } } /// 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. /// M4.B — interpretation of `#property(...)` modifiers for ARC. /// `assign` is the default for primitives (direct store, no ARC ops); /// `strong` is the default for pointer-to-object types (retain on /// assign, release on dealloc); `weak` and `copy` are explicit. The /// helper rejects ambiguous combinations loudly per the silent-error /// budget — `*void` requires explicit modifier, `weak` requires an /// object-pointer slot. const ObjcPropertyKind = enum { assign, // primitives or explicitly opted-out object slots strong, // default for * — retain on assign, release on dealloc weak, // objc_storeWeak / objc_loadWeakRetained — auto-nilling copy, // [val copy] on assign — for immutable-wanting String/Array slots pub fn isObject(k: ObjcPropertyKind) bool { return k == .strong or k == .weak or k == .copy; } }; /// Resolve a `#property(...)` field's ARC kind. Loud at compile time /// for known footguns (per the silent-error budget in the plan): /// - unknown modifier name (typo) → diagnostic /// - `weak` on a non-object field type → diagnostic /// - `strong` (explicit or defaulted) on `*void` (ambiguous: Obj-C /// object vs raw memory) → require explicit modifier fn objcPropertyKind(self: *Lowering, field: ast.ForeignFieldDecl) ObjcPropertyKind { // Survey the modifier list. var has_strong = false; var has_weak = false; var has_copy = false; var has_assign = false; for (field.property_modifiers) |mod| { if (std.mem.eql(u8, mod, "strong")) has_strong = true else if (std.mem.eql(u8, mod, "weak")) has_weak = true else if (std.mem.eql(u8, mod, "copy")) has_copy = true else if (std.mem.eql(u8, mod, "assign")) has_assign = true else if (std.mem.eql(u8, mod, "readonly")) { // Orthogonal to ARC kind — no-op here. } else if (std.mem.eql(u8, mod, "nonatomic") or std.mem.eql(u8, mod, "atomic")) { // Atomicity — recorded for the property attribute string; // doesn't affect the ARC kind. } else if (std.mem.startsWith(u8, mod, "getter(") or std.mem.startsWith(u8, mod, "setter(")) { // Selector overrides — handled elsewhere. } else { if (self.diagnostics) |d| { const span = ast.Span{ .start = 0, .end = 0 }; d.addFmt(.err, span, "unknown #property modifier '{s}' on field '{s}' — expected one of: strong, weak, copy, assign, readonly, nonatomic, atomic, getter(\"...\"), setter(\"...\")", .{ mod, field.name }); } } } // Mutually-exclusive ARC modifiers — at most one. const explicit_count: u32 = (@as(u32, if (has_strong) 1 else 0)) + (@as(u32, if (has_weak) 1 else 0)) + (@as(u32, if (has_copy) 1 else 0)) + (@as(u32, if (has_assign) 1 else 0)); if (explicit_count > 1) { if (self.diagnostics) |d| { const span = ast.Span{ .start = 0, .end = 0 }; d.addFmt(.err, span, "conflicting #property modifiers on field '{s}' — strong/weak/copy/assign are mutually exclusive", .{field.name}); } } // Resolve the field's type to decide defaults + validate. const field_ty = self.resolveType(field.field_type); const is_pointer = !field_ty.isBuiltin() and self.module.types.get(field_ty) == .pointer; const is_object_ptr = is_pointer and blk: { const pointee = self.module.types.get(field_ty).pointer.pointee; // `*void` is NOT considered an object pointer — ambiguous. if (pointee == .void) break :blk false; // `*T` where T is a foreign-class struct (Obj-C class). if (pointee.isBuiltin()) break :blk false; const pointee_info = self.module.types.get(pointee); if (pointee_info != .@"struct") break :blk false; const struct_name = self.module.types.getString(pointee_info.@"struct".name); const fcd = self.foreign_class_map.get(struct_name) orelse break :blk false; break :blk fcd.runtime == .objc_class or fcd.runtime == .objc_protocol; }; // `weak` requires an object pointer — `weak s32` is meaningless and // would invoke objc_storeWeak on a non-object slot. if (has_weak and !is_object_ptr) { if (self.diagnostics) |d| { const span = ast.Span{ .start = 0, .end = 0 }; d.addFmt(.err, span, "#property(weak) on field '{s}' requires a pointer-to-Obj-C-class type; got '{s}'", .{ field.name, self.module.types.typeName(field_ty) }); } } // `copy` requires an object pointer — `copy s32` makes no sense. if (has_copy and !is_object_ptr) { if (self.diagnostics) |d| { const span = ast.Span{ .start = 0, .end = 0 }; d.addFmt(.err, span, "#property(copy) on field '{s}' requires a pointer-to-Obj-C-class type (typically NSString or NSArray)", .{field.name}); } } // `*void` is ambiguous (Obj-C object vs raw memory): require explicit // modifier so the user opts into ARC semantics consciously. if (is_pointer) { const pointee = self.module.types.get(field_ty).pointer.pointee; if (pointee == .void and explicit_count == 0) { if (self.diagnostics) |d| { const span = ast.Span{ .start = 0, .end = 0 }; d.addFmt(.err, span, "#property on field '{s}' of type '*void' is ambiguous — specify `#property(strong|weak|copy|assign)` explicitly (Obj-C object vs raw memory)", .{field.name}); } return .assign; // assume safe default to keep compilation going } } // Apply explicit modifier or default. if (has_weak) return .weak; if (has_copy) return .copy; if (has_strong) return .strong; if (has_assign) return .assign; // Default: object pointers → strong; everything else → assign. return if (is_object_ptr) .strong else .assign; } /// 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); } fn emitObjcDefinedClassPropertyImps(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl) void { const state_ty = self.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.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.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.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.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.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.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.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.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.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.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.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.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; } fn synthesizeJniMainStubs(self: *Lowering) void { var seen = std.StringHashMap(void).init(self.alloc); defer seen.deinit(); var it = self.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 = 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.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(); } }; /// 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); } /// Whether emit_llvm's `jni_msg_send` lowering can dispatch a CallMethod /// for this return type. Anything outside this set falls into the `else` /// arm of the switches in `emit_llvm.zig` and would silently produce /// `LLVMGetUndef` — a footgun that previously shipped (chess Android touch /// went undef because `MotionEvent.getX() -> f32` wasn't in the switch). /// Pointer-typed returns route through `CallObjectMethod`. pub fn isJniReturnTypeSupported(table: *const @import("types.zig").TypeTable, ret_ty: TypeId) bool { return switch (ret_ty) { .void, .bool, .s32, .s64, .f32, .f64 => true, else => blk: { if (ret_ty.isBuiltin()) break :blk false; const info = table.get(ret_ty); break :blk info == .pointer or info == .many_pointer; }, }; } /// Encode a (foreign_path, method_name) pair as the JNI-resolved symbol /// `Java___sx_1`. JNI mangling: /// `/` → `_`, `_` → `_1`. The `sx_` prefix matches the Java-side /// `private native sx_(...)` delegate. fn jniMangleNativeName(allocator: std.mem.Allocator, foreign_path: []const u8, method_name: []const u8) ![]u8 { var buf = std.ArrayList(u8).empty; try buf.appendSlice(allocator, "Java_"); for (foreign_path) |ch| { if (ch == '/') { try buf.append(allocator, '_'); } else if (ch == '_') { try buf.appendSlice(allocator, "_1"); } else { try buf.append(allocator, ch); } } try buf.append(allocator, '_'); try buf.appendSlice(allocator, "sx_1"); for (method_name) |ch| { if (ch == '_') { try buf.appendSlice(allocator, "_1"); } else { try buf.append(allocator, ch); } } return buf.toOwnedSlice(allocator); }