Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
c9cc000
[ty] [WIP] handle recursive type inference properly
mtshiba Sep 24, 2025
2cfdf1d
Merge branch 'main' into recursive-inference
mtshiba Oct 24, 2025
041bb24
use the last provisional value of `cycle_fn`
mtshiba Oct 27, 2025
074f18f
`Divergent` has `salsa::Id`
mtshiba Oct 27, 2025
c71fd0b
handle cycles in more query functions
mtshiba Oct 27, 2025
de28c13
Merge branch 'main' into recursive-inference
mtshiba Oct 27, 2025
a1e3d93
Update 1377_iteration_count_mismatch.md
mtshiba Oct 27, 2025
e63e3bd
Merge branch 'main' into recursive-inference
mtshiba Oct 30, 2025
b852be7
update salsa
mtshiba Oct 30, 2025
b45ebbd
refactor
mtshiba Oct 31, 2025
d8cab29
refactor
mtshiba Oct 31, 2025
9115e40
simplify type joining performed within `cycle_recovery`
mtshiba Oct 31, 2025
d68e5c0
revert unnecessary changes
mtshiba Oct 31, 2025
b0a7804
Add examples of self-referential types
mtshiba Oct 31, 2025
0caa4b1
Merge branch 'main' into recursive-inference
mtshiba Nov 3, 2025
36c5257
refactor
mtshiba Nov 3, 2025
6cc30e4
implement `ProtocolInstanceType::recursive_type_normalized_impl`
mtshiba Nov 3, 2025
1a9d899
Merge branch 'main' into recursive-inference
mtshiba Nov 3, 2025
eda8017
the code in `pr_20962_comprehension_panics.md` no longer panics
mtshiba Nov 3, 2025
fc63d5f
Update attributes.md
mtshiba Nov 5, 2025
8c06a05
follow the changes in salsa-rs/salsa#1021
mtshiba Nov 5, 2025
3653427
Merge branch 'main' into recursive-inference
mtshiba Nov 5, 2025
ab84bdf
Do not use `UnionBuilder` to union the last provisional type and the …
mtshiba Nov 6, 2025
fa88ca3
Merge branch 'main' into recursive-inference
mtshiba Nov 6, 2025
d0145c6
Revert "Do not use `UnionBuilder` to union the last provisional type …
mtshiba Nov 6, 2025
682a2e8
don't create a query cycle in the cycle recovery function
mtshiba Nov 6, 2025
d0b68f9
use `CycleHeads` to remove `Divergent` types
mtshiba Nov 6, 2025
55f9ae9
Merge branch 'main' into recursive-inference
mtshiba Nov 6, 2025
65bb223
Update builder.rs
mtshiba Nov 6, 2025
0cf4eaf
revert unnecessary changes
mtshiba Nov 6, 2025
91167f9
Merge branch 'main' into recursive-inference
mtshiba Nov 7, 2025
2393f6a
fix fuzzer hang
mtshiba Nov 7, 2025
02705c4
the second parameter `Id` of `cycle_fn` is not necessary
mtshiba Nov 10, 2025
664b8d8
Revert "fix fuzzer hang"
mtshiba Nov 10, 2025
4aad144
specify cycle_fn for `PEP695TypeAliasType::raw_value_type`
mtshiba Nov 10, 2025
74a3758
Merge branch 'main' into recursive-inference
mtshiba Nov 11, 2025
e7b0dc5
revert unnecessary changes
mtshiba Nov 11, 2025
e879f8b
update salsa
mtshiba Nov 11, 2025
0353244
Merge branch 'main' into recursive-inference
mtshiba Nov 13, 2025
597a4ae
refactor
mtshiba Nov 13, 2025
9a3973a
set the cycle initial function to `ClassLiteral::decorators`
mtshiba Nov 13, 2025
78fa2ec
Merge branch 'main' into recursive-inference
mtshiba Nov 14, 2025
eede68f
fix recursive type normalization for implicit union type alias
mtshiba Nov 14, 2025
ac44afa
add `Type::expand_eagerly`
mtshiba Nov 14, 2025
2a9b9a2
add a new rule `cyclic-type-alias-definition`
mtshiba Nov 14, 2025
3b4db33
Merge branch 'main' into recursive-inference
mtshiba Nov 14, 2025
2eea054
Update e2e__commands__debug_command.snap
mtshiba Nov 14, 2025
9222af3
more comments
mtshiba Nov 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "05a9af7f554b64b8aadc2eeb6f2caf73d0408d09", default-features = false, features = [
salsa = { git = "https://github.com/mtshiba/salsa.git", rev = "9333447c568e5095a1376164ddaeb8135692b253", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
Expand Down
164 changes: 96 additions & 68 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
try:
type name_4 = name_1
finally:
from .. import name_3

try:
pass
except* 0:
pass
else:
def name_1() -> name_4:
pass

@name_1
def name_3():
pass
finally:
try:
pass
except* 0:
assert name_3
finally:

@name_3
class name_1:
pass
20 changes: 20 additions & 0 deletions crates/ty_python_semantic/resources/corpus/cyclic_type_alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name_3: Foo = 0
name_4 = 0

if _0:
type name_3 = name_5
type name_4 = name_3

_1: name_3

def name_1(_2: name_4):
pass

match 0:
case name_1._3:
pass
case 1:
type name_5 = name_4
case name_5:
pass
name_3 = name_5
24 changes: 21 additions & 3 deletions crates/ty_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2382,6 +2382,24 @@ class B:

reveal_type(B().x) # revealed: Unknown | Literal[1]
reveal_type(A().x) # revealed: Unknown | Literal[1]

class Base:
def flip(self) -> "Sub":
return Sub()

class Sub(Base):
# TODO invalid override error
def flip(self) -> "Base":
return Base()

class C2:
def __init__(self, x: Sub):
self.x = x

def replace_with(self, other: "C2"):
self.x = other.x.flip()

reveal_type(C2(Sub()).x) # revealed: Unknown | Base
```

And cycles between many attributes:
Expand Down Expand Up @@ -2654,11 +2672,11 @@ And it also works for homogeneous tuples:
def make_homogeneous_tuple(x: T) -> tuple[T, ...]:
return (x, x)

class E:
def f(self, other: "E"):
class F:
def f(self, other: "F"):
self.x = make_homogeneous_tuple(other.x)

reveal_type(E().x) # revealed: Unknown | tuple[Divergent, ...]
reveal_type(F().x) # revealed: Unknown | tuple[Divergent, ...]
```

## Attributes of standard library modules that aren't yet defined
Expand Down
49 changes: 49 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,55 @@ reveal_type(p.x) # revealed: Unknown | int
reveal_type(p.y) # revealed: Unknown | int
```

## Self-referential bare type alias

```toml
[environment]
python-version = "3.12" # typing.TypeAliasType
```

```py
from typing import Union, TypeAliasType, Sequence, Mapping

A = list["A" | None]

def f(x: A):
# TODO: should be `list[A | None]`?
reveal_type(x) # revealed: list[Divergent]
# TODO: should be `A | None`?
reveal_type(x[0]) # revealed: Divergent

JSONPrimitive = Union[str, int, float, bool, None]
JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]')
```

## Self-referential legacy type variables

```py
from typing import Generic, TypeVar

B = TypeVar("B", bound="Base")

class Base(Generic[B]):
pass

T = TypeVar("T", bound="Foo[int]")

class Foo(Generic[T]): ...
```

## Self-referential PEP-695 type variables

```toml
[environment]
python-version = "3.12"
```

```py
class Node[T: "Node[int]"]:
pass
```

## Parameter default values

This is a regression test for <https://github.com/astral-sh/ty/issues/1402>. When a parameter has a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,8 @@ from typing_extensions import Generic, TypeVar

T = TypeVar("T")

# TODO: no error "Unsupported class base with type `<class 'list[Derived[T@Derived]]'> | <class 'list[@Todo]'>`"
# error: [unsupported-base]
class Derived(list[Derived[T]], Generic[T]): ...
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ We do not support PEP 613 type aliases yet. For now, just make sure that we don'

```py
from typing import TypeAlias
from types import UnionType

RecursiveTuple: TypeAlias = tuple[int | "RecursiveTuple", str]

Expand All @@ -14,4 +15,21 @@ RecursiveHomogeneousTuple: TypeAlias = tuple[int | "RecursiveHomogeneousTuple",

def _(rec: RecursiveHomogeneousTuple):
reveal_type(rec) # revealed: tuple[Divergent, ...]

ClassInfo: TypeAlias = type | UnionType | tuple["ClassInfo", ...]
reveal_type(ClassInfo) # revealed: types.UnionType

def my_isinstance(obj: object, classinfo: ClassInfo) -> bool:
reveal_type(classinfo) # revealed: type | UnionType | tuple[Divergent, ...]
return isinstance(obj, classinfo)

my_isinstance(1, int)
my_isinstance(1, int | str)
my_isinstance(1, (int, str))
my_isinstance(1, (int, (str, float)))
my_isinstance(1, (int, (str | float)))
# error: [invalid-argument-type]
my_isinstance(1, 1)
# TODO should be an invalid-argument-type error
my_isinstance(1, (int, (str, 1)))
```
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,39 @@ def f(x: IntOr, y: OrInt):
reveal_type(x) # revealed: Never
if not isinstance(y, int):
reveal_type(y) # revealed: Never

# error: [cyclic-type-alias-definition] "Cyclic definition of `Itself`"
type Itself = Itself

def foo(
# this is a very strange thing to do, but this is a regression test to ensure it doesn't panic
Itself: Itself,
):
x: Itself
reveal_type(Itself) # revealed: Divergent

# A type alias defined with invalid recursion behaves as a dynamic type.
foo(42)
foo("hello")

# error: [cyclic-type-alias-definition] "Cyclic definition of `A`"
type A = B
# error: [cyclic-type-alias-definition] "Cyclic definition of `B`"
type B = A

def bar(B: B):
x: B
reveal_type(B) # revealed: Divergent

# error: [cyclic-type-alias-definition] "Cyclic definition of `G`"
type G[T] = G[T]
# error: [cyclic-type-alias-definition] "Cyclic definition of `H`"
type H[T] = I[T]
# error: [cyclic-type-alias-definition] "Cyclic definition of `I`"
type I[T] = H[T]

# It's not possible to create an element of this type, but it's not an error for now
type DirectRecursiveList[T] = list[DirectRecursiveList[T]]
```

### With legacy generic
Expand Down Expand Up @@ -327,7 +360,7 @@ class C(P[T]):
pass

reveal_type(C[int]()) # revealed: C[int]
reveal_type(C()) # revealed: C[Divergent]
reveal_type(C()) # revealed: C[C[Divergent]]
```

### Union inside generic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

Regression test for <https://github.com/astral-sh/ty/issues/1377>.

The code is an excerpt from <https://github.com/Gobot1234/steam.py> that is minimal enough to
trigger the iteration count mismatch bug in Salsa.

<!-- expect-panic: execute: too many cycle iterations -->
The code is an excerpt from <https://github.com/Gobot1234/steam.py>.

```toml
[environment]
Expand Down
54 changes: 49 additions & 5 deletions crates/ty_python_semantic/src/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl TypeOrigin {
/// bound_or_declared: Place::Defined(Literal[1], TypeOrigin::Inferred, Definedness::PossiblyUndefined),
/// non_existent: Place::Undefined,
/// ```
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) enum Place<'db> {
Defined(Type<'db>, TypeOrigin, Definedness),
Undefined,
Expand Down Expand Up @@ -532,7 +532,7 @@ impl<'db> PlaceFromDeclarationsResult<'db> {
/// that this comes with a [`CLASS_VAR`] type qualifier.
///
/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) struct PlaceAndQualifiers<'db> {
pub(crate) place: Place<'db>,
pub(crate) qualifiers: TypeQualifiers,
Expand Down Expand Up @@ -689,6 +689,35 @@ impl<'db> PlaceAndQualifiers<'db> {
.or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn()))
.into()
}

pub(crate) fn cycle_normalized(
self,
db: &'db dyn Db,
previous_place: Self,
cycle_heads: &salsa::CycleHeads,
) -> Self {
let place = match (previous_place.place, self.place) {
// In fixed-point iteration of type inference, the member type must be monotonically widened and not "oscillate".
// Here, monotonicity is guaranteed by pre-unioning the type of the previous iteration into the current result.
(Place::Defined(prev_ty, _, _), Place::Defined(ty, origin, definedness)) => {
Place::Defined(
ty.cycle_normalized(db, prev_ty, cycle_heads),
origin,
definedness,
)
}
(_, Place::Defined(ty, origin, definedness)) => Place::Defined(
ty.recursive_type_normalized(db, cycle_heads),
origin,
definedness,
),
(_, Place::Undefined) => Place::Undefined,
};
PlaceAndQualifiers {
place,
qualifiers: self.qualifiers,
}
}
}

