Files
sx/issues/0177-array-element-closure-direct-call-crashes.md
agra 3605165398 fix: dispatch unwrapped optional-closure call g!() through call_closure (issue 0170)
Calling through an unwrapped optional closure (g!()) crashed with LLVM
'Called function must be a pointer!': the indirect-call catch-all else
arm emitted call_indirect on the whole {fn,env} closure struct with a
hardcoded .i64 return. The else arm now inspects inferExprType(callee):
a .closure callee dispatches through call_closure (threads env + ctx via
the [ctx, env, user_args] ABI, returns closure.ret); a plain fn pointer
uses call_indirect with the callee's real function.ret instead of i64.

The filed repro's ?(() -> void) spelling is a tuple-optional (now
diagnosed by the 0165 fix); the real ?Closure(...) layout was already
correct. Verified load-bearing (HEAD crashes) by 3 adversarial reviews,
suite 785/0. Regression: examples/closures/0311-closures-optional-closure.sx.
Filed adjacent bug 0177 (array-element closure direct call crashes).
2026-06-23 01:02:13 +03:00

2.0 KiB

0177 — calling a closure stored in an array element directly (fns[i](args)) crashes / miscompiles

Symptom

A closure (or Closure(...)-typed value) stored in an array, called DIRECTLY via index (fns[i](args)), does not dispatch through the closure ABI: it emits a bare call_indirect on the whole {fn,env} struct → LLVM "Called function must be a pointer!" (verify fail) for some return/arg shapes, or returns garbage for others. Pre-existing (reproduces on master); distinct from issue 0170 (which fixed the unwrap-through-optional call g!()). Here the callee is a non-optional closure reached via array index, called directly without unwrap.

Reproduction

#import "modules/std.sx";
add :: (a: i64, b: i64) -> i64 { return a + b; }
main :: () {
  fns : [1](Closure(i64, i64) -> i64) = .{ add };
  print("{}\n", fns[0](3, 4));   // LLVM "Called function must be a pointer!" — expected 7
}

Expected: 7. Observed: LLVM verification failure (or, for other shapes, garbage return / f64-arg verify failure).

Investigation prompt

src/ir/lower/call.zig: issue 0170 added closure-vs-fn-pointer dispatch to the indirect-call catch-all else arm via inferExprType(callee).closurecall_closure. A direct call whose callee is an ARRAY-INDEX expression (fns[0]) of closure type apparently does not reach that dispatch — either it takes an earlier call arm that still emits call_indirect, or inferExprType(index_expr) does not return .closure so the else arm falls to the fn-pointer path. Trace which arm fns[0](args) lowers through and ensure a closure-typed callee — regardless of whether it's a bare ident, field access, index, or call result — dispatches through call_closure (threading env + ctx via the [ctx, env, user_args] ABI). Compare with the working arr[i]!() (unwrap) path. Follow the no-silent-fallback rule. Verify: fns[0](3,4) → 7; array-of-closure with captures; non-i64 returns (void/f64/struct); f64 args. Add an examples/closures/03xx-array-of-closures-call.sx regression.