Files
sx/src/ir/lower/ffi.zig
agra 82500931ce cleanup(B-review): fix stale section banners, merge alias block, prune imports
Review follow-up to the ARCH-B split (comment/import hygiene only, no
code changes):

- Section banners that travelled to the wrong file with the B1-B8 cuts
  are reworded to describe the section that actually follows (e.g.
  stmt.zig's trailing "Expression lowering", expr.zig's "Control flow"
  before lowerChainedComparison) or deleted where nothing follows
  (4 trailing-at-EOF banners). ffi.zig's facade note no longer claims
  the IMP builders "stay here" (they live in lower/objc_class.zig);
  protocol.zig's namespace-lookup banner now points at
  pack.zig:resolvePackProjection for the orchestrator.
- lower.zig's two lower/expr.zig alias blocks (B8.1 + B8.2 appends)
  merged into one.
- 448 unused header decls pruned from the 15 lower/*.zig files (each
  had inherited lower.zig's full import block; pruned to fixpoint so
  cascading type-extraction consts went too).

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 16:09:24 +03:00

1179 lines
52 KiB
Zig

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_<sel>`
/// 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_<Cls>`.
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 <ABI(T)> @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 = &registry,
}, 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
// Call<T>Method 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(<name>)` 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 `*<Cls>` or `?*<Cls>`) 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("<init>", "(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 = &registry,
}, 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("<init>");
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 +
/// CallNonvirtual<T>Method` 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 = &registry,
}, 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 `<ClassName>.<methodName>`. Lazy lowering
/// then handles the body via the standard path; `*Self` is
/// substituted to `*<ClassName>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 `__<ClassName>_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 `__<ClassName>_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 `<ClassName>.<methodName>`, 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 (`<ClassName>.<methodName>`), 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);
}