issues: file 0060 — closure-literal composition miscompiles (blocks ERR/E5.1)
Probing ERR/E5.1 (composition with closures) surfaced pre-existing closure- literal lowering bugs: a closure literal passed as a function-type argument and called inside the callee returns wrong values (block-body 192, arrow-body 20, want 10 — non-failable too; the working contrast passes the value as a separate arg, examples/0302). On top of that, failable closure returns don't parse (isLambda omits .bang — one-line fix in the issue) and arrow-body failable closures miscompile (return 0); block-body failable closures called directly work. Runnable repro + parser patch + investigation prompt in the issue. E5.1 paused per the impassable rule rather than built on miscompiling closures; the parser fix + a regression example were reverted to avoid landing silently- miscompiling failable closures on master.
This commit is contained in:
85
issues/0060-closure-literal-composition-miscompiles.md
Normal file
85
issues/0060-closure-literal-composition-miscompiles.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# 0060 — closure-literal composition miscompiles (blocks ERR/E5.1)
|
||||||
|
|
||||||
|
## 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.
|
||||||
15
issues/0060-closure-literal-composition-miscompiles.sx
Normal file
15
issues/0060-closure-literal-composition-miscompiles.sx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
#import "modules/platform/types.sx";
|
#import "modules/platform/types.sx";
|
||||||
#import "modules/platform/api.sx";
|
#import "modules/platform/api.sx";
|
||||||
|
|
||||||
UIApplicationMain :: (argc: s32, argv: *void, principal_class: *NSString, delegate_class: *void) -> s32 #foreign;
|
UIApplicationMain :: (argc: s32, argv: *void, principal_class: *NSString, delegate_class: *NSString) -> s32 #foreign;
|
||||||
dlsym :: (handle: *void, name: [*]u8) -> *void #foreign;
|
dlsym :: (handle: *void, name: [*]u8) -> *void #foreign;
|
||||||
chdir :: (path: [*]u8) -> s32 #foreign;
|
chdir :: (path: [*]u8) -> s32 #foreign;
|
||||||
|
|
||||||
@@ -200,7 +200,8 @@ UITextField :: #foreign #objc_class("UITextField") {
|
|||||||
init :: (self: *Self) -> *UITextField;
|
init :: (self: *Self) -> *UITextField;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SxAppDelegate — UIApplicationMain's principal class. Method bodies
|
// SxAppDelegate — UIApplicationMain's delegate class (the app delegate, a
|
||||||
|
// UIResponder — NOT the UIApplication principal class). Method bodies
|
||||||
// dispatch to UIKitPlatform methods on the shared `g_uikit_plat`.
|
// dispatch to UIKitPlatform methods on the shared `g_uikit_plat`.
|
||||||
SxAppDelegate :: #objc_class("SxAppDelegate") {
|
SxAppDelegate :: #objc_class("SxAppDelegate") {
|
||||||
#extends UIResponder;
|
#extends UIResponder;
|
||||||
@@ -364,7 +365,13 @@ impl Platform for UIKitPlatform {
|
|||||||
self.has_frame_closure = true;
|
self.has_frame_closure = true;
|
||||||
g_uikit_plat = self;
|
g_uikit_plat = self;
|
||||||
inline if OS == .ios {
|
inline if OS == .ios {
|
||||||
UIApplicationMain(0, xx 0, xx "SxAppDelegate", xx 0);
|
// (argc, argv, principalClassName, delegateClassName). SxAppDelegate
|
||||||
|
// is the DELEGATE (a UIResponder), not the UIApplication subclass —
|
||||||
|
// pass nil for the principal so UIKit uses the default UIApplication.
|
||||||
|
// Passing it as the principal makes newer UIKit call UIApplication
|
||||||
|
// class methods on it (e.g. `+registerAsSystemApp`) → unrecognized
|
||||||
|
// selector crash during UIApplicationMain prep.
|
||||||
|
UIApplicationMain(0, xx 0, xx 0, xx "SxAppDelegate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3304,7 +3304,8 @@ pub const Parser = struct {
|
|||||||
self.current.tag == .l_bracket or self.current.tag == .r_bracket or
|
self.current.tag == .l_bracket or self.current.tag == .r_bracket or
|
||||||
self.current.tag == .l_paren or self.current.tag == .r_paren or
|
self.current.tag == .l_paren or self.current.tag == .r_paren or
|
||||||
self.current.tag == .comma or self.current.tag == .int_literal or
|
self.current.tag == .comma or self.current.tag == .int_literal or
|
||||||
self.current.tag == .star or self.current.tag == .question)
|
self.current.tag == .star or self.current.tag == .question or
|
||||||
|
self.current.tag == .bang)
|
||||||
{
|
{
|
||||||
self.advance();
|
self.advance();
|
||||||
} else break;
|
} else break;
|
||||||
|
|||||||
Reference in New Issue
Block a user