ffi M1.2 A.2c + A.3: eager body lowering + self.field via state struct

Adds Pass 4b 'lowerObjcDefinedClassMethods' to lowerRoot: after
scan, walk objc_defined_class_cache and force-lower each bodied
instance method. The Obj-C runtime invokes these via the IMP
pointers wired up in A.4 — no sx-side call path drives lazy
lowering, so we trigger it here. Mirrors the JNI eager-lower
pattern in Pass 5.

Bug fix: lazyLowerFunction has its OWN inline body-lowering
path (separate from lowerFunction) that re-resolves param types
at line 1025. It was running without current_foreign_class set,
so '*Self' fell through to the type_bridge fallback and got
interned as a 0-field struct named 'Self' — body's
'self.counter' GEP'd into '{}' and LLVM verification rejected.
Fix: set current_foreign_class at the top of lazyLowerFunction
via the same lookupObjcDefinedClassForMethod path lowerFunction
uses. Save+restore via defer.

A.3 ('self.field access via the ivar') falls out for free —
'*Self' resolves to '*__SxFooState' so 'self.counter' is a
plain struct field access. IR snapshot in
142-objc-class-method-lowering.ir shows the round-trip:

    define internal void @SxFoo.bump(ptr, ptr self) {
        %gep = getelementptr inbounds { i32 }, ptr %self, 0, 0
        %v = load i32, ptr %gep
        store i32 (%v + 1), ptr %gep
        ret void
    }

171 examples pass (+1 from 142); zig build test green.

Still gated: Obj-C runtime dispatch (A.7) — sx-side
'f.bump()' calls bail at lower.zig:4407 with the existing
diagnostic. IMP-trampoline emission (the C-ABI shim that bridges
'objc_msgSend' → this body) lands in A.4 alongside class-pair
init.
This commit is contained in:
agra
2026-05-25 22:08:23 +03:00
parent ae1072d415
commit 659cdc2276
5 changed files with 863 additions and 0 deletions

View File

@@ -247,6 +247,12 @@ pub const Lowering = struct {
self.lowerDeferredTypeFns();
// Pass 4: target-specific entry-point sanity checks
self.checkRequiredEntryPoints();
// Pass 4b: eagerly lower bodied methods on sx-defined `#objc_class`
// declarations. The Obj-C runtime calls these via IMP pointers
// registered in M1.2 A.4 — no sx-side call path drives lazy
// lowering, so we trigger it here. Mirrors the JNI eager-lower
// pattern in Pass 5.
self.lowerObjcDefinedClassMethods();
// Pass 5: synthesize JNI-mangled exports for `#jni_main` bodied methods.
// Android's JNI runtime resolves `private native sx_<m>(...)` declared in
// the bundled classes.dex by looking up the symbol
@@ -886,6 +892,17 @@ pub const Lowering = struct {
fn lazyLowerFunction(self: *Lowering, name: []const u8) void {
// Already lowered?
if (self.lowered_functions.contains(name)) return;
// For sx-defined `#objc_class` methods, pin current_foreign_class
// so `*Self` substitutions in resolveTypeWithBindings find the
// state-struct type (M1.2 A.2b). The inline body-lowering path
// below re-resolves param types, so the context must be set
// BEFORE any resolveReturnType / resolveParamType call.
const saved_fc_lazy = self.current_foreign_class;
defer self.current_foreign_class = saved_fc_lazy;
if (self.lookupObjcDefinedClassForMethod(name)) |fcd| {
self.current_foreign_class = fcd;
}
// No AST? (builtins, foreign functions, or imported functions not in this file)
const fd = self.fn_ast_map.get(name) orelse return;
// Foreign declarations stay as extern stubs but need to be REGISTERED
@@ -11399,6 +11416,28 @@ pub const Lowering = struct {
/// receiver), followed by the user-declared params with pointer types
/// type-erased to `*void` (JNI carries jobjects, not sx-typed handles —
/// future work can keep richer typing inside the body when needed).
/// Eagerly lower bodied instance methods on every sx-defined
/// `#objc_class`. The Obj-C runtime invokes these via the IMP
/// pointers wired up in M1.2 A.4 — no sx-side call path triggers
/// lazy lowering, so we walk the cache and force-lower here.
/// `lowerFunction` sets `current_foreign_class` automatically based
/// on the qualified name, so `*Self` substitutions in the body
/// resolve correctly (M1.2 A.2b).
fn lowerObjcDefinedClassMethods(self: *Lowering) void {
for (self.module.objc_defined_class_cache.items) |entry| {
const fcd = entry.decl;
for (fcd.members) |m| {
const method = switch (m) {
.method => |md| md,
else => continue,
};
if (method.body == null) continue;
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, method.name }) catch continue;
self.lazyLowerFunction(qualified);
}
}
}
fn synthesizeJniMainStubs(self: *Lowering) void {
var seen = std.StringHashMap(void).init(self.alloc);
defer seen.deinit();