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.
1179 lines
52 KiB
Zig
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 = ®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
|
|
// 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 = ®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("<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 = ®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 `<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);
|
|
}
|