A closure's underlying function carries a hidden env arg that a bare (T)->U slot
doesn't pass, so a closure flowing into a bare function-type slot dropped the
env — the first user arg landed in the env slot and the rest read garbage
(apply(closure((x)->s64 { x*2 })) returned 192 instead of 10; non-failable too).
- createClosureToBareFnAdapter: a capture-free closure into a bare (T)->U slot is
bridged by a generated adapter carrying the bare ABI (forwards a null env);
lowerLambda returns its func_ref. Rejected (no silent miscompile): a capturing
closure into a bare slot (env has nowhere to live) and a failable closure into
a non-failable slot (the ERR E5.1 FFI-boundary rule).
- Arrow-body failable closures (-> (T,!) => expr) now wrap the bare success value
into {value, 0} via lowerFailableSuccessReturn (the implicit return previously
returned a malformed tuple → caught value read as 0).
The isLambda .bang parser fix (failable closure literals parse) already landed in
485b4fa. Regressions: examples/0309-closures-literal-as-bare-fn-param (non-
failable, block + arrow, called in callee) + 1039-errors-failable-closure-literal
(failable, block + arrow, direct + Closure(...) param). Resolves issue 0060
(remaining E5.1 follow-ups noted in the .md). Suite: 328 passed.
114 lines
5.9 KiB
Markdown
114 lines
5.9 KiB
Markdown
# 0060 — closure-literal composition miscompiles (blocks ERR/E5.1)
|
|
|
|
> **✅ RESOLVED.** A closure's underlying function carries a hidden `env` arg
|
|
> that a bare `(T) -> U` slot doesn't pass, so a closure flowing into a bare
|
|
> function-type slot dropped the env (the first user arg landed in the env slot;
|
|
> the rest read garbage). Fixes (all in this commit):
|
|
> - **`src/parser.zig`** — `isLambda` now accepts `.bang` in the return-type
|
|
> lookahead, so failable closure literals (`-> !` / `-> (T, !)`) parse.
|
|
> - **`src/ir/lower.zig`** — `createClosureToBareFnAdapter`: a capture-free
|
|
> closure flowing into a bare `(T) -> U` slot is bridged by a generated adapter
|
|
> carrying the bare ABI (forwards a null env). `lowerLambda` returns the
|
|
> adapter `func_ref` for that case. Rejected (no silent miscompile): a
|
|
> **capturing** closure into a bare slot (env has nowhere to live), and a
|
|
> **failable** closure into a **non-failable** slot (the FFI-boundary rule).
|
|
> - **`src/ir/lower.zig`** — arrow-body failable closures (`-> (T, !) => expr`)
|
|
> now wrap the bare success value into `{value, 0}` via
|
|
> `lowerFailableSuccessReturn` (the implicit return previously coerced a bare
|
|
> value into the failable tuple and returned `0`).
|
|
>
|
|
> Regression tests: `examples/0309-closures-literal-as-bare-fn-param.sx`
|
|
> (non-failable, block + arrow, called inside the callee) and
|
|
> `examples/1039-errors-failable-closure-literal.sx` (failable closures, block +
|
|
> arrow, direct + `Closure(...)` param).
|
|
>
|
|
> **Remaining E5.1 follow-up (not 0060):** calling a **bare** failable
|
|
> function-type param (`cb: (s64) -> (s64, !E)`) resolves the call result as
|
|
> `unresolved` (the idiomatic `Closure(s64) -> (s64, !E)` form works); the
|
|
> non-failable→failable widening adapter is currently *rejected* rather than
|
|
> generated; and the program-wide SCC union per closure shape is unimplemented.
|
|
|
|
## Symptom
|
|
|
|
A `closure(...)` literal passed **directly as a function-type argument**, where
|
|
the callee invokes it, produces wrong values. Surfaced while implementing ERR
|
|
E5.1 (composition with closures), but it is **not** error-specific — plain
|
|
non-failable closures miscompile too.
|
|
|
|
`issues/0060-closure-literal-composition-miscompiles.sx`:
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
apply :: (f: (s64) -> s64) -> s64 { return f(5); }
|
|
main :: () {
|
|
print("block={}\n", apply(closure((x: s64) -> s64 { return x * 2; }))); // want 10
|
|
print("arrow={}\n", apply(closure((x: s64) -> s64 => x * 2))); // want 10
|
|
}
|
|
```
|
|
|
|
- **Expected:** `block=10`, `arrow=10`.
|
|
- **Actual:** `block=192`, `arrow=20` (exit 0 — silent miscompile, no diagnostic).
|
|
|
|
**Working contrast:** `examples/0302-closures-closures.sx` —
|
|
`apply :: (f, x) -> s64 { return f(x); }` called as `apply(closure(... => ...), 10)`
|
|
works. There the piped value arrives as a *separate* argument; here the callee
|
|
calls the closure param with a *literal*, and the literal/closure-env marshalling
|
|
is wrong. Likely an env/arg-slot mixup when a closure literal is materialized as a
|
|
call argument and then invoked with a constant inside the callee.
|
|
|
|
## Failable-closure follow-ons (the actual E5.1 surface)
|
|
|
|
Failable closures (`closure((x) -> (T, !) { ... })`) are the point of E5.1.
|
|
Two further gaps sit on top of 0060:
|
|
|
|
1. **Parser — `isLambda` doesn't accept a `!` return type.** A closure/lambda
|
|
literal with `-> !` / `-> (T, !)` fails to parse ("expected ','") because the
|
|
return-type token-skipper in `isLambda` (`src/parser.zig`, the `arrow` branch
|
|
~line 3302) omits `.bang`. One-line fix:
|
|
|
|
```zig
|
|
// self.current.tag == .star or self.current.tag == .question)
|
|
// becomes:
|
|
self.current.tag == .star or self.current.tag == .question or
|
|
self.current.tag == .bang)
|
|
```
|
|
|
|
With that patch, failable closure literals parse and **block-body, directly-
|
|
called** ones work end-to-end (success / `catch` / `or` all correct).
|
|
|
|
2. **Arrow-body failable closures miscompile.** After the parser patch,
|
|
`n := closure((x: s64) -> (s64, !E) => x + 1); n(40) catch e 0` returns `0`
|
|
instead of `41` — the value slot reads as undef/0. Block-body equivalents
|
|
are correct, so it's an arrow-body (`=>`) failable-closure lowering bug
|
|
(the expression-body return isn't assembled into the `{value, error}` tuple
|
|
the same way the block-body path does). Compare `lowerLambda`
|
|
(`src/ir/lower.zig` ~7617) block vs arrow return handling against the named-
|
|
function failable return path (`lowerFailableSuccessReturn`).
|
|
|
|
## Investigation prompt (paste into a fresh session)
|
|
|
|
> Closure literals passed as a function-type argument miscompile when the callee
|
|
> calls them: `apply :: (f: (s64)->s64) -> s64 { return f(5); }` then
|
|
> `apply(closure((x: s64) -> s64 { return x*2; }))` prints 192 (want 10); the
|
|
> arrow form prints 20. The working pattern (examples/0302) passes the value as a
|
|
> separate arg. Suspect the closure-literal-as-call-argument lowering: the
|
|
> closure env / the inner call's constant argument is marshalled into the wrong
|
|
> slot. Look at how a `closure(...)` literal in argument position is lowered
|
|
> (closure construction + the `Closure` calling convention) in `src/ir/lower.zig`
|
|
> / `src/ir/emit_llvm.zig`, vs the working separate-arg path.
|
|
>
|
|
> Then unblock ERR/E5.1: (a) apply the one-line `isLambda` `.bang` patch above;
|
|
> (b) fix arrow-body failable closure lowering (returns 0). Verify with a new
|
|
> `examples/XXXX-errors-failable-closure-literal.sx`: a block-body and an
|
|
> arrow-body failable closure, called directly, consumed by `catch` / `or`; and a
|
|
> failable closure passed as a `(T)->(U,!)` parameter and `try`-called inside the
|
|
> callee.
|
|
|
|
## Impact
|
|
|
|
Blocks ERR/E5.1 (composition with closures/methods/generics): every E5.1
|
|
sub-feature — failable closures as parameters, the program-wide SCC union per
|
|
closure shape, the FFI rejection check, and the non-failable→failable widening
|
|
adapter — needs closure literals to compose correctly first. Per the project's
|
|
impassable rule, E5.1 is paused here rather than built on miscompiling closures.
|