Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
5.9 KiB
0060 — closure-literal composition miscompiles (blocks ERR/E5.1)
✅ RESOLVED. A closure's underlying function carries a hidden
envarg that a bare(T) -> Uslot 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—isLambdanow accepts.bangin the return-type lookahead, so failable closure literals (-> !/-> (T, !)) parse.src/ir/lower.zig—createClosureToBareFnAdapter: a capture-free closure flowing into a bare(T) -> Uslot is bridged by a generated adapter carrying the bare ABI (forwards a null env).lowerLambdareturns the adapterfunc_reffor 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}vialowerFailableSuccessReturn(the implicit return previously coerced a bare value into the failable tuple and returned0).Regression tests:
examples/0309-closures-literal-as-bare-fn-param.sx(non-failable, block + arrow, called inside the callee) andexamples/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: (i64) -> (i64, !E)) resolves the call result asunresolved(the idiomaticClosure(i64) -> (i64, !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:
#import "modules/std.sx";
apply :: (f: (i64) -> i64) -> i64 { return f(5); }
main :: () {
print("block={}\n", apply(closure((x: i64) -> i64 { return x * 2; }))); // want 10
print("arrow={}\n", apply(closure((x: i64) -> i64 => 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) -> i64 { 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:
-
Parser —
isLambdadoesn't accept a!return type. A closure/lambda literal with-> !/-> (T, !)fails to parse ("expected ','") because the return-type token-skipper inisLambda(src/parser.zig, thearrowbranch ~line 3302) omits.bang. One-line fix:// 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/orall correct). -
Arrow-body failable closures miscompile. After the parser patch,
n := closure((x: i64) -> (i64, !E) => x + 1); n(40) catch e 0returns0instead of41— 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). ComparelowerLambda(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: (i64)->i64) -> i64 { return f(5); }thenapply(closure((x: i64) -> i64 { 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 aclosure(...)literal in argument position is lowered (closure construction + theClosurecalling convention) insrc/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.bangpatch above; (b) fix arrow-body failable closure lowering (returns 0). Verify with a newexamples/XXXX-errors-failable-closure-literal.sx: a block-body and an arrow-body failable closure, called directly, consumed bycatch/or; and a failable closure passed as a(T)->(U,!)parameter andtry-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.