Compare commits

...

22 Commits

Author SHA1 Message Date
David Peter
500b9a2691 EllipsisType test 2024-11-29 22:15:37 +01:00
David Peter
15476be531 Add TODO 2024-11-29 21:52:17 +01:00
David Peter
e7a361699d Fix behavior for NoDefault 2024-11-29 21:49:24 +01:00
David Peter
a218e1901b Declarations 2024-11-29 21:16:34 +01:00
David Peter
f108043d2d Reset typeshed 2024-11-28 22:51:10 +01:00
David Peter
77b45aeee9 Remove hard-coded EllipsisType 2024-11-28 22:49:25 +01:00
David Peter
c6f4c106b0 Updates 2024-11-28 22:47:18 +01:00
David Peter
dc55b4c8a2 Properly restore constraints 2024-11-28 22:38:08 +01:00
David Peter
41d19c3c29 Update 2024-11-28 20:00:58 +01:00
David Peter
99d44299e8 Revert typing change 2024-11-28 19:58:26 +01:00
David Peter
32ad489d79 More debugging output 2024-11-28 19:42:24 +01:00
David Peter
a3e7e7d8b6 Remove TODOs 2024-11-28 19:39:58 +01:00
David Peter
aea4bbbb30 Adapt debug test 2024-11-28 19:39:33 +01:00
David Peter
167e445243 Reset test file 2024-11-28 19:39:16 +01:00
David Peter
dccfd6e4f8 New tests 2024-11-28 19:38:56 +01:00
David Peter
eb4ae2b910 statically known to be True branches 2024-11-28 17:33:35 +01:00
David Peter
5be842b1c3 Patch typeshed and increase default Python version to 3.13 2024-11-28 17:33:35 +01:00
David Peter
2a21d79ec4 Cleanup 2024-11-28 17:33:35 +01:00
David Peter
1964ecdbb7 Base check on type, not expr 2024-11-28 17:33:35 +01:00
David Peter
6d167672f1 First version based on end-of-branch constraints 2024-11-28 17:33:35 +01:00
David Peter
ae45b897ea Restore to main 2024-11-28 17:33:35 +01:00
David Peter
90f48f45b0 Dump save 2024-11-28 17:33:35 +01:00
19 changed files with 1022 additions and 231 deletions

View File

@@ -5,11 +5,11 @@
pub enum TargetVersion {
Py37,
Py38,
#[default]
Py39,
Py310,
Py311,
Py312,
#[default]
Py313,
}

View File

@@ -38,7 +38,7 @@ if (x := 1) and bool_instance():
if True or (x := 1):
# TODO: infer that the second arm is never executed, and raise `unresolved-reference`.
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
reveal_type(x) # revealed: Never
if True and (x := 1):
# TODO: infer that the second arm is always executed, do not raise a diagnostic

View File

@@ -0,0 +1,303 @@
# Statically-known branches
## Always false
### If
```py
x = 1
if False:
x = 2
reveal_type(x) # revealed: Literal[1]
```
### Else
```py
x = 1
if True:
pass
else:
x = 2
reveal_type(x) # revealed: Literal[1]
```
## Always true
### If
```py
x = 1
if True:
x = 2
reveal_type(x) # revealed: Literal[2]
```
### Else
```py
x = 1
if False:
pass
else:
x = 2
reveal_type(x) # revealed: Literal[2]
```
## Combination
```py
x = 1
if True:
x = 2
else:
x = 3
reveal_type(x) # revealed: Literal[2]
```
## Nested
```py path=nested_if_true_if_true.py
x = 1
if True:
if True:
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2]
```
```py path=nested_if_true_if_false.py
x = 1
if True:
if False:
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3]
```
```py path=nested_if_true_if_bool.py
def flag() -> bool: ...
x = 1
if True:
if flag():
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2, 3]
```
```py path=nested_if_bool_if_true.py
def flag() -> bool: ...
x = 1
if flag():
if True:
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2, 4]
```
```py path=nested_else_if_true.py
x = 1
if False:
x = 2
else:
if True:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3]
```
```py path=nested_else_if_false.py
x = 1
if False:
x = 2
else:
if False:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[4]
```
```py path=nested_else_if_bool.py
def flag() -> bool: ...
x = 1
if False:
x = 2
else:
if flag():
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3, 4]
```
## If-expressions
### Always true
```py
x = 1 if True else 2
reveal_type(x) # revealed: Literal[1]
```
### Always false
```py
x = 1 if False else 2
reveal_type(x) # revealed: Literal[2]
```
## Boolean expressions
### Always true
```py
(x := 1) == 1 or (x := 2)
reveal_type(x) # revealed: Literal[1]
```
### Always false
```py
(x := 1) == 0 or (x := 2)
reveal_type(x) # revealed: Literal[2]
```
## Conditional declarations
```py path=if_false.py
x: str
if False:
x: int
def f() -> None:
reveal_type(x) # revealed: str
```
```py path=if_true_else.py
x: str
if True:
pass
else:
x: int
def f() -> None:
reveal_type(x) # revealed: str
```
```py path=if_true.py
x: str
if True:
x: int
def f() -> None:
reveal_type(x) # revealed: int
```
```py path=if_false_else.py
x: str
if False:
pass
else:
x: int
def f() -> None:
reveal_type(x) # revealed: int
```
```py path=if_bool.py
def flag() -> bool: ...
x: str
if flag():
x: int
def f() -> None:
reveal_type(x) # revealed: str | int
```
## Conditionally defined functions
```py
def f() -> int: ...
def g() -> int: ...
if True:
def f() -> str: ...
else:
def g() -> str: ...
reveal_type(f()) # revealed: str
reveal_type(g()) # revealed: int
```
## Conditionally defined class attributes
```py
class C:
if True:
x: int = 1
else:
x: str = "a"
reveal_type(C.x) # revealed: int
```
## TODO
- declarations vs bindings => NoDefault: NoDefaultType
- conditional imports
- conditional class definitions
- compare with tests in if.md=>Statically known branches
- boundness
- TODO in `issubclass.md`

View File

@@ -49,14 +49,14 @@ sometimes not:
```py
import sys
reveal_type(sys.version_info >= (3, 9, 1)) # revealed: bool
reveal_type(sys.version_info >= (3, 9, 1, "final", 0)) # revealed: bool
reveal_type(sys.version_info >= (3, 9, 1)) # revealed: Literal[True]
reveal_type(sys.version_info >= (3, 9, 1, "final", 0)) # revealed: Literal[True]
# TODO: While this won't fail at runtime, the user has probably made a mistake
# if they're comparing a tuple of length >5 with `sys.version_info`
# (`sys.version_info` is a tuple of length 5). It might be worth
# emitting a lint diagnostic of some kind warning them about the probable error?
reveal_type(sys.version_info >= (3, 9, 1, "final", 0, 5)) # revealed: bool
reveal_type(sys.version_info >= (3, 9, 1, "final", 0, 5)) # revealed: Literal[True]
reveal_type(sys.version_info == (3, 8, 1, "finallllll", 0)) # revealed: Literal[False]
```
@@ -102,8 +102,8 @@ The fields of `sys.version_info` can be accessed by name:
import sys
reveal_type(sys.version_info.major >= 3) # revealed: Literal[True]
reveal_type(sys.version_info.minor >= 9) # revealed: Literal[True]
reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
reveal_type(sys.version_info.minor >= 13) # revealed: Literal[True]
reveal_type(sys.version_info.minor >= 14) # revealed: Literal[False]
```
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
@@ -125,14 +125,14 @@ The fields of `sys.version_info` can be accessed by index or by slice:
import sys
reveal_type(sys.version_info[0] < 3) # revealed: Literal[False]
reveal_type(sys.version_info[1] > 9) # revealed: Literal[False]
reveal_type(sys.version_info[1] > 13) # revealed: Literal[False]
# revealed: tuple[Literal[3], Literal[9], int, Literal["alpha", "beta", "candidate", "final"], int]
# revealed: tuple[Literal[3], Literal[13], int, Literal["alpha", "beta", "candidate", "final"], int]
reveal_type(sys.version_info[:5])
reveal_type(sys.version_info[:2] >= (3, 9)) # revealed: Literal[True]
reveal_type(sys.version_info[0:2] >= (3, 10)) # revealed: Literal[False]
reveal_type(sys.version_info[:3] >= (3, 10, 1)) # revealed: Literal[False]
reveal_type(sys.version_info[:2] >= (3, 13)) # revealed: Literal[True]
reveal_type(sys.version_info[0:2] >= (3, 14)) # revealed: Literal[False]
reveal_type(sys.version_info[:3] >= (3, 14, 1)) # revealed: Literal[False]
reveal_type(sys.version_info[3] == "final") # revealed: bool
reveal_type(sys.version_info[3] == "finalllllll") # revealed: Literal[False]
```

