Skip to content

Commit b411f23

Browse files
committed
Merge remote-tracking branch 'origin/main' into dcreager/object-variant
* origin/main: [ty] Minor fixes to `Protocol` tests (#20347) [ty] use Type::Divergent to avoid panic in infinitely-nested-tuple implicit attribute (#20333) [ty] Require that implementors of `Constraints` also implement `Debug` (#20348)
2 parents e4c5b14 + 0e3697a commit b411f23

File tree

13 files changed

+168
-86
lines changed

13 files changed

+168
-86
lines changed

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ regex-automata = { version = "0.4.9" }
143143
rustc-hash = { version = "2.0.0" }
144144
rustc-stable-hash = { version = "0.1.2" }
145145
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
146-
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a3ffa22cb26756473d56f867aedec3fd907c4dd9", default-features = false, features = [
146+
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3713cd7eb30821c0c086591832dd6f59f2af7fe7", default-features = false, features = [
147147
"compact_str",
148148
"macros",
149149
"salsa_unstable",

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2419,6 +2419,16 @@ reveal_type(Answer.NO.value) # revealed: Any
24192419
reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown]
24202420
```
24212421

2422+
## Divergent inferred implicit instance attribute types
2423+
2424+
```py
2425+
class C:
2426+
def f(self, other: "C"):
2427+
self.x = (other.x, 1)
2428+
2429+
reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
2430+
```
2431+
24222432
## References
24232433

24242434
Some of the tests in the *Class and instance variables* section draw inspiration from

crates/ty_python_semantic/resources/mdtest/expression/len.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ class SecondRequiredArgument:
260260
def __len__(self, v: int) -> Literal[1]:
261261
return 1
262262

263-
# TODO: Emit a diagnostic
263+
# this is fine: the call succeeds at runtime since the second argument is optional
264264
reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0]
265265

266266
# TODO: Emit a diagnostic

crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ from typing import Protocol, TypeVar
7777
T = TypeVar("T")
7878

7979
class CanIndex(Protocol[T]):
80-
def __getitem__(self, index: int) -> T: ...
80+
def __getitem__(self, index: int, /) -> T: ...
8181

8282
class ExplicitlyImplements(CanIndex[T]): ...
8383

crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ from typing import Protocol, TypeVar
7272
S = TypeVar("S")
7373

7474
class CanIndex(Protocol[S]):
75-
def __getitem__(self, index: int) -> S: ...
75+
def __getitem__(self, index: int, /) -> S: ...
7676

7777
class ExplicitlyImplements[T](CanIndex[T]): ...
7878

crates/ty_python_semantic/resources/mdtest/protocols.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,9 +1813,28 @@ class Foo:
18131813
static_assert(not is_assignable_to(Foo, Iterable[Any]))
18141814
```
18151815

1816-
Because method members must always be available on the class, it is safe to access a method on
1817-
`type[P]`, where `P` is a protocol class, just like it is generally safe to access a method on
1818-
`type[C]` where `C` is a nominal class:
1816+
Because method members are always looked up on the meta-type of an object when testing assignability
1817+
and subtyping, we understand that `IterableClass` here is a subtype of `Iterable[int]` even though
1818+
`IterableClass.__iter__` has the wrong signature:
1819+
1820+
```py
1821+
from typing import Iterator, Iterable
1822+
from ty_extensions import static_assert, is_subtype_of, TypeOf
1823+
1824+
class Meta(type):
1825+
def __iter__(self) -> Iterator[int]:
1826+
yield from range(42)
1827+
1828+
class IterableClass(metaclass=Meta):
1829+
def __iter__(self) -> Iterator[str]:
1830+
yield from "abc"
1831+
1832+
static_assert(is_subtype_of(TypeOf[IterableClass], Iterable[int]))
1833+
```
1834+
1835+
Enforcing that members must always be available on the class also means that it is safe to access a
1836+
method on `type[P]`, where `P` is a protocol class, just like it is generally safe to access a
1837+
method on `type[C]` where `C` is a nominal class:
18191838

