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 jni_descriptor = @import("../jni_descriptor.zig"); const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; const TypeId = types.TypeId; const Ref = inst_mod.Ref; const FuncId = inst_mod.FuncId; const Function = inst_mod.Function; const Module = mod_mod.Module; const lower = @import("../lower.zig"); const Lowering = lower.Lowering; const Scope = lower.Scope; /// 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. pub 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_`. pub 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. pub 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. pub 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); } pub 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. pub fn lowerForeignMethodCall( self: *Lowering, fcd: *const ast.ForeignClassDecl, method_name: []const u8, target: Ref, method_args: []const Ref, span: ast.Span, ) Ref { // M2.3 — walk the `#extends` chain when the method isn't // declared directly on this fcd. The dispatch target stays // the original receiver — objc_msgSend's runtime walks the // class hierarchy by isa, so we just need to find ANY // ancestor that declared the method (for the selector // mangling + signature info). The receiver-class fcd is // still used for `*Self` substitution at the dispatch site // — the inherited method's *Self should resolve to the // child receiver, not the parent. const found = self.findForeignMethodInChain(fcd, method_name) orelse { if (self.diagnostics) |d| { d.addFmt(.err, span, "no method '{s}' on foreign class '{s}' (or any `#extends` ancestor)", .{ method_name, fcd.name }); } return Ref.none; }; const method = found.method; // Obj-C instance dispatch (Phase 3 step 3.0 + M1.2 A.7). // `inst.method(args)` on an `#objc_class` / `#objc_protocol` // receiver derives a selector from the sx method name (default // mangling: split on `_`, each piece becomes a keyword with a // trailing `:`; niladic stays verbatim) and lowers to // `objc_msg_send`. Both foreign and sx-defined classes flow // through the same path — sx-defined classes have their IMPs // registered at module-init (M1.2 A.4b.iii) so `objc_msgSend` // finds them. The Swift runtimes still bail — Phase 4. if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) { return self.lowerObjcMethodCall(fcd, method, target, method_args, span); } if (!fcd.is_foreign) { if (self.diagnostics) |d| { d.addFmt(.err, span, "sx-defined classes on non-Obj-C runtimes can't yet be dispatched into (class '{s}', runtime '{s}')", .{ fcd.name, @tagName(fcd.runtime) }); } return Ref.none; } if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) { if (self.diagnostics) |d| { d.addFmt(.err, span, "method calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)}); } return Ref.none; } if (self.jni_env_stack.items.len == 0) { if (self.diagnostics) |d| { d.addFmt(.err, span, "method call on '{s}' requires an enclosing '#jni_env' scope", .{fcd.name}); } return Ref.none; } const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1]; // Build a ClassRegistry snapshot so descriptor derivation can // resolve `*Foo` cross-class refs to their foreign paths. var registry = jni_descriptor.ClassRegistry.init(self.alloc); defer registry.deinit(); var it = self.program_index.foreign_class_map.iterator(); while (it.next()) |entry| { registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path) catch {}; } const desc_str = jni_descriptor.deriveMethod(self.alloc, .{ .enclosing_path = fcd.foreign_path, .classes = ®istry, }, method) catch |err| { if (self.diagnostics) |d| { d.addFmt(.err, span, "JNI descriptor derivation failed for '{s}.{s}': {s}", .{ fcd.name, method.name, @errorName(err) }); } return Ref.none; }; const name_sid = self.module.types.internString(method_name); const name_ref = self.builder.constString(name_sid); const sig_sid = self.module.types.internString(desc_str); const sig_ref = self.builder.constString(sig_sid); const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void; // Reject return types the JNI emit path can't dispatch — emit_llvm's // CallMethod switch only covers void / bool / s32 / s64 / f32 / f64 // / pointer-returning. Anything else (s8 / s16 / u8 / u16 / aggregates) // would silently lower to LLVMGetUndef and produce wrong arguments at // the call site (chess Android touch shipped broken because s32→s32+ // f32 returns hit the undef path before .f32 was wired up). if (!jni_descriptor.isJniReturnTypeSupported(&self.module.types, ret_ty)) { if (self.diagnostics) |d| { d.addFmt(.err, span, "JNI method '{s}.{s}' returns '{s}', which isn't supported by the JNI call-method lowering yet — only void/bool/s32/s64/f32/f64 and pointers are wired up", .{ fcd.name, method.name, self.module.types.typeName(ret_ty) }); } return Ref.none; } const cache_key: inst_mod.CacheKey = .{ .name_str = method_name, .sig_str = desc_str, }; const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; return self.builder.emit(.{ .jni_msg_send = .{ .env = env_ref, .target = target, .name = name_ref, .sig = sig_ref, .args = args_owned, .is_static = method.is_static, .cache_key = cache_key, } }, ret_ty); } // Pure Obj-C decision helpers (selector derivation, type-encoding, ARC // property-kind, class-pointer recognition, state-struct planning) live in // `ffi_objc.zig` (`ObjcLowering`, a `*Lowering` facade). Reached via // `self.objc()`. Emission-heavy IMP builders live in lower/objc_class.zig; // the `lowerObjc*Call` lowering paths are below. /// 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. pub fn resolveForeignClassMemberType( self: *Lowering, fcd: *const ast.ForeignClassDecl, type_node: *const ast.Node, ) TypeId { if (type_node.data == .type_expr and std.mem.eql(u8, type_node.data.type_expr.name, "Self")) { return self.foreignClassStructType(fcd); } if (type_node.data == .pointer_type_expr) { const pt = type_node.data.pointer_type_expr; if (pt.pointee_type.data == .type_expr and std.mem.eql(u8, pt.pointee_type.data.type_expr.name, "Self")) { return self.module.types.ptrTo(self.foreignClassStructType(fcd)); } } return self.resolveType(type_node); } pub fn resolveForeignMethodReturnType( self: *Lowering, fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl, ) TypeId { const rt = method.return_type orelse return .void; return self.resolveForeignClassMemberType(fcd, rt); } pub fn foreignClassStructType(self: *Lowering, fcd: *const ast.ForeignClassDecl) TypeId { const name_id = self.module.types.internString(fcd.name); if (self.module.types.findByName(name_id)) |existing| return existing; return self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); } /// Lower `inst.method(args)` on an `#objc_class` / `#objc_protocol` /// receiver. The selector is derived by `deriveObjcSelector`; arity /// is validated against the keyword count produced by the mangling /// (excluding self). Dispatch then runs through `objc_msg_send`, /// sharing the cached-SEL slot path with explicit `#objc_call`. pub fn lowerObjcMethodCall( self: *Lowering, fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl, target: Ref, method_args: []const Ref, span: ast.Span, ) Ref { const arity = method_args.len; const derived = self.objc().deriveObjcSelector(method, arity); // Arity validation: the keyword count (number of `:` in the // selector) must equal the number of args passed at the call // site. For methods using the default mangling rule, a mismatch // is an error because the user can fix the sx-side name. For // `#selector("...")` overrides, the user has deliberately // chosen the selector — downgrade to a warning so the build // proceeds, but still surface the typo case (Obj-C's runtime // doesn't validate colon-vs-arg, so this is the last defense). if (arity > 0 and derived.keyword_count != arity) { if (self.diagnostics) |d| { if (derived.is_override) { d.addFmt( .warn, span, "Obj-C selector \"{s}\" (override for '{s}.{s}') has {} keyword(s) but the call passes {} argument(s); the runtime will dispatch but the colon count is inconsistent with the arity — double-check the selector string", .{ derived.sel, fcd.name, method.name, derived.keyword_count, arity }, ); } else { d.addFmt( .err, span, "Obj-C selector for '{s}.{s}' has {} keyword(s) but the call passes {} argument(s); split the sx method name on '_' so it produces exactly {} keyword(s), or override with `#selector(\"...\")`", .{ fcd.name, method.name, derived.keyword_count, arity, arity }, ); return Ref.none; } } } const ret_ty = self.resolveForeignMethodReturnType(fcd, method); // Cache the SEL slot per (selector-string, module) like // `#objc_call` does. The mangling produces the literal selector // string; we don't need a runtime sel_registerName call at the // dispatch site because the global initializer already does it. const vptr_ty = self.module.types.ptrTo(.void); const slot_gid = self.internObjcSelector(derived.sel); const slot_ptr = self.builder.emit(.{ .global_addr = slot_gid }, self.module.types.ptrTo(vptr_ty)); const sel = self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty); const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; return self.builder.emit(.{ .objc_msg_send = .{ .recv = target, .sel = sel, .args = args_owned, } }, ret_ty); } /// Lower `Cls.static_method(args)` on an `#objc_class` / /// `#objc_protocol` alias. Loads the class object through the /// module-scoped cached slot (populated by `objc_getClass` at /// module-init) and dispatches `objc_msg_send` with the same /// selector mangling as instance methods (Phase 3.0). pub fn lowerObjcStaticCall( self: *Lowering, fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl, method_args: []const Ref, span: ast.Span, ) Ref { const arity = method_args.len; const derived = self.objc().deriveObjcSelector(method, arity); if (arity > 0 and derived.keyword_count != arity) { if (self.diagnostics) |d| { if (derived.is_override) { d.addFmt( .warn, span, "Obj-C selector \"{s}\" (override for static call '{s}.{s}') has {} keyword(s) but the call passes {} argument(s); the runtime will dispatch but the colon count is inconsistent with the arity — double-check the selector string", .{ derived.sel, fcd.name, method.name, derived.keyword_count, arity }, ); } else { d.addFmt( .err, span, "Obj-C selector for static call '{s}.{s}' has {} keyword(s) but the call passes {} argument(s); split the sx method name on '_' so it produces exactly {} keyword(s), or override with `#selector(\"...\")`", .{ fcd.name, method.name, derived.keyword_count, arity, arity }, ); return Ref.none; } } } const ret_ty = self.resolveForeignMethodReturnType(fcd, method); const vptr_ty = self.module.types.ptrTo(.void); // Load the class object from its module-scoped cached slot. // `objc_getClass()` runs once at module-init via the // constructor emit_llvm synthesizes (see `emitObjcClassInit`). const class_slot_gid = self.internObjcClassObject(fcd.foreign_path); const class_slot_ptr = self.builder.emit(.{ .global_addr = class_slot_gid }, self.module.types.ptrTo(vptr_ty)); const class_obj = self.builder.emit(.{ .load = .{ .operand = class_slot_ptr } }, vptr_ty); // M4.0b: intercept `Cls.alloc()` for sx-defined classes — emit the // inline alloc-and-init sequence using the caller's `context.allocator` // instead of going through `objc_msgSend` (which would land in the // +alloc IMP and use `__sx_default_context.allocator`). This honors // a surrounding `push Context.{ allocator = ... }`. if (!fcd.is_foreign and fcd.runtime == .objc_class and method_args.len == 0 and std.mem.eql(u8, method.name, "alloc")) { const ctx_addr = if (self.current_ctx_ref != Ref.none) self.current_ctx_ref else blk: { // Fallback: no current ctx (e.g. compiler-internal callers). // Use the default context — same as the IMP would. const default_ctx_gi = self.program_index.global_names.get("__sx_default_context") orelse { if (self.diagnostics) |d| { d.addFmt(.err, span, "Cls.alloc() on sx-defined class '{s}': no current context and __sx_default_context missing", .{fcd.name}); } return Ref.none; }; break :blk self.builder.emit(.{ .global_addr = default_ctx_gi.id }, vptr_ty); }; const instance = self.emitObjcDefinedAllocAndInit(fcd, class_obj, ctx_addr) orelse return Ref.none; // class_createInstance returns *void; bitcast to the method's // declared return type (typically `*` or `?*`) so // downstream `let f := Cls.alloc();` binds f at the right type // (lowerVarDecl reads the Ref's IR type when no annotation is // present). coerceToType is a no-op for ptr→ptr; we need an // explicit bitcast IR op to retype the Ref. if (ret_ty == vptr_ty) return instance; // Optional-wrapped returns (e.g. `-> ?*Cls`): emit optional_wrap. if (!ret_ty.isBuiltin()) { const ret_info = self.module.types.get(ret_ty); if (ret_info == .optional) { const inner = ret_info.optional.child; const cast = if (inner == vptr_ty) instance else self.builder.emit(.{ .bitcast = .{ .operand = instance, .from = vptr_ty, .to = inner } }, inner); return self.builder.optionalWrap(cast, ret_ty); } } return self.builder.emit(.{ .bitcast = .{ .operand = instance, .from = vptr_ty, .to = ret_ty } }, ret_ty); } // Load the SEL from its slot. const sel_slot_gid = self.internObjcSelector(derived.sel); const sel_slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty)); const sel = self.builder.emit(.{ .load = .{ .operand = sel_slot_ptr } }, vptr_ty); const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; return self.builder.emit(.{ .objc_msg_send = .{ .recv = class_obj, .sel = sel, .args = args_owned, } }, ret_ty); } /// Lower `Alias.new(args)` where `Alias` is a foreign-class identifier /// with `static new :: (...) -> *Self;` — JNI constructor dispatch: /// `FindClass + GetMethodID("", "(args)V") + NewObject(env, /// clazz, mid, args...)`. Returns the new jobject. /// /// Non-`new` static methods aren't supported via this path yet — the /// user can use `#jni_static_call(T)(class, "name", sig, args...)` /// for those. Constructor is the common case for #jni_main bodies /// that need to instantiate Android classes (SurfaceView, etc.). pub fn lowerForeignStaticCall( self: *Lowering, fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl, method_args: []const Ref, span: ast.Span, ) Ref { // Obj-C static dispatch (Phase 3 step 3.1). `Cls.static_method(args)` // on an `#objc_class` alias loads the class object through a // module-scoped cached slot (populated once per module via // `objc_getClass`) and dispatches with the derived selector. if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) { return self.lowerObjcStaticCall(fcd, method, method_args, span); } if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) { if (self.diagnostics) |d| d.addFmt(.err, span, "static calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)}); return Ref.none; } if (!std.mem.eql(u8, method.name, "new")) { if (self.diagnostics) |d| d.addFmt(.err, span, "static foreign-class call '{s}.{s}' not yet supported via `Alias.method()` syntax \u{2014} only `new` is wired today; use `#jni_static_call` directly for other static methods", .{ fcd.name, method.name }); return Ref.none; } if (self.jni_env_stack.items.len <= self.jni_env_stack_base) { if (self.diagnostics) |d| d.addFmt(.err, span, "constructor `{s}.new(...)` requires an enclosing `#jni_env` scope (or `#jni_main` body)", .{fcd.name}); return Ref.none; } const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1]; // Build class registry snapshot for `*Foo` cross-class refs. var registry = jni_descriptor.ClassRegistry.init(self.alloc); defer registry.deinit(); var it = self.program_index.foreign_class_map.iterator(); while (it.next()) |entry| { registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path) catch {}; } // For `new`, the JNI descriptor's return position is `V` (the // constructor returns void; the new jobject comes back from // `NewObject` itself). Patch the AST by overriding return_type // to null during derivation. const m_for_desc: ast.ForeignMethodDecl = .{ .name = method.name, .params = method.params, .param_names = method.param_names, .return_type = null, .is_static = method.is_static, .jni_descriptor_override = method.jni_descriptor_override, .body = method.body, }; const descriptor = jni_descriptor.deriveMethod(self.alloc, .{ .enclosing_path = fcd.foreign_path, .classes = ®istry, }, m_for_desc) catch |err| { if (self.diagnostics) |d| d.addFmt(.err, span, "JNI descriptor derivation failed for '{s}.new': {s}", .{ fcd.name, @errorName(err) }); return Ref.none; }; // sx-side return type is `*Self` — resolve to a pointer to the // foreign-class struct type so method dispatch on the new // jobject works (`view := SurfaceView.new(ctx); view.getHolder()`). // At LLVM level still ptr; the sx type table is what method // resolution consults. const self_struct_name = self.module.types.internString(fcd.name); const self_struct_id = if (self.module.types.findByName(self_struct_name)) |existing| existing else blk: { const info: types.TypeInfo = .{ .@"struct" = .{ .name = self_struct_name, .fields = &.{} } }; break :blk self.module.types.intern(info); }; const ret_ty = self.module.types.ptrTo(self_struct_id); const name_sid = self.module.types.internString(""); const name_ref = self.builder.constString(name_sid); const sig_sid = self.module.types.internString(descriptor); const sig_ref = self.builder.constString(sig_sid); const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; return self.builder.emit(.{ .jni_msg_send = .{ .env = env_ref, .target = Ref.none, // unused for ctor — class is resolved via parent_class_path .name = name_ref, .sig = sig_ref, .args = args_owned, .is_static = false, .is_constructor = true, .parent_class_path = self.alloc.dupe(u8, fcd.foreign_path) catch fcd.foreign_path, .cache_key = null, } }, ret_ty); } /// Lower `super.method(args)` inside a `#jni_main` / sx-defined /// `#jni_class` bodied method. Resolves the parent class from the /// enclosing fcd's `#extends` clause (default `android.app.Activity`) /// and emits a `JniMsgSend` with `is_nonvirtual=true`, which /// emit_llvm expands into a `FindClass(parent) + GetMethodID + /// CallNonvirtualMethod` chain. /// /// Signature derivation: when `method_name` matches the enclosing /// method's name (the common case — `super.onCreate(b)` from inside /// `onCreate :: (self, b)` override), the enclosing method's /// signature is reused. Other method names require the parent class /// to be declared via `#foreign #jni_class` so the signature can be /// looked up. pub fn lowerSuperCall( self: *Lowering, method_name: []const u8, method_args: []const Ref, span: ast.Span, ) Ref { const fcd = self.current_foreign_class orelse { if (self.diagnostics) |d| d.addFmt(.err, span, "'super' is only valid inside a `#jni_class` method body", .{}); return Ref.none; }; // Resolve parent foreign_path from the fcd's `#extends`. Default to // android.app.Activity to match the jni_java_emit default. var parent_path: []const u8 = "android/app/Activity"; for (fcd.members) |m| switch (m) { .extends => |alias| { if (self.program_index.foreign_class_map.get(alias)) |parent_fcd| { parent_path = parent_fcd.foreign_path; } else { parent_path = alias; } break; }, else => {}, }; // Resolve method signature. Same-name fast path reuses the // enclosing method's descriptor; cross-method super calls require // the parent class to be declared via `#foreign #jni_class`. var descriptor: []const u8 = ""; var resolved_method: ?ast.ForeignMethodDecl = null; if (self.current_foreign_method) |em| { if (std.mem.eql(u8, em.name, method_name)) { resolved_method = em; } } if (resolved_method == null) { const parent_fcd = blk: for (fcd.members) |m| switch (m) { .extends => |alias| if (self.program_index.foreign_class_map.get(alias)) |pf| break :blk pf else continue, else => {}, } else null; if (parent_fcd) |pf| { for (pf.members) |pm| switch (pm) { .method => |pmd| if (std.mem.eql(u8, pmd.name, method_name)) { resolved_method = pmd; break; }, else => {}, }; } } const method = resolved_method orelse { if (self.diagnostics) |d| d.addFmt(.err, span, "no method '{s}' found for `super.{s}(...)` — declare the parent class via `#foreign #jni_class` to make cross-method super calls available", .{ method_name, method_name }); return Ref.none; }; // Derive descriptor against the parent path (used as enclosing_path // for `*Self` resolution). var registry = jni_descriptor.ClassRegistry.init(self.alloc); defer registry.deinit(); var it = self.program_index.foreign_class_map.iterator(); while (it.next()) |entry| { registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path) catch {}; } descriptor = jni_descriptor.deriveMethod(self.alloc, .{ .enclosing_path = parent_path, .classes = ®istry, }, method) catch |err| { if (self.diagnostics) |d| d.addFmt(.err, span, "super-call descriptor derivation failed for '{s}.{s}': {s}", .{ parent_path, method_name, @errorName(err) }); return Ref.none; }; // env from the lexical stack (pushed by synthesizeJniMainStub). if (self.jni_env_stack.items.len <= self.jni_env_stack_base) { if (self.diagnostics) |d| d.addFmt(.err, span, "`super.{s}(...)` requires an enclosing `#jni_main` method scope (env is unavailable)", .{method_name}); return Ref.none; } const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1]; // `self` is the first param of the synthesized `Java_*` fn. Bound // in scope as `self` by synthesizeJniMainStub. const self_binding = if (self.scope) |s| s.lookup("self") else null; const self_ref = if (self_binding) |b| (if (b.is_alloca) self.builder.load(b.ref, b.ty) else b.ref) else Ref.none; const name_sid = self.module.types.internString(method_name); const name_ref = self.builder.constString(name_sid); const sig_sid = self.module.types.internString(descriptor); const sig_ref = self.builder.constString(sig_sid); const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void; const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; return self.builder.emit(.{ .jni_msg_send = .{ .env = env_ref, .target = self_ref, .name = name_ref, .sig = sig_ref, .args = args_owned, .is_static = false, .is_nonvirtual = true, .parent_class_path = self.alloc.dupe(u8, parent_path) catch parent_path, .cache_key = null, // per-call FindClass + GetMethodID; caching is a follow-up } }, ret_ty); } // ── Foreign-class registration ────────────────────────────────── /// Register a foreign-class declaration. The alias goes into /// `foreign_class_map` for method-dispatch lookup. The underlying /// type (e.g. `*Activity`) is resolved via the existing struct /// fallback in `type_bridge.resolveTypeName` (which interns unknown /// named types as 0-field structs). /// /// sx-defined Obj-C classes (no `#foreign`, runtime == .objc_class) /// also land in `module.objc_defined_class_cache` in declaration /// order AND have their bodied methods registered into `fn_ast_map` /// under qualified names `.`. Lazy lowering /// then handles the body via the standard path; `*Self` is /// substituted to `*State` during body lowering (M1.2 A.2b). pub fn registerForeignClassDecl(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { self.program_index.foreign_class_map.put(fcd.name, fcd) catch {}; if (!fcd.is_foreign and fcd.runtime == .objc_class) { if (self.module.lookupObjcDefinedClass(fcd.name) == null) { self.module.appendObjcDefinedClass(fcd.name, fcd); // M2.3 — resolve the `#extends` alias to the actual // Obj-C runtime class name. `#extends NSObjectBase` // where NSObjectBase is aliased to "NSObject" must // pass "NSObject" to objc_allocateClassPair, otherwise // the runtime's class-hierarchy link is broken and // inherited-method dispatch fails. self.module.setObjcDefinedClassParent(fcd.name, self.resolveObjcParentName(fcd)); // M1.2 A.4b.i: per-class ivar handle global. The class-pair // init constructor (emit_llvm) populates it via // class_getInstanceVariable after the class is registered; // IMP trampolines read it to find the __sx_state ivar. self.declareObjcDefinedStateIvarGlobal(fcd.name); // M1.2 A.6: per-class class-object global. -dealloc reads // it to build an `objc_super` struct for `[super dealloc]` // dispatch via `objc_msgSendSuper2`. self.declareObjcDefinedClassGlobal(fcd.name); } self.registerObjcDefinedClassMethods(fcd); } } /// Resolve the `#extends ParentAlias` declaration on a sx-defined /// `#objc_class` to the actual Obj-C runtime class name. Falls /// back to "NSObject" when no `#extends` is declared. /// Aliases that resolve to foreign Obj-C classes use the /// foreign_path; aliases for OTHER sx-defined classes use the /// alias name directly (which equals the Obj-C class name for /// sx-defined classes). pub fn resolveObjcParentName(self: *Lowering, fcd: *const ast.ForeignClassDecl) []const u8 { for (fcd.members) |m| switch (m) { .extends => |alias| { if (self.program_index.foreign_class_map.get(alias)) |parent_fcd| { if (parent_fcd.is_foreign) return parent_fcd.foreign_path; // Sx-defined parent — its alias IS its Obj-C name. return parent_fcd.name; } // Unknown alias — pass through as-is and let the // runtime diagnose if it's genuinely wrong. return alias; }, else => {}, }; return "NSObject"; } /// Declare a per-class global `___state_ivar : *void = null`. /// emit_llvm's `emitObjcDefinedClassInit` constructor fills it in via /// `class_getInstanceVariable(cls, "__sx_state")` once per module load. pub 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. pub 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. pub fn registerObjcDefinedClassMethods(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { // Set current_foreign_class so `*Self` substitutions in // declareFunction's type resolution find the state struct. const saved = self.current_foreign_class; self.current_foreign_class = fcd; defer self.current_foreign_class = saved; var method_infos = std.ArrayList(Module.ObjcDefinedMethodEntry).empty; for (fcd.members) |m| { const method = switch (m) { .method => |md| md, else => continue, }; const body = method.body orelse continue; const fd = self.synthesizeFnDeclFromObjcMethod(method, body) orelse continue; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, method.name }) catch continue; self.program_index.fn_ast_map.put(qualified, fd) catch {}; self.declareFunction(fd, qualified); // Selector mangling — A.1's deriveObjcSelector handles // `#selector("...")` override + the default rule. Static // methods use the same mangling rule (their first param // ISN'T *Self, so no offset). // // ABI for the IMP signature (both instance + class methods): // `(recv: id|Class, _cmd: SEL, ...user_args) -> ret` // For instance methods the user-declared self is at param[0] // (skipped); class methods have no self in the AST. const user_param_start: usize = if (method.is_static) 0 else 1; const user_arg_count = if (method.params.len > user_param_start) method.params.len - user_param_start else 0; const sel_info = self.objc().deriveObjcSelector(method, user_arg_count); const ret_ty: TypeId = if (method.return_type) |rt| self.resolveType(rt) else .void; var arg_tys = std.ArrayList(TypeId).empty; defer arg_tys.deinit(self.alloc); if (method.params.len > user_param_start) { for (method.params[user_param_start..]) |p_node| { arg_tys.append(self.alloc, self.resolveType(p_node)) catch unreachable; } } const encoding = self.objc().objcTypeEncodingFromSignature(ret_ty, arg_tys.items, null) catch continue; const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, method.name }) catch continue; method_infos.append(self.alloc, .{ .sel = sel_info.sel, .encoding = encoding, .imp_name = imp_name, .is_class = method.is_static, }) catch unreachable; } if (method_infos.items.len > 0) { const methods_slice = method_infos.toOwnedSlice(self.alloc) catch return; self.module.setObjcDefinedClassMethods(fcd.name, methods_slice); } } /// Build an `FnDecl` whose params are zipped from the /// `ForeignMethodDecl.params` (type nodes) and `param_names`. Used /// to feed sx-defined class methods through the standard /// fn-lowering pipeline. Allocator-owned; lives for the duration /// of the Lowering pass. pub fn synthesizeFnDeclFromObjcMethod(self: *Lowering, method: ast.ForeignMethodDecl, body: *ast.Node) ?*ast.FnDecl { if (method.params.len != method.param_names.len) return null; var params = std.ArrayList(ast.Param).empty; for (method.params, method.param_names) |type_node, p_name| { params.append(self.alloc, .{ .name = p_name, .name_span = .{ .start = 0, .end = 0 }, .type_expr = type_node, }) catch unreachable; } const fd = self.alloc.create(ast.FnDecl) catch return null; fd.* = .{ .name = method.name, .params = params.toOwnedSlice(self.alloc) catch unreachable, .return_type = method.return_type, .body = body, }; return fd; } /// If `name` matches an sx-defined `#objc_class`'s qualified-method /// pattern (`.`), return the class's /// ForeignClassDecl. Used by `lowerFunction` to set /// `current_foreign_class` so `*Self` resolves to the state struct /// during body lowering. pub fn lookupObjcDefinedClassForMethod(self: *Lowering, name: []const u8) ?*const ast.ForeignClassDecl { const dot = std.mem.indexOf(u8, name, ".") orelse return null; return self.module.lookupObjcDefinedClass(name[0..dot]); } /// Lazily declare the `sx_jni_env_tl_get` / `sx_jni_env_tl_set` /// runtime externs (step 2.16c). The storage lives in /// `library/vendors/sx_jni_runtime/sx_jni_env_tl.c` as a /// `_Thread_local` slot — keeping it OUT of the user's IR module /// is what lets the LLVM ORC JIT load the module cleanly without /// orc_rt platform support. AOT targets get the same .c file /// linked in via `needs_jni_env_tl_runtime`, which Compilation /// reads to append a synthetic c_import alongside the user's. pub fn getJniEnvTlFids(self: *Lowering) struct { get: FuncId, set: FuncId } { self.needs_jni_env_tl_runtime = true; const ptr_ty = self.module.types.ptrTo(.void); if (self.jni_env_tl_get_fid == null) { const name = self.module.types.internString("sx_jni_env_tl_get"); const fid = self.builder.declareExtern(name, &.{}, ptr_ty); const func = self.module.getFunctionMut(fid); func.call_conv = .c; self.jni_env_tl_get_fid = fid; } if (self.jni_env_tl_set_fid == null) { const name = self.module.types.internString("sx_jni_env_tl_set"); const env_param = self.module.types.internString("env"); var params = std.ArrayList(inst_mod.Function.Param).empty; params.append(self.alloc, .{ .name = env_param, .ty = ptr_ty }) catch unreachable; const fid = self.builder.declareExtern(name, params.toOwnedSlice(self.alloc) catch unreachable, .void); const func = self.module.getFunctionMut(fid); func.call_conv = .c; self.jni_env_tl_set_fid = fid; } return .{ .get = self.jni_env_tl_get_fid.?, .set = self.jni_env_tl_set_fid.? }; } /// When a namespaced import (`Ns :: #import "..."`) contains foreign-class /// declarations, ALSO register them under their qualified name `Ns.Class` /// so receiver types like `*Ns.Class` can find the fcd. The recursive /// scan/lower already handles bare-name registration; this only adds the /// qualified-name entry, so cross-class refs in method signatures /// (`*View` → bare lookup) still work. pub fn registerNamespacedForeignClasses(self: *Lowering, ns: ast.NamespaceDecl) void { for (ns.decls) |inner| { if (inner.data == .foreign_class_decl) { const fcd = &inner.data.foreign_class_decl; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ns.name, fcd.name }) catch fcd.name; self.program_index.foreign_class_map.put(qualified, fcd) catch {}; } else if (inner.data == .namespace_decl) { // Nested namespaces — qualify with both prefixes. self.registerNamespacedForeignClasses(inner.data.namespace_decl); } } } // ── JNI main stubs ───────────────────────────────────────────── pub fn synthesizeJniMainStubs(self: *Lowering) void { var seen = std.StringHashMap(void).init(self.alloc); defer seen.deinit(); var it = self.program_index.foreign_class_map.iterator(); while (it.next()) |entry| { const fcd = entry.value_ptr.*; if (!fcd.is_main) continue; if (fcd.is_foreign) continue; if (fcd.runtime != .jni_class) continue; if (seen.contains(fcd.foreign_path)) continue; seen.put(fcd.foreign_path, {}) catch continue; for (fcd.members) |m| switch (m) { .method => |md| { if (md.body == null) continue; if (md.is_static) continue; // future: emit static native ABI without `self` self.synthesizeJniMainStub(fcd, md); }, else => {}, }; } } pub fn synthesizeJniMainStub(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void { const mangled = jni_descriptor.jniMangleNativeName(self.alloc, fcd.foreign_path, md.name) catch return; const name_id = self.module.types.internString(mangled); const ptr_void = self.module.types.ptrTo(.void); var params = std.ArrayList(inst_mod.Function.Param).empty; params.append(self.alloc, .{ .name = self.module.types.internString("env"), .ty = ptr_void, }) catch return; params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void, }) catch return; // User's declared params (skip the implicit `*Self` at index 0 for // instance methods — we synthesized `self` above as the jobject). const param_start: usize = 1; for (md.params[param_start..], 0..) |p_node, i| { const pty = jniMapParamType(self, p_node); params.append(self.alloc, .{ .name = self.module.types.internString(md.param_names[param_start + i]), .ty = pty, }) catch return; } const ret_ty = if (md.return_type) |rt| jniMapParamType(self, rt) else .void; const params_slice = params.toOwnedSlice(self.alloc) catch return; _ = self.builder.beginFunction(name_id, params_slice, ret_ty); self.builder.currentFunc().linkage = .external; self.builder.currentFunc().call_conv = .c; const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); var scope = Scope.init(self.alloc, self.scope); defer scope.deinit(); const saved_scope = self.scope; self.scope = &scope; defer self.scope = saved_scope; for (params_slice, 0..) |p, i| { const slot = self.builder.alloca(p.ty); const param_ref = Ref.fromIndex(@intCast(i)); self.builder.store(slot, param_ref); scope.put(self.module.types.getString(p.name), .{ .ref = slot, .ty = p.ty, .is_alloca = true }); } // Push the JNIEnv* arg onto the lexical `#jni_env` stack so the // method body's `#jni_call(...)` / `super.method(...)` sites pick // it up without an explicit `#jni_env(env) { ... }` wrapper. The // JNI runtime guarantees the env passed to a native method is // valid for the calling thread. const env_slot = scope.lookup("env").?.ref; const env_loaded = self.builder.load(env_slot, ptr_void); const env_stack_base = self.jni_env_stack_base; self.jni_env_stack_base = self.jni_env_stack.items.len; self.jni_env_stack.append(self.alloc, env_loaded) catch {}; defer { _ = self.jni_env_stack.pop(); self.jni_env_stack_base = env_stack_base; } // Record method context so `super.method(args)` inside the body // can find the parent class (via `#extends`) and the method's // signature. const saved_fcd = self.current_foreign_class; const saved_method = self.current_foreign_method; self.current_foreign_class = fcd; self.current_foreign_method = md; defer { self.current_foreign_class = saved_fcd; self.current_foreign_method = saved_method; } // JNI native methods are C-callable entry points — install the // static default Context so `context.X` reads in the method body // resolve through `current_ctx_ref`. Mirror the same binding // `lowerFunction` does for callconv(.c) / isExportedEntryName. const saved_ctx_ref_jni = self.current_ctx_ref; defer self.current_ctx_ref = saved_ctx_ref_jni; if (self.implicit_ctx_enabled) { if (self.program_index.global_names.get("__sx_default_context")) |dctx_gi| { self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, ptr_void); } } const saved_target = self.target_type; self.target_type = if (ret_ty != .void) ret_ty else null; if (ret_ty != .void) { const body_val = self.lowerBlockValue(md.body.?); if (!self.currentBlockHasTerminator()) { if (body_val) |val| { const val_ty = self.builder.getRefType(val); if (val_ty == .void) { self.ensureTerminator(ret_ty); } else { const coerced = self.coerceToType(val, val_ty, ret_ty); self.builder.ret(coerced, ret_ty); } } else { self.ensureTerminator(ret_ty); } } } else { self.lowerBlock(md.body.?); self.ensureTerminator(ret_ty); } self.target_type = saved_target; self.builder.finalize(); } /// 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); }