[ty] fix deferred name loading in PEP695 generic classes/functions (#19888)

## Summary

For PEP 695 generic functions and classes, there is an extra "type
params scope" (a child of the outer scope, and wrapping the body scope)
in which the type parameters are defined; class bases and function
parameter/return annotations are resolved in that type-params scope.

This PR fixes some longstanding bugs in how we resolve name loads from
inside these PEP 695 type parameter scopes, and also defers type
inference of PEP 695 typevar bounds/constraints/default, so we can
handle cycles without panicking.

We were previously treating these type-param scopes as lazy nested
scopes, which is wrong. In fact they are eager nested scopes; the class
`C` here inherits `int`, not `str`, and previously we got that wrong:

```py
Base = int

class C[T](Base): ...

Base = str
```

But certain syntactic positions within type param scopes (typevar
bounds/constraints/defaults) are lazy at runtime, and we should use
deferred name resolution for them. This also means they can have cycles;
in order to handle that without panicking in type inference, we need to
actually defer their type inference until after we have constructed the
`TypeVarInstance`.

PEP 695 does specify that typevar bounds and constraints cannot be
generic, and that typevar defaults can only reference prior typevars,
not later ones. This reduces the scope of (valid from the type-system
perspective) cycles somewhat, although cycles are still possible (e.g.
`class C[T: list[C]]`). And this is a type-system-only restriction; from
the runtime perspective an "invalid" case like `class C[T: T]` actually
works fine.

I debated whether to implement the PEP 695 restrictions as a way to
avoid some cycles up-front, but I ended up deciding against that; I'd
rather model the runtime name-resolution semantics accurately, and
implement the PEP 695 restrictions as a separate diagnostic on top.
(This PR doesn't yet implement those diagnostics, thus some `# TODO:
error` in the added tests.)

Introducing the possibility of cyclic typevars made typevar display
potentially stack overflow. For now I've handled this by simply removing
typevar details (bounds/constraints/default) from typevar display. This
impacts display of two kinds of types. If you `reveal_type(T)` on an
unbound `T` you now get just `typing.TypeVar` instead of
`typing.TypeVar("T", ...)` where `...` is the bound/constraints/default.
This matches pyright and mypy; pyrefly uses `type[TypeVar[T]]` which
seems a bit confusing, but does include the name. (We could easily
include the name without cycle issues, if there's a syntax we like for
that.)

It also means that displaying a generic function type like `def f[T:
int](x: T) -> T: ...` now displays as `f[T](x: T) -> T` instead of `f[T:
int](x: T) -> T`. This matches pyright and pyrefly; mypy does include
bound/constraints/defaults of typevars in function/callable type
display. If we wanted to add this, we would either need to thread a
visitor through all the type display code, or add a `decycle` type
transformation that replaced recursive reoccurrence of a type with a
marker.

## Test Plan

Added mdtests and modified existing tests to improve their correctness.

After this PR, there's only a single remaining py-fuzzer seed in the
0-500 range that panics! (Before this PR, there were 10; the fuzzer
likes to generate cyclic PEP 695 syntax.)

## Ecosystem report

It's all just the changes to `TypeVar` display.
This commit is contained in:
Carl Meyer 2025-08-13 15:51:59 -07:00 committed by GitHub
parent baadb5a78d
commit 5a570c8e6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 429 additions and 239 deletions

View file

@ -167,7 +167,7 @@ pub(crate) fn attribute_scopes<'db, 's>(
ChildrenIter::new(index, class_scope_id).filter_map(move |(child_scope_id, scope)| {
let (function_scope_id, function_scope) =
if scope.node().scope_kind() == ScopeKind::Annotation {
if scope.node().scope_kind() == ScopeKind::TypeParams {
// This could be a generic method with a type-params scope.
// Go one level deeper to find the function scope. The first
// descendant is the (potential) function scope.
@ -597,8 +597,8 @@ impl<'a> Iterator for VisibleAncestorsIter<'a> {
// Skip class scopes for subsequent scopes (following Python's lexical scoping rules)
// Exception: type parameter scopes can see names defined in an immediately-enclosing class scope
if scope.kind() == ScopeKind::Class {
// Allow type parameter scopes to see their immediately-enclosing class scope exactly once
if self.starting_scope_kind.is_type_parameter() && self.yielded_count == 2 {
// Allow annotation scopes to see their immediately-enclosing class scope exactly once
if self.starting_scope_kind.is_annotation() && self.yielded_count == 2 {
return Some((scope_id, scope));
}
continue;
@ -1317,7 +1317,7 @@ def func[T]():
panic!("expected one child scope");
};
assert_eq!(ann_scope.kind(), ScopeKind::Annotation);
assert_eq!(ann_scope.kind(), ScopeKind::TypeParams);
assert_eq!(
ann_scope_id.to_scope_id(&db, file).name(&db, &module),
"func"
@ -1361,7 +1361,7 @@ class C[T]:
panic!("expected one child scope");
};
assert_eq!(ann_scope.kind(), ScopeKind::Annotation);
assert_eq!(ann_scope.kind(), ScopeKind::TypeParams);
assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db, &module), "C");
let ann_table = index.place_table(ann_scope_id);
assert_eq!(names(&ann_table), vec!["T"]);

View file

@ -199,7 +199,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
match self.scopes[parent.file_scope_id].kind() {
ScopeKind::Class => Some(parent.file_scope_id),
ScopeKind::Annotation => {
ScopeKind::TypeParams => {
// If the function is generic, the parent scope is an annotation scope.
// In this case, we need to go up one level higher to find the class scope.
let grandparent = scopes_rev.next()?;
@ -2637,7 +2637,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
ScopeKind::Comprehension
| ScopeKind::Module
| ScopeKind::TypeAlias
| ScopeKind::Annotation => {}
| ScopeKind::TypeParams => {}
}
}
false
@ -2652,7 +2652,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
ScopeKind::Comprehension
| ScopeKind::Module
| ScopeKind::TypeAlias
| ScopeKind::Annotation => {}
| ScopeKind::TypeParams => {}
}
}
false
@ -2664,7 +2664,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
match scope.kind() {
ScopeKind::Class | ScopeKind::Comprehension => return false,
ScopeKind::Function | ScopeKind::Lambda => return true,
ScopeKind::Module | ScopeKind::TypeAlias | ScopeKind::Annotation => {}
ScopeKind::Module | ScopeKind::TypeAlias | ScopeKind::TypeParams => {}
}
}
false

View file

@ -702,6 +702,13 @@ impl DefinitionKind<'_> {
)
}
pub(crate) fn as_typevar(&self) -> Option<&AstNodeRef<ast::TypeParamTypeVar>> {
match self {
DefinitionKind::TypeVar(type_var) => Some(type_var),
_ => None,
}
}
/// Returns the [`TextRange`] of the definition target.
///
/// A definition target would mainly be the node representing the place being defined i.e.,

View file

@ -29,8 +29,8 @@ impl<'db> ScopeId<'db> {
self.node(db).scope_kind().is_function_like()
}
pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool {
self.node(db).scope_kind().is_type_parameter()
pub(crate) fn is_annotation(self, db: &'db dyn Db) -> bool {
self.node(db).scope_kind().is_annotation()
}
pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind {
@ -200,7 +200,7 @@ impl ScopeLaziness {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum ScopeKind {
Module,
Annotation,
TypeParams,
Class,
Function,
Lambda,
@ -215,18 +215,18 @@ impl ScopeKind {
pub(crate) const fn laziness(self) -> ScopeLaziness {
match self {
ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => ScopeLaziness::Eager,
ScopeKind::Annotation
| ScopeKind::Function
| ScopeKind::Lambda
| ScopeKind::TypeAlias => ScopeLaziness::Lazy,
ScopeKind::Module
| ScopeKind::Class
| ScopeKind::Comprehension
| ScopeKind::TypeParams => ScopeLaziness::Eager,
ScopeKind::Function | ScopeKind::Lambda | ScopeKind::TypeAlias => ScopeLaziness::Lazy,
}
}
pub(crate) const fn visibility(self) -> ScopeVisibility {
match self {
ScopeKind::Module | ScopeKind::Class => ScopeVisibility::Public,
ScopeKind::Annotation
ScopeKind::TypeParams
| ScopeKind::TypeAlias
| ScopeKind::Function
| ScopeKind::Lambda
@ -239,7 +239,7 @@ impl ScopeKind {
// symbol table also uses the term "function-like" for these scopes.
matches!(
self,
ScopeKind::Annotation
ScopeKind::TypeParams
| ScopeKind::Function
| ScopeKind::Lambda
| ScopeKind::TypeAlias
@ -255,8 +255,8 @@ impl ScopeKind {
matches!(self, ScopeKind::Module)
}
pub(crate) const fn is_type_parameter(self) -> bool {
matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias)
pub(crate) const fn is_annotation(self) -> bool {
matches!(self, ScopeKind::TypeParams | ScopeKind::TypeAlias)
}
pub(crate) const fn is_non_lambda_function(self) -> bool {
@ -388,7 +388,7 @@ impl NodeWithScopeKind {
Self::Lambda(_) => ScopeKind::Lambda,
Self::FunctionTypeParameters(_)
| Self::ClassTypeParameters(_)
| Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation,
| Self::TypeAliasTypeParameters(_) => ScopeKind::TypeParams,
Self::TypeAlias(_) => ScopeKind::TypeAlias,
Self::ListComprehension(_)
| Self::SetComprehension(_)

View file

@ -5291,7 +5291,7 @@ impl<'db> Type<'db> {
db,
Name::new(format!("{}'instance", typevar.name(db))),
None,
Some(bound_or_constraints),
Some(bound_or_constraints.into()),
typevar.variance(db),
None,
typevar.kind(db),
@ -5482,7 +5482,7 @@ impl<'db> Type<'db> {
db,
ast::name::Name::new_static("Self"),
Some(class_definition),
Some(TypeVarBoundOrConstraints::UpperBound(instance)),
Some(TypeVarBoundOrConstraints::UpperBound(instance).into()),
TypeVarVariance::Invariant,
None,
TypeVarKind::Implicit,
@ -6439,9 +6439,7 @@ impl<'db> KnownInstanceType<'db> {
// This is a legacy `TypeVar` _outside_ of any generic class or function, so we render
// it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll
// have a `Type::TypeVar(_)`, which is rendered as the typevar's name.
KnownInstanceType::TypeVar(typevar) => {
write!(f, "typing.TypeVar({})", typevar.display(self.db))
}
KnownInstanceType::TypeVar(_) => f.write_str("typing.TypeVar"),
KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"),
KnownInstanceType::Field(field) => {
f.write_str("dataclasses.Field[")?;
@ -6862,14 +6860,17 @@ pub struct TypeVarInstance<'db> {
/// The type var's definition (None if synthesized)
pub definition: Option<Definition<'db>>,
/// The upper bound or constraint on the type of this TypeVar
bound_or_constraints: Option<TypeVarBoundOrConstraints<'db>>,
/// The upper bound or constraint on the type of this TypeVar, if any. Don't use this field
/// directly; use the `bound_or_constraints` (or `upper_bound` and `constraints`) methods
/// instead (to evaluate any lazy bound or constraints).
_bound_or_constraints: Option<TypeVarBoundOrConstraintsEvaluation<'db>>,
/// The variance of the TypeVar
variance: TypeVarVariance,
/// The default type for this TypeVar
default_ty: Option<Type<'db>>,
/// The default type for this TypeVar, if any. Don't use this field directly, use the
/// `default_type` method instead (to evaluate any lazy default).
_default: Option<TypeVarDefaultEvaluation<'db>>,
pub kind: TypeVarKind,
}
@ -6885,11 +6886,12 @@ fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
if let Some(bounds) = typevar.bound_or_constraints(db) {
walk_type_var_bounds(db, bounds, visitor);
}
if let Some(default_type) = typevar.default_ty(db) {
if let Some(default_type) = typevar.default_type(db) {
visitor.visit_type(db, default_type);
}
}
#[salsa::tracked]
impl<'db> TypeVarInstance<'db> {
pub(crate) fn with_binding_context(
self,
@ -6919,15 +6921,50 @@ impl<'db> TypeVarInstance<'db> {
}
}
pub(crate) fn bound_or_constraints(
self,
db: &'db dyn Db,
) -> Option<TypeVarBoundOrConstraints<'db>> {
self._bound_or_constraints(db).and_then(|w| match w {
TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => {
Some(bound_or_constraints)
}
TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self.lazy_bound(db),
TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self.lazy_constraints(db),
})
}
pub(crate) fn default_type(self, db: &'db dyn Db) -> Option<Type<'db>> {
self._default(db).and_then(|d| match d {
TypeVarDefaultEvaluation::Eager(ty) => Some(ty),
TypeVarDefaultEvaluation::Lazy => self.lazy_default(db),
})
}
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
Self::new(
db,
self.name(db),
self.definition(db),
self.bound_or_constraints(db)
.map(|b| b.normalized_impl(db, visitor)),
self._bound_or_constraints(db)
.and_then(|bound_or_constraints| match bound_or_constraints {
TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => {
Some(bound_or_constraints.normalized_impl(db, visitor).into())
}
TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self
.lazy_bound(db)
.map(|bound| bound.normalized_impl(db, visitor).into()),
TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self
.lazy_constraints(db)
.map(|constraints| constraints.normalized_impl(db, visitor).into()),
}),
self.variance(db),
self.default_ty(db).map(|d| d.normalized_impl(db, visitor)),
self._default(db).and_then(|default| match default {
TypeVarDefaultEvaluation::Eager(ty) => Some(ty.normalized_impl(db, visitor).into()),
TypeVarDefaultEvaluation::Lazy => self
.lazy_default(db)
.map(|ty| ty.normalized_impl(db, visitor).into()),
}),
self.kind(db),
)
}
@ -6937,13 +6974,59 @@ impl<'db> TypeVarInstance<'db> {
db,
self.name(db),
self.definition(db),
self.bound_or_constraints(db)
.map(|b| b.materialize(db, variance)),
self._bound_or_constraints(db)
.and_then(|bound_or_constraints| match bound_or_constraints {
TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => {
Some(bound_or_constraints.materialize(db, variance).into())
}
TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self
.lazy_bound(db)
.map(|bound| bound.materialize(db, variance).into()),
TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self
.lazy_constraints(db)
.map(|constraints| constraints.materialize(db, variance).into()),
}),
self.variance(db),
self.default_ty(db),
self._default(db).and_then(|default| match default {
TypeVarDefaultEvaluation::Eager(ty) => Some(ty.materialize(db, variance).into()),
TypeVarDefaultEvaluation::Lazy => self
.lazy_default(db)
.map(|ty| ty.materialize(db, variance).into()),
}),
self.kind(db),
)
}
#[salsa::tracked]
fn lazy_bound(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
let definition = self.definition(db)?;
let module = parsed_module(db, definition.file(db)).load(db);
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?);
Some(TypeVarBoundOrConstraints::UpperBound(ty))
}
#[salsa::tracked]
fn lazy_constraints(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
let definition = self.definition(db)?;
let module = parsed_module(db, definition.file(db)).load(db);
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?)
.into_union()?;
Some(TypeVarBoundOrConstraints::Constraints(ty))
}
#[salsa::tracked]
fn lazy_default(self, db: &'db dyn Db) -> Option<Type<'db>> {
let definition = self.definition(db)?;
let module = parsed_module(db, definition.file(db)).load(db);
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
Some(definition_expression_type(
db,
definition,
typevar_node.default.as_ref()?,
))
}
}
/// Where a type variable is bound and usable.
@ -7008,10 +7091,10 @@ impl<'db> BoundTypeVarInstance<'db> {
/// By using `U` in the generic class, it becomes bound, and so we have a
/// `BoundTypeVarInstance`. As part of binding `U` we must also bind its default value
/// (resulting in `T@C`).
pub(crate) fn default_ty(self, db: &'db dyn Db) -> Option<Type<'db>> {
pub(crate) fn default_type(self, db: &'db dyn Db) -> Option<Type<'db>> {
let binding_context = self.binding_context(db);
self.typevar(db)
.default_ty(db)
.default_type(db)
.map(|ty| ty.apply_type_mapping(db, &TypeMapping::BindLegacyTypevars(binding_context)))
}
@ -7054,6 +7137,38 @@ impl TypeVarVariance {
}
}
/// Whether a typevar default is eagerly specified or lazily evaluated.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub enum TypeVarDefaultEvaluation<'db> {
/// The default type is lazily evaluated.
Lazy,
/// The default type is eagerly specified.
Eager(Type<'db>),
}
impl<'db> From<Type<'db>> for TypeVarDefaultEvaluation<'db> {
fn from(value: Type<'db>) -> Self {
TypeVarDefaultEvaluation::Eager(value)
}
}
/// Whether a typevar bound/constraints is eagerly specified or lazily evaluated.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub enum TypeVarBoundOrConstraintsEvaluation<'db> {
/// There is a lazily-evaluated upper bound.
LazyUpperBound,
/// There is a lazily-evaluated set of constraints.
LazyConstraints,
/// The upper bound/constraints are eagerly specified.
Eager(TypeVarBoundOrConstraints<'db>),
}
impl<'db> From<TypeVarBoundOrConstraints<'db>> for TypeVarBoundOrConstraintsEvaluation<'db> {
fn from(value: TypeVarBoundOrConstraints<'db>) -> Self {
TypeVarBoundOrConstraintsEvaluation::Eager(value)
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub enum TypeVarBoundOrConstraints<'db> {
UpperBound(Type<'db>),

View file

@ -397,7 +397,7 @@ impl<'db> Bindings<'db> {
}
Some("__default__") => {
overload.set_return_type(
typevar.default_ty(db).unwrap_or_else(|| {
typevar.default_type(db).unwrap_or_else(|| {
KnownClass::NoDefaultType.to_instance(db)
}),
);

View file

@ -4520,7 +4520,9 @@ impl KnownClass {
}
let bound_or_constraint = match (bound, constraints) {
(Some(bound), None) => Some(TypeVarBoundOrConstraints::UpperBound(*bound)),
(Some(bound), None) => {
Some(TypeVarBoundOrConstraints::UpperBound(*bound).into())
}
(None, Some(_constraints)) => {
// We don't use UnionType::from_elements or UnionBuilder here,
@ -4536,7 +4538,7 @@ impl KnownClass {
.map(|(_, ty)| ty)
.collect::<Box<_>>(),
);
Some(TypeVarBoundOrConstraints::Constraints(elements))
Some(TypeVarBoundOrConstraints::Constraints(elements).into())
}
// TODO: Emit a diagnostic that TypeVar cannot be both bounded and
@ -4554,7 +4556,7 @@ impl KnownClass {
Some(containing_assignment),
bound_or_constraint,
variance,
*default,
default.map(Into::into),
TypeVarKind::Legacy,
),
)));

View file

@ -14,9 +14,8 @@ use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::TupleSpec;
use crate::types::{
BoundTypeVarInstance, CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol,
StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance,
UnionType, WrapperDescriptorKind,
CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol, StringLiteralType,
SubclassOfInner, Type, UnionType, WrapperDescriptorKind,
};
impl<'db> Type<'db> {
@ -417,83 +416,6 @@ impl Display for DisplayGenericAlias<'_> {
}
}
impl<'db> TypeVarInstance<'db> {
pub(crate) fn display(self, db: &'db dyn Db) -> DisplayTypeVarInstance<'db> {
DisplayTypeVarInstance { typevar: self, db }
}
}
pub(crate) struct DisplayTypeVarInstance<'db> {
typevar: TypeVarInstance<'db>,
db: &'db dyn Db,
}
impl Display for DisplayTypeVarInstance<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
display_quoted_string(self.typevar.name(self.db)).fmt(f)?;
match self.typevar.bound_or_constraints(self.db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
write!(f, ", bound={}", bound.display(self.db))?;
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
for constraint in constraints.iter(self.db) {
write!(f, ", {}", constraint.display(self.db))?;
}
}
None => {}
}
if let Some(default_type) = self.typevar.default_ty(self.db) {
write!(f, ", default={}", default_type.display(self.db))?;
}
Ok(())
}
}
impl<'db> BoundTypeVarInstance<'db> {
pub(crate) fn display(self, db: &'db dyn Db) -> DisplayBoundTypeVarInstance<'db> {
DisplayBoundTypeVarInstance {
bound_typevar: self,
db,
}
}
}
pub(crate) struct DisplayBoundTypeVarInstance<'db> {
bound_typevar: BoundTypeVarInstance<'db>,
db: &'db dyn Db,
}
impl Display for DisplayBoundTypeVarInstance<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// This looks very much like DisplayTypeVarInstance::fmt, but note that we have typevar
// default values in a subtly different way: if the default value contains other typevars,
// here those must be bound as well, whereas in DisplayTypeVarInstance they should not. See
// BoundTypeVarInstance::default_ty for more details.
let typevar = self.bound_typevar.typevar(self.db);
f.write_str(typevar.name(self.db))?;
match typevar.bound_or_constraints(self.db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
write!(f, ": {}", bound.display(self.db))?;
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
f.write_str(": (")?;
for (idx, constraint) in constraints.iter(self.db).enumerate() {
if idx > 0 {
f.write_str(", ")?;
}
constraint.display(self.db).fmt(f)?;
}
f.write_char(')')?;
}
None => {}
}
if let Some(default_type) = self.bound_typevar.default_ty(self.db) {
write!(f, " = {}", default_type.display(self.db))?;
}
Ok(())
}
}
impl<'db> GenericContext<'db> {
pub fn display(&'db self, db: &'db dyn Db) -> DisplayGenericContext<'db> {
DisplayGenericContext {
@ -545,7 +467,7 @@ impl Display for DisplayGenericContext<'_> {
if idx > 0 {
f.write_str(", ")?;
}
bound_typevar.display(self.db).fmt(f)?;
f.write_str(bound_typevar.typevar(self.db).name(self.db))?;
}
f.write_char(']')
}

View file

@ -225,7 +225,7 @@ impl<'db> GenericContext<'db> {
}
None => {}
}
if let Some(default_ty) = bound_typevar.default_ty(db) {
if let Some(default_ty) = bound_typevar.default_type(db) {
parameter = parameter.with_default_type(default_ty);
}
parameter
@ -337,7 +337,7 @@ impl<'db> GenericContext<'db> {
continue;
}
let Some(default) = typevar.default_ty(db) else {
let Some(default) = typevar.default_type(db) else {
continue;
};
@ -756,7 +756,7 @@ impl<'db> SpecializationBuilder<'db> {
self.types
.get(variable)
.copied()
.unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown()))
.unwrap_or(variable.default_type(self.db).unwrap_or(Type::unknown()))
})
.collect();
// TODO Infer the tuple spec for a tuple type

View file

@ -121,8 +121,9 @@ use crate::types::{
IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard,
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm,
Parameters, SpecialFormType, SubclassOfType, Truthiness, Type, TypeAliasType,
TypeAndQualifiers, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
TypeAndQualifiers, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation,
TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder,
UnionType, binding_type, todo_type,
};
use crate::unpack::{EvaluationMode, Unpack, UnpackPosition};
use crate::util::diagnostics::format_enumeration;
@ -1748,6 +1749,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
DefinitionKind::Class(class) => {
self.infer_class_deferred(definition, class.node(self.module()));
}
DefinitionKind::TypeVar(typevar) => {
self.infer_typevar_deferred(typevar.node(self.module()));
}
_ => {}
}
}
@ -2243,6 +2247,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_type_parameters(type_params);
if let Some(arguments) = class.arguments.as_deref() {
let in_stub = self.in_stub();
let previous_deferred_state =
std::mem::replace(&mut self.deferred_state, in_stub.into());
let mut call_arguments =
CallArguments::from_arguments(self.db(), arguments, |argument, splatted_value| {
let ty = self.infer_expression(splatted_value);
@ -2251,6 +2258,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
});
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
self.infer_argument_types(arguments, &mut call_arguments, &argument_forms);
self.deferred_state = previous_deferred_state;
}
self.typevar_binding_context = previous_typevar_binding_context;
@ -2271,7 +2279,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.typevar_binding_context.replace(binding_context);
self.infer_return_type_annotation(
function.returns.as_deref(),
DeferredExpressionState::None,
self.defer_annotations().into(),
);
self.infer_type_parameters(type_params);
self.infer_parameters(&function.parameters);
@ -2316,7 +2324,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let class_scope = match parent_scope.kind() {
ScopeKind::Class => parent_scope,
ScopeKind::Annotation => {
ScopeKind::TypeParams => {
let class_scope_id = parent_scope.parent()?;
let potentially_class_scope = self.index.scope(class_scope_id);
@ -2757,7 +2765,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let annotated = self.infer_optional_annotation_expression(
parameter.annotation.as_deref(),
DeferredExpressionState::None,
self.defer_annotations().into(),
);
if let Some(qualifiers) = annotated.map(|annotated| annotated.qualifiers) {
@ -2792,7 +2800,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_optional_annotation_expression(
annotation.as_deref(),
DeferredExpressionState::None,
self.defer_annotations().into(),
);
}
@ -3402,44 +3410,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
{
builder.into_diagnostic("TypeVar must have at least two constrained types");
}
self.infer_expression(expr);
None
} else {
// We don't use UnionType::from_elements or UnionBuilder here, because we don't
// want to simplify the list of constraints like we do with the elements of an
// actual union type.
// TODO: Consider using a new `OneOfType` connective here instead, since that
// more accurately represents the actual semantics of typevar constraints.
let elements = UnionType::new(
self.db(),
elts.iter()
.map(|expr| self.infer_type_expression(expr))
.collect::<Box<[_]>>(),
);
let constraints = TypeVarBoundOrConstraints::Constraints(elements);
// But when we construct an actual union type for the constraint expression as
// a whole, we do use UnionType::from_elements to maintain the invariant that
// all union types are simplified.
self.store_expression_type(
expr,
UnionType::from_elements(self.db(), elements.elements(self.db())),
);
Some(constraints)
Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints)
}
}
Some(expr) => Some(TypeVarBoundOrConstraints::UpperBound(
self.infer_type_expression(expr),
)),
Some(_) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound),
None => None,
};
let default_ty = self.infer_optional_type_expression(default.as_deref());
if bound_or_constraint.is_some() || default.is_some() {
self.deferred.insert(definition);
}
let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new(
self.db(),
&name.id,
Some(definition),
bound_or_constraint,
TypeVarVariance::Invariant, // TODO: infer this
default_ty,
default.as_deref().map(|_| TypeVarDefaultEvaluation::Lazy),
TypeVarKind::Pep695,
)));
self.add_declaration_with_binding(
@ -3449,6 +3437,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
}
fn infer_typevar_deferred(&mut self, node: &ast::TypeParamTypeVar) {
let ast::TypeParamTypeVar {
range: _,
node_index: _,
name: _,
bound,
default,
} = node;
match bound.as_deref() {
Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => {
// We don't use UnionType::from_elements or UnionBuilder here, because we don't
// want to simplify the list of constraints like we do with the elements of an
// actual union type.
// TODO: Consider using a new `OneOfType` connective here instead, since that
// more accurately represents the actual semantics of typevar constraints.
let ty = Type::Union(UnionType::new(
self.db(),
elts.iter()
.map(|expr| self.infer_type_expression(expr))
.collect::<Box<[_]>>(),
));
self.store_expression_type(expr, ty);
}
Some(expr) => {
self.infer_type_expression(expr);
}
None => {}
}
self.infer_optional_type_expression(default.as_deref());
}
fn infer_paramspec_definition(
&mut self,
node: &ast::TypeParamParamSpec,
@ -6573,7 +6592,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let place_table = self.index.place_table(file_scope_id);
let use_def = self.index.use_def_map(file_scope_id);
// If we're inferring types of deferred expressions, always treat them as public symbols
// If we're inferring types of deferred expressions, look them up from end-of-scope.
if self.is_deferred() {
let place = if let Some(place_id) = place_table.place_id(expr) {
place_from_bindings(db, use_def.all_reachable_bindings(place_id))
@ -6684,11 +6703,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// Class scopes are not visible to nested scopes, and we need to handle global
// scope differently (because an unbound name there falls back to builtins), so
// check only function-like scopes.
// There is one exception to this rule: type parameter scopes can see
// There is one exception to this rule: annotation scopes can see
// names defined in an immediately-enclosing class scope.
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file);
let is_immediately_enclosing_scope = scope.is_type_parameter(db)
let is_immediately_enclosing_scope = scope.is_annotation(db)
&& scope
.scope(db)
.parent()
@ -9535,17 +9554,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
ty
}
/// Similar to [`infer_type_expression`], but accepts an optional type expression and returns
/// [`None`] if the expression is [`None`].
///
/// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression
fn infer_optional_type_expression(
&mut self,
expression: Option<&ast::Expr>,
) -> Option<Type<'db>> {
expression.map(|expr| self.infer_type_expression(expr))
}
/// Similar to [`infer_type_expression`], but accepts a [`DeferredExpressionState`].
///
/// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression
@ -9560,6 +9568,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
annotation_ty
}
/// Similar to [`infer_type_expression`], but accepts an optional expression.
///
/// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression_with_state
fn infer_optional_type_expression(
&mut self,
expression: Option<&ast::Expr>,
) -> Option<Type<'db>> {
expression.map(|expr| self.infer_type_expression(expr))
}
fn report_invalid_type_expression(
&self,
expression: &ast::Expr,
@ -11541,38 +11559,20 @@ mod tests {
);
assert_eq!(
typevar
.default_ty(&db)
.default_type(&db)
.map(|ty| ty.display(&db).to_string()),
default.map(std::borrow::ToOwned::to_owned)
);
};
check_typevar("T", "typing.TypeVar(\"T\")", None, None, None);
check_typevar("U", "typing.TypeVar(\"U\", bound=A)", Some("A"), None, None);
check_typevar(
"V",
"typing.TypeVar(\"V\", A, B)",
None,
Some(&["A", "B"]),
None,
);
check_typevar(
"W",
"typing.TypeVar(\"W\", default=A)",
None,
None,
Some("A"),
);
check_typevar(
"X",
"typing.TypeVar(\"X\", bound=A, default=A1)",
Some("A"),
None,
Some("A1"),
);
check_typevar("T", "typing.TypeVar", None, None, None);
check_typevar("U", "typing.TypeVar", Some("A"), None, None);
check_typevar("V", "typing.TypeVar", None, Some(&["A", "B"]), None);
check_typevar("W", "typing.TypeVar", None, None, Some("A"));
check_typevar("X", "typing.TypeVar", Some("A"), None, Some("A1"));
// a typevar with less than two constraints is treated as unconstrained
check_typevar("Y", "typing.TypeVar(\"Y\")", None, None, None);
check_typevar("Y", "typing.TypeVar", None, None, None);
}
/// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing

View file

@ -1797,11 +1797,7 @@ mod tests {
};
assert_eq!(a_name, "a");
assert_eq!(b_name, "b");
// TODO resolution should not be deferred; we should see A, not A | B
assert_eq!(
a_annotated_ty.unwrap().display(&db).to_string(),
"Unknown | A | B"
);
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "A");
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T@f");
}

View file

@ -97,9 +97,12 @@ impl<'db> SubclassOfType<'db> {
db,
Name::new_static("T_all"),
None,
Some(TypeVarBoundOrConstraints::UpperBound(
KnownClass::Type.to_instance(db),
)),
Some(
TypeVarBoundOrConstraints::UpperBound(
KnownClass::Type.to_instance(db),
)
.into(),
),
variance,
None,
TypeVarKind::Pep695,