From 485b4fa6185a46c8594f757a3ee4f69a437dbc77 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 1 Jun 2026 20:18:25 +0300 Subject: [PATCH] =?UTF-8?q?issues:=20file=200060=20=E2=80=94=20closure-lit?= =?UTF-8?q?eral=20composition=20miscompiles=20(blocks=20ERR/E5.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...closure-literal-composition-miscompiles.md | 85 +++++++++++++++++++ ...closure-literal-composition-miscompiles.sx | 15 ++++ library/modules/platform/uikit.sx | 13 ++- src/parser.zig | 3 +- 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 issues/0060-closure-literal-composition-miscompiles.md create mode 100644 issues/0060-closure-literal-composition-miscompiles.sx diff --git a/issues/0060-closure-literal-composition-miscompiles.md b/issues/0060-closure-literal-composition-miscompiles.md new file mode 100644 index 0000000..74e9ba1 --- /dev/null +++ b/issues/0060-closure-literal-composition-miscompiles.md @@ -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. diff --git a/issues/0060-closure-literal-composition-miscompiles.sx b/issues/0060-closure-literal-composition-miscompiles.sx new file mode 100644 index 0000000..437b17c --- /dev/null +++ b/issues/0060-closure-literal-composition-miscompiles.sx @@ -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 +} diff --git a/library/modules/platform/uikit.sx b/library/modules/platform/uikit.sx index bae9331..9fb0846 100644 --- a/library/modules/platform/uikit.sx +++ b/library/modules/platform/uikit.sx @@ -15,7 +15,7 @@ #import "modules/platform/types.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; chdir :: (path: [*]u8) -> s32 #foreign; @@ -200,7 +200,8 @@ UITextField :: #foreign #objc_class("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`. SxAppDelegate :: #objc_class("SxAppDelegate") { #extends UIResponder; @@ -364,7 +365,13 @@ impl Platform for UIKitPlatform { self.has_frame_closure = true; g_uikit_plat = self; 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"); } } diff --git a/src/parser.zig b/src/parser.zig index f3f2588..b261c5a 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -3304,7 +3304,8 @@ pub const Parser = struct { 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 == .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(); } else break;