Skip to content

Commit b7662e9

Browse files
committed
[ty] defer inference of legacy TypeVar bound/constraints/defaults
1 parent 6b3c493 commit b7662e9

File tree

8 files changed

+208
-62
lines changed

8 files changed

+208
-62
lines changed

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,69 @@ def constrained(x: T_constrained):
231231
reveal_type(type(x)) # revealed: type[int] | type[str]
232232
```
233233

234+
## Cycles
235+
236+
### Bounds and constraints
237+
238+
A typevar's bounds and constraints cannot be generic, cyclic or otherwise:
239+
240+
```py
241+
from typing import Any, TypeVar
242+
243+
S = TypeVar("S")
244+
245+
# TODO: error
246+
T = TypeVar("T", bound=list[S])
247+
248+
# TODO: error
249+
U = TypeVar("U", list["T"])
250+
251+
# TODO: error
252+
V = TypeVar("V", list[S], str)
253+
254+
# TODO: error
255+
W = TypeVar("W", list["W"], str)
256+
```
257+
258+
However, they are lazily evaluated and can cyclically refer to their own type:
259+
260+
```py
261+
from typing import TypeVar, Generic
262+
263+
T = TypeVar("T", bound=list["G"])
264+
265+
class G(Generic[T]):
266+
x: T
267+
268+
reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]]
269+
```
270+
271+
### Defaults
272+
273+
Defaults can be generic, but can only refer to earlier typevars:
274+
275+
```py
276+
from typing import Generic, TypeVar
277+
278+
T = TypeVar("T")
279+
U = TypeVar("U", default=T)
280+
281+
class C(Generic[T, U]):
282+
x: T
283+
y: U
284+
285+
reveal_type(C[int, str]().x) # revealed: int
286+
reveal_type(C[int, str]().y) # revealed: str
287+
reveal_type(C[int]().x) # revealed: int
288+
reveal_type(C[int]().y) # revealed: int
289+
290+
# TODO: error
291+
V = TypeVar("V", default="V")
292+
293+
class D(Generic[V]):
294+
x: V
295+
296+
reveal_type(D().x) # revealed: V@D
297+
```
298+
234299
[generics]: https://typing.python.org/en/latest/spec/generics.html

crates/ty_python_semantic/src/ast_node_ref.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use ruff_text_size::Ranged;
3030
/// This means that changes to expressions in other scopes don't invalidate the expression's id, giving
3131
/// us some form of scope-stable identity for expressions. Only queries accessing the node field
3232
/// run on every AST change. All other queries only run when the expression's identity changes.
33-
#[derive(Clone)]
33+
#[derive(Clone, PartialEq)]
3434
pub struct AstNodeRef<T> {
3535
/// The index of the node in the AST.
3636
index: NodeIndex,
@@ -40,15 +40,14 @@ pub struct AstNodeRef<T> {
4040
kind: ruff_python_ast::NodeKind,
4141
#[cfg(debug_assertions)]
4242
range: ruff_text_size::TextRange,
43-
// Note that because the module address is not stored in release builds, `AstNodeRef`
44-
// cannot implement `Eq`, as indices are only unique within a given instance of the
45-
// AST.
4643
#[cfg(debug_assertions)]
4744
module_addr: usize,
4845

4946
_node: PhantomData<T>,
5047
}
5148

49+
impl<T> Eq for AstNodeRef<T> where T: PartialEq {}
50+
5251
impl<T> AstNodeRef<T> {
5352
pub(crate) fn index(&self) -> NodeIndex {
5453
self.index

crates/ty_python_semantic/src/semantic_index/definition.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -706,13 +706,6 @@ impl DefinitionKind<'_> {
706706
matches!(self, DefinitionKind::Assignment(_))
707707
}
708708

709-
pub(crate) fn as_typevar(&self) -> Option<&AstNodeRef<ast::TypeParamTypeVar>> {
710-
match self {
711-
DefinitionKind::TypeVar(type_var) => Some(type_var),
712-
_ => None,
713-
}
714-
}
715-
716709
/// Returns the [`TextRange`] of the definition target.
717710
///
718711
/// A definition target would mainly be the node representing the place being defined i.e.,

crates/ty_python_semantic/src/types.rs

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
3333
use crate::module_name::ModuleName;
3434
use crate::module_resolver::{KnownModule, resolve_module};
3535
use crate::place::{Boundness, Place, PlaceAndQualifiers, imported_symbol};
36-
use crate::semantic_index::definition::Definition;
36+
use crate::semantic_index::definition::{Definition, DefinitionKind};
3737
use crate::semantic_index::place::ScopedPlaceId;
3838
use crate::semantic_index::scope::ScopeId;
3939
use crate::semantic_index::{imported_modules, place_table, semantic_index};
@@ -4514,17 +4514,17 @@ impl<'db> Type<'db> {
45144514
Parameter::positional_or_keyword(Name::new_static("name"))
45154515
.with_annotated_type(Type::LiteralString),
45164516
Parameter::variadic(Name::new_static("constraints"))
4517-
.type_form()
4517+
.deferred_type_form()
45184518
.with_annotated_type(Type::any()),
45194519
Parameter::keyword_only(Name::new_static("bound"))
4520-
.type_form()
4520+
.deferred_type_form()
45214521
.with_annotated_type(UnionType::from_elements(
45224522
db,
45234523
[Type::any(), Type::none(db)],
45244524
))
45254525
.with_default_type(Type::none(db)),
45264526
Parameter::keyword_only(Name::new_static("default"))
4527-
.type_form()
4527+
.deferred_type_form()
45284528
.with_annotated_type(Type::any())
45294529
.with_default_type(KnownClass::NoneType.to_instance(db)),
45304530
Parameter::keyword_only(Name::new_static("contravariant"))
@@ -7775,35 +7775,87 @@ impl<'db> TypeVarInstance<'db> {
77757775
))
77767776
}
77777777

7778-
#[salsa::tracked(cycle_fn=lazy_bound_cycle_recover, cycle_initial=lazy_bound_cycle_initial)]
7778+
#[salsa::tracked(cycle_fn=lazy_bound_cycle_recover, cycle_initial=lazy_bound_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
77797779
fn lazy_bound(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
77807780
let definition = self.definition(db)?;
77817781
let module = parsed_module(db, definition.file(db)).load(db);
7782-
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
7783-
let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?);
7782+
let ty = match definition.kind(db) {
7783+
// PEP 695 typevar
7784+
DefinitionKind::TypeVar(typevar) => {
7785+
let typevar_node = typevar.node(&module);
7786+
Some(definition_expression_type(
7787+
db,
7788+
definition,
7789+
typevar_node.bound.as_ref()?,
7790+
))
7791+
}
7792+
// legacy typevar
7793+
DefinitionKind::Assignment(assignment) => {
7794+
let call_expr = assignment.value(&module).as_call_expr()?;
7795+
let expr = &call_expr.arguments.find_keyword("bound")?.value;
7796+
Some(definition_expression_type(db, definition, expr))
7797+
}
7798+
_ => None,
7799+
}?;
77847800
Some(TypeVarBoundOrConstraints::UpperBound(ty))
77857801
}
77867802

7787-
#[salsa::tracked]
7803+
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
77887804
fn lazy_constraints(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
77897805
let definition = self.definition(db)?;
77907806
let module = parsed_module(db, definition.file(db)).load(db);
7791-
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
7792-
let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?)
7793-
.into_union()?;
7807+
let ty = match definition.kind(db) {
7808+
// PEP 695 typevar
7809+
DefinitionKind::TypeVar(typevar) => {
7810+
let typevar_node = typevar.node(&module);
7811+
Some(
7812+
definition_expression_type(db, definition, typevar_node.bound.as_ref()?)
7813+
.into_union()?,
7814+
)
7815+
}
7816+
// legacy typevar
7817+
DefinitionKind::Assignment(assignment) => {
7818+
let call_expr = assignment.value(&module).as_call_expr()?;
7819+
Some(
7820+
UnionType::from_elements(
7821+
db,
7822+
call_expr
7823+
.arguments
7824+
.args
7825+
.iter()
7826+
.skip(1)
7827+
.map(|arg| definition_expression_type(db, definition, arg)),
7828+
)
7829+
.into_union()?,
7830+
)
7831+
}
7832+
_ => None,
7833+
}?;
77947834
Some(TypeVarBoundOrConstraints::Constraints(ty))
77957835
}
77967836

7797-
#[salsa::tracked]
7837+
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
77987838
fn lazy_default(self, db: &'db dyn Db) -> Option<Type<'db>> {
77997839
let definition = self.definition(db)?;
78007840
let module = parsed_module(db, definition.file(db)).load(db);
7801-
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
7802-
Some(definition_expression_type(
7803-
db,
7804-
definition,
7805-
typevar_node.default.as_ref()?,
7806-
))
7841+
match definition.kind(db) {
7842+
// PEP 695 typevar
7843+
DefinitionKind::TypeVar(typevar) => {
7844+
let typevar_node = typevar.node(&module);
7845+
Some(definition_expression_type(
7846+
db,
7847+
definition,
7848+
typevar_node.default.as_ref()?,
7849+
))
7850+
}
7851+
// legacy typevar
7852+
DefinitionKind::Assignment(assignment) => {
7853+
let call_expr = assignment.value(&module).as_call_expr()?;
7854+
let expr = &call_expr.arguments.find_keyword("default")?.value;
7855+
Some(definition_expression_type(db, definition, expr))
7856+
}
7857+
_ => None,
7858+
}
78077859
}
78087860
}
78097861

crates/ty_python_semantic/src/types/class.rs

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ use crate::types::{
2929
DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
3030
IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind,
3131
NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeContext,
32-
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
33-
TypedDictParams, UnionBuilder, VarianceInferable, declaration_type, determine_upper_bound,
34-
infer_definition_types,
32+
TypeMapping, TypeRelation, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation,
33+
TypeVarInstance, TypeVarKind, TypedDictParams, UnionBuilder, VarianceInferable,
34+
declaration_type, determine_upper_bound, infer_definition_types,
3535
};
3636
use crate::{
3737
Db, FxIndexMap, FxOrderSet, Program,
@@ -4908,7 +4908,6 @@ impl KnownClass {
49084908
context: &InferContext<'db, '_>,
49094909
index: &SemanticIndex<'db>,
49104910
overload: &mut Binding<'db>,
4911-
call_arguments: &CallArguments<'_, 'db>,
49124911
call_expression: &ast::ExprCall,
49134912
) {
49144913
let db = context.db();
@@ -5113,26 +5112,9 @@ impl KnownClass {
51135112
}
51145113

51155114
let bound_or_constraint = match (bound, constraints) {
5116-
(Some(bound), None) => {
5117-
Some(TypeVarBoundOrConstraints::UpperBound(*bound).into())
5118-
}
5115+
(Some(_), None) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound),
51195116

5120-
(None, Some(_constraints)) => {
5121-
// We don't use UnionType::from_elements or UnionBuilder here,
5122-
// because we don't want to simplify the list of constraints like
5123-
// we do with the elements of an actual union type.
5124-
// TODO: Consider using a new `OneOfType` connective here instead,
5125-
// since that more accurately represents the actual semantics of
5126-
// typevar constraints.
5127-
let elements = UnionType::new(
5128-
db,
5129-
overload
5130-
.arguments_for_parameter(call_arguments, 1)
5131-
.map(|(_, ty)| ty)
5132-
.collect::<Box<_>>(),
5133-
);
5134-
Some(TypeVarBoundOrConstraints::Constraints(elements).into())
5135-
}
5117+
(None, Some(_)) => Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints),
51365118

51375119
// TODO: Emit a diagnostic that TypeVar cannot be both bounded and
51385120
// constrained
@@ -5149,7 +5131,7 @@ impl KnownClass {
51495131
Some(containing_assignment),
51505132
bound_or_constraint,
51515133
Some(variance),
5152-
default.map(Into::into),
5134+
default.map(|_| TypeVarDefaultEvaluation::Lazy),
51535135
TypeVarKind::Legacy,
51545136
),
51555137
)));

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ use salsa;
4444
use salsa::plumbing::AsId;
4545

4646
use crate::Db;
47+
use crate::ast_node_ref::AstNodeRef;
4748
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
4849
use crate::semantic_index::definition::Definition;
4950
use crate::semantic_index::expression::Expression;
@@ -631,9 +632,12 @@ struct DefinitionInferenceExtra<'db> {
631632
/// Is this a cycle-recovery inference result, and if so, what kind?
632633
cycle_recovery: Option<CycleRecovery<'db>>,
633634

634-
/// The definitions that are deferred.
635+
/// The definitions that have some deferred parts.
635636
deferred: Box<[Definition<'db>]>,
636637

638+
/// Deferred expressions within a deferred definition.
639+
deferred_expressions: Box<[AstNodeRef<ast::Expr>]>,
640+
637641
/// The diagnostics for this region.
638642
diagnostics: TypeCheckDiagnostics,
639643

0 commit comments

Comments
 (0)