diff --git a/pyrefly/lib/binding/scope.rs b/pyrefly/lib/binding/scope.rs index ca0d0083f3..0e8be05fd9 100644 --- a/pyrefly/lib/binding/scope.rs +++ b/pyrefly/lib/binding/scope.rs @@ -4343,6 +4343,7 @@ impl<'a> BindingsBuilder<'a> { is_while_true: bool, loop_definitely_runs: bool, ) { + let loop_raises = self.scopes.terminated_by_raise(); let finished_loop = self.scopes.finish_loop(); let (breaks, other_exits): (Vec, Vec) = finished_loop .exits @@ -4381,8 +4382,13 @@ impl<'a> BindingsBuilder<'a> { // also know that breaks are the only way to exit, so we drop the current flow, // which is actually unreachable. // - // TODO(stroxler): in the `is_while_true` case, empty breaks might have implications - // for flow termination and/or `NoReturn` behaviors, we should investigate. + if is_while_true && breaks.is_empty() { + self.scopes.mark_flow_termination(if loop_raises { + TerminationKind::Raise + } else { + TerminationKind::Jump + }); + } if let Some(base) = base_if_breaks { if is_while_true { self.merge_flow(base, breaks, other_range, MergeStyle::Exclusive) diff --git a/pyrefly/lib/binding/stmt.rs b/pyrefly/lib/binding/stmt.rs index ddba945fa1..914860eac5 100644 --- a/pyrefly/lib/binding/stmt.rs +++ b/pyrefly/lib/binding/stmt.rs @@ -1401,6 +1401,7 @@ impl<'a> BindingsBuilder<'a> { ); } Stmt::While(mut x) => { + let was_unreachable = self.scopes.is_definitely_unreachable(); self.setup_loop(x.range, &SmallSet::new()); // Note that it is important we ensure *after* we set up the loop, so that both the // narrowing and type checking are aware that the test might be impacted by changes @@ -1451,6 +1452,16 @@ impl<'a> BindingsBuilder<'a> { is_while_true, is_while_true, ); + if is_while_true + && !test_is_environment_independent + && self.scopes.has_terminated() + && !was_unreachable + { + // Another Python environment can skip the loop. + self.scopes.set_definitely_unreachable(false); + self.scopes + .mark_flow_termination(TerminationKind::StaticTest); + } } Stmt::If(mut x) => { let is_definitely_unreachable = self.scopes.is_definitely_unreachable(); diff --git a/pyrefly/lib/test/delayed_inference.rs b/pyrefly/lib/test/delayed_inference.rs index b74bdb36dd..204dc0a83c 100644 --- a/pyrefly/lib/test/delayed_inference.rs +++ b/pyrefly/lib/test/delayed_inference.rs @@ -45,7 +45,7 @@ from typing import assert_type, Literal x = 5 while True: x = x + 1 -y = x +y = x # E: This code is unreachable assert_type(y, int) "#, ); diff --git a/pyrefly/lib/test/flow_branching.rs b/pyrefly/lib/test/flow_branching.rs index d7fe0b4e66..51c6344185 100644 --- a/pyrefly/lib/test/flow_branching.rs +++ b/pyrefly/lib/test/flow_branching.rs @@ -432,6 +432,7 @@ c = C() while True: if True: c = C() + break assert_type(c, C) "#, @@ -989,7 +990,7 @@ for x in []: pass # E: Expected an indented block while True: -pass # E: Expected an indented block +pass # E: Expected an indented block # E: This code is unreachable "#, ); diff --git a/pyrefly/lib/test/flow_looping.rs b/pyrefly/lib/test/flow_looping.rs index 3ffb14c217..f35e1c2adf 100644 --- a/pyrefly/lib/test/flow_looping.rs +++ b/pyrefly/lib/test/flow_looping.rs @@ -958,3 +958,49 @@ for item in items: last # E: `last` may be uninitialized "#, ); + +testcase!( + test_unreachable_after_breakless_while_true, + r#" +def retry(flag: bool) -> None: + while True: + if flag: + return + raise RuntimeError("unreachable") # E: This code is unreachable + +def spin() -> None: + while True: + pass + raise RuntimeError("unreachable") # E: This code is unreachable +"#, +); + +testcase!( + test_while_environment_dependent_fallthrough, + r#" +import sys + +def configured_true() -> int: + while sys.version_info >= (3, 0): + pass + return 1 + +def configured_false() -> int: + while sys.version_info < (3, 0): + pass + return 1 +"#, +); + +testcase!( + test_while_true_raise_can_be_suppressed, + r#" +from contextlib import suppress + +def f() -> int: + with suppress(ValueError): + while True: + raise ValueError() + return 1 +"#, +); diff --git a/pyrefly/lib/test/lsp/hover_type.rs b/pyrefly/lib/test/lsp/hover_type.rs index fd9bbf7099..c512c8e962 100644 --- a/pyrefly/lib/test/lsp/hover_type.rs +++ b/pyrefly/lib/test/lsp/hover_type.rs @@ -478,7 +478,7 @@ while True: y = x # ^ "#; - let report = get_batched_lsp_operations_report(&[("main", code)], get_test_report); + let report = get_batched_lsp_operations_report_allow_error(&[("main", code)], get_test_report); assert_eq!( r#" # main.py diff --git a/pyrefly/lib/test/paramspec.rs b/pyrefly/lib/test/paramspec.rs index 3a2337e5a9..e0128661d9 100644 --- a/pyrefly/lib/test/paramspec.rs +++ b/pyrefly/lib/test/paramspec.rs @@ -487,7 +487,7 @@ def run[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R: return func(*args,**kwargs) except Exception: pass - raise NotImplementedError("Unreachable") + raise NotImplementedError("Unreachable") # E: This code is unreachable "#, );