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

@@ -284,6 +284,19 @@ pub const Lowering = struct {
current_runtime_method: ?ast.RuntimeMethodDecl = null, // the specific method whose body is being lowered; `super.<same_name>(...)` reuses its signature
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
/// Flow-sensitive narrowing (issue 0179). The set of local variable names
/// currently PROVEN present (`?T` known to carry a value) by a `!= null`
/// guard / branch. Region-scoped: `lowerBlock` snapshots+restores it, the
/// if-then branch narrows on `!= null`, a divergent `== null` guard narrows
/// the rest of the enclosing block, and an assignment kills the name's
/// narrowing. Consulted at the implicit `?T → concrete` unwrap (`coerceMode`):
/// a non-narrowed unwrap is REJECTED instead of silently yielding the zero
/// payload of a null optional.
narrowed: std.StringHashMap(void) = undefined,
/// The SSA `Ref`s produced by lowering a narrowed identifier — the bridge
/// from name-keyed narrowing to the Ref-keyed `coerceMode` unwrap gate.
/// Cleared per function body (the `Ref` space is per-function).
narrowed_refs: std.AutoHashMap(Ref, void) = undefined,
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
block_terminated: bool = false, // set when constant-folded if emits a return/br into current block
in_lambda_body: bool = false, // true while lowering a closure-literal body; sharpens the `raise`-not-failable diagnostic (ERR E5.1: tell the user to annotate `-> (T, !)`)
@@ -474,6 +487,8 @@ pub const Lowering = struct {
pack_param_count: ?std.StringHashMap(u32),
pack_arg_types: ?std.StringHashMap([]const TypeId),
inline_return_target: ?InlineReturnInfo,
narrowed: std.StringHashMap(void),
narrowed_refs: std.AutoHashMap(Ref, void),
pub fn enter(l: *Lowering) FnBodyReentry {
const g = FnBodyReentry{
@@ -491,7 +506,14 @@ pub const Lowering = struct {
.pack_param_count = l.pack_param_count,
.pack_arg_types = l.pack_arg_types,
.inline_return_target = l.inline_return_target,
// Flow narrowing is lexical to one function body — a nested
// (closure / local-fn) lowering starts with a fresh, empty
// narrowing state and the outer state is restored after.
.narrowed = l.narrowed,
.narrowed_refs = l.narrowed_refs,
};
l.narrowed = std.StringHashMap(void).init(l.alloc);
l.narrowed_refs = std.AutoHashMap(Ref, void).init(l.alloc);
// The `#jni_env` Ref stack is lexical to ONE function's instruction
// stream; move the visible base to the current top. Pack-fn mono
// state is likewise lexical to the pack-fn body — null it so a
@@ -523,6 +545,38 @@ pub const Lowering = struct {
l.pack_param_count = g.pack_param_count;
l.pack_arg_types = g.pack_arg_types;
l.inline_return_target = g.inline_return_target;
l.narrowed.deinit();
l.narrowed = g.narrowed;
l.narrowed_refs.deinit();
l.narrowed_refs = g.narrowed_refs;
}
};
/// Save + clear + restore JUST the flow-narrowing state (issue 0179) around
/// a nested body lowering that does NOT go through `FnBodyReentry` —
/// closure literals, generic/pack/comptime monomorphization. Each lowers a
/// SEPARATE function whose `Ref` space (reset by `beginFunction`) OVERLAPS
/// the outer function's, so the outer `narrowed_refs` indices would falsely
/// match the nested body's `Ref`s and permit an UNSOUND unwrap of a
/// non-present optional. Clearing on entry isolates the nested body (it
/// builds its own narrowing from scratch); restore re-arms the outer.
pub const NarrowGuard = struct {
l: *Lowering,
narrowed: std.StringHashMap(void),
narrowed_refs: std.AutoHashMap(Ref, void),
pub fn enter(l: *Lowering) NarrowGuard {
const g = NarrowGuard{ .l = l, .narrowed = l.narrowed, .narrowed_refs = l.narrowed_refs };
l.narrowed = std.StringHashMap(void).init(l.alloc);
l.narrowed_refs = std.AutoHashMap(Ref, void).init(l.alloc);
return g;
}
pub fn restore(g: NarrowGuard) void {
g.l.narrowed.deinit();
g.l.narrowed = g.narrowed;
g.l.narrowed_refs.deinit();
g.l.narrowed_refs = g.narrowed_refs;
}
};
@@ -548,6 +602,8 @@ pub const Lowering = struct {
.struct_const_map = std.StringHashMap(StructConstInfo).init(module.alloc),
.extern_name_map = std.StringHashMap([]const u8).init(module.alloc),
.comptime_constants = std.StringHashMap(ComptimeValue).init(module.alloc),
.narrowed = std.StringHashMap(void).init(module.alloc),
.narrowed_refs = std.AutoHashMap(Ref, void).init(module.alloc),
.xx_reentrancy = std.AutoHashMap(u64, void).init(module.alloc),
.inferred_error_sets = std.StringHashMap([]const u32).init(module.alloc),
.impl_method_names = std.StringHashMap(void).init(module.alloc),
@@ -1685,6 +1741,13 @@ pub const Lowering = struct {
// --- moved to lower/control_flow.zig (lower_control_flow) ---
pub const lowerIfExpr = lower_control_flow.lowerIfExpr;
pub const narrowableLocal = lower_control_flow.narrowableLocal;
pub const nullCmpName = lower_control_flow.nullCmpName;
pub const collectPresentIfTrue = lower_control_flow.collectPresentIfTrue;
pub const collectPresentIfFalse = lower_control_flow.collectPresentIfFalse;
pub const narrowSnapshot = lower_control_flow.narrowSnapshot;
pub const narrowRestore = lower_control_flow.narrowRestore;
pub const applyNarrowing = lower_control_flow.applyNarrowing;
pub const tryConstBoolCondition = lower_control_flow.tryConstBoolCondition;
pub const lowerWhile = lower_control_flow.lowerWhile;
pub const listView = lower_control_flow.listView;
@@ -2008,6 +2071,7 @@ pub const Lowering = struct {
pub const lowerTupleLiteral = lower_expr.lowerTupleLiteral;
pub const lowerDerefExpr = lower_expr.lowerDerefExpr;
pub const lowerForceUnwrap = lower_expr.lowerForceUnwrap;
pub const diagOptionalOperand = lower_expr.diagOptionalOperand;
pub const lowerNullCoalesce = lower_expr.lowerNullCoalesce;
pub const resolveOptionalInner = lower_expr.resolveOptionalInner;
pub const lowerExpr = lower_expr.lowerExpr;