From 2b717d9b38d34ad5a69bf5aca6431ac7b6d1b71d Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 25 May 2026 17:52:53 +0300 Subject: [PATCH] ffi: resolve foreign-class member types through `Self` substitution (issue-0043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `inferExprType` for a chained call `Cls.static().instance(...)` never looked the inner call's foreign-class declaration up, so the outer dispatch saw a `.s64` receiver, the `foreign_class_map.get(...)` lookup missed, and lowering emitted `error: unresolved 'method'`. The macOS target appeared to work because `inline if OS == .ios { ... }` strips the gated body before lowering — eliding every call that would have exercised the broken path. The "lazy-lower" framing in the original issue file was a red herring. Fix in `src/ir/lower.zig`: 1. `inferExprType` for `.call` with `.field_access` callee now checks `foreign_class_map` for both shapes — `Cls.static_method(args)` (object identifier matches a foreign-class alias, look up static members) and `inst.instance_method(args)` (receiver is a pointer to a foreign-class struct, look up non-static members). 2. New helpers `resolveForeignMethodReturnType` and `resolveForeignClassMemberType` substitute `*Self` / `Self` to the foreign-class struct so a `*Self` return doesn't synthesize a phantom `Self`-named struct that future dispatches can't resolve. 3. The Obj-C lowering paths (`lowerObjcMethodCall`, `lowerObjcStaticCall`) route through the same helper for `ret_ty` so the IR Ref's type matches what `inferExprType` reports. Regression test at `examples/138-foreign-class-chained-dispatch.sx` exercises NSObject's `+alloc` / `-init` chain in both shapes — `*NSObject` return then `*Self` return, and `*Self` then `*Self`. Runs on the host (macOS) for live exercise; non-macOS hosts fall through to a stub matching the expected output. This unblocks Phase 3.2 C4/C5 — the `UIWindow.alloc().initWithWindowScene(scene)` pattern that surfaced the bug is the cluster's bread-and-butter shape. 167/167 example tests; chess builds clean on macOS, iOS-sim, Android. --- .../138-foreign-class-chained-dispatch.sx | 41 +++++++++++ src/ir/lower.zig | 73 ++++++++++++++++++- .../138-foreign-class-chained-dispatch.exit | 1 + .../138-foreign-class-chained-dispatch.txt | 2 + 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 examples/138-foreign-class-chained-dispatch.sx create mode 100644 tests/expected/138-foreign-class-chained-dispatch.exit create mode 100644 tests/expected/138-foreign-class-chained-dispatch.txt diff --git a/examples/138-foreign-class-chained-dispatch.sx b/examples/138-foreign-class-chained-dispatch.sx new file mode 100644 index 0000000..f940188 --- /dev/null +++ b/examples/138-foreign-class-chained-dispatch.sx @@ -0,0 +1,41 @@ +// Chained foreign-class method dispatch: `Cls.static().instance(...)` +// resolves the inner call's return type so the outer dispatch's +// receiver type is known. Pre-fix this collapsed to s64 in +// `inferExprType`, the foreign_class_map lookup missed, and lowering +// emitted `error: unresolved 'init'` (or 'initWithWindowScene' etc.) +// — see issues/0043 for the chess uikit.sx C4 migration that hit it. +// +// Two return-type shapes covered: explicit `*ClassName` (alloc here) +// and `*Self` (init). Both must propagate through the chain so the +// next `.method(...)` finds the foreign-class declaration. + +#import "modules/std.sx"; +#import "modules/compiler.sx"; + +NSObject :: #foreign #objc_class("NSObject") { + alloc :: () -> *NSObject; + init :: (self: *Self) -> *Self; +} + +NSObjectSelfReturn :: #foreign #objc_class("NSObject") { + alloc :: () -> *Self; + init :: (self: *Self) -> *Self; +} + +main :: () -> s32 { + inline if OS == .macos { + a := NSObject.alloc().init(); + if a != null { + print("explicit-then-self ok\n"); + } + b := NSObjectSelfReturn.alloc().init(); + if b != null { + print("self-then-self ok\n"); + } + } + inline if OS != .macos { + print("explicit-then-self ok\n"); + print("self-then-self ok\n"); + } + 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index b8fc904..77e85b4 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -4528,6 +4528,42 @@ pub const Lowering = struct { return .{ .sel = out, .keyword_count = pieces, .is_override = false }; } + /// Resolve a foreign-class member type, substituting `Self` (and `*Self`) + /// with the foreign class's own struct type. Without this substitution + /// chained calls like `Cls.alloc().init()` see the inner result as a + /// fictitious `Self` struct and the next dispatch lookup fails. + fn resolveForeignClassMemberType( + self: *Lowering, + fcd: *const ast.ForeignClassDecl, + type_node: *const ast.Node, + ) TypeId { + if (type_node.data == .type_expr and std.mem.eql(u8, type_node.data.type_expr.name, "Self")) { + return self.foreignClassStructType(fcd); + } + if (type_node.data == .pointer_type_expr) { + const pt = type_node.data.pointer_type_expr; + if (pt.pointee_type.data == .type_expr and std.mem.eql(u8, pt.pointee_type.data.type_expr.name, "Self")) { + return self.module.types.ptrTo(self.foreignClassStructType(fcd)); + } + } + return self.resolveType(type_node); + } + + fn resolveForeignMethodReturnType( + self: *Lowering, + fcd: *const ast.ForeignClassDecl, + method: ast.ForeignMethodDecl, + ) TypeId { + const rt = method.return_type orelse return .void; + return self.resolveForeignClassMemberType(fcd, rt); + } + + fn foreignClassStructType(self: *Lowering, fcd: *const ast.ForeignClassDecl) TypeId { + const name_id = self.module.types.internString(fcd.name); + if (self.module.types.findByName(name_id)) |existing| return existing; + return self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); + } + /// 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 @@ -4573,7 +4609,7 @@ pub const Lowering = struct { } } - const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void; + 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 @@ -4628,7 +4664,7 @@ pub const Lowering = struct { } } - const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void; + const ret_ty = self.resolveForeignMethodReturnType(fcd, method); const vptr_ty = self.module.types.ptrTo(.void); @@ -10075,6 +10111,30 @@ pub const Lowering = struct { } } } + // Foreign-class instance method: look up the method's + // declared return type so chained calls (e.g. + // `UIWindow.alloc().initWithWindowScene(scene)`) resolve. + { + var recv_inner = recv_ty; + if (!recv_inner.isBuiltin()) { + const ri = self.module.types.get(recv_inner); + if (ri == .pointer) recv_inner = ri.pointer.pointee; + } + if (!recv_inner.isBuiltin()) { + const inner_info = self.module.types.get(recv_inner); + if (inner_info == .@"struct") { + const sn = self.module.types.getString(inner_info.@"struct".name); + if (self.foreign_class_map.get(sn)) |fcd| { + for (fcd.members) |m| switch (m) { + .method => |md| if (!md.is_static and std.mem.eql(u8, md.name, cfa.field)) { + return self.resolveForeignMethodReturnType(fcd, md); + }, + else => {}, + }; + } + } + } + } // Instance method call: obj.method(args) → look up StructName.method { var obj_ty = recv_ty; @@ -10107,6 +10167,15 @@ pub const Lowering = struct { else => null, }; if (type_name) |tn| { + // Foreign-class static method: `Alias.static_method(args)`. + if (self.foreign_class_map.get(tn)) |fcd| { + for (fcd.members) |m| switch (m) { + .method => |md| if (md.is_static and std.mem.eql(u8, md.name, cfa.field)) { + return self.resolveForeignMethodReturnType(fcd, md); + }, + else => {}, + }; + } const type_name_id = self.module.types.internString(tn); if (self.module.types.findByName(type_name_id)) |ty| { const ti = self.module.types.get(ty); diff --git a/tests/expected/138-foreign-class-chained-dispatch.exit b/tests/expected/138-foreign-class-chained-dispatch.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/138-foreign-class-chained-dispatch.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/138-foreign-class-chained-dispatch.txt b/tests/expected/138-foreign-class-chained-dispatch.txt new file mode 100644 index 0000000..1c2c09e --- /dev/null +++ b/tests/expected/138-foreign-class-chained-dispatch.txt @@ -0,0 +1,2 @@ +explicit-then-self ok +self-then-self ok