Skip to content

Commit e101400

Browse files
committed
Initial attempt
1 parent 3f890c3 commit e101400

File tree

7 files changed

+93
-36
lines changed

7 files changed

+93
-36
lines changed

crates/ty_python_semantic/resources/mdtest/public_types.md

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ def outer() -> None:
1818
x = A()
1919

2020
def inner() -> None:
21-
# TODO: should be `Unknown | A | B`
22-
reveal_type(x) # revealed: Unknown | B
21+
reveal_type(x) # revealed: Unknown | A | B
2322
inner()
2423

2524
x = B()
@@ -34,8 +33,7 @@ def outer(flag: bool) -> None:
3433
x = A()
3534

3635
def inner() -> None:
37-
# TODO: should be `Unknown | A | B | C`
38-
reveal_type(x) # revealed: Unknown | B | C
36+
reveal_type(x) # revealed: Unknown | A | B | C
3937
inner()
4038

4139
if flag:
@@ -53,18 +51,34 @@ def outer(flag: bool) -> None:
5351
If a binding is not reachable, it is not considered in the public type:
5452

5553
```py
56-
def outer(flag: bool) -> None:
54+
def outer() -> None:
5755
x = A()
5856

5957
def inner() -> None:
60-
# TODO: should be `Unknown | A | C`
61-
reveal_type(x) # revealed: Unknown | C
58+
reveal_type(x) # revealed: Unknown | A | C
59+
inner()
60+
6261
if False:
6362
x = B()
6463
inner()
6564

6665
x = C()
6766
inner()
67+
68+
def outer(flag: bool) -> None:
69+
x = A()
70+
71+
def inner() -> None:
72+
reveal_type(x) # revealed: Unknown | A | C
73+
inner()
74+
75+
if flag:
76+
return
77+
78+
x = B()
79+
80+
x = C()
81+
inner()
6882
```
6983

7084
If a symbol is only conditionally bound, we do not raise any errors:
@@ -113,8 +127,7 @@ def outer(flag: bool) -> None:
113127
x = A()
114128

115129
def inner() -> None:
116-
# TODO: should be `Unknown | A | B`
117-
reveal_type(x) # revealed: Unknown | A
130+
reveal_type(x) # revealed: Unknown | A | B
118131
if flag:
119132
x = B()
120133
inner()
@@ -124,6 +137,24 @@ def outer(flag: bool) -> None:
124137
inner()
125138
```
126139

140+
Interplay with type narrowing:
141+
142+
```py
143+
def outer(x: A | None):
144+
def inner() -> None:
145+
reveal_type(x) # revealed: A | None
146+
inner()
147+
if x is None:
148+
inner()
149+
150+
def outer(x: A | None):
151+
if x is not None:
152+
def inner() -> None:
153+
# TODO: should ideally be `A`
154+
reveal_type(x) # revealed: A | None
155+
inner()
156+
```
157+
127158
The same set of problems can appear at module scope:
128159

129160
```py
@@ -134,9 +165,8 @@ if flag():
134165
x = 1
135166

136167
def f() -> None:
137-
# TODO: no error, type should be `Unknown | Literal[1, 2]`
138168
# error: [possibly-unresolved-reference]
139-
reveal_type(x) # revealed: Unknown | Literal[2]
169+
reveal_type(x) # revealed: Unknown | Literal[1, 2]
140170
# Function only used inside this branch
141171
f()
142172

crates/ty_python_semantic/resources/mdtest/scopes/eager.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Function definitions are evaluated lazily.
1212
x = 1
1313

1414
def f():
15-
reveal_type(x) # revealed: Unknown | Literal[2]
15+
reveal_type(x) # revealed: Unknown | Literal[1, 2]
1616

1717
x = 2
1818
```
@@ -299,7 +299,7 @@ def _():
299299
x = 1
300300

301301
def f():
302-
# revealed: Unknown | Literal[2]
302+
# revealed: Unknown | Literal[1, 2]
303303
[reveal_type(x) for a in range(1)]
304304
x = 2
305305
```
@@ -316,7 +316,7 @@ def _():
316316

317317
class A:
318318
def f():
319-
# revealed: Unknown | Literal[2]
319+
# revealed: Unknown | Literal[1, 2]
320320
reveal_type(x)
321321

322322
x = 2
@@ -333,7 +333,7 @@ def _():
333333

334334
def f():
335335
def g():
336-
# revealed: Unknown | Literal[2]
336+
# revealed: Unknown | Literal[1, 2]
337337
reveal_type(x)
338338
x = 2
339339
```
@@ -351,7 +351,7 @@ def _():
351351

352352
class A:
353353
def f():
354-
# revealed: Unknown | Literal[2]
354+
# revealed: Unknown | Literal[1, 2]
355355
[reveal_type(x) for a in range(1)]
356356

357357
x = 2

crates/ty_python_semantic/resources/mdtest/terminal_statements.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -580,15 +580,13 @@ resolve free references _at the end of the containing scope_. That means that in
580580
all of the `x` bindings should be visible to the `reveal_type`, regardless of where we place the
581581
`return` statements.
582582

583-
TODO: These currently produce the wrong results, but not because of our terminal statement support.
584-
See [ruff#15777](https://github.com/astral-sh/ruff/issues/15777) for more details.
585-
586583
```py
587584
def top_level_return(cond1: bool, cond2: bool):
588585
x = 1
589586

590587
def g():
591-
# TODO eliminate Unknown
588+
# TODO We could potentially eliminate Unknown here, if we make sure
589+
# that `g` is only called from within `top_level_return`.
592590
reveal_type(x) # revealed: Unknown | Literal[1, 2, 3]
593591
if cond1:
594592
if cond2:
@@ -601,8 +599,7 @@ def return_from_if(cond1: bool, cond2: bool):
601599
x = 1
602600

603601
def g():
604-
# TODO: Literal[1, 2, 3]
605-
reveal_type(x) # revealed: Unknown | Literal[1]
602+
reveal_type(x) # revealed: Unknown | Literal[1, 2, 3]
606603
if cond1:
607604
if cond2:
608605
x = 2
@@ -614,8 +611,7 @@ def return_from_nested_if(cond1: bool, cond2: bool):
614611
x = 1
615612

616613
def g():
617-
# TODO: Literal[1, 2, 3]
618-
reveal_type(x) # revealed: Unknown | Literal[1, 3]
614+
reveal_type(x) # revealed: Unknown | Literal[1, 2, 3]
619615
if cond1:
620616
if cond2:
621617
x = 2

crates/ty_python_semantic/resources/mdtest/unreachable.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,16 +241,16 @@ def f():
241241

242242
### Use of variable in nested function
243243

244-
In the example below, since we use `x` in the `inner` function, we use the "public" type of `x`,
245-
which currently refers to the end-of-scope type of `x`. Since the end of the `outer` scope is
246-
unreachable, we need to make sure that we do not emit an `unresolved-reference` diagnostic:
244+
This is a regression test for a behavior that previously caused problems when the public type still
245+
referred to the end-of-scope, which would result in an unresolved-reference error here since the end
246+
of the scope is unreachable.
247247

248248
```py
249249
def outer():
250250
x = 1
251251

252252
def inner():
253-
reveal_type(x) # revealed: Unknown
253+
reveal_type(x) # revealed: Unknown | Literal[1]
254254
while True:
255255
pass
256256
```

crates/ty_python_semantic/src/place.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@ fn place_by_id<'db>(
622622
place: Place::Type(declared_ty, Boundness::PossiblyUnbound),
623623
qualifiers,
624624
}) => {
625-
let bindings = use_def.public_bindings(place_id);
625+
let bindings = use_def.all_bindings(place_id);
626626
let inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport);
627627

628628
let place = match inferred {
@@ -647,7 +647,7 @@ fn place_by_id<'db>(
647647
place: Place::Unbound,
648648
qualifiers: _,
649649
}) => {
650-
let bindings = use_def.public_bindings(place_id);
650+
let bindings = use_def.all_bindings(place_id);
651651
let inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport);
652652

653653
// `__slots__` is a symbol with special behavior in Python's runtime. It can be
@@ -870,9 +870,10 @@ fn place_from_bindings_impl<'db>(
870870
if let Some(first) = types.next() {
871871
let boundness = match unbound_reachability() {
872872
Some(Truthiness::AlwaysTrue) => {
873-
unreachable!(
874-
"If we have at least one binding, the implicit `unbound` binding should not be definitely visible"
875-
)
873+
// unreachable!(
874+
// "If we have at least one binding, the implicit `unbound` binding should not be definitely visible"
875+
// )
876+
Boundness::Bound // TODO
876877
}
877878
Some(Truthiness::AlwaysFalse) | None => Boundness::Bound,
878879
Some(Truthiness::Ambiguous) => Boundness::PossiblyUnbound,

crates/ty_python_semantic/src/semantic_index/use_def.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,8 @@ pub(crate) struct UseDefMap<'db> {
298298
/// [`PlaceState`] visible at end of scope for each place.
299299
end_of_scope_places: IndexVec<ScopedPlaceId, PlaceState>,
300300

301+
potentially_reachable_bindings: IndexVec<ScopedPlaceId, Bindings>,
302+
301303
/// Snapshot of bindings in this scope that can be used to resolve a reference in a nested
302304
/// eager scope.
303305
eager_snapshots: EagerSnapshots,
@@ -401,6 +403,13 @@ impl<'db> UseDefMap<'db> {
401403
self.bindings_iterator(self.end_of_scope_places[place].bindings())
402404
}
403405

406+
pub(crate) fn all_bindings(
407+
&self,
408+
place: ScopedPlaceId,
409+
) -> BindingWithConstraintsIterator<'_, 'db> {
410+
self.bindings_iterator(&self.potentially_reachable_bindings[place])
411+
}
412+
404413
pub(crate) fn eager_snapshot(
405414
&self,
406415
eager_bindings: ScopedEagerSnapshotId,
@@ -648,7 +657,7 @@ pub(super) struct FlowSnapshot {
648657

649658
#[derive(Debug)]
650659
pub(super) struct UseDefMapBuilder<'db> {
651-
/// Append-only array of [`Definition`].
660+
/// Append-only array of [`DefinitionState`].
652661
all_definitions: IndexVec<ScopedDefinitionId, DefinitionState<'db>>,
653662

654663
/// Builder of predicates.
@@ -679,6 +688,9 @@ pub(super) struct UseDefMapBuilder<'db> {
679688
/// Currently live bindings and declarations for each place.
680689
place_states: IndexVec<ScopedPlaceId, PlaceState>,
681690

691+
/// All potentially reachable bindings, for each place.
692+
potentially_reachable_bindings: IndexVec<ScopedPlaceId, Bindings>,
693+
682694
/// Snapshots of place states in this scope that can be used to resolve a reference in a
683695
/// nested eager scope.
684696
eager_snapshots: EagerSnapshots,
@@ -700,6 +712,7 @@ impl<'db> UseDefMapBuilder<'db> {
700712
declarations_by_binding: FxHashMap::default(),
701713
bindings_by_declaration: FxHashMap::default(),
702714
place_states: IndexVec::new(),
715+
potentially_reachable_bindings: IndexVec::new(),
703716
eager_snapshots: EagerSnapshots::default(),
704717
is_class_scope,
705718
}
@@ -720,6 +733,10 @@ impl<'db> UseDefMapBuilder<'db> {
720733
.place_states
721734
.push(PlaceState::undefined(self.reachability));
722735
debug_assert_eq!(place, new_place);
736+
let new_place = self
737+
.potentially_reachable_bindings
738+
.push(Bindings::unbound(self.reachability));
739+
debug_assert_eq!(place, new_place);
723740
}
724741

725742
pub(super) fn record_binding(
@@ -738,6 +755,14 @@ impl<'db> UseDefMapBuilder<'db> {
738755
self.is_class_scope,
739756
is_place_name,
740757
);
758+
759+
self.potentially_reachable_bindings[place].record_binding(
760+
def_id,
761+
self.reachability,
762+
self.is_class_scope,
763+
is_place_name,
764+
false,
765+
);
741766
}
742767

743768
pub(super) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId {
@@ -1014,6 +1039,7 @@ impl<'db> UseDefMapBuilder<'db> {
10141039
bindings_by_use: self.bindings_by_use,
10151040
node_reachability: self.node_reachability,
10161041
end_of_scope_places: self.place_states,
1042+
potentially_reachable_bindings: self.potentially_reachable_bindings,
10171043
declarations_by_binding: self.declarations_by_binding,
10181044
bindings_by_declaration: self.bindings_by_declaration,
10191045
eager_snapshots: self.eager_snapshots,

crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ pub(super) struct LiveBinding {
205205
pub(super) type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>;
206206

207207
impl Bindings {
208-
fn unbound(reachability_constraint: ScopedReachabilityConstraintId) -> Self {
208+
pub(super) fn unbound(reachability_constraint: ScopedReachabilityConstraintId) -> Self {
209209
let initial_binding = LiveBinding {
210210
binding: ScopedDefinitionId::UNBOUND,
211211
narrowing_constraint: ScopedNarrowingConstraint::empty(),
@@ -224,6 +224,7 @@ impl Bindings {
224224
reachability_constraint: ScopedReachabilityConstraintId,
225225
is_class_scope: bool,
226226
is_place_name: bool,
227+
clear: bool,
227228
) {
228229
// If we are in a class scope, and the unbound name binding was previously visible, but we will
229230
// now replace it, record the narrowing constraints on it:
@@ -232,7 +233,9 @@ impl Bindings {
232233
}
233234
// The new binding replaces all previous live bindings in this path, and has no
234235
// constraints.
235-
self.live_bindings.clear();
236+
if clear {
237+
self.live_bindings.clear();
238+
}
236239
self.live_bindings.push(LiveBinding {
237240
binding,
238241
narrowing_constraint: ScopedNarrowingConstraint::empty(),
@@ -349,6 +352,7 @@ impl PlaceState {
349352
reachability_constraint,
350353
is_class_scope,
351354
is_place_name,
355+
true,
352356
);
353357
}
354358

0 commit comments

Comments
 (0)