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: { // `$` is overloaded in expression position: // - Inside a pack-fn mono (or a `tryPackImplMatch` // impl mono), `name` is a pack binding → slice of // element types (`[]Type` lowered as `[]Any`). // - Inside an impl mono whose impl pattern bound a // single-type generic (`$R: Type` in // `Closure(..$args) -> $R`), `name` is in // `type_bindings` → single `const_type(R)` value. // Pack arg types are checked first (the slice form), // then pack_bindings (the impl-mono mirror), then // type_bindings (single-type binding); only if all // miss is it a real "outside an active binding" error. if (self.pack_arg_types) |pat| { if (pat.get(cpr.pack_name)) |arg_tys| { break :blk self.buildPackSliceValue(arg_tys); } } if (self.pack_bindings) |pb| { if (pb.get(cpr.pack_name)) |arg_tys| { break :blk self.buildPackSliceValue(arg_tys); } } if (self.type_bindings) |tb| { if (tb.get(cpr.pack_name)) |ty| { break :blk self.builder.constType(ty); } } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack reference ${s} used outside an active pack binding", .{cpr.pack_name}); } break :blk self.builder.constNull(self.module.types.sliceOf(.any)); }, // Pack-index in expression position: `$[]` → // `const_type(arg_types[index])`. Yields a comptime-only // Type value (`Value.type_tag(TypeId)` in the interp). // OOB / no-active-pack-binding → focused diagnostic; the // emitted Ref is a const_type(.void) placeholder so the // verifier downstream catches misuse rather than silently // succeeding with .void. .pack_index_type_expr => |pi| blk: { if (self.pack_arg_types) |pat| { if (pat.get(pi.pack_name)) |arg_tys| { if (pi.index < arg_tys.len) { break :blk self.builder.constType(arg_tys[pi.index]); } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index value ${s}[{}] out of bounds: '{s}' has {} element{s}", .{ pi.pack_name, pi.index, pi.pack_name, arg_tys.len, if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"), }); } break :blk self.builder.constType(.void); } } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index value ${s}[{}] used outside an active pack binding", .{ pi.pack_name, pi.index, }); } break :blk self.builder.constType(.void); }, .int_literal => |lit| { // If target is a float type, emit as float literal if (self.target_type) |tt| { if (tt == .f32 or tt == .f64) { return self.builder.constFloat(@floatFromInt(lit.value), tt); } } const ty = if (self.target_type) |tt| blk: { break :blk if (self.isIntEx(tt)) tt else .s64; } else .s64; return self.builder.constInt(lit.value, ty); }, .float_literal => |lit| { const fty: TypeId = if (self.target_type) |tt| (if (tt == .f32 or tt == .f64) tt else .f64) else .f64; return self.builder.constFloat(lit.value, fty); }, .bool_literal => |lit| self.builder.constBool(lit.value), .string_literal => |lit| blk: { const str = if (lit.is_raw) lit.raw else unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; const sid = self.module.types.internString(str); break :blk self.builder.constString(sid); }, .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. The two surface forms differ in // what `p.type_expr` resolves to: legacy `name: ..T` declares T // (element type), new `..name: []T` declares []T (already a // slice). Unwrap the latter so the per-element packing below // sees T in both cases. var variadic_idx: ?usize = null; var elem_ty: TypeId = .any; for (fd.params, 0..) |p, i| { if (p.is_variadic) { variadic_idx = i; const declared = self.resolveTypeWithBindings(p.type_expr); elem_ty = declared; if (!declared.isBuiltin()) { const info = self.module.types.get(declared); if (info == .slice) elem_ty = info.slice.element; } break; } } const vi = variadic_idx orelse return; // no variadic param // Number of non-variadic args const fixed_count = vi; const variadic_count = if (args.items.len > fixed_count) args.items.len - fixed_count else 0; const slice_ty = self.module.types.sliceOf(elem_ty); // Check for spread operator: sum(..arr) — single spread arg becomes the slice directly if (variadic_count == 1 and fixed_count < c.args.len) { const arg_node = c.args[fixed_count]; if (arg_node.data == .spread_expr) { const spread = arg_node.data.spread_expr; const arr_val = self.lowerExpr(spread.operand); const arr_ty = self.inferExprType(spread.operand); const arr_info = self.module.types.get(arr_ty); // Convert array to slice const slice_val = switch (arr_info) { .array => self.builder.emit(.{ .array_to_slice = .{ .operand = arr_val } }, slice_ty), .slice => arr_val, else => arr_val, }; args.shrinkRetainingCapacity(fixed_count); args.append(self.alloc, slice_val) catch unreachable; return; } } if (variadic_count == 0) { // Empty slice const null_ptr = self.builder.constNull(self.module.types.ptrTo(elem_ty)); const zero_len = self.builder.constInt(0, .s64); const slice_slot = self.builder.alloca(slice_ty); const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty); self.builder.store(ptr_gep, null_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); self.builder.store(len_gep, zero_len); const slice_val = self.builder.load(slice_slot, slice_ty); // Replace args: keep fixed args, append slice args.shrinkRetainingCapacity(fixed_count); args.append(self.alloc, slice_val) catch unreachable; return; } // Determine if we need to box as Any (for ..Any params) or use raw type const is_any = (elem_ty == .any); // 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; // Pack-fn mono state is lexical to the pack-fn body. A generic // function called from inside a pack-fn mono (e.g. // `build(args: []Type, $ret: Type)` invoked from // `probe(..$args) { build($args, void) }`) must not inherit the // caller's pack maps — `lowerFieldAccess`'s `.len` // intercept would otherwise constant-fold the callee's // same-named param to whichever shape triggered the first mono // and bake the wrong arity into the cached IR. Same shape of // fix as `lazyLowerFunction` (issue-0048, commit 0ede097). const saved_pan = self.pack_arg_nodes; const saved_ppc = self.pack_param_count; const saved_pat = self.pack_arg_types; const saved_iri = self.inline_return_target; self.pack_arg_nodes = null; self.pack_param_count = null; self.pack_arg_types = null; self.inline_return_target = null; defer { self.pack_arg_nodes = saved_pan; self.pack_param_count = saved_ppc; self.pack_arg_types = saved_pat; self.inline_return_target = saved_iri; } self.func_defer_base = self.defer_stack.items.len; self.block_terminated = false; // Install type bindings self.type_bindings = bindings.*; // 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; } // Bare `$` in a type-arg position. Single-type generic // bindings (`$R: Type` in `Closure(..$args) -> $R`) live in // `type_bindings`; if the name is bound there, return the // bound TypeId directly. Pack bindings would otherwise resolve // to a slice value, not a single Type — the caller (e.g. // `type_name(...)`) expects a single arg. if (node.data == .comptime_pack_ref) { const cpr = node.data.comptime_pack_ref; if (self.type_bindings) |tb| { if (tb.get(cpr.pack_name)) |ty| return ty; } } switch (node.data) { .identifier => |id| { // Check type bindings first (from generic monomorphization) if (self.type_bindings) |tb| { if (tb.get(id.name)) |ty| return ty; } 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, optional }; const cat: ?Category = if (std.mem.eql(u8, name, "struct")) .@"struct" else if (std.mem.eql(u8, name, "enum") or std.mem.eql(u8, name, "union")) .@"enum" else if (std.mem.eql(u8, name, "slice")) .slice else if (std.mem.eql(u8, name, "array")) .array else if (std.mem.eql(u8, name, "pointer")) .pointer else if (std.mem.eql(u8, name, "vector")) .vector else if (std.mem.eql(u8, name, "optional")) .optional else null; if (cat) |c| { for (self.module.types.infos.items, 0..) |info, idx| { const matches = switch (c) { .@"struct" => info == .@"struct", .@"enum" => info == .@"enum" or info == .tagged_union, .@"union" => info == .@"union" or info == .tagged_union, .slice => info == .slice, .array => info == .array, .pointer => info == .pointer or info == .many_pointer, .vector => info == .vector, .optional => info == .optional, }; 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 declared_ty = self.resolveTypeWithBindings(p.type_expr); if (p.is_variadic) { // Two surface forms: // - legacy `name: ..T` — declared_ty is the element type; // wrap to receive a `[]T` slice. // - new `..name: []T` — declared_ty is already the slice // type; use it as-is. Wrapping here would double up to // `[][]T` and downstream LLVM emission crashes when the // caller's argument-marshal pack produces a `[]T` that // doesn't match the callee's stored param shape. if (!declared_ty.isBuiltin()) { const info = self.module.types.get(declared_ty); if (info == .slice) return declared_ty; } return self.module.types.sliceOf(declared_ty); } return declared_ty; } 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, .in_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. `resolveFuncByName` // only finds ALREADY-LOWERED functions; namespace // imports are typically lowered lazily on demand, so // a fresh `pkg.hello()` call site may resolve through // `fn_ast_map` first. Without this, the call's return // type silently falls through to `.s64` and any // pack-fn caller (e.g. `print("{}\n", pkg.hello())`) // mangles the arg as s64, mis-tagging the actual // string in the Any box. 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; } if (self.fn_ast_map.get(qualified)) |qfd| { if (qfd.return_type) |rt| return self.resolveType(rt); return .void; } // Namespace aliases sometimes register the function // under its bare name (matches `lowerCall`'s effective- // name resolution order). if (self.fn_ast_map.get(cfa.field)) |bfd| { if (bfd.return_type) |rt| return self.resolveType(rt); return .void; } } } 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 whatever bindings we built. Even an // empty `tmp_bindings` is a valid input — non-generic literal // return types (e.g. `walk(..$args) -> string`) still need to // resolve through `resolveTypeWithBindings`, not fall through // to the historical `.s64` default. The default silently // misclassified pack-fn calls whose return type was a fixed // literal — every consumer (e.g. print's pack-shape mangling) // inferred `s64` and routed the value through the wrong Any // tag. const saved = self.type_bindings; self.type_bindings = tmp_bindings; const ret = self.resolveTypeWithBindings(fd.return_type.?); self.type_bindings = saved; return ret; } /// 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); }