ruff
093a88f6 - [ty] Fix infinite hang on mutually recursive TypeAliasType definitions (#23397)

Commit
150 days ago
[ty] Fix infinite hang on mutually recursive TypeAliasType definitions (#23397) ## Summary Fix an infinite hang when type-checking files with mutually recursive `TypeAliasType` definitions created via the manual constructor: ```python from typing import Union from typing_extensions import TypeAliasType A = TypeAliasType('A', Union[str, 'B']) B = TypeAliasType('B', list[A]) ``` This was discovered while testing ty on pydantic's [`tests/test_type_alias_type.py`](https://github.com/pydantic/pydantic/blob/main/tests/test_type_alias_type.py), which contains self-referential `TypeAliasType` definitions such as: ```python JsonType = TypeAliasType( "JsonType", Union[list["JsonType"], dict[str, "JsonType"], str, int, float, bool, None] ) ``` ## Root Cause `ManualPEP695TypeAliasType` was a `#[salsa::interned]` struct with the resolved value type as one of its fields: ```rust #[salsa::interned] pub struct ManualPEP695TypeAliasType<'db> { pub name: ast::name::Name, pub definition: Option>, pub value: Type<'db>, // <-- causes non-convergence } ``` With `#[salsa::interned]`, **all fields contribute to identity/deduplication**. When Salsa detects a cycle in `infer_definition_types`, it re-executes the function iteratively until the result converges (stops changing). But each iteration produces a different `value` type as the cycle resolves, creating a new interned ID each time: - **Iteration 1**: value = `Union[str, Divergent]` (B not yet resolved) → interned ID **X1** - **Iteration 2**: value = `Union[str, TypeAlias_B]` (B resolved) → interned ID **X2** - **Iteration 3**: yet another value → yet another interned ID **X3** - ... **never converges** This contrasts with `PEP695TypeAliasType` (the `type X = ...` form), which does NOT store the value in the interned struct and instead computes it lazily via a separate tracked method with its own cycle detection. ## Fix Remove the `value` field from `ManualPEP695TypeAliasType` and compute it lazily (matching the `PEP695TypeAliasType` pattern): 1. **`types.rs`**: Removed `value` field. Added a lazy `value_type()` tracked method with Salsa cycle annotations that re-parses the value from the definition's AST on demand. 2. **`infer/builder.rs`**: Created a dedicated `infer_typealiastype_call` method, following the same pattern used by `TypeVar`, `ParamSpec`, and `NewType`. This validates the arguments, constructs the `ManualPEP695TypeAliasType`, and defers inference of the value argument to avoid cycles. Also added an "invalid context" diagnostic when `TypeAliasType` is used outside a simple variable assignment. 3. **`class.rs`**: Removed the `TypeAliasType` handling from `KnownClass::check_call` entirely, since it is now fully handled by the dedicated inference method. Additional improvements: - Added validation that the first argument is a string literal matching the assignment target name (like `TypeVar` and `NewType`). - Corrected the error message for non-string-literal names (was referencing `typing.TypeAlias`, now says `TypeAliasType`). - The self-referential `JSONValue` test case in `cycle.md` now correctly resolves to the actual type instead of `Divergent`, since lazy evaluation means the alias is already bound by the time `value_type()` runs. - Goto-type-definition for `TypeAliasType` variables now correctly navigates to the assignment (previously returned "No type definitions found" because the definition was `None`). ## Approaches Explored (and rejected) ### `#[salsa::tracked]` with `#[no_eq]` on value Changed from `#[salsa::interned]` to `#[salsa::tracked]` with `#[no_eq]` on the value field. **Result**: Still hangs — tracked struct recycling does NOT work during Salsa cycle re-iterations. Each iteration creates a new tracked struct with a fresh ID (observed IDs growing unboundedly: 9800, 9801, 9802, ...). ### Register RHS as standalone expression in the semantic index builder Registered the call expression RHS as a standalone expression so that `check_call` could find the definition via `try_expression`. **Result**: Breaks `TypeVar`, `ParamSpec`, `NamedTuple`, and `NewType` handling, since `infer_assignment_definition_impl` has a code path for standalone expressions (generic inference) and a separate code path with specialized handlers for these known classes. Registering call expressions as standalone causes all of them to take the wrong path. ## Test plan - Added new mdtest for mutually recursive `TypeAliasType` definitions in `pep695_type_aliases.md` - Added mdtest for name-mismatch and invalid-context diagnostics - Updated `cycle.md` test expectation (improved from `Divergent` to actual resolved type) - Updated `ty_ide` snapshot for `goto_type_of_bare_type_alias_type` (now correctly finds the definition) - All 325 mdtests pass - All unit tests pass - All corpus tests pass --------- Co-authored-by: Carl Meyer <carl@astral.sh>
Author
Parents
Loading