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:
@@ -22,10 +22,14 @@ pub fn lowerBlock(self: *Lowering, node: *const Node) void {
|
||||
const saved_scope = self.scope;
|
||||
self.scope = &block_scope;
|
||||
const saved_defer_len = self.defer_stack.items.len;
|
||||
// Flow narrowing (issue 0179) is block-scoped: a guard inside this
|
||||
// block narrows the rest of THIS block, no further.
|
||||
var narrow_snap = self.narrowSnapshot();
|
||||
defer {
|
||||
self.emitBlockDefers(saved_defer_len);
|
||||
self.scope = saved_scope;
|
||||
block_scope.deinit();
|
||||
self.narrowRestore(&narrow_snap);
|
||||
}
|
||||
for (blk.stmts) |stmt| {
|
||||
if (self.block_terminated) break;
|
||||
@@ -76,10 +80,12 @@ pub fn lowerBlockValue(self: *Lowering, node: *const Node) ?Ref {
|
||||
const saved_scope = self.scope;
|
||||
self.scope = &block_scope;
|
||||
const saved_defer_len = self.defer_stack.items.len;
|
||||
var narrow_snap = self.narrowSnapshot();
|
||||
defer {
|
||||
self.emitBlockDefers(saved_defer_len);
|
||||
self.scope = saved_scope;
|
||||
block_scope.deinit();
|
||||
self.narrowRestore(&narrow_snap);
|
||||
}
|
||||
// A block whose last statement is `;`-terminated (or not an
|
||||
// expression) discards its value: lower every statement as a
|
||||
@@ -801,6 +807,13 @@ fn tryLowerPropertyAssignment(self: *Lowering, asgn: *const ast.Assignment) bool
|
||||
}
|
||||
|
||||
pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
||||
// Reassignment kills flow narrowing (issue 0179 / specs.md §Flow-Sensitive
|
||||
// Narrowing): a fresh value may be null, so the name is no longer proven
|
||||
// present. Drop it from the narrowed set before lowering the store.
|
||||
if (asgn.target.data == .identifier) {
|
||||
_ = self.narrowed.remove(asgn.target.data.identifier.name);
|
||||
}
|
||||
|
||||
// Writes through a constant are rejected at compile time (issue 0116):
|
||||
// the target chain's root naming a const global (array/struct consts,
|
||||
// #run consts) or a module value const cannot be stored to — for a
|
||||
|
||||
Reference in New Issue
Block a user