100/100 regression tests pass; ffi-objc-call-02-void-return flips
from xfail (codegen rejection) to passing ("ok").
Lowering for `#objc_call(void)(recv, "selector:")` lands in
lower.zig as `lowerFfiIntrinsicCall`:
%sel = call ptr @sel_registerName(<"selector:">)
%call = call ptr @objc_msgSend(<recv>, %sel)
Two extern decls (`sel_registerName(*u8) -> *void` and
`objc_msgSend(*void, *void) -> *void`) are declared lazily and
cached on the Lowering instance via `objc_msg_send_fid` /
`sel_register_name_fid`, so multiple call sites share one
declaration each.
Phase 1.3 deliberately keeps scope tight: only `void` return + just
(recv, selector) arity is wired. Non-void returns + variadic arity
fall through with a diagnostic and are owned by subsequent phase-1
steps (1.6 primitive returns; 1.7..1.9 struct shapes; 1.10 multi-
keyword selectors).
Selector resolution is still per-call-site `sel_registerName` —
the planned 1.5 interning turns the per-call hashtable lookup into
a single static-global load. Chess Android + iOS-sim builds clean
— no regression on the existing typed-`objc_msgSend`-cast pattern.
100/100 regression tests pass (+ffi-objc-call-02-void-return xfail
snapshot).
The intrinsic with no `inline if false` guard reaches sema/codegen
and trips an "unresolved: 'unknown_expr'" — the FfiIntrinsicCall
AST node from Phase 1.1 has no lowering rules in lower.zig /
emit_llvm.zig yet.
nil receiver was chosen so the test doesn't need a real Obj-C
object graph: the runtime guarantees `[nil msg]` is a no-op with
zero result for void returns. macOS-gated via `inline if OS == .macos`
so the runner stays portable.
Next commit: emit_llvm.zig produces the per-call-site
%sel = call ptr @sel_registerName(ptr "init.0")
call void @objc_msgSend(ptr null, ptr %sel)
lowering. Snapshot flips to "ok". Selector interning (one shared
global per unique selector string) lands as a separate step (1.5).