# 0188 — closure-VALUE calls skip argument validation: no arity check + runtime-tuple spread not expanded ## Symptom Calling a closure VALUE (a `:=`-bound lambda, struct-field closure, fn-pointer value) does NOT validate arguments the way a top-level function call does. Two distinct gaps, both pre-existing (surfaced during the adversarial review of the issue-0186 fix; 0186 fixed only arg COERCION for correctly-counted args): 1. **No arity check.** A closure value called with the WRONG number of args compiles and silently drops/ignores extras (or reads garbage for missing ones), exit 0. A top-level fn call diagnoses arity. 2. **Runtime-tuple spread `f(..tuple)` is never expanded for a closure value.** The spread leaves a `Ref.none` placeholder (`call.zig` ~line 404) that the `call_closure` sites emit as `undef`, so the call passes garbage. ## Reproduction ```sx #import "modules/std.sx"; main :: () { // (1) arity: extra arg silently dropped one := (a: i64) -> i64 => a; print("arity: {}\n", one(1, 2)); // prints 1 (no error; `2` dropped) // (2) spread into a closure value → garbage add := (a: i64, b: i64) -> i64 => a + b; pair := (10, 20); print("spread: {}\n", add(..pair)); // prints garbage (e.g. -17590042754976), not 30 } ``` Expected: (1) an arity diagnostic (as for a top-level fn); (2) `add(..pair)` expands to `add(10, 20)` → `30`, OR a clear diagnostic that spread into a closure value is unsupported (never silent garbage). ## Root cause (hypothesis) The closure-value call paths in `src/ir/lower/call.zig` (the three `call_closure` emission sites + the local `call_indirect` fn-pointer path) build the arg list and emit directly without (a) an arity check against `closure.params.len` / `function.params.len`, and (b) without running the runtime-slice/tuple spread expansion that the normal call path uses (`packVariadicCallArgs` / the `Ref.none` spread placeholder is never resolved for closures). The pack-spread `..xs` path (`packSpreadRefs`) handles comptime packs but not a runtime tuple value spread into a closure. ## Investigation prompt In `src/ir/lower/call.zig`, for each closure-value / fn-pointer-value call site (grep `call_closure` and the local `call_indirect` path ~line 655): 1. Add an arity check against the callee value's `closure.params` / `function.params` length (mirror `checkCallArity` used for top-level fns), accounting for the implicit `__sx_ctx` slot. 2. Either expand a runtime-tuple/slice spread argument into positional args for closure values (as the normal call path does), or emit a located diagnostic that spread into a callable value is unsupported — never emit the `Ref.none` placeholder as `undef`. 3. Regression: extend `examples/closures/0312-...` or add `examples/closures/03xx-closure-value-arity.sx` covering both the arity diagnostic and the spread behavior. Unrelated to the arg-COERCION fix (issue 0186, already landed) — that fix correctly coerces a correctly-COUNTED arg; these gaps are about COUNT and spread expansion.