From 39b1bd03a617132f3a9eb966f8bfc2305611082d Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 19 May 2026 18:57:26 +0300 Subject: [PATCH] issue-0038: closure free-var analysis skips FfiIntrinsicCall nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: '' 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. --- examples/issue-0038.sx | 41 ++++++++++++++++++++++++++++++++++ tests/expected/issue-0038.exit | 1 + tests/expected/issue-0038.txt | 1 + 3 files changed, 43 insertions(+) create mode 100644 examples/issue-0038.sx create mode 100644 tests/expected/issue-0038.exit create mode 100644 tests/expected/issue-0038.txt diff --git a/examples/issue-0038.sx b/examples/issue-0038.sx new file mode 100644 index 0000000..bd613bf --- /dev/null +++ b/examples/issue-0038.sx @@ -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; +} diff --git a/tests/expected/issue-0038.exit b/tests/expected/issue-0038.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/issue-0038.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/issue-0038.txt b/tests/expected/issue-0038.txt new file mode 100644 index 0000000..6d77b8c --- /dev/null +++ b/tests/expected/issue-0038.txt @@ -0,0 +1 @@ +ok (passthrough works) = true