impl<'db> From<Place<'db>> for PlaceAndQualifiers<'db> {
Expand All @@ -699,16 +728,31 @@ impl<'db> From<Place<'db>> for PlaceAndQualifiers<'db> {

fn place_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
id: salsa::Id,
_scope: ScopeId<'db>,
_place_id: ScopedPlaceId,
_requires_explicit_reexport: RequiresExplicitReExport,
_considered_definitions: ConsideredDefinitions,
) -> PlaceAndQualifiers<'db> {
Place::bound(Type::divergent(id)).into()
}

#[allow(clippy::too_many_arguments)]
fn place_cycle_recover<'db>(
db: &'db dyn Db,
cycle_heads: &salsa::CycleHeads,
previous_place: &PlaceAndQualifiers<'db>,
place: PlaceAndQualifiers<'db>,
_count: u32,
_scope: ScopeId<'db>,
_place_id: ScopedPlaceId,
_requires_explicit_reexport: RequiresExplicitReExport,
_considered_definitions: ConsideredDefinitions,
) -> PlaceAndQualifiers<'db> {
Place::bound(Type::Never).into()
place.cycle_normalized(db, *previous_place, cycle_heads)
}

#[salsa::tracked(cycle_initial=place_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
#[salsa::tracked(cycle_fn=place_cycle_recover, cycle_initial=place_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn place_by_id<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
Expand Down
3 changes: 1 addition & 2 deletions crates/ty_python_semantic/src/semantic_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::FileScopeId;
use crate::semantic_index::semantic_index;
use crate::types::ide_support::all_declarations_and_bindings;
use crate::types::ide_support::{Member, all_members};
use crate::types::ide_support::{Member, all_declarations_and_bindings, all_members};
use crate::types::{Type, binding_type, infer_scope_types};

pub struct SemanticModel<'db> {
Expand Down
Loading