Skip to content

Commit 4225c28

Browse files
committed
Introduce SnapshotCompleteness to retain and utilize incomplete snapshots
1 parent 6898535 commit 4225c28

File tree

8 files changed

+269
-137
lines changed

8 files changed

+269
-137
lines changed

crates/ty_python_semantic/resources/mdtest/public_types.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,7 @@ def outer() -> None:
214214
x = 1
215215

216216
def inner() -> None:
217-
# TODO: ideally, this should be `Literal[1, 2]`
218-
reveal_type(x) # revealed: None | Literal[1, 2]
217+
reveal_type(x) # revealed: Literal[1, 2]
219218
inner()
220219

221220
x = 2

crates/ty_python_semantic/src/place.rs

Lines changed: 19 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ use crate::dunder_all::dunder_all_names;
44
use crate::module_resolver::{KnownModule, file_to_module};
55
use crate::semantic_index::definition::{Definition, DefinitionState};
66
use crate::semantic_index::place::{PlaceExprRef, ScopedPlaceId};
7-
use crate::semantic_index::scope::ScopeId;
7+
use crate::semantic_index::scope::{ScopeId, ScopeLaziness};
88
use crate::semantic_index::{
9-
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator, place_table,
9+
BindingWithConstraints, BindingWithConstraintsIterator, BoundnessAnalysis,
10+
ConsideredDefinitions, DeclarationsIterator, EnclosingSnapshotResult, place_table,
1011
};
1112
use crate::semantic_index::{DeclarationWithConstraint, global_scope, use_def_map};
1213
use crate::types::{
@@ -293,7 +294,7 @@ pub(crate) fn explicit_global_symbol<'db>(
293294
global_scope(db, file),
294295
name,
295296
RequiresExplicitReExport::No,
296-
ConsideredDefinitions::AllReachable,
297+
ConsideredDefinitions::AllReachable(None),
297298
)
298299
}
299300

@@ -700,15 +701,24 @@ fn place_by_id<'db>(
700701

701702
let declarations = match considered_definitions {
702703
ConsideredDefinitions::EndOfScope => use_def.end_of_scope_declarations(place_id),
703-
ConsideredDefinitions::AllReachable => use_def.all_reachable_declarations(place_id),
704+
ConsideredDefinitions::AllReachable(_) => use_def.all_reachable_declarations(place_id),
704705
};
705706

706707
let declared = place_from_declarations_impl(db, declarations, requires_explicit_reexport)
707708
.ignore_conflicting_declarations();
708709

709710
let all_considered_bindings = || match considered_definitions {
710711
ConsideredDefinitions::EndOfScope => use_def.end_of_scope_bindings(place_id),
711-
ConsideredDefinitions::AllReachable => use_def.all_reachable_bindings(place_id),
712+
ConsideredDefinitions::AllReachable(None) => use_def.all_reachable_bindings(place_id),
713+
ConsideredDefinitions::AllReachable(Some(snapshot)) => {
714+
if let EnclosingSnapshotResult::FoundBindings(bindings, _) =
715+
use_def.enclosing_snapshot(snapshot, ScopeLaziness::Lazy)
716+
{
717+
bindings
718+
} else {
719+
use_def.all_reachable_bindings(place_id)
720+
}
721+
}
712722
};
713723

714724
// If a symbol is undeclared, but qualified with `typing.Final`, we use the right-hand side
@@ -763,7 +773,7 @@ fn place_by_id<'db>(
763773
// Place is possibly undeclared and (possibly) bound
764774
Place::Type(inferred_ty, boundness) => Place::Type(
765775
UnionType::from_elements(db, [inferred_ty, declared_ty]),
766-
if boundness_analysis == BoundnessAnalysis::AssumeBound {
776+
if boundness_analysis.is_assume_bound() {
767777
Boundness::Bound
768778
} else {
769779
boundness
@@ -782,7 +792,7 @@ fn place_by_id<'db>(
782792
let boundness_analysis = bindings.boundness_analysis;
783793
let mut inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport);
784794

785-
if boundness_analysis == BoundnessAnalysis::AssumeBound {
795+
if boundness_analysis.is_assume_bound() {
786796
if let Place::Type(ty, Boundness::PossiblyUnbound) = inferred {
787797
inferred = Place::Type(ty, Boundness::Bound);
788798
}
@@ -1037,7 +1047,7 @@ fn place_from_bindings_impl<'db>(
10371047
};
10381048

10391049
let boundness = match boundness_analysis {
1040-
BoundnessAnalysis::AssumeBound => Boundness::Bound,
1050+
BoundnessAnalysis::AssumeBound(_) => Boundness::Bound,
10411051
BoundnessAnalysis::BasedOnUnboundVisibility => match unbound_visibility() {
10421052
Some(Truthiness::AlwaysTrue) => {
10431053
unreachable!(
@@ -1245,7 +1255,7 @@ fn place_from_declarations_impl<'db>(
12451255
};
12461256

12471257
let boundness = match boundness_analysis {
1248-
BoundnessAnalysis::AssumeBound => {
1258+
BoundnessAnalysis::AssumeBound(_) => {
12491259
if all_declarations_definitely_reachable {
12501260
Boundness::Bound
12511261
} else {
@@ -1458,48 +1468,6 @@ impl RequiresExplicitReExport {
14581468
}
14591469
}
14601470

1461-
/// Specifies which definitions should be considered when looking up a place.
1462-
///
1463-
/// In the example below, the `EndOfScope` variant would consider the `x = 2` and `x = 3` definitions,
1464-
/// while the `AllReachable` variant would also consider the `x = 1` definition.
1465-
/// ```py
1466-
/// def _():
1467-
/// x = 1
1468-
///
1469-
/// x = 2
1470-
///
1471-
/// if flag():
1472-
/// x = 3
1473-
/// ```
1474-
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
1475-
pub(crate) enum ConsideredDefinitions {
1476-
/// Consider only the definitions that are "live" at the end of the scope, i.e. those
1477-
/// that have not been shadowed or deleted.
1478-
EndOfScope,
1479-
/// Consider all definitions that are reachable from the start of the scope.
1480-
AllReachable,
1481-
}
1482-
1483-
/// Specifies how the boundness of a place should be determined.
1484-
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
1485-
pub(crate) enum BoundnessAnalysis {
1486-
/// The place is always considered bound.
1487-
AssumeBound,
1488-
/// The boundness of the place is determined based on the visibility of the implicit
1489-
/// `unbound` binding. In the example below, when analyzing the visibility of the
1490-
/// `x = <unbound>` binding from the position of the end of the scope, it would be
1491-
/// `Truthiness::Ambiguous`, because it could either be visible or not, depending on the
1492-
/// `flag()` return value. This would result in a `Boundness::PossiblyUnbound` for `x`.
1493-
///
1494-
/// ```py
1495-
/// x = <unbound>
1496-
///
1497-
/// if flag():
1498-
/// x = 1
1499-
/// ```
1500-
BasedOnUnboundVisibility,
1501-
}
1502-
15031471
/// Computes a possibly-widened type `Unknown | T_inferred` from the inferred type `T_inferred`
15041472
/// of a symbol, unless the type is a known-instance type (e.g. `typing.Any`) or the symbol is
15051473
/// considered non-modifiable (e.g. when the symbol is `@Final`). We need this for public uses

crates/ty_python_semantic/src/semantic_index.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ use crate::semantic_index::scope::{
2626
NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopeLaziness,
2727
};
2828
use crate::semantic_index::symbol::ScopedSymbolId;
29+
pub(crate) use crate::semantic_index::use_def::{
30+
BoundnessAnalysis, ConsideredDefinitions, SnapshotCompleteness,
31+
};
2932
use crate::semantic_index::use_def::{EnclosingSnapshotKey, ScopedEnclosingSnapshotId, UseDefMap};
3033
use crate::semantic_model::HasTrackedScope;
3134

@@ -190,7 +193,10 @@ pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
190193

191194
pub(crate) enum EnclosingSnapshotResult<'map, 'db> {
192195
FoundConstraint(ScopedNarrowingConstraint),
193-
FoundBindings(BindingWithConstraintsIterator<'map, 'db>),
196+
FoundBindings(
197+
BindingWithConstraintsIterator<'map, 'db>,
198+
SnapshotCompleteness,
199+
),
194200
NotFound,
195201
NoLongerInEagerContext,
196202
}

crates/ty_python_semantic/src/semantic_index/builder.rs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,8 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
400400
}
401401
}
402402

403-
/// Any lazy snapshots of the place that have been reassigned are no longer valid, so delete them.
404-
/// If there is no reassignment, the state of the symbol just before the closure definition is recorded as a lazy snapshot.
403+
/// Any lazy snapshots of the place that have been reassigned are no longer valid on their own, so mark as incomplete.
404+
/// If there is no reassignment, the state of the symbol just before the closure definition is recorded as a complete lazy snapshot.
405405
/// We have not yet tracked where the closure is actually referenced, but we can assume that the closure becomes referenceable after the closure is defined.
406406
/// ```py
407407
/// def outer() -> None:
@@ -416,44 +416,44 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
416416
/// reveal_type(x) # revealed: None | Literal[1]
417417
///
418418
/// # Reassignment of `x` after the definition of `inner2`.
419-
/// # Sweep lazy snapshots of `x` for `inner2`.
419+
/// # lazy snapshots of `x` for `inner2` is incomplete.
420420
/// x = 1
421421
///
422422
/// def inner() -> None:
423423
/// # In this scope, `x = None` appears as being shadowed by `x = 1`.
424424
/// reveal_type(x) # revealed: Literal[1]
425425
///
426-
/// # No reassignment of `x` after the definition of `inner`, so we can safely record a lazy snapshot for `inner`.
426+
/// # No reassignment of `x` after the definition of `inner`, so we can safely use a complete lazy snapshot for `inner`.
427427
/// inner()
428428
/// inner2()
429429
/// ```
430-
fn sweep_lazy_snapshots(&mut self, popped_scope_id: FileScopeId) {
431-
// Retain only snapshots that are either eager
432-
// || (enclosing_scope != popped_scope && popped_scope is not a visible ancestor of enclosing_scope)
433-
// || enclosing_place is not a symbol or not reassigned
434-
// <=> remove those that are lazy
435-
// && (enclosing_scope == popped_scope || popped_scope is a visible ancestor of enclosing_scope)
436-
// && enclosing_place is a symbol and reassigned
437-
self.enclosing_snapshots.retain(|key, snapshot_id| {
438-
let popped_place_table = &self.place_tables[popped_scope_id];
439-
key.nested_laziness.is_eager()
440-
|| (key.enclosing_scope != popped_scope_id
441-
&& VisibleAncestorsIter::new(&self.scopes, key.enclosing_scope)
442-
.all(|(ancestor, _)| ancestor != popped_scope_id))
443-
|| !key.enclosing_place.as_symbol().is_some_and(|symbol_id| {
430+
fn check_lazy_snapshots(&mut self, popped_scope_id: FileScopeId) {
431+
for (key, snapshot_id) in &self.enclosing_snapshots {
432+
if let Some(enclosing_symbol) = key.enclosing_place.as_symbol() {
433+
if key.nested_laziness.is_lazy()
434+
&& (key.enclosing_scope == popped_scope_id
435+
|| VisibleAncestorsIter::new(&self.scopes, key.enclosing_scope)
436+
.any(|(ancestor, _)| ancestor == popped_scope_id))
437+
{
444438
let name = &self.place_tables[key.enclosing_scope]
445-
.symbol(symbol_id)
439+
.symbol(enclosing_symbol)
446440
.name();
447-
popped_place_table.symbol_id(name).is_some_and(|symbol_id| {
448-
popped_place_table.symbol(symbol_id).is_reassigned()
441+
let popped_place_table = &self.place_tables[popped_scope_id];
442+
if let Some(symbol_id) = popped_place_table.symbol_id(name) {
443+
if popped_place_table.symbol(symbol_id).is_reassigned()
449444
&& !self.use_def_maps[popped_scope_id].bindings_are_unchanged(
450445
&self.use_def_maps[key.enclosing_scope],
451446
*snapshot_id,
452447
symbol_id,
453448
)
454-
})
455-
})
456-
});
449+
{
450+
self.use_def_maps[key.enclosing_scope]
451+
.mark_snapshot_incomplete(*snapshot_id, enclosing_symbol);
452+
}
453+
}
454+
}
455+
}
456+
}
457457
}
458458

459459
fn sweep_nonlocal_lazy_snapshots(&mut self) {
@@ -495,7 +495,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
495495
.pop()
496496
.expect("Root scope should be present");
497497

498-
self.sweep_lazy_snapshots(popped_scope_id);
498+
self.check_lazy_snapshots(popped_scope_id);
499499

500500
let children_end = self.scopes.next_index();
501501

@@ -781,7 +781,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
781781
/// Records that all remaining statements in the current block are unreachable.
782782
fn mark_unreachable(&mut self) {
783783
self.current_use_def_map_mut().mark_unreachable();
784-
self.sweep_lazy_snapshots(self.current_scope());
784+
self.check_lazy_snapshots(self.current_scope());
785785
}
786786

787787
/// Records a reachability constraint that always evaluates to "ambiguous".

crates/ty_python_semantic/src/semantic_index/scope.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ impl ScopeLaziness {
195195
pub(crate) const fn is_eager(self) -> bool {
196196
matches!(self, ScopeLaziness::Eager)
197197
}
198+
199+
pub(crate) const fn is_lazy(self) -> bool {
200+
matches!(self, ScopeLaziness::Lazy)
201+
}
198202
}
199203

200204
#[derive(Copy, Clone, Debug, PartialEq, Eq)]

0 commit comments

Comments
 (0)