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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user