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)
66 lines
3.0 KiB
Markdown
66 lines
3.0 KiB
Markdown
# 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.
|