issue-0038: closure free-var analysis skips FfiIntrinsicCall nodes

Surfaced while writing the Phase 1.11 in-construct test. The
closure free-variable analyzer doesn't recursively visit the
`ffi_intrinsic_call` AST node introduced in Phase 1.1, so any
identifier used inside `#objc_call` / `#jni_call` /
`#jni_static_call` from a closure body trips:

  error: unresolved: '<name>'

The same identifier captured from the same scope into a plain
expression resolves fine — so the bug is localized to whatever
recursive arm-walk powers the capture analysis.

Likely fix: add an `ffi_intrinsic_call => { ... }` arm wherever
the `.call =>` arm visits `callee` + `args`. Candidate files:
  - src/sema.zig (capture / scope tracking)
  - src/ir/lower.zig (closure body lowering / `lowerLambda`)
Both should be checked.

Workaround in the meantime: reach the captured value via a
module-level global from inside the closure body. See the
`g_hasher_recv` pattern in
examples/ffi-objc-call-09-in-construct.sx for an applied
instance.
This commit is contained in:
agra
2026-05-19 18:57:26 +03:00
parent f4b6cdae18
commit 39b1bd03a6
3 changed files with 43 additions and 0 deletions

41
examples/issue-0038.sx Normal file
View File

@@ -0,0 +1,41 @@
// Closure capture analysis doesn't trace into the `FfiIntrinsicCall`
// AST node — identifiers used inside `#objc_call` / `#jni_call` /
// `#jni_static_call` from a closure body aren't recognized as
// captured variables. Surfaced when writing `ffi-objc-call-09-in-
// construct.sx`.
//
// Reduced repro: capture in a closure body works fine for
// "normal" expressions (see `passthrough_works`), but the same
// capture inside `#objc_call`'s arg list trips
// "unresolved: 'recv'" (see `passthrough_via_objc_call` — would
// fail at parse time, so it's commented out).
//
// Likely fix: in the closure free-variable analyzer (sema.zig /
// lower.zig), add a recursive arm for `ffi_intrinsic_call` that
// visits `return_type` + every `args[i]` the same way the `.call`
// arm walks `callee` + `args`.
#import "modules/std.sx";
#import "modules/compiler.sx";
passthrough_works :: (recv: *void) -> Closure(s32) -> *void {
closure((d: s32) -> *void => recv); // captures `recv` — fine
}
// passthrough_via_objc_call :: (recv: *void) -> Closure(s32) -> s64 {
// // Same `recv` capture, but inside `#objc_call(...)`:
// // error: unresolved: 'recv'
// closure((d: s32) -> s64 => #objc_call(s64)(recv, "hash"));
// }
main :: () -> s32 {
inline if OS == .macos {
f := passthrough_works(null);
p := f(0);
print("ok (passthrough works) = {}\n", p == null);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
ok (passthrough works) = true