[red-knot] small efficiency improvements and bugfixes to use-def map building (#12373)

Adds inference tests sufficient to give full test coverage of the
`UseDefMapBuilder::merge` method.

In the process I realized that we could implement visiting of if
statements in `SemanticBuilder` with fewer `snapshot`, `restore`, and
`merge` operations, so I restructured that visit a bit.

I also found one correctness bug in the `merge` method (it failed to
extend the given snapshot with "unbound" for any missing symbols,
meaning we would just lose the fact that the symbol could be unbound in
the merged-in path), and two efficiency bugs (if one of the ranges to
merge is empty, we can just use the other one, no need for copies, and
if the ranges are overlapping -- which can occur with nested branches --
we can still just merge them with no copies), and fixed all three.
This commit is contained in:
Carl Meyer 2024-07-18 09:24:58 -07:00 committed by GitHub
parent 8f1be31289
commit 811f78d94d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 156 additions and 57 deletions

View file

@ -143,7 +143,7 @@ impl<'db> SemanticIndexBuilder<'db> {
self.current_use_def_map().restore(state);
}
fn flow_merge(&mut self, state: FlowSnapshot) {
fn flow_merge(&mut self, state: &FlowSnapshot) {
self.current_use_def_map().merge(state);
}
@ -393,27 +393,27 @@ where
self.visit_expr(&node.test);
let pre_if = self.flow_snapshot();
self.visit_body(&node.body);
let mut last_clause_is_else = false;
let mut post_clauses: Vec<FlowSnapshot> = vec![self.flow_snapshot()];
let mut post_clauses: Vec<FlowSnapshot> = vec![];
for clause in &node.elif_else_clauses {
// we can only take an elif/else clause if none of the previous ones were taken
// snapshot after every block except the last; the last one will just become
// the state that we merge the other snapshots into
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.visit_elif_else_clause(clause);
post_clauses.push(self.flow_snapshot());
if clause.test.is_none() {
last_clause_is_else = true;
}
}
let mut post_clause_iter = post_clauses.into_iter();
if last_clause_is_else {
// if the last clause was an else, the pre_if state can't directly reach the
// post-state; we must enter one of the clauses.
self.flow_restore(post_clause_iter.next().unwrap());
} else {
self.flow_restore(pre_if);
for post_clause_state in post_clauses {
self.flow_merge(&post_clause_state);
}
for post_clause_state in post_clause_iter {
self.flow_merge(post_clause_state);
let has_else = node
.elif_else_clauses
.last()
.is_some_and(|clause| clause.test.is_none());
if !has_else {
// if there's no else clause, then it's possible we took none of the branches,
// and the pre_if state can reach here
self.flow_merge(&pre_if);
}
}
_ => {
@ -485,7 +485,7 @@ where
let post_body = self.flow_snapshot();
self.flow_restore(pre_if);
self.visit_expr(orelse);
self.flow_merge(post_body);
self.flow_merge(&post_body);
}
_ => {
walk_expr(self, expr);