18201839
```py
18211840
from typing import Protocol

crates/ty_python_semantic/src/types.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,10 @@ impl<'db> Type<'db> {
762762
Self::Dynamic(DynamicType::Unknown)
763763
}
764764

765+
pub(crate) fn divergent(scope: ScopeId<'db>) -> Self {
766+
Self::Dynamic(DynamicType::Divergent(DivergentType { scope }))
767+
}
768+
765769
pub const fn is_unknown(&self) -> bool {
766770
matches!(self, Type::Dynamic(DynamicType::Unknown))
767771
}
@@ -6499,7 +6503,6 @@ impl<'db> Type<'db> {
64996503
}
65006504
}
65016505

6502-
#[allow(unused)]
65036506
pub(super) fn has_divergent_type(self, db: &'db dyn Db, div: Type<'db>) -> bool {
65046507
any_over_type(db, self, &|ty| match ty {
65056508
Type::Dynamic(DynamicType::Divergent(_)) => ty == div,

crates/ty_python_semantic/src/types/constraints.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ fn incomparable<'db>(db: &'db dyn Db, left: Type<'db>, right: Type<'db>) -> bool
111111
}
112112

113113
/// Encodes the constraints under which a type property (e.g. assignability) holds.
114-
pub(crate) trait Constraints<'db>: Clone + Sized {
114+
pub(crate) trait Constraints<'db>: Clone + Sized + std::fmt::Debug {
115115
/// Returns a constraint set that never holds
116116
fn unsatisfiable(db: &'db dyn Db) -> Self;
117117

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 81 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ fn scope_cycle_recover<'db>(
8585
}
8686

8787
fn scope_cycle_initial<'db>(_db: &'db dyn Db, scope: ScopeId<'db>) -> ScopeInference<'db> {
88-
ScopeInference::cycle_fallback(scope)
88+
ScopeInference::cycle_initial(scope)
8989
}
9090

9191
/// Infer all types for a [`Definition`] (including sub-expressions).
@@ -123,7 +123,7 @@ fn definition_cycle_initial<'db>(
123123
db: &'db dyn Db,
124124
definition: Definition<'db>,
125125
) -> DefinitionInference<'db> {
126-
DefinitionInference::cycle_fallback(definition.scope(db))
126+
DefinitionInference::cycle_initial(definition.scope(db))
127127
}
128128

129129
/// Infer types for all deferred type expressions in a [`Definition`].
@@ -164,7 +164,7 @@ fn deferred_cycle_initial<'db>(
164164
db: &'db dyn Db,
165165
definition: Definition<'db>,
166166
) -> DefinitionInference<'db> {
167-
DefinitionInference::cycle_fallback(definition.scope(db))
167+
DefinitionInference::cycle_initial(definition.scope(db))
168168
}
169169

170170
/// Infer all types for an [`Expression`] (including sub-expressions).
@@ -192,20 +192,29 @@ pub(crate) fn infer_expression_types<'db>(
192192
.finish_expression()
193193
}
194194

195+
/// How many fixpoint iterations to allow before falling back to Divergent type.
196+
const ITERATIONS_BEFORE_FALLBACK: u32 = 10;
197+
195198
fn expression_cycle_recover<'db>(
196-
_db: &'db dyn Db,
199+
db: &'db dyn Db,
197200
_value: &ExpressionInference<'db>,
198-
_count: u32,
199-
_expression: Expression<'db>,
201+
count: u32,
202+
expression: Expression<'db>,
200203
) -> salsa::CycleRecoveryAction<ExpressionInference<'db>> {
201-
salsa::CycleRecoveryAction::Iterate
204+
if count == ITERATIONS_BEFORE_FALLBACK {
205+
salsa::CycleRecoveryAction::Fallback(ExpressionInference::cycle_fallback(
206+
expression.scope(db),
207+
))
208+
} else {
209+
salsa::CycleRecoveryAction::Iterate
210+
}
202211
}
203212

204213
fn expression_cycle_initial<'db>(
205214
db: &'db dyn Db,
206215
expression: Expression<'db>,
207216
) -> ExpressionInference<'db> {
208-
ExpressionInference::cycle_fallback(expression.scope(db))
217+
ExpressionInference::cycle_initial(expression.scope(db))
209218
}
210219

211220
/// Infers the type of an `expression` that is guaranteed to be in the same file as the calling query.
@@ -324,7 +333,7 @@ fn unpack_cycle_recover<'db>(
324333
}
325334

326335
fn unpack_cycle_initial<'db>(_db: &'db dyn Db, _unpack: Unpack<'db>) -> UnpackResult<'db> {
327-
UnpackResult::cycle_fallback(Type::Never)
336+
UnpackResult::cycle_initial(Type::Never)
328337
}
329338

330339
/// Returns the type of the nearest enclosing class for the given scope.
@@ -378,34 +387,61 @@ impl<'db> InferenceRegion<'db> {
378387
}
379388
}
380389

390+
#[derive(Debug, Clone, Copy, Eq, PartialEq, get_size2::GetSize, salsa::Update)]
391+
enum CycleRecovery<'db> {
392+
/// An initial-value for fixpoint iteration; all types are `Type::Never`.
393+
Initial,
394+
/// A divergence-fallback value for fixpoint iteration; all types are `Divergent`.
395+
Divergent(ScopeId<'db>),
396+
}
397+
398+
impl<'db> CycleRecovery<'db> {
399+
fn merge(self, other: Option<CycleRecovery<'db>>) -> Self {
400+
if let Some(other) = other {
401+
match (self, other) {
402+
// It's important here that we keep the scope of `self` if merging two `Divergent`.
403+
(Self::Divergent(scope), _) | (_, Self::Divergent(scope)) => Self::Divergent(scope),
404+
_ => Self::Initial,
405+
}
406+
} else {
407+
self
408+
}
409+
}
410+
411+
fn fallback_type(self) -> Type<'db> {
412+
match self {
413+
Self::Initial => Type::Never,
414+
Self::Divergent(scope) => Type::divergent(scope),
415+
}
416+
}
417+
}
418+
381419
/// The inferred types for a scope region.
382420
#[derive(Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)]
383421
pub(crate) struct ScopeInference<'db> {
384422
/// The types of every expression in this region.
385423
expressions: FxHashMap<ExpressionNodeKey, Type<'db>>,
386424

387425
/// The extra data that is only present for few inference regions.
388-
extra: Option<Box<ScopeInferenceExtra>>,
426+
extra: Option<Box<ScopeInferenceExtra<'db>>>,
389427
}
390428

391429
#[derive(Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update, Default)]
392-
struct ScopeInferenceExtra {
393-
/// The fallback type for missing expressions/bindings/declarations.
394-
///
395-
/// This is used only when constructing a cycle-recovery `TypeInference`.
396-
cycle_fallback: bool,
430+
struct ScopeInferenceExtra<'db> {
431+
/// Is this a cycle-recovery inference result, and if so, what kind?
432+
cycle_recovery: Option<CycleRecovery<'db>>,
397433

398434
/// The diagnostics for this region.
399435
diagnostics: TypeCheckDiagnostics,
400436
}
401437

402438
impl<'db> ScopeInference<'db> {
403-
fn cycle_fallback(scope: ScopeId<'db>) -> Self {
439+
fn cycle_initial(scope: ScopeId<'db>) -> Self {
404440
let _ = scope;
405441

406442
Self {
407443
extra: Some(Box::new(ScopeInferenceExtra {
408-
cycle_fallback: true,
444+
cycle_recovery: Some(CycleRecovery::Initial),
409445
..ScopeInferenceExtra::default()
410446
})),
411447
expressions: FxHashMap::default(),
@@ -431,14 +467,10 @@ impl<'db> ScopeInference<'db> {
431467
.or_else(|| self.fallback_type())
432468
}
433469

434-
fn is_cycle_callback(&self) -> bool {
470+
fn fallback_type(&self) -> Option<Type<'db>> {
435471
self.extra
436472
.as_ref()
437-
.is_some_and(|extra| extra.cycle_fallback)
438-
}
439-
440-
fn fallback_type(&self) -> Option<Type<'db>> {
441-
self.is_cycle_callback().then_some(Type::Never)
473+
.and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type))
442474
}
443475
}
444476

@@ -471,10 +503,8 @@ pub(crate) struct DefinitionInference<'db> {
471503

472504
#[derive(Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update, Default)]
473505
struct DefinitionInferenceExtra<'db> {
474-
/// The fallback type for missing expressions/bindings/declarations.
475-
///
476-
/// This is used only when constructing a cycle-recovery `TypeInference`.
477-
cycle_fallback: bool,
506+
/// Is this a cycle-recovery inference result, and if so, what kind?
507+
cycle_recovery: Option<CycleRecovery<'db>>,
478508

479509
/// The definitions that are deferred.
480510
deferred: Box<[Definition<'db>]>,
@@ -487,7 +517,7 @@ struct DefinitionInferenceExtra<'db> {
487517
}
488518

489519
impl<'db> DefinitionInference<'db> {
490-
fn cycle_fallback(scope: ScopeId<'db>) -> Self {
520+
fn cycle_initial(scope: ScopeId<'db>) -> Self {
491521
let _ = scope;
492522

493523
Self {
@@ -497,7 +527,7 @@ impl<'db> DefinitionInference<'db> {
497527
#[cfg(debug_assertions)]
498528
scope,
499529
extra: Some(Box::new(DefinitionInferenceExtra {
500-
cycle_fallback: true,
530+
cycle_recovery: Some(CycleRecovery::Initial),
501531
..DefinitionInferenceExtra::default()
502532
})),
503533
}
@@ -566,14 +596,10 @@ impl<'db> DefinitionInference<'db> {
566596
self.declarations.iter().map(|(_, qualifiers)| *qualifiers)
567597
}
568598

569-
fn is_cycle_callback(&self) -> bool {
599+
fn fallback_type(&self) -> Option<Type<'db>> {
570600
self.extra
571601
.as_ref()
572-
.is_some_and(|extra| extra.cycle_fallback)
573-
}
574-
575-
fn fallback_type(&self) -> Option<Type<'db>> {
576-
self.is_cycle_callback().then_some(Type::Never)
602+
.and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type))
577603
}
578604

579605
pub(crate) fn undecorated_type(&self) -> Option<Type<'db>> {
@@ -605,21 +631,34 @@ struct ExpressionInferenceExtra<'db> {
605631
/// The diagnostics for this region.
606632
diagnostics: TypeCheckDiagnostics,
607633

608-
/// `true` if this region is part of a cycle-recovery `TypeInference`.
609-
///
610-
/// Falls back to `Type::Never` if an expression is missing.
611-
cycle_fallback: bool,
634+
/// Is this a cycle recovery inference result, and if so, what kind?
635+
cycle_recovery: Option<CycleRecovery<'db>>,
612636

613637
/// `true` if all places in this expression are definitely bound
614638
all_definitely_bound: bool,
615639
}
616640

617641
impl<'db> ExpressionInference<'db> {
642+
fn cycle_initial(scope: ScopeId<'db>) -> Self {
643+
let _ = scope;
644+
Self {
645+
extra: Some(Box::new(ExpressionInferenceExtra {
646+
cycle_recovery: Some(CycleRecovery::Initial),
647+
all_definitely_bound: true,
648+
..ExpressionInferenceExtra::default()
649+
})),
650+
expressions: FxHashMap::default(),
651+
652+
#[cfg(debug_assertions)]
653+
scope,
654+
}
655+
}
656+
618657
fn cycle_fallback(scope: ScopeId<'db>) -> Self {
619658
let _ = scope;
620659
Self {
621660
extra: Some(Box::new(ExpressionInferenceExtra {
622-
cycle_fallback: true,
661+
cycle_recovery: Some(CycleRecovery::Divergent(scope)),
623662
all_definitely_bound: true,
624663
..ExpressionInferenceExtra::default()
625664
})),
@@ -645,14 +684,10 @@ impl<'db> ExpressionInference<'db> {
645684
.unwrap_or_else(Type::unknown)
646685
}
647686

648-
fn is_cycle_callback(&self) -> bool {
687+
fn fallback_type(&self) -> Option<Type<'db>> {
649688
self.extra
650689
.as_ref()
651-
.is_some_and(|extra| extra.cycle_fallback)
652-
}
653-
654-
fn fallback_type(&self) -> Option<Type<'db>> {
655-
self.is_cycle_callback().then_some(Type::Never)
690+
.and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type))
656691
}
657692

658693
/// Returns true if all places in this expression are definitely bound.

0 commit comments

Comments
 (0)