ffi: resolve foreign-class member types through Self substitution (issue-0043)
`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.
This commit is contained in:
41
examples/138-foreign-class-chained-dispatch.sx
Normal file
41
examples/138-foreign-class-chained-dispatch.sx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -4528,6 +4528,42 @@ pub const Lowering = struct {
|
|||||||
return .{ .sel = out, .keyword_count = pieces, .is_override = false };
|
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`
|
/// Lower `inst.method(args)` on an `#objc_class` / `#objc_protocol`
|
||||||
/// receiver. The selector is derived by `deriveObjcSelector`; arity
|
/// receiver. The selector is derived by `deriveObjcSelector`; arity
|
||||||
/// is validated against the keyword count produced by the mangling
|
/// 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
|
// Cache the SEL slot per (selector-string, module) like
|
||||||
// `#objc_call` does. The mangling produces the literal selector
|
// `#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);
|
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
|
// Instance method call: obj.method(args) → look up StructName.method
|
||||||
{
|
{
|
||||||
var obj_ty = recv_ty;
|
var obj_ty = recv_ty;
|
||||||
@@ -10107,6 +10167,15 @@ pub const Lowering = struct {
|
|||||||
else => null,
|
else => null,
|
||||||
};
|
};
|
||||||
if (type_name) |tn| {
|
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);
|
const type_name_id = self.module.types.internString(tn);
|
||||||
if (self.module.types.findByName(type_name_id)) |ty| {
|
if (self.module.types.findByName(type_name_id)) |ty| {
|
||||||
const ti = self.module.types.get(ty);
|
const ti = self.module.types.get(ty);
|
||||||
|
|||||||
1
tests/expected/138-foreign-class-chained-dispatch.exit
Normal file
1
tests/expected/138-foreign-class-chained-dispatch.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
2
tests/expected/138-foreign-class-chained-dispatch.txt
Normal file
2
tests/expected/138-foreign-class-chained-dispatch.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
explicit-then-self ok
|
||||||
|
self-then-self ok
|
||||||
Reference in New Issue
Block a user