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).
42 lines
2.0 KiB
Markdown
42 lines
2.0 KiB
Markdown
# 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
|
|
|
|
```sx
|
|
#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)` → `.closure` →
|
|
`call_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.
|