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
66 changes: 54 additions & 12 deletions pyrefly/lib/binding/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -415,7 +416,12 @@ impl Static {
sys_info: SysInfo,
get_annotation_idx: &mut impl FnMut(ShortIdentifier) -> Idx<KeyAnnotation>,
scopes: Option<&Scopes>,
) -> (SmallSet<Name>, SmallSet<Name>, SmallMap<Name, String>) {
) -> (
SmallSet<Name>,
SmallSet<Name>,
SmallMap<Name, String>,
SmallMap<Name, bool>,
) {
let mut d = Definitions::new(
x,
module_info.name(),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<Name, String>,
/// 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<Name, bool>,
}

impl Scope {
Expand All @@ -1233,6 +1248,7 @@ impl Scope {
implicit_captures: SmallSet::new(),
final_names: SmallSet::new(),
final_string_values: SmallMap::new(),
final_bool_values: SmallMap::new(),
}
}

Expand Down Expand Up @@ -1678,18 +1694,20 @@ impl Scopes {
get_annotation_idx: &mut impl FnMut(ShortIdentifier) -> Idx<KeyAnnotation>,
) {
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());
};
Expand Down Expand Up @@ -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<bool> {
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<bool> {
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,
Expand Down
3 changes: 2 additions & 1 deletion pyrefly/lib/binding/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
14 changes: 14 additions & 0 deletions pyrefly/lib/export/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ pub struct Definitions {
/// The `Option<String>` 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<Name, Option<String>>,
/// 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<Name, bool>,
/// Special exports defined in this module
pub special_exports: SmallMap<Name, SpecialExport>,
/// Names that are read (not just defined) in this scope.
Expand Down Expand Up @@ -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(
Expand All @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions pyrefly/lib/export/exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions pyrefly/lib/test/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down
1 change: 0 additions & 1 deletion pyrefly_wasm/rust-toolchain

This file was deleted.

Loading