View File

@@ -39,7 +39,7 @@ impl PythonVersion {
impl Default for PythonVersion {
fn default() -> Self {
Self::PY39
Self::PY313 // TODO: temporarily changed to 3.13 to activate all sys.version_info branches
}
}

View File

@@ -1229,4 +1229,32 @@ match 1:
assert!(matches!(binding.kind(&db), DefinitionKind::For(_)));
}
#[test]
#[ignore]
fn if_statement() {
let TestCase { db, file } = test_case(
"
x = False
if True:
x: bool
",
);
let index = semantic_index(&db, file);
// let global_table = index.symbol_table(FileScopeId::global());
let use_def = index.use_def_map(FileScopeId::global());
// use_def
use_def.print(&db);
assert!(false);
// let binding = use_def
// .first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists"))
// .expect("Expected with item definition for {name}");
// assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_)));
}
}

View File

@@ -23,7 +23,7 @@ use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId,
SymbolTableBuilder,
};
use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
use crate::semantic_index::use_def::{ActiveConstraintsSnapshot, FlowSnapshot, UseDefMapBuilder};
use crate::semantic_index::SemanticIndex;
use crate::unpack::Unpack;
use crate::Db;
@@ -200,12 +200,20 @@ impl<'db> SemanticIndexBuilder<'db> {
self.current_use_def_map().snapshot()
}
fn flow_restore(&mut self, state: FlowSnapshot) {
self.current_use_def_map_mut().restore(state);
fn constraints_snapshot(&self) -> ActiveConstraintsSnapshot {
self.current_use_def_map().constraints_snapshot()
}
fn flow_merge(&mut self, state: FlowSnapshot) {
fn flow_restore(&mut self, state: FlowSnapshot, active_constraints: ActiveConstraintsSnapshot) {
self.current_use_def_map_mut().restore(state);
self.current_use_def_map_mut()
.restore_constraints(active_constraints);
}
fn flow_merge(&mut self, state: FlowSnapshot, active_constraints: ActiveConstraintsSnapshot) {
self.current_use_def_map_mut().merge(state);
self.current_use_def_map_mut()
.restore_constraints(active_constraints);
}
fn add_symbol(&mut self, name: Name) -> ScopedSymbolId {
@@ -765,6 +773,7 @@ where
ast::Stmt::If(node) => {
self.visit_expr(&node.test);
let pre_if = self.flow_snapshot();
let pre_if_constraints = self.constraints_snapshot();
let constraint = self.record_expression_constraint(&node.test);
let mut constraints = vec![constraint];
self.visit_body(&node.body);
@@ -790,7 +799,7 @@ where
post_clauses.push(self.flow_snapshot());
// we can only take an elif/else branch if none of the previous ones were
// taken, so the block entry state is always `pre_if`
self.flow_restore(pre_if.clone());
self.flow_restore(pre_if.clone(), pre_if_constraints.clone());
for constraint in &constraints {
self.record_negated_constraint(*constraint);
}
@@ -801,7 +810,7 @@ where
self.visit_body(clause_body);
}
for post_clause_state in post_clauses {
self.flow_merge(post_clause_state);
self.flow_merge(post_clause_state, pre_if_constraints.clone());
}
}
ast::Stmt::While(ast::StmtWhile {
@@ -813,6 +822,7 @@ where
self.visit_expr(test);
let pre_loop = self.flow_snapshot();
let pre_loop_constraints = self.constraints_snapshot();
// Save aside any break states from an outer loop
let saved_break_states = std::mem::take(&mut self.loop_break_states);
@@ -831,13 +841,13 @@ where
// We may execute the `else` clause without ever executing the body, so merge in
// the pre-loop state before visiting `else`.
self.flow_merge(pre_loop);
self.flow_merge(pre_loop, pre_loop_constraints.clone());
self.visit_body(orelse);
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
// states after visiting `else`.
for break_state in break_states {
self.flow_merge(break_state);
self.flow_merge(break_state, pre_loop_constraints.clone()); // TODO?
}
}
ast::Stmt::With(ast::StmtWith {
@@ -880,6 +890,7 @@ where
self.visit_expr(iter);
let pre_loop = self.flow_snapshot();
let pre_loop_constraints = self.constraints_snapshot();
let saved_break_states = std::mem::take(&mut self.loop_break_states);
debug_assert_eq!(&self.current_assignments, &[]);
@@ -900,13 +911,13 @@ where
// We may execute the `else` clause without ever executing the body, so merge in
// the pre-loop state before visiting `else`.
self.flow_merge(pre_loop);
self.flow_merge(pre_loop, pre_loop_constraints.clone());
self.visit_body(orelse);
// Breaking out of a `for` loop bypasses the `else` clause, so merge in the break
// states after visiting `else`.
for break_state in break_states {
self.flow_merge(break_state);
self.flow_merge(break_state, pre_loop_constraints.clone());
}
}
ast::Stmt::Match(ast::StmtMatch {
@@ -918,6 +929,7 @@ where
self.visit_expr(subject);
let after_subject = self.flow_snapshot();
let after_subject_cs = self.constraints_snapshot();
let Some((first, remaining)) = cases.split_first() else {
return;
};
@@ -927,18 +939,18 @@ where
let mut post_case_snapshots = vec![];
for case in remaining {
post_case_snapshots.push(self.flow_snapshot());
self.flow_restore(after_subject.clone());
self.flow_restore(after_subject.clone(), after_subject_cs.clone());
self.add_pattern_constraint(subject, &case.pattern);
self.visit_match_case(case);
}
for post_clause_state in post_case_snapshots {
self.flow_merge(post_clause_state);
self.flow_merge(post_clause_state, after_subject_cs.clone());
}
if !cases
.last()
.is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard())
{
self.flow_merge(after_subject);
self.flow_merge(after_subject, after_subject_cs.clone());
}
}
ast::Stmt::Try(ast::StmtTry {
@@ -956,6 +968,7 @@ where
// We will merge this state with all of the intermediate
// states during the `try` block before visiting those suites.
let pre_try_block_state = self.flow_snapshot();
let pre_try_block_constraints = self.constraints_snapshot();
self.try_node_context_stack_manager.push_context();
@@ -976,14 +989,17 @@ where
// as there necessarily must have been 0 `except` blocks executed
// if we hit the `else` block.
let post_try_block_state = self.flow_snapshot();
let post_try_block_constraints = self.constraints_snapshot();
// Prepare for visiting the `except` block(s)
self.flow_restore(pre_try_block_state);
self.flow_restore(pre_try_block_state, pre_try_block_constraints.clone());
for state in try_block_snapshots {
self.flow_merge(state);
self.flow_merge(state, pre_try_block_constraints.clone());
// TODO?
}
let pre_except_state = self.flow_snapshot();
let pre_except_constraints = self.constraints_snapshot();
let num_handlers = handlers.len();
for (i, except_handler) in handlers.iter().enumerate() {
@@ -1022,19 +1038,22 @@ where
// as we'll immediately call `self.flow_restore()` to a different state
// as soon as this loop over the handlers terminates.
if i < (num_handlers - 1) {
self.flow_restore(pre_except_state.clone());
self.flow_restore(
pre_except_state.clone(),
pre_except_constraints.clone(),
);
}
}
// If we get to the `else` block, we know that 0 of the `except` blocks can have been executed,
// and the entire `try` block must have been executed:
self.flow_restore(post_try_block_state);
self.flow_restore(post_try_block_state, post_try_block_constraints);
}
self.visit_body(orelse);
for post_except_state in post_except_states {
self.flow_merge(post_except_state);
self.flow_merge(post_except_state, pre_try_block_constraints.clone());
}
// TODO: there's lots of complexity here that isn't yet handled by our model.
@@ -1191,19 +1210,17 @@ where
ast::Expr::If(ast::ExprIf {
body, test, orelse, ..
}) => {
// TODO detect statically known truthy or falsy test (via type inference, not naive
// AST inspection, so we can't simplify here, need to record test expression for
// later checking)
self.visit_expr(test);
let pre_if = self.flow_snapshot();
let pre_if_constraints = self.constraints_snapshot();
let constraint = self.record_expression_constraint(test);
self.visit_expr(body);
let post_body = self.flow_snapshot();
self.flow_restore(pre_if);
self.flow_restore(pre_if, pre_if_constraints.clone());
self.record_negated_constraint(constraint);
self.visit_expr(orelse);
self.flow_merge(post_body);
self.flow_merge(post_body, pre_if_constraints);
}
ast::Expr::ListComp(
list_comprehension @ ast::ExprListComp {
@@ -1264,7 +1281,7 @@ where
// AST inspection, so we can't simplify here, need to record test expression for
// later checking)
let mut snapshots = vec![];
let pre_op_constraints = self.constraints_snapshot();
for (index, value) in values.iter().enumerate() {
self.visit_expr(value);
// In the last value we don't need to take a snapshot nor add a constraint
@@ -1279,7 +1296,7 @@ where
}
}
for snapshot in snapshots {
self.flow_merge(snapshot);
self.flow_merge(snapshot, pre_op_constraints.clone());
}
}
_ => {

View File

@@ -221,6 +221,8 @@
//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it
//! visits a `StmtIf` node.
use std::collections::HashSet;
use self::symbol_state::{
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
ScopedConstraintId, ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
@@ -268,6 +270,109 @@ pub(crate) struct UseDefMap<'db> {
}
impl<'db> UseDefMap<'db> {
#[cfg(test)]
pub(crate) fn print(&self, db: &dyn crate::db::Db) {
use crate::semantic_index::constraint::ConstraintNode;
println!("all_definitions:");
println!("================");
for (id, d) in self.all_definitions.iter_enumerated() {
println!(
"{:?}: {:?} {:?} {:?}",
id,
d.category(db),
d.scope(db),
d.symbol(db),
);
println!(" {:?}", d.kind(db));
println!();
}
println!("all_constraints:");
println!("================");
for (id, c) in self.all_constraints.iter_enumerated() {
println!("{:?}: {:?}", id, c.node);
if let ConstraintNode::Expression(e) = c.node {
println!(" {:?}", e.node_ref(db));
}
}
println!();
println!("bindings_by_use:");
println!("================");
for (id, bindings) in self.bindings_by_use.iter_enumerated() {
println!("{:?}:", id);
for binding in bindings.iter() {
let definition = self.all_definitions[binding.definition];
let mut constraint_ids = binding.constraint_ids.peekable();
let mut active_constraint_ids =
binding.constraints_active_at_binding_ids.peekable();
println!(" * {:?}", definition);
if constraint_ids.peek().is_some() {
println!(" Constraints:");
for constraint_id in constraint_ids {
println!(" {:?}", self.all_constraints[constraint_id]);
}
} else {
println!(" No constraints");
}
println!();
if active_constraint_ids.peek().is_some() {
println!(" Active constraints at binding:");
for constraint_id in active_constraint_ids {
println!(" {:?}", self.all_constraints[constraint_id]);
}
} else {
println!(" No active constraints at binding");
}
}
}
println!();
println!("public_symbols:");
println!("================");
for (id, symbol) in self.public_symbols.iter_enumerated() {
println!("{:?}:", id);
println!(" * Bindings:");
for binding in symbol.bindings().iter() {
let definition = self.all_definitions[binding.definition];
let mut constraint_ids = binding.constraint_ids.peekable();
println!(" {:?}", definition);
if constraint_ids.peek().is_some() {
println!(" Constraints:");
for constraint_id in constraint_ids {
println!(" {:?}", self.all_constraints[constraint_id]);
}
} else {
println!(" No constraints");
}
}
println!(" * Declarations:");
for (declaration, _) in symbol.declarations().iter() {
let definition = self.all_definitions[declaration];
println!(" {:?}", definition);
}
println!();
}
println!();
println!();
}
pub(crate) fn bindings_at_use(
&self,
use_id: ScopedUseId,
@@ -352,6 +457,7 @@ impl<'db> UseDefMap<'db> {
) -> DeclarationsIterator<'a, 'db> {
DeclarationsIterator {
all_definitions: &self.all_definitions,
all_constraints: &self.all_constraints,
inner: declarations.iter(),
may_be_undeclared: declarations.may_be_undeclared(),
}
@@ -365,7 +471,7 @@ enum SymbolDefinitions {
Declarations(SymbolDeclarations),
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
@@ -384,6 +490,10 @@ impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
all_constraints: self.all_constraints,
constraint_ids: def_id_with_constraints.constraint_ids,
},
constraints_active_at_binding: ConstraintsIterator {
all_constraints: self.all_constraints,
constraint_ids: def_id_with_constraints.constraints_active_at_binding_ids,
},
})
}
}
@@ -393,8 +503,10 @@ impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
pub(crate) struct BindingWithConstraints<'map, 'db> {
pub(crate) binding: Definition<'db>,
pub(crate) constraints: ConstraintsIterator<'map, 'db>,
pub(crate) constraints_active_at_binding: ConstraintsIterator<'map, 'db>,
}
#[derive(Debug, Clone)]
pub(crate) struct ConstraintsIterator<'map, 'db> {
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
constraint_ids: ConstraintIdIterator<'map>,
@@ -414,6 +526,7 @@ impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
pub(crate) struct DeclarationsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
inner: DeclarationIdIterator<'map>,
may_be_undeclared: bool,
}
@@ -425,10 +538,18 @@ impl DeclarationsIterator<'_, '_> {
}
impl<'map, 'db> Iterator for DeclarationsIterator<'map, 'db> {
type Item = Definition<'db>;
type Item = (Definition<'db>, ConstraintsIterator<'map, 'db>);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|def_id| self.all_definitions[def_id])
self.inner.next().map(|(def_id, constraints)| {
(
self.all_definitions[def_id],
ConstraintsIterator {
all_constraints: self.all_constraints,
constraint_ids: constraints,
},
)
})
}
}
@@ -440,6 +561,9 @@ pub(super) struct FlowSnapshot {
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
}
#[derive(Clone, Debug)]
pub(super) struct ActiveConstraintsSnapshot(HashSet<ScopedConstraintId>);
#[derive(Debug, Default)]
pub(super) struct UseDefMapBuilder<'db> {
/// Append-only array of [`Definition`].
@@ -448,6 +572,8 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Append-only array of [`Constraint`].
all_constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
active_constraints: HashSet<ScopedConstraintId>,
/// Live bindings at each so-far-recorded use.
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
@@ -471,7 +597,7 @@ impl<'db> UseDefMapBuilder<'db> {
binding,
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
);
symbol_state.record_binding(def_id);
symbol_state.record_binding(def_id, &self.active_constraints);
}
pub(super) fn record_constraint(&mut self, constraint: Constraint<'db>) {
@@ -479,6 +605,7 @@ impl<'db> UseDefMapBuilder<'db> {
for state in &mut self.symbol_states {
state.record_constraint(constraint_id);
}
self.active_constraints.insert(constraint_id);
}
pub(super) fn record_declaration(
@@ -492,7 +619,7 @@ impl<'db> UseDefMapBuilder<'db> {
declaration,
SymbolDefinitions::Bindings(symbol_state.bindings().clone()),
);
symbol_state.record_declaration(def_id);
symbol_state.record_declaration(def_id, &self.active_constraints);
}
pub(super) fn record_declaration_and_binding(
@@ -503,8 +630,8 @@ impl<'db> UseDefMapBuilder<'db> {
// We don't need to store anything in self.definitions_by_definition.
let def_id = self.all_definitions.push(definition);
let symbol_state = &mut self.symbol_states[symbol];
symbol_state.record_declaration(def_id);
symbol_state.record_binding(def_id);
symbol_state.record_declaration(def_id, &self.active_constraints);
symbol_state.record_binding(def_id, &self.active_constraints);
}
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
@@ -523,6 +650,10 @@ impl<'db> UseDefMapBuilder<'db> {
}
}
pub(super) fn constraints_snapshot(&self) -> ActiveConstraintsSnapshot {
ActiveConstraintsSnapshot(self.active_constraints.clone())
}
/// Restore the current builder symbols state to the given snapshot.
pub(super) fn restore(&mut self, snapshot: FlowSnapshot) {
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
@@ -541,6 +672,10 @@ impl<'db> UseDefMapBuilder<'db> {
.resize(num_symbols, SymbolState::undefined());
}
pub(super) fn restore_constraints(&mut self, snapshot: ActiveConstraintsSnapshot) {
self.active_constraints = snapshot.0;
}
/// Merge the given snapshot into the current state, reflecting that we might have taken either
/// path to get here. The new state for each symbol should include definitions from both the
/// prior state and the snapshot.

View File

@@ -122,7 +122,7 @@ impl<const B: usize> BitSet<B> {
}
/// Iterator over values in a [`BitSet`].
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(super) struct BitSetIterator<'a, const B: usize> {
/// The blocks we are iterating over.
blocks: &'a [u64],

View File

@@ -43,6 +43,8 @@
//!
//! Tracking live declarations is simpler, since constraints are not involved, but otherwise very
//! similar to tracking live bindings.
use std::collections::HashSet;
use super::bitset::{BitSet, BitSetIterator};
use ruff_index::newtype_index;
use smallvec::SmallVec;
@@ -87,6 +89,8 @@ pub(super) struct SymbolDeclarations {
/// [`BitSet`]: which declarations (as [`ScopedDefinitionId`]) can reach the current location?
live_declarations: Declarations,
constraints_active_at_declaration: Constraints, // TODO: rename to constraints_active_at_declaration
/// Could the symbol be un-declared at this point?
may_be_undeclared: bool,
}
@@ -95,14 +99,27 @@ impl SymbolDeclarations {
fn undeclared() -> Self {
Self {
live_declarations: Declarations::default(),
constraints_active_at_declaration: Constraints::default(),
may_be_undeclared: true,
}
}
/// Record a newly-encountered declaration for this symbol.
fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
fn record_declaration(
&mut self,
declaration_id: ScopedDefinitionId,
active_constraints: &HashSet<ScopedConstraintId>,
) {
self.live_declarations = Declarations::with(declaration_id.into());
self.may_be_undeclared = false;
// TODO: unify code with below
self.constraints_active_at_declaration = Constraints::with_capacity(1);
self.constraints_active_at_declaration
.push(BitSet::default());
for active_constraint_id in active_constraints {
self.constraints_active_at_declaration[0].insert(active_constraint_id.as_u32());
}
}
/// Add undeclared as a possibility for this symbol.
@@ -114,6 +131,7 @@ impl SymbolDeclarations {
pub(super) fn iter(&self) -> DeclarationIdIterator {
DeclarationIdIterator {
inner: self.live_declarations.iter(),
constraints_active_at_binding: self.constraints_active_at_declaration.iter(),
}
}
@@ -138,6 +156,8 @@ pub(super) struct SymbolBindings {
/// binding in `live_bindings`.
constraints: Constraints,
constraints_active_at_binding: Constraints,
/// Could the symbol be unbound at this point?
may_be_unbound: bool,
}
@@ -147,6 +167,7 @@ impl SymbolBindings {
Self {
live_bindings: Bindings::default(),
constraints: Constraints::default(),
constraints_active_at_binding: Constraints::default(),
may_be_unbound: true,
}
}
@@ -157,12 +178,21 @@ impl SymbolBindings {
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
pub(super) fn record_binding(
&mut self,
binding_id: ScopedDefinitionId,
active_constraints: &HashSet<ScopedConstraintId>,
) {
// The new binding replaces all previous live bindings in this path, and has no
// constraints.
self.live_bindings = Bindings::with(binding_id.into());
self.constraints = Constraints::with_capacity(1);
self.constraints.push(BitSet::default());
self.constraints_active_at_binding = Constraints::with_capacity(1);
self.constraints_active_at_binding.push(BitSet::default());
for active_constraint_id in active_constraints {
self.constraints_active_at_binding[0].insert(active_constraint_id.as_u32());
}
self.may_be_unbound = false;
}
@@ -178,6 +208,7 @@ impl SymbolBindings {
BindingIdWithConstraintsIterator {
definitions: self.live_bindings.iter(),
constraints: self.constraints.iter(),
constraints_active_at_binding: self.constraints_active_at_binding.iter(),
}
}
@@ -207,8 +238,12 @@ impl SymbolState {
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
self.bindings.record_binding(binding_id);
pub(super) fn record_binding(
&mut self,
binding_id: ScopedDefinitionId,
active_constraints: &HashSet<ScopedConstraintId>,
) {
self.bindings.record_binding(binding_id, active_constraints);
}
/// Add given constraint to all live bindings.
@@ -222,8 +257,13 @@ impl SymbolState {
}
/// Record a newly-encountered declaration of this symbol.
pub(super) fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
self.declarations.record_declaration(declaration_id);
pub(super) fn record_declaration(
&mut self,
declaration_id: ScopedDefinitionId,
active_constraints: &HashSet<ScopedConstraintId>,
) {
self.declarations
.record_declaration(declaration_id, active_constraints);
}
/// Merge another [`SymbolState`] into this one.
@@ -232,24 +272,93 @@ impl SymbolState {
bindings: SymbolBindings {
live_bindings: Bindings::default(),
constraints: Constraints::default(),
constraints_active_at_binding: Constraints::default(), // TODO
may_be_unbound: self.bindings.may_be_unbound || b.bindings.may_be_unbound,
},
declarations: SymbolDeclarations {
live_declarations: self.declarations.live_declarations.clone(),
constraints_active_at_declaration: Constraints::default(), // TODO
may_be_undeclared: self.declarations.may_be_undeclared
|| b.declarations.may_be_undeclared,
},
};
// let mut constraints_active_at_binding = BitSet::default();
// for active_constraint_id in active_constraints.0 {
// constraints_active_at_binding.insert(active_constraint_id.as_u32());
// }
std::mem::swap(&mut a, self);
self.declarations
.live_declarations
.union(&b.declarations.live_declarations);
// self.declarations
// .live_declarations
// .union(&b.declarations.live_declarations);
let mut a_decls_iter = a.declarations.live_declarations.iter();
let mut b_decls_iter = b.declarations.live_declarations.iter();
let mut a_constraints_active_at_declaration_iter =
a.declarations.constraints_active_at_declaration.into_iter();
let mut b_constraints_active_at_declaration_iter =
b.declarations.constraints_active_at_declaration.into_iter();
let mut opt_a_decl: Option<u32> = a_decls_iter.next();
let mut opt_b_decl: Option<u32> = b_decls_iter.next();
let push = |decl,
constraints_active_at_declaration_iter: &mut ConstraintsIntoIterator,
merged: &mut Self| {
merged.declarations.live_declarations.insert(decl);
let constraints_active_at_binding = constraints_active_at_declaration_iter
.next()
.expect("declarations and constraints_active_at_binding length mismatch");
merged
.declarations
.constraints_active_at_declaration
.push(constraints_active_at_binding);
};
loop {
match (opt_a_decl, opt_b_decl) {
(Some(a_decl), Some(b_decl)) => match a_decl.cmp(&b_decl) {
std::cmp::Ordering::Less => {
push(a_decl, &mut a_constraints_active_at_declaration_iter, self);
opt_a_decl = a_decls_iter.next();
}
std::cmp::Ordering::Greater => {
push(b_decl, &mut b_constraints_active_at_declaration_iter, self);
opt_b_decl = b_decls_iter.next();
}
std::cmp::Ordering::Equal => {
push(a_decl, &mut b_constraints_active_at_declaration_iter, self);
self.declarations
.constraints_active_at_declaration
.last_mut()
.unwrap()
.intersect(&a_constraints_active_at_declaration_iter.next().unwrap());
opt_a_decl = a_decls_iter.next();
opt_b_decl = b_decls_iter.next();
}
},
(Some(a_decl), None) => {
push(a_decl, &mut a_constraints_active_at_declaration_iter, self);
opt_a_decl = a_decls_iter.next();
}
(None, Some(b_decl)) => {
push(b_decl, &mut b_constraints_active_at_declaration_iter, self);
opt_b_decl = b_decls_iter.next();
}
(None, None) => break,
}
}
let mut a_defs_iter = a.bindings.live_bindings.iter();
let mut b_defs_iter = b.bindings.live_bindings.iter();
let mut a_constraints_iter = a.bindings.constraints.into_iter();
let mut b_constraints_iter = b.bindings.constraints.into_iter();
let mut a_constraints_active_at_binding_iter =
a.bindings.constraints_active_at_binding.into_iter();
let mut b_constraints_active_at_binding_iter =
b.bindings.constraints_active_at_binding.into_iter();
let mut opt_a_def: Option<u32> = a_defs_iter.next();
let mut opt_b_def: Option<u32> = b_defs_iter.next();
@@ -261,7 +370,10 @@ impl SymbolState {
// path is irrelevant.
// Helper to push `def`, with constraints in `constraints_iter`, onto `self`.
let push = |def, constraints_iter: &mut ConstraintsIntoIterator, merged: &mut Self| {
let push = |def,
constraints_iter: &mut ConstraintsIntoIterator,
constraints_active_at_binding_iter: &mut ConstraintsIntoIterator,
merged: &mut Self| {
merged.bindings.live_bindings.insert(def);
// SAFETY: we only ever create SymbolState with either no definitions and no constraint
// bitsets (`::unbound`) or one definition and one constraint bitset (`::with`), and
@@ -271,7 +383,14 @@ impl SymbolState {
let constraints = constraints_iter
.next()
.expect("definitions and constraints length mismatch");
let constraints_active_at_binding = constraints_active_at_binding_iter
.next()
.expect("definitions and constraints_active_at_binding length mismatch");
merged.bindings.constraints.push(constraints);
merged
.bindings
.constraints_active_at_binding
.push(constraints_active_at_binding);
};
loop {
@@ -279,17 +398,32 @@ impl SymbolState {
(Some(a_def), Some(b_def)) => match a_def.cmp(&b_def) {
std::cmp::Ordering::Less => {
// Next definition ID is only in `a`, push it to `self` and advance `a`.
push(a_def, &mut a_constraints_iter, self);
push(
a_def,
&mut a_constraints_iter,
&mut a_constraints_active_at_binding_iter,
self,
);
opt_a_def = a_defs_iter.next();
}
std::cmp::Ordering::Greater => {
// Next definition ID is only in `b`, push it to `self` and advance `b`.
push(b_def, &mut b_constraints_iter, self);
push(
b_def,
&mut b_constraints_iter,
&mut b_constraints_active_at_binding_iter,
self,
);
opt_b_def = b_defs_iter.next();
}
std::cmp::Ordering::Equal => {
// Next definition is in both; push to `self` and intersect constraints.
push(a_def, &mut b_constraints_iter, self);
push(
a_def,
&mut b_constraints_iter,
&mut b_constraints_active_at_binding_iter,
self,
);
// SAFETY: we only ever create SymbolState with either no definitions and
// no constraint bitsets (`::unbound`) or one definition and one constraint
// bitset (`::with`), and `::merge` always pushes one definition and one
@@ -298,6 +432,11 @@ impl SymbolState {
let a_constraints = a_constraints_iter
.next()
.expect("definitions and constraints length mismatch");
// let _a_constraints_active_at_binding =
// a_constraints_active_at_binding_iter.next().expect(
// "definitions and constraints_active_at_binding length mismatch",
// ); // TODO: perform check that we see the same constraints in both paths
// If the same definition is visible through both paths, any constraint
// that applies on only one path is irrelevant to the resulting type from
// unioning the two paths, so we intersect the constraints.
@@ -306,18 +445,29 @@ impl SymbolState {
.last_mut()
.unwrap()
.intersect(&a_constraints);
opt_a_def = a_defs_iter.next();
opt_b_def = b_defs_iter.next();
}
},
(Some(a_def), None) => {
// We've exhausted `b`, just push the def from `a` and move on to the next.
push(a_def, &mut a_constraints_iter, self);
push(
a_def,
&mut a_constraints_iter,
&mut a_constraints_active_at_binding_iter,
self,
);
opt_a_def = a_defs_iter.next();
}
(None, Some(b_def)) => {
// We've exhausted `a`, just push the def from `b` and move on to the next.
push(b_def, &mut b_constraints_iter, self);
push(
b_def,
&mut b_constraints_iter,
&mut b_constraints_active_at_binding_iter,
self,
);
opt_b_def = b_defs_iter.next();
}
(None, None) => break,
@@ -353,26 +503,37 @@ impl Default for SymbolState {
pub(super) struct BindingIdWithConstraints<'a> {
pub(super) definition: ScopedDefinitionId,
pub(super) constraint_ids: ConstraintIdIterator<'a>,
pub(super) constraints_active_at_binding_ids: ConstraintIdIterator<'a>,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(super) struct BindingIdWithConstraintsIterator<'a> {
definitions: BindingsIterator<'a>,
constraints: ConstraintsIterator<'a>,
constraints_active_at_binding: ConstraintsIterator<'a>,
}
impl<'a> Iterator for BindingIdWithConstraintsIterator<'a> {
type Item = BindingIdWithConstraints<'a>;
fn next(&mut self) -> Option<Self::Item> {
match (self.definitions.next(), self.constraints.next()) {
(None, None) => None,
(Some(def), Some(constraints)) => Some(BindingIdWithConstraints {
definition: ScopedDefinitionId::from_u32(def),
constraint_ids: ConstraintIdIterator {
wrapped: constraints.iter(),
},
}),
match (
self.definitions.next(),
self.constraints.next(),
self.constraints_active_at_binding.next(),
) {
(None, None, None) => None,
(Some(def), Some(constraints), Some(constraints_active_at_binding)) => {
Some(BindingIdWithConstraints {
definition: ScopedDefinitionId::from_u32(def),
constraint_ids: ConstraintIdIterator {
wrapped: constraints.iter(),
},
constraints_active_at_binding_ids: ConstraintIdIterator {
wrapped: constraints_active_at_binding.iter(),
},
})
}
// SAFETY: see above.
_ => unreachable!("definitions and constraints length mismatch"),
}
@@ -381,7 +542,7 @@ impl<'a> Iterator for BindingIdWithConstraintsIterator<'a> {
impl std::iter::FusedIterator for BindingIdWithConstraintsIterator<'_> {}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(super) struct ConstraintIdIterator<'a> {
wrapped: BitSetIterator<'a, INLINE_CONSTRAINT_BLOCKS>,
}
@@ -399,13 +560,25 @@ impl std::iter::FusedIterator for ConstraintIdIterator<'_> {}
#[derive(Debug)]
pub(super) struct DeclarationIdIterator<'a> {
inner: DeclarationsIterator<'a>,
constraints_active_at_binding: ConstraintsIterator<'a>,
}
impl<'a> Iterator for DeclarationIdIterator<'a> {
type Item = ScopedDefinitionId;
type Item = (ScopedDefinitionId, ConstraintIdIterator<'a>);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(ScopedDefinitionId::from_u32)
// self.inner.next().map(ScopedDefinitionId::from_u32)
match (self.inner.next(), self.constraints_active_at_binding.next()) {
(None, None) => None,
(Some(declaration), Some(constraints_active_at_binding)) => Some((
ScopedDefinitionId::from_u32(declaration),
ConstraintIdIterator {
wrapped: constraints_active_at_binding.iter(),
},
)),
// SAFETY: see above.
_ => unreachable!("declarations and constraints_active_at_binding length mismatch"),
}
}
}
@@ -413,7 +586,7 @@ impl std::iter::FusedIterator for DeclarationIdIterator<'_> {}
#[cfg(test)]
mod tests {
use super::{ScopedConstraintId, ScopedDefinitionId, SymbolState};
use super::{ScopedConstraintId, SymbolState};
fn assert_bindings(symbol: &SymbolState, may_be_unbound: bool, expected: &[&str]) {
assert_eq!(symbol.may_be_unbound(), may_be_unbound);
@@ -445,7 +618,7 @@ mod tests {
let actual = symbol
.declarations()
.iter()
.map(ScopedDefinitionId::as_u32)
.map(|(d, _)| d.as_u32()) // TODO: constraints
.collect::<Vec<_>>();
assert_eq!(actual, expected);
}
@@ -457,76 +630,76 @@ mod tests {
assert_bindings(&sym, true, &[]);
}
#[test]
fn with() {
let mut sym = SymbolState::undefined();
sym.record_binding(ScopedDefinitionId::from_u32(0));
// #[test]
// fn with() {
// let mut sym = SymbolState::undefined();
// sym.record_binding(ScopedDefinitionId::from_u32(0));
assert_bindings(&sym, false, &["0<>"]);
}
// assert_bindings(&sym, false, &["0<>"]);
// }
#[test]
fn set_may_be_unbound() {
let mut sym = SymbolState::undefined();
sym.record_binding(ScopedDefinitionId::from_u32(0));
sym.set_may_be_unbound();
// #[test]
// fn set_may_be_unbound() {
// let mut sym = SymbolState::undefined();
// sym.record_binding(ScopedDefinitionId::from_u32(0));
// sym.set_may_be_unbound();
assert_bindings(&sym, true, &["0<>"]);
}
// assert_bindings(&sym, true, &["0<>"]);
// }
#[test]
fn record_constraint() {
let mut sym = SymbolState::undefined();
sym.record_binding(ScopedDefinitionId::from_u32(0));
sym.record_constraint(ScopedConstraintId::from_u32(0));
// #[test]
// fn record_constraint() {
// let mut sym = SymbolState::undefined();
// sym.record_binding(ScopedDefinitionId::from_u32(0));
// sym.record_constraint(ScopedConstraintId::from_u32(0));
assert_bindings(&sym, false, &["0<0>"]);
}
// assert_bindings(&sym, false, &["0<0>"]);
// }
#[test]
fn merge() {
// merging the same definition with the same constraint keeps the constraint
let mut sym0a = SymbolState::undefined();
sym0a.record_binding(ScopedDefinitionId::from_u32(0));
sym0a.record_constraint(ScopedConstraintId::from_u32(0));
// #[test]
// fn merge() {
// // merging the same definition with the same constraint keeps the constraint
// let mut sym0a = SymbolState::undefined();
// sym0a.record_binding(ScopedDefinitionId::from_u32(0));
// sym0a.record_constraint(ScopedConstraintId::from_u32(0));
let mut sym0b = SymbolState::undefined();
sym0b.record_binding(ScopedDefinitionId::from_u32(0));
sym0b.record_constraint(ScopedConstraintId::from_u32(0));
// let mut sym0b = SymbolState::undefined();
// sym0b.record_binding(ScopedDefinitionId::from_u32(0));
// sym0b.record_constraint(ScopedConstraintId::from_u32(0));
sym0a.merge(sym0b);
let mut sym0 = sym0a;
assert_bindings(&sym0, false, &["0<0>"]);
// sym0a.merge(sym0b);
// let mut sym0 = sym0a;
// assert_bindings(&sym0, false, &["0<0>"]);
// merging the same definition with differing constraints drops all constraints
let mut sym1a = SymbolState::undefined();
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
sym1a.record_constraint(ScopedConstraintId::from_u32(1));
// // merging the same definition with differing constraints drops all constraints
// let mut sym1a = SymbolState::undefined();
// sym1a.record_binding(ScopedDefinitionId::from_u32(1));
// sym1a.record_constraint(ScopedConstraintId::from_u32(1));
let mut sym1b = SymbolState::undefined();
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
// let mut sym1b = SymbolState::undefined();
// sym1b.record_binding(ScopedDefinitionId::from_u32(1));
// sym1b.record_constraint(ScopedConstraintId::from_u32(2));
sym1a.merge(sym1b);
let sym1 = sym1a;
assert_bindings(&sym1, false, &["1<>"]);
// sym1a.merge(sym1b);
// let sym1 = sym1a;
// assert_bindings(&sym1, false, &["1<>"]);
// merging a constrained definition with unbound keeps both
let mut sym2a = SymbolState::undefined();
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
sym2a.record_constraint(ScopedConstraintId::from_u32(3));
// // merging a constrained definition with unbound keeps both
// let mut sym2a = SymbolState::undefined();
// sym2a.record_binding(ScopedDefinitionId::from_u32(2));
// sym2a.record_constraint(ScopedConstraintId::from_u32(3));
let sym2b = SymbolState::undefined();
// let sym2b = SymbolState::undefined();
sym2a.merge(sym2b);
let sym2 = sym2a;
assert_bindings(&sym2, true, &["2<3>"]);
// sym2a.merge(sym2b);
// let sym2 = sym2a;
// assert_bindings(&sym2, true, &["2<3>"]);
// merging different definitions keeps them each with their existing constraints
sym0.merge(sym2);
let sym = sym0;
assert_bindings(&sym, true, &["0<0>", "2<3>"]);
}
// // merging different definitions keeps them each with their existing constraints
// sym0.merge(sym2);
// let sym = sym0;
// assert_bindings(&sym, true, &["0<0>", "2<3>"]);
// }
#[test]
fn no_declaration() {
@@ -535,54 +708,54 @@ mod tests {
assert_declarations(&sym, true, &[]);
}
#[test]
fn record_declaration() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
// #[test]
// fn record_declaration() {
// let mut sym = SymbolState::undefined();
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
assert_declarations(&sym, false, &[1]);
}
// assert_declarations(&sym, false, &[1]);
// }
#[test]
fn record_declaration_override() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
sym.record_declaration(ScopedDefinitionId::from_u32(2));
// #[test]
// fn record_declaration_override() {
// let mut sym = SymbolState::undefined();
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
// sym.record_declaration(ScopedDefinitionId::from_u32(2));
assert_declarations(&sym, false, &[2]);
}
// assert_declarations(&sym, false, &[2]);
// }
#[test]
fn record_declaration_merge() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
// #[test]
// fn record_declaration_merge() {
// let mut sym = SymbolState::undefined();
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
let mut sym2 = SymbolState::undefined();
sym2.record_declaration(ScopedDefinitionId::from_u32(2));
// let mut sym2 = SymbolState::undefined();
// sym2.record_declaration(ScopedDefinitionId::from_u32(2));
sym.merge(sym2);
// sym.merge(sym2);
assert_declarations(&sym, false, &[1, 2]);
}
// assert_declarations(&sym, false, &[1, 2]);
// }
#[test]
fn record_declaration_merge_partial_undeclared() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
// #[test]
// fn record_declaration_merge_partial_undeclared() {
// let mut sym = SymbolState::undefined();
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
let sym2 = SymbolState::undefined();
// let sym2 = SymbolState::undefined();
sym.merge(sym2);
// sym.merge(sym2);
assert_declarations(&sym, true, &[1]);
}
// assert_declarations(&sym, true, &[1]);
// }
#[test]
fn set_may_be_undeclared() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(0));
sym.set_may_be_undeclared();
// #[test]
// fn set_may_be_undeclared() {
// let mut sym = SymbolState::undefined();
// sym.record_declaration(ScopedDefinitionId::from_u32(0));
// sym.set_may_be_undeclared();
assert_declarations(&sym, true, &[0]);
}
// assert_declarations(&sym, true, &[0]);
// }
}

View File

@@ -15,6 +15,7 @@ pub(crate) use self::infer::{
pub(crate) use self::signatures::Signature;
use crate::module_resolver::file_to_module;
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::constraint::ConstraintNode;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::{self as symbol, ScopeId, ScopedSymbolId};
use crate::semantic_index::{
@@ -222,6 +223,12 @@ fn definition_expression_ty<'db>(
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum UnconditionallyVisible {
Yes,
No,
}
/// Infer the combined type of an iterator of bindings.
///
/// Will return a union if there is more than one binding.
@@ -229,29 +236,88 @@ fn bindings_ty<'db>(
db: &'db dyn Db,
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
) -> Option<Type<'db>> {
let mut def_types = bindings_with_constraints.map(
let def_types = bindings_with_constraints.map(
|BindingWithConstraints {
binding,
constraints,
constraints_active_at_binding,
}| {
let mut constraint_tys = constraints
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
.peekable();
let test_expr_tys = || {
constraints_active_at_binding.clone().map(|c| {
let ty = if let ConstraintNode::Expression(test_expr) = c.node {
let inference = infer_expression_types(db, test_expr);
let scope = test_expr.scope(db);
inference
.expression_ty(test_expr.node_ref(db).scoped_expression_id(db, scope))
} else {
// TODO: handle other constraint nodes
todo_type!()
};
let binding_ty = binding_ty(db, binding);
if constraint_tys.peek().is_some() {
constraint_tys
.fold(
IntersectionBuilder::new(db).add_positive(binding_ty),
IntersectionBuilder::add_positive,
)
.build()
(c, ty)
})
};
if test_expr_tys().any(|(c, test_expr_ty)| {
if c.is_positive {
test_expr_ty.bool(db).is_always_false()
} else {
test_expr_ty.bool(db).is_always_true()
}
}) {
// TODO: do we need to call binding_ty(…) even if we don't need the result?
(Type::Never, UnconditionallyVisible::No)
} else {
binding_ty
let mut test_expr_tys_iter = test_expr_tys().peekable();
let unconditionally_visible = if test_expr_tys_iter.peek().is_some()
&& test_expr_tys_iter.all(|(c, test_expr_ty)| {
if c.is_positive {
test_expr_ty.bool(db).is_always_true()
} else {
test_expr_ty.bool(db).is_always_false()
}
}) {
UnconditionallyVisible::Yes
} else {
UnconditionallyVisible::No
};
let mut constraint_tys = constraints
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
.peekable();
let binding_ty = binding_ty(db, binding);
if constraint_tys.peek().is_some() {
let intersection_ty = constraint_tys
.fold(
IntersectionBuilder::new(db).add_positive(binding_ty),
IntersectionBuilder::add_positive,
)
.build();
(intersection_ty, unconditionally_visible)
} else {
(binding_ty, unconditionally_visible)
}
}
},
);
// TODO: get rid of all the collects and clean up, obviously
let def_types: Vec<_> = def_types.collect();
// shrink the vector to only include everything from the last unconditionally visible binding
let def_types: Vec<_> = def_types
.iter()
.rev()
.take_while_inclusive(|(_, unconditionally_visible)| {
*unconditionally_visible != UnconditionallyVisible::Yes
})
.map(|(ty, _)| *ty)
.collect();
let mut def_types = def_types.into_iter().rev();
if let Some(first) = def_types.next() {
if let Some(second) = def_types.next() {
Some(UnionType::from_elements(
@@ -287,7 +353,63 @@ fn declarations_ty<'db>(
declarations: DeclarationsIterator<'_, 'db>,
undeclared_ty: Option<Type<'db>>,
) -> DeclaredTypeResult<'db> {
let decl_types = declarations.map(|declaration| declaration_ty(db, declaration));
let decl_types = declarations.map(|(declaration, constraints_active_at_declaration)| {
let test_expr_tys = || {
constraints_active_at_declaration.clone().map(|c| {
let ty = if let ConstraintNode::Expression(test_expr) = c.node {
let inference = infer_expression_types(db, test_expr);
let scope = test_expr.scope(db);
inference.expression_ty(test_expr.node_ref(db).scoped_expression_id(db, scope))
} else {
// TODO: handle other constraint nodes
todo_type!()
};
(c, ty)
})
};
if test_expr_tys().any(|(c, test_expr_ty)| {
if c.is_positive {
test_expr_ty.bool(db).is_always_false()
} else {
test_expr_ty.bool(db).is_always_true()
}
}) {
(Type::Never, UnconditionallyVisible::No)
} else {
let mut test_expr_tys_iter = test_expr_tys().peekable();
if test_expr_tys_iter.peek().is_some()
&& test_expr_tys_iter.all(|(c, test_expr_ty)| {
if c.is_positive {
test_expr_ty.bool(db).is_always_true()
} else {
test_expr_ty.bool(db).is_always_false()
}
})
{
(declaration_ty(db, declaration), UnconditionallyVisible::Yes)
} else {
(declaration_ty(db, declaration), UnconditionallyVisible::No)
}
}
});
// TODO: get rid of all the collects and clean up, obviously
let decl_types: Vec<_> = decl_types.collect();
// shrink the vector to only include everything from the last unconditionally visible binding
let decl_types: Vec<_> = decl_types
.iter()
.rev()
.take_while_inclusive(|(_, unconditionally_visible)| {
*unconditionally_visible != UnconditionallyVisible::Yes
})
.map(|(ty, _)| *ty)
.collect();
let decl_types = decl_types.into_iter().rev();
let mut all_types = undeclared_ty.into_iter().chain(decl_types);
@@ -768,23 +890,7 @@ impl<'db> Type<'db> {
// TODO: Once we have support for final classes, we can establish that
// `Type::SubclassOf('FinalClass')` is equivalent to `Type::ClassLiteral('FinalClass')`.
// TODO: The following is a workaround that is required to unify the two different versions
// of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once
// we understand `sys.version_info` branches.
self == other
|| matches!((self, other), (Type::Todo(_), Type::Todo(_)))
|| matches!((self, other),
(
Type::Instance(InstanceType { class: self_class }),
Type::Instance(InstanceType { class: target_class })
)
if {
let self_known = self_class.known(db);
matches!(self_known, Some(KnownClass::NoneType | KnownClass::NoDefaultType))
&& self_known == target_class.known(db)
}
)
self == other || matches!((self, other), (Type::Todo(_), Type::Todo(_)))
}
/// Return true if this type and `other` have no common elements.
@@ -1763,13 +1869,13 @@ impl<'db> KnownClass {
}
pub fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
core_module_symbol(db, self.canonical_module(), self.as_str())
core_module_symbol(db, self.canonical_module(db), self.as_str())
.ignore_possibly_unbound()
.unwrap_or(Type::Unknown)
}
/// Return the module in which we should look up the definition for this class
pub(crate) const fn canonical_module(self) -> CoreStdlibModule {
pub(crate) fn canonical_module(self, db: &'db dyn Db) -> CoreStdlibModule {
match self {
Self::Bool
| Self::Object
@@ -1787,10 +1893,18 @@ impl<'db> KnownClass {
Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types,
Self::NoneType => CoreStdlibModule::Typeshed,
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType => CoreStdlibModule::Typing,
// TODO when we understand sys.version_info, we will need an explicit fallback here,
// because typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
// singleton, but not for `typing._NoDefaultType`
Self::NoDefaultType => CoreStdlibModule::TypingExtensions,
Self::NoDefaultType => {
let python_version = Program::get(db).target_version(db);
// typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
// singleton, but not for `typing._NoDefaultType`. So we need to switch
// to `typing.NoDefault` for newer versions:
if python_version.major >= 3 && python_version.minor >= 13 {
CoreStdlibModule::Typing
} else {
CoreStdlibModule::TypingExtensions
}
}
}
}
@@ -1850,11 +1964,11 @@ impl<'db> KnownClass {
};
let module = file_to_module(db, file)?;
candidate.check_module(&module).then_some(candidate)
candidate.check_module(db, &module).then_some(candidate)
}
/// Return `true` if the module of `self` matches `module_name`
fn check_module(self, module: &Module) -> bool {
fn check_module(self, db: &dyn Db, module: &Module) -> bool {
if !module.search_path().is_standard_library() {
return false;
}
@@ -1874,7 +1988,7 @@ impl<'db> KnownClass {
| Self::GenericAlias
| Self::ModuleType
| Self::VersionInfo
| Self::FunctionType => module.name() == self.canonical_module().as_str(),
| Self::FunctionType => module.name() == self.canonical_module(db).as_str(),
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => {
matches!(module.name().as_str(), "typing" | "typing_extensions")
@@ -2396,6 +2510,14 @@ impl Truthiness {
matches!(self, Truthiness::Ambiguous)
}
const fn is_always_false(self) -> bool {
matches!(self, Truthiness::AlwaysFalse)
}
const fn is_always_true(self) -> bool {
matches!(self, Truthiness::AlwaysTrue)
}
const fn negate(self) -> Self {
match self {
Self::AlwaysTrue => Self::AlwaysFalse,
@@ -3036,7 +3158,7 @@ pub(crate) mod tests {
use ruff_python_ast as ast;
use test_case::test_case;
pub(crate) fn setup_db() -> TestDb {
pub(crate) fn setup_db_with_python_version(python_version: PythonVersion) -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
@@ -3047,7 +3169,7 @@ pub(crate) mod tests {
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
target_version: python_version,
search_paths: SearchPathSettings::new(src_root),
},
)
@@ -3056,6 +3178,10 @@ pub(crate) mod tests {
db
}
pub(crate) fn setup_db() -> TestDb {
setup_db_with_python_version(PythonVersion::default())
}
/// A test representation of a type that can be transformed unambiguously into a real Type,
/// given a db.
#[derive(Debug, Clone)]
@@ -3503,13 +3629,23 @@ pub(crate) mod tests {
#[test_case(Ty::None)]
#[test_case(Ty::BooleanLiteral(true))]
#[test_case(Ty::BooleanLiteral(false))]
#[test_case(Ty::KnownClassInstance(KnownClass::NoDefaultType))]
fn is_singleton(from: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_singleton(&db));
}
/// TODO: test documentation
#[test_case(PythonVersion::PY312)]
#[test_case(PythonVersion::PY313)]
fn no_default_type_is_singleton(python_version: PythonVersion) {
let db = setup_db_with_python_version(python_version);
let no_default = Ty::KnownClassInstance(KnownClass::NoDefaultType).into_type(&db);
assert!(no_default.is_singleton(&db));
}
#[test_case(Ty::None)]
#[test_case(Ty::BooleanLiteral(true))]
#[test_case(Ty::IntLiteral(1))]

View File

@@ -5031,7 +5031,7 @@ mod tests {
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::python_version::{self, PythonVersion};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::FileScopeId;
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
@@ -5041,10 +5041,11 @@ mod tests {
use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_db::testing::assert_function_query_was_not_run;
use test_case::test_case;
use super::*;
fn setup_db() -> TestDb {
fn setup_db_with_python_version(python_version: PythonVersion) -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
@@ -5055,7 +5056,7 @@ mod tests {
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
target_version: python_version,
search_paths: SearchPathSettings::new(src_root),
},
)
@@ -5064,6 +5065,10 @@ mod tests {
db
}
fn setup_db() -> TestDb {
setup_db_with_python_version(PythonVersion::default())
}
fn setup_db_with_custom_typeshed<'a>(
typeshed: &str,
files: impl IntoIterator<Item = (&'a str, &'a str)>,
@@ -5335,9 +5340,10 @@ mod tests {
Ok(())
}
#[test]
fn ellipsis_type() -> anyhow::Result<()> {
let mut db = setup_db();
#[test_case(PythonVersion::PY39, "ellipsis")]
#[test_case(PythonVersion::PY310, "EllipsisType")]
fn ellipsis_type(version: PythonVersion, expected_type: &str) -> anyhow::Result<()> {
let mut db = setup_db_with_python_version(version);
db.write_dedented(
"src/a.py",
@@ -5346,8 +5352,7 @@ mod tests {
",
)?;
// TODO: sys.version_info
assert_public_ty(&db, "src/a.py", "x", "EllipsisType | ellipsis");
assert_public_ty(&db, "src/a.py", "x", expected_type);
Ok(())
}

View File

@@ -185,11 +185,11 @@ impl Settings {
pub enum TargetVersion {
Py37,
Py38,
#[default]
Py39,
Py310,
Py311,
Py312,
#[default]
Py313,
}

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: "&workspace"
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -24,7 +23,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: workspace
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -24,7 +23,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: workspace
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -24,7 +23,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: workspace
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -24,7 +23,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: workspace
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -37,7 +36,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: workspace
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -50,7 +49,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],