File four issue write-ups discovered alongside the 0179 work: - 0185: binary-op operand auto-unwrap silently miscompiles a NULL ?T - 0186: closure VALUE call does not coerce arg to ?T parameter - 0187: lambda with inferred return type + block body with early returns mis-infers its return type - 0188: closure-VALUE calls skip argument validation (arity + tuple spread)
3.0 KiB
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):
- 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.
- Runtime-tuple spread
f(..tuple)is never expanded for a closure value. The spread leaves aRef.noneplaceholder (call.zig~line 404) that thecall_closuresites emit asundef, so the call passes garbage.
Reproduction
#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):
- Add an arity check against the callee value's
closure.params/function.paramslength (mirrorcheckCallArityused for top-level fns), accounting for the implicit__sx_ctxslot. - 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.noneplaceholder asundef. - Regression: extend
examples/closures/0312-...or addexamples/closures/03xx-closure-value-arity.sxcovering 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.