ffi 3.0: inst.method(args) DSL dispatch on #objc_class receivers
Implementation half of the cadence step started in the previous commit.
`lowerForeignMethodCall` for `#objc_class` / `#objc_protocol` runtimes
no longer bails; it routes through a new `lowerObjcMethodCall` helper
that derives the Obj-C selector from the sx method name and lowers to
`objc_msg_send` against the cached SEL slot (same intern path as
explicit `#objc_call`).
Default selector mangling (matches clang's keyword-method convention):
- Niladic (arity 0 excluding self): name verbatim. `length()` → "length".
- Arity ≥ 1: split the sx method name on `_`; each piece becomes a
keyword with a trailing `:`. `addObject(o)` → "addObject:";
`combine_and(a, b)` → "combine:and:";
`initWithFrame_options(f, o)` → "initWithFrame:options:".
Arity validation: keyword count (pieces from the `_`-split) must equal
call-site arity excluding self. Mismatch diagnoses at the call site
with a hint pointing at the forthcoming `#selector("...")` override
(Phase 3.2) for selectors that don't fit the underscore-split rule.
Mangling helper `deriveObjcSelector` and dispatch helper
`lowerObjcMethodCall` sit alongside `lowerForeignMethodCall`. The
existing fall-through diagnostic for non-JNI/non-Obj-C runtimes
remains for Swift (Phase 4 territory).
Tests `examples/ffi-objc-dsl-{01-niladic,02-one-arg,03-multi-keyword,
04-mismatch}.sx` snapshots flip from the pre-3.0 bail diagnostic
(exit=1) to working output (exit=0 for cases 01-03) and the specific
keyword-count mismatch diagnostic for case 04. Each test follows the
established pattern from `ffi-objc-call-08-multi-keyword.sx`:
synthesize a class at runtime via `objc_allocateClassPair` /
`class_addMethod`, declare a matching `#objc_class`, invoke the DSL
form. 163/163 tests; chess unaffected (JNI dispatch path untouched).
This commit is contained in:
@@ -472,23 +472,21 @@ JNI return + parameter type validation lives in lowering with source-
|
||||
spanned diagnostics; Call<T>Method coverage spans bool / s8 / s16 /
|
||||
u16 / s32 / s64 / f32 / f64 / pointer; varargs promotion is wired.
|
||||
|
||||
**Correction: Phase 3 step 3.0 had NOT landed at the time the previous
|
||||
checkpoint entry claimed it did.** [lower.zig's
|
||||
`lowerForeignMethodCall`](../src/ir/lower.zig#L4353) bails for any non-
|
||||
JNI runtime with `method calls on '{runtime}' runtime not yet supported
|
||||
(Phase 3/4)`; no commit in `git log` introduced an Obj-C DSL dispatch
|
||||
path; the planned regression files
|
||||
(`examples/ffi-objc-dsl-{01-niladic,02-one-arg,03-multi-keyword,04-mismatch}.sx`)
|
||||
didn't exist. As of this commit the four tests DO exist and snapshot
|
||||
the current bail diagnostic — they're the xfail half of the Phase 3.0
|
||||
cadence; the next commit implements the dispatch and the snapshots
|
||||
flip to the working output.
|
||||
Phase 3 step 3.0 landed (for real this time): `inst.method(args)` on
|
||||
an `#objc_class` / `#objc_protocol` receiver derives the selector via
|
||||
default mangling (niladic → name verbatim; arity ≥ 1 → split on `_`,
|
||||
each piece becomes a keyword with a trailing `:`) and lowers to
|
||||
`objc_msg_send` against the cached SEL slot. Arity mismatches diagnose
|
||||
at the call site with a remediation hint pointing at `#selector(...)`
|
||||
override (3.2). New helpers `deriveObjcSelector` and
|
||||
`lowerObjcMethodCall` at [lower.zig](../src/ir/lower.zig). Tests:
|
||||
`examples/ffi-objc-dsl-{01-niladic,02-one-arg,03-multi-keyword,04-mismatch}.sx`
|
||||
— landed previously as xfail-with-diagnostic, snapshots now flipped to
|
||||
working output (and the mismatch case to the specific keyword-count
|
||||
error).
|
||||
|
||||
Open work, in roughly the order they make sense:
|
||||
|
||||
- **Phase 3 step 3.0** — `inst.method(args)` DSL dispatch on
|
||||
`#objc_class` receivers with default selector mangling. The xfail
|
||||
tests are in place; next commit makes them green.
|
||||
- **Phase 3 step 3.1** — static call `Cls::class_method(args)` lowers
|
||||
to `#objc_call` on the class object (loaded via `objc_getClass` once
|
||||
and interned per module). Same pattern as 3.0 for the niladic /
|
||||
|
||||
@@ -4384,6 +4384,15 @@ pub const Lowering = struct {
|
||||
return Ref.none;
|
||||
}
|
||||
|
||||
// Obj-C instance dispatch (Phase 3 step 3.0). `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`. 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.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)});
|
||||
@@ -4455,6 +4464,85 @@ pub const Lowering = struct {
|
||||
} }, ret_ty);
|
||||
}
|
||||
|
||||
/// Derive the default Obj-C selector from a sx-side method name plus
|
||||
/// its call-site arity (excluding self). Mangling:
|
||||
/// - niladic: name verbatim (`length` → `length`).
|
||||
/// - arity ≥ 1: split the name on `_`; each piece becomes a keyword
|
||||
/// with a trailing `:` (`addObject` → `addObject:`,
|
||||
/// `combine_and` → `combine:and:`).
|
||||
/// Returns the selector string allocated on `self.alloc` and the
|
||||
/// number of keyword pieces (matters for arity validation; the
|
||||
/// niladic case returns 0).
|
||||
fn deriveObjcSelector(self: *Lowering, method_name: []const u8, arity: usize) struct { sel: []const u8, keyword_count: usize } {
|
||||
if (arity == 0) {
|
||||
return .{ .sel = method_name, .keyword_count = 0 };
|
||||
}
|
||||
// Each `_` in the sx name becomes a `:` (one-byte-for-one), plus
|
||||
// one trailing `:` regardless of how many pieces. Piece count
|
||||
// = (number of `_`) + 1.
|
||||
var pieces: usize = 1;
|
||||
for (method_name) |ch| {
|
||||
if (ch == '_') pieces += 1;
|
||||
}
|
||||
const out = self.alloc.alloc(u8, method_name.len + 1) catch unreachable;
|
||||
for (method_name, 0..) |ch, i| {
|
||||
out[i] = if (ch == '_') ':' else ch;
|
||||
}
|
||||
out[method_name.len] = ':';
|
||||
return .{ .sel = out, .keyword_count = pieces };
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
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.deriveObjcSelector(method.name, arity);
|
||||
|
||||
// Arity validation: the keyword count (number of `:` in the
|
||||
// selector) must equal the number of args passed at the call
|
||||
// site. For niladic methods the selector has no `:`; we already
|
||||
// accept arity == 0 in the mangling above.
|
||||
if (arity > 0 and derived.keyword_count != arity) {
|
||||
if (self.diagnostics) |d| {
|
||||
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(\"...\")` once that lands (3.2)",
|
||||
.{ fcd.name, method.name, derived.keyword_count, arity, arity },
|
||||
);
|
||||
}
|
||||
return Ref.none;
|
||||
}
|
||||
|
||||
const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void;
|
||||
|
||||
// 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 `Alias.new(args)` where `Alias` is a foreign-class identifier
|
||||
/// with `static new :: (...) -> *Self;` — JNI constructor dispatch:
|
||||
/// `FindClass + GetMethodID("<init>", "(args)V") + NewObject(env,
|
||||
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
0
|
||||
|
||||
@@ -1 +1 @@
|
||||
/Users/agra/projects/sx/examples/ffi-objc-dsl-01-niladic.sx:36:14: error: method calls on 'objc_class' runtime not yet supported (Phase 3/4)
|
||||
length = 42
|
||||
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
0
|
||||
|
||||
@@ -1 +1 @@
|
||||
/Users/agra/projects/sx/examples/ffi-objc-dsl-02-one-arg.sx:28:14: error: method calls on 'objc_class' runtime not yet supported (Phase 3/4)
|
||||
addObject(21) = 42
|
||||
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
0
|
||||
|
||||
@@ -1 +1 @@
|
||||
/Users/agra/projects/sx/examples/ffi-objc-dsl-03-multi-keyword.sx:27:14: error: method calls on 'objc_class' runtime not yet supported (Phase 3/4)
|
||||
combine_and(7, 42) = 742
|
||||
|
||||
@@ -1 +1 @@
|
||||
/Users/agra/projects/sx/examples/ffi-objc-dsl-04-mismatch.sx:18:14: error: method calls on 'objc_class' runtime not yet supported (Phase 3/4)
|
||||
/Users/agra/projects/sx/examples/ffi-objc-dsl-04-mismatch.sx:18:14: error: Obj-C selector for 'SxProbeMismatch.something_extra' has 2 keyword(s) but the call passes 1 argument(s); split the sx method name on '_' so it produces exactly 1 keyword(s), or override with `#selector("...")` once that lands (3.2)
|
||||
|
||||
Reference in New Issue
Block a user