ffi 3.2 A2: implement #selector("explicit:string") override
Make-green half of the cadence step started in A1. Wires the
`#selector` directive end-to-end:
- Lexer token `hash_selector` at src/token.zig + lookup row in
src/lexer.zig.
- AST field `selector_override: ?[]const u8 = null` on
`ForeignMethodDecl` (src/ast.zig).
- Parser block in src/parser.zig that mirrors
`#jni_method_descriptor` — both occupy the same slot after the
optional `-> ReturnType` and before the body/terminator. Not
mutually exclusive at parse time.
- LSP semantic-token list (src/lsp/server.zig) updated.
- Lowering: `deriveObjcSelector` returns
`{ sel, keyword_count, is_override }`. When `is_override` is true,
the selector string is the user's literal and `keyword_count` is
the colon count in that literal. Both `lowerObjcMethodCall` and
`lowerObjcStaticCall` use the result.
Diagnostic policy when override colon-count ≠ call arity:
- Default mangling path: stays an error (`.err`). The user can fix
the sx-side name to produce the right keyword count.
- Override path: downgrades to a warning (`.warn`). Rationale:
Obj-C's `objc_msgSend` doesn't validate colon-vs-arg the way JNI's
`GetMethodID` validates the descriptor — the runtime dispatches
regardless and the wrong-arity case becomes silent calling-
convention corruption. The compiler is the last line of defense
for this typo class, but the warning preserves the override's
escape-hatch character (deliberate mismatches still proceed).
Snapshot for `examples/ffi-objc-dsl-06-selector-override.sx` flips
from the pre-3.2 parser-error to working output:
static override non-null: true
The mismatch diagnostic text in
`examples/ffi-objc-dsl-04-mismatch.sx`'s snapshot is updated to
drop the "once that lands (3.2)" phrasing now that 3.2 is here.
165/165 example tests.
This commit is contained in:
@@ -4491,32 +4491,41 @@ 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:
|
||||
/// Resolve the Obj-C selector for a foreign-class method, honoring
|
||||
/// any `#selector("...")` override on the declaration. When an
|
||||
/// override is present the selector string is the user's literal;
|
||||
/// `keyword_count` is the `:` count in the literal (so callers can
|
||||
/// still cross-check arity, downgrading the diagnostic to a
|
||||
/// warning). When no override exists, the default mangling rule
|
||||
/// runs:
|
||||
/// - niladic: name verbatim (`length` → `length`).
|
||||
/// - arity ≥ 1: split the name on `_`; each piece becomes a keyword
|
||||
/// with a trailing `:` (`addObject` → `addObject:`,
|
||||
/// - arity ≥ 1: split the sx 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 } {
|
||||
fn deriveObjcSelector(self: *Lowering, method: ast.ForeignMethodDecl, arity: usize) struct { sel: []const u8, keyword_count: usize, is_override: bool } {
|
||||
if (method.selector_override) |sel| {
|
||||
var colons: usize = 0;
|
||||
for (sel) |ch| {
|
||||
if (ch == ':') colons += 1;
|
||||
}
|
||||
return .{ .sel = sel, .keyword_count = colons, .is_override = true };
|
||||
}
|
||||
if (arity == 0) {
|
||||
return .{ .sel = method_name, .keyword_count = 0 };
|
||||
return .{ .sel = method.name, .keyword_count = 0, .is_override = false };
|
||||
}
|
||||
// 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| {
|
||||
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| {
|
||||
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 };
|
||||
out[method.name.len] = ':';
|
||||
return .{ .sel = out, .keyword_count = pieces, .is_override = false };
|
||||
}
|
||||
|
||||
/// Lower `inst.method(args)` on an `#objc_class` / `#objc_protocol`
|
||||
@@ -4533,22 +4542,35 @@ pub const Lowering = struct {
|
||||
span: ast.Span,
|
||||
) Ref {
|
||||
const arity = method_args.len;
|
||||
const derived = self.deriveObjcSelector(method.name, arity);
|
||||
const derived = self.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 niladic methods the selector has no `:`; we already
|
||||
// accept arity == 0 in the mangling above.
|
||||
// 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| {
|
||||
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 },
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
return Ref.none;
|
||||
}
|
||||
|
||||
const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void;
|
||||
@@ -4583,18 +4605,27 @@ pub const Lowering = struct {
|
||||
span: ast.Span,
|
||||
) Ref {
|
||||
const arity = method_args.len;
|
||||
const derived = self.deriveObjcSelector(method.name, arity);
|
||||
const derived = self.deriveObjcSelector(method, arity);
|
||||
|
||||
if (arity > 0 and derived.keyword_count != arity) {
|
||||
if (self.diagnostics) |d| {
|
||||
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(\"...\")` once that lands (3.2)",
|
||||
.{ fcd.name, method.name, derived.keyword_count, arity, arity },
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
return Ref.none;
|
||||
}
|
||||
|
||||
const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void;
|
||||
|
||||
Reference in New Issue
Block a user