fix(lower): closure literals compose with bare function-type slots (issue 0060)

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.
This commit is contained in:
agra
2026-06-01 20:35:25 +03:00
parent 485b4fa618
commit 06e2685350
11 changed files with 176 additions and 17 deletions

View File

@@ -1,5 +1,33 @@
# 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

View File

@@ -1,15 +0,0 @@
// Repro for issue 0060: a closure LITERAL passed directly as a function-type
// argument, where the callee calls it with a literal, miscompiles. The working
// contrast is examples/0302-closures-closures.sx, where the value flows in as a
// SEPARATE argument (`apply(f, x) { return f(x); }`).
//
// Expected: block=10, arrow=10. Actual: block=192, arrow=20.
#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
}