diff --git a/pyrefly/lib/binding/scope.rs b/pyrefly/lib/binding/scope.rs index 70784c1888..280b337fc4 100644 --- a/pyrefly/lib/binding/scope.rs +++ b/pyrefly/lib/binding/scope.rs @@ -405,7 +405,8 @@ impl Static { /// Populate static definitions from a list of statements. /// Returns the set of implicit captures (names read but not locally defined), - /// the set of all Final names, and a map of Final variable string values. + /// the set of all Final names, a map of Final variable string values, and + /// a map of Final variable bool values. fn stmts( &mut self, x: &[Stmt], @@ -415,7 +416,12 @@ impl Static { sys_info: SysInfo, get_annotation_idx: &mut impl FnMut(ShortIdentifier) -> Idx, scopes: Option<&Scopes>, - ) -> (SmallSet, SmallSet, SmallMap) { + ) -> ( + SmallSet, + SmallSet, + SmallMap, + SmallMap, + ) { let mut d = Definitions::new( x, module_info.name(), @@ -470,7 +476,13 @@ impl Static { .into_iter() .filter_map(|(name, value)| value.map(|v| (name, v))) .collect(); - (implicit_captures, final_names, final_string_values) + let final_bool_values = d.final_bool_values; + ( + implicit_captures, + final_names, + final_string_values, + final_bool_values, + ) } fn expr_lvalue(&mut self, x: &Expr) { @@ -1213,6 +1225,9 @@ pub struct Scope { /// Names marked `Final` with string literal values, e.g. `X: Final = "x"`. /// Used to resolve Final variable references in synthesized class field names. final_string_values: SmallMap, + /// Names marked `Final` with bool literal values, e.g. `FLAG: Final = False`. + /// Used to statically evaluate control-flow tests like `if FLAG:`. + final_bool_values: SmallMap, } impl Scope { @@ -1233,6 +1248,7 @@ impl Scope { implicit_captures: SmallSet::new(), final_names: SmallSet::new(), final_string_values: SmallMap::new(), + final_bool_values: SmallMap::new(), } } @@ -1678,18 +1694,20 @@ impl Scopes { get_annotation_idx: &mut impl FnMut(ShortIdentifier) -> Idx, ) { let mut initialize = |scope: &mut Scope, myself: Option<&Self>| { - let (implicit_captures, final_names, final_string_values) = scope.stat.stmts( - x, - module_info, - top_level, - lookup, - sys_info, - get_annotation_idx, - myself, - ); + let (implicit_captures, final_names, final_string_values, final_bool_values) = + scope.stat.stmts( + x, + module_info, + top_level, + lookup, + sys_info, + get_annotation_idx, + myself, + ); scope.implicit_captures = implicit_captures; scope.final_names = final_names; scope.final_string_values = final_string_values; + scope.final_bool_values = final_bool_values; // Presize the flow, as its likely to need as much space as static scope.flow.info.reserve(scope.stat.0.capacity()); }; @@ -1727,6 +1745,30 @@ impl Scopes { None } + /// Look up a Final variable's bool literal value in the current scope stack. + pub fn lookup_final_bool_value(&self, name: &Name) -> Option { + for node in self.scopes.iter().rev() { + if let Some(value) = node.scope.final_bool_values.get(name) { + return Some(*value); + } + if node.scope.stat.0.contains_key(name) { + return None; + } + } + None + } + + /// Return true/false if a control-flow test can be statically evaluated. + pub fn evaluate_bool_for_control_flow(&self, sys_info: SysInfo, x: &Expr) -> Option { + if let Some(value) = sys_info.evaluate_bool(x) { + return Some(value); + } + if let Expr::Name(name) = x { + return self.lookup_final_bool_value(&name.id); + } + None + } + pub fn push(&mut self, scope: Scope) { self.scopes.push(ScopeTreeNode { scope, diff --git a/pyrefly/lib/binding/stmt.rs b/pyrefly/lib/binding/stmt.rs index 4e2de086b7..ffb8c1df01 100644 --- a/pyrefly/lib/binding/stmt.rs +++ b/pyrefly/lib/binding/stmt.rs @@ -1086,7 +1086,8 @@ impl<'a> BindingsBuilder<'a> { Some(true) } Some(x) => { - let result = self.sys_info.evaluate_bool(x); + let result = + self.scopes.evaluate_bool_for_control_flow(self.sys_info, x); if result.is_some() { contains_static_test_with_no_else = true; } diff --git a/pyrefly/lib/export/definitions.rs b/pyrefly/lib/export/definitions.rs index d6c036d79e..87e482ae06 100644 --- a/pyrefly/lib/export/definitions.rs +++ b/pyrefly/lib/export/definitions.rs @@ -167,6 +167,9 @@ pub struct Definitions { /// The `Option` is `Some` when the RHS is a string literal (e.g. `X: Final = "x"`), /// used to resolve Final variable references in synthesized class field names. pub final_names: SmallMap>, + /// Names marked `Final` with bool literal values, e.g. `FLAG: Final = False`. + /// Used to statically evaluate `if FLAG:` / `if not FLAG:` control flow. + pub final_bool_values: SmallMap, /// Special exports defined in this module pub special_exports: SmallMap, /// Names that are read (not just defined) in this scope. @@ -682,6 +685,14 @@ impl DefinitionsBuilder { } else { None }; + let final_bool_value = if has_final_annotation { + match x.value.as_deref() { + Some(Expr::BooleanLiteral(b)) => Some(b.value), + _ => None, + } + } else { + None + }; match &*x.target { Expr::Name(x) => { self.add_name( @@ -696,6 +707,9 @@ impl DefinitionsBuilder { self.inner .final_names .insert(x.id.clone(), final_string_value); + if let Some(value) = final_bool_value { + self.inner.final_bool_values.insert(x.id.clone(), value); + } } } _ => self.expr_lvalue(&x.target), diff --git a/pyrefly/lib/export/exports.rs b/pyrefly/lib/export/exports.rs index 1cef1e4034..36ce6056d4 100644 --- a/pyrefly/lib/export/exports.rs +++ b/pyrefly/lib/export/exports.rs @@ -223,6 +223,8 @@ impl Exports { // for import variants — others carry positional data || (self_def.style.is_import() && self_def.style != other_def.style) || self_defs.final_names.get(name) != other_defs.final_names.get(name) + || self_defs.final_bool_values.get(name) + != other_defs.final_bool_values.get(name) || self_defs.implicitly_imported_submodules.contains(name) != other_defs.implicitly_imported_submodules.contains(name) || self_defs.deprecated.get(name) != other_defs.deprecated.get(name) diff --git a/pyrefly/lib/test/narrow.rs b/pyrefly/lib/test/narrow.rs index a35777546d..ad5d8319a3 100644 --- a/pyrefly/lib/test/narrow.rs +++ b/pyrefly/lib/test/narrow.rs @@ -107,6 +107,32 @@ def f(x: str | None): "#, ); +testcase!( + test_final_bool_unreachable_if_branch, + r#" +from typing import Final, Literal, reveal_type +asdf: Final = False +if asdf: + foo = 1 +else: + foo = 2 +reveal_type(foo) # E: revealed type: Literal[2] +"#, +); + +testcase!( + test_final_bool_unreachable_else_branch, + r#" +from typing import Final, Literal, reveal_type +flag: Final = True +if flag: + bar = 1 +else: + bar = 2 +reveal_type(bar) # E: revealed type: Literal[1] +"#, +); + testcase!( test_is_subtype, r#" diff --git a/pyrefly_wasm/rust-toolchain b/pyrefly_wasm/rust-toolchain deleted file mode 120000 index becc4c6088..0000000000 --- a/pyrefly_wasm/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -../pyrefly/rust-toolchain \ No newline at end of file