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.
This commit is contained in:
agra
2026-06-25 13:57:48 +03:00
parent 6c89a0aa3e
commit 468461becc
38 changed files with 576 additions and 3 deletions

View File

@@ -1127,6 +1127,12 @@ pub fn createComptimeFunctionWithPrelude(self: *Lowering, prefix: []const u8, pr
const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix;
self.comptime_counter += 1;
// Flow narrowing (issue 0179) is per-function: this wrapper body has its
// own `Ref` space (overlapping the caller's), so isolate it from the
// caller's `narrowed`/`narrowed_refs` to avoid a false-positive unwrap gate.
var narrow_guard = Lowering.NarrowGuard.enter(self);
defer narrow_guard.restore();
// Save current builder + lowering state. The wrapper fn we're
// about to build runs the comptime expression in isolation —
// it must NOT inherit the enclosing call's `inline_return_target`