Files
sx/examples/optionals/0923-optionals-narrowing-no-closure-leak.sx
agra 468461becc fix: gate implicit optional unwrap on flow narrowing (issue 0179)
Optional (?T) operands were implicitly unwrapped without proof of
presence, silently miscompiling a NULL ?T to garbage. Unwraps in
binary ops and other expression positions are now gated on flow
narrowing: a ?T value is only auto-unwrapped where control flow has
established it is non-null (the narrowed_refs set). Outside a narrowed
region, an implicit unwrap is rejected rather than producing garbage.

Touches the lowering pipeline (lower.zig + lower/{call,closure,coerce,
comptime,control_flow,expr,ffi,generic,pack,stmt}.zig). Adds optionals
examples 0919-0923 and closures example 0312 covering flow narrowing,
binop narrowing, no-implicit-unwrap rejection, and no closure leak of
narrowed state. Updates specs.md and readme.md.
2026-06-25 13:57:48 +03:00

24 lines
1.0 KiB
Plaintext

// Flow narrowing (issue 0179) does NOT cross into a nested function body. A
// closure capturing a local that is narrowed in the ENCLOSING scope still sees
// it as `?T` inside the closure — using it unguarded is a compile error.
//
// This guards the soundness fix from the adversarial review of 0179/0185: the
// narrowing gate is keyed by per-function SSA `Ref`, and a closure body's `Ref`
// space overlaps the enclosing function's, so without isolation an outer
// narrowed `Ref` would falsely permit unwrapping a not-proven-present optional
// inside the closure (`Lowering.NarrowGuard`). The closure could run later when
// the captured value is null, so the reject is mandatory — write `n!` to assert.
#import "modules/std.sx";
takes_i64 :: (x: i64) { print("{}\n", x); }
main :: () {
n : ?i64 = 7;
if n != null {
// `n` is narrowed HERE, but the closure body is a separate function:
// `n` is `?i64` inside it, so this implicit unwrap must be rejected.
g := () => { takes_i64(n); };
g();
}
}