Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions pyrefly/lib/binding/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,9 @@ impl<'a> BindingsBuilder<'a> {
self.defer_bound_name(key, lookup_result_idx, usage, promote)
}
NameLookupResult::NotFound => {
if self.scopes.is_definitely_unreachable() {
return self.insert_binding(key, Binding::Any(AnyStyle::Implicit));
}
let suggestion = self
.scopes
.suggest_similar_name(&name.id, name.range.start());
Expand Down Expand Up @@ -764,6 +767,14 @@ impl<'a> BindingsBuilder<'a> {
self.ensure_expr(value, &mut Usage::narrowing_from(usage));
self.start_fork_and_branch(*range);
let mut narrow_ops = get_narrow_ops(self, value, *op);

let short_circuit_trigger: Option<bool> = match op {
BoolOp::And => Some(false),
BoolOp::Or => Some(true),
};
if self.sys_info.evaluate_bool(value) == short_circuit_trigger {
self.scopes.set_definitely_unreachable(true);
}
for value in values {
self.bind_narrow_ops(
&narrow_ops,
Expand All @@ -773,6 +784,9 @@ impl<'a> BindingsBuilder<'a> {
self.ensure_expr(value, &mut Usage::narrowing_from(usage));
let new_narrow_ops = get_narrow_ops(self, value, *op);
narrow_ops.and_all(new_narrow_ops);
if self.sys_info.evaluate_bool(value) == short_circuit_trigger {
self.scopes.set_definitely_unreachable(true);
}
}
// Negate the narrow ops in the base flow and merge.
// TODO(stroxler): We eventually want to drop all narrows but merge values.
Expand Down
58 changes: 58 additions & 0 deletions pyrefly/lib/test/sys_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,64 @@ def f(**kwargs: Unpack[Kwargs]) -> None:
"#,
);

testcase!(
test_version_guard_and_short_circuit,
r#"
if False:
A = 3

if False:
B = 3

if False:
C = 3


def test_and_basic() -> None:
# `and` short-circuits on False: A in the condition and body are unreachable.
if False and A:
_ = A

def test_and_longer_chain() -> None:
# Both B and C are gated: the guard short-circuits before reaching them.
if False and B and C:
_ = B
_ = C

def test_and_three_guards() -> None:
# Three consecutive False guards in a single and-chain.
if False and A and B and C:
_ = A
_ = B
_ = C

def test_or_basic() -> None:
# `or` short-circuits when the left side is *true*. False on the left
# means the right side IS evaluated — the name is unknown.
if False or A: # E: Could not find name `A`
pass

def test_or_always_true_guard() -> None:
# True → right side is unreachable.
if True or A:
pass

def test_or_longer_chain() -> None:
# Chain: first element is always-true, so B and C are never evaluated.
if True or B or C:
pass

def test_and_body_unreachable() -> None:
# An `if False` body is also unreachable; accessing A inside it is fine.
if False:
_ = A

def test_no_guard_produces_error() -> None:
# Baseline: without any guard, accessing A must produce an error.
A # E: Could not find name `A`
"#,
);

testcase!(
test_typechecking_unpack_alias,
r#"
Expand Down
Loading