Files
sx/examples/ffi-objc-call-09-in-construct.sx
agra df2ccf77bd issue-0038 fixed: closure capture through FfiIntrinsicCall args
`collectCaptures` in `src/ir/lower.zig` was the closure free-variable
analyzer that decides which names from a closure body need to be
boxed into the env struct at lambda-build time. Its switch on AST
node kind enumerated every other shape (`.call`, `.if_expr`,
`.match_expr`, `.for_expr`, etc.) but no arm for `.ffi_intrinsic_call`,
so the trailing `else => {}` quietly dropped its `args[]` and
`return_type` walks. Names referenced inside `#objc_call(T)(recv,
"sel:", ...)` from a closure body never made it into the captures
list, so when lowering bound the closure scope from env, those names
came back as "unresolved".

The fix adds the missing arm — walk `return_type` and every `args[i]`
the same way `.call` walks `callee` + `args`.

Companion changes:
- `examples/issue-0038.sx` → `examples/103-ffi-closure-capture.sx`
  (out of the open-issue namespace; comment header tightened to
  describe the feature, not the historical bug).
- `examples/ffi-objc-call-09-in-construct.sx` drops the
  `g_hasher_recv` module-global workaround that was added for this
  bug — the closure now captures `recv` from `make_hasher`'s arg
  list normally.
2026-05-19 21:14:31 +03:00

81 lines
2.9 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Phase 1 steps 1.111.13 (PLAN-FFI.md): `#objc_call` call sites
// embedded inside the sx surface constructs. None touch a new ABI
// path — the lowering routes the call identically regardless of
// the enclosing scope, and this test pins that lemma.
//
// 1. Struct method body Probe.fetch
// 2. Protocol impl method body impl Hashable for Probe
// 3. Closure value body closure that calls hash
//
// 1.14 (separate test): `inline if OS == { case }` gating across
// targets — verified by `tests/cross_compile.sh`.
#import "modules/std.sx";
#import "modules/compiler.sx";
#import "modules/std/objc.sx";
// ── 1. Struct method calling #objc_call ─────────────────────────────
Probe :: struct {
receiver: *void = null;
fetch :: (self: *Probe) -> s64 {
#objc_call(s64)(self.receiver, "hash");
}
}
// ── 2. Protocol impl method ────────────────────────────────────────
Hashable :: protocol {
sx_hash :: (self: *Self) -> s64;
}
impl Hashable for Probe {
sx_hash :: (self: *Probe) -> s64 {
#objc_call(s64)(self.receiver, "hash") * 2;
}
}
// ── 3. Closure body invoking #objc_call ─────────────────────────────
// The closure captures `recv` from its enclosing function and
// references it inside the `#objc_call` arg list. Locked in by
// `examples/103-ffi-closure-capture.sx`.
make_hasher :: (recv: *void) -> Closure(s32) -> s64 {
closure((dummy: s32) -> s64 => #objc_call(s64)(recv, "hash"));
}
// ── 4. Generic function body — instantiated per call site ───────────
hash_through :: (recv: $T) -> s64 {
p : *void = xx recv;
#objc_call(s64)(p, "hash");
}
main :: () -> s32 {
inline if OS == .macos {
ns_object := objc_getClass("NSObject".ptr);
p : Probe = .{ receiver = ns_object };
// 1. struct method
h1 := p.fetch();
print("fetch != 0 = {}\n", h1 != 0);
// 2. protocol method (doubles the raw hash; mostly checking
// dispatch / arg threading, not the math)
h2 := p.sx_hash();
print("protocol h2 = {}\n", h2 == h1 * 2);
// 3. closure (receives a dummy arg to keep the `Closure(T) -> R`
// arity matching 35-closures.sx; `recv` is captured from
// `make_hasher`'s arg list and used inside the `#objc_call`).
hasher := make_hasher(ns_object);
h3 := hasher(0);
print("closure h3 = {}\n", h3 == h1);
// 4. generic function — instantiates with T = *void here
h4 := hash_through(ns_object);
print("generic h4 = {}\n", h4 == h1);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0;
}