[ty] Infer parameter specializations of generic aliases (#18021)
Some checks are pending
CI / cargo build (msrv) (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This updates our function specialization inference to infer type
mappings from parameters that are generic aliases, e.g.:

```py
def f[T](x: list[T]) -> T: ...

reveal_type(f(["a", "b"]))  # revealed: str
```

Though note that we're still inferring the type of list literals as
`list[Unknown]`, so for now we actually need something like the
following in our tests:

```py
def _(x: list[str]):
    reveal_type(f(x))  # revealed: str
```
This commit is contained in:
Douglas Creager 2025-05-12 22:12:44 -04:00 committed by GitHub
parent 55df9271ba
commit 0fb94c052e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 98 additions and 85 deletions

View file

@ -17,7 +17,6 @@ def _(x: tuple[int, str], y: tuple[None, tuple[int]]):
```py ```py
def _(x: tuple[int, ...], y: tuple[str, ...]): def _(x: tuple[int, ...], y: tuple[str, ...]):
# TODO: should be `tuple[int | str, ...]` reveal_type(x + y) # revealed: tuple[int | str, ...]
reveal_type(x + y) # revealed: tuple[int | Unknown, ...]
reveal_type(x + (1, 2)) # revealed: tuple[int, ...] reveal_type(x + (1, 2)) # revealed: tuple[int, ...]
``` ```

View file

@ -88,14 +88,12 @@ def takes_in_protocol(x: CanIndex[T]) -> T:
return x[0] return x[0]
def deep_list(x: list[str]) -> None: def deep_list(x: list[str]) -> None:
# TODO: revealed: list[str] reveal_type(takes_in_list(x)) # revealed: list[str]
reveal_type(takes_in_list(x)) # revealed: list[Unknown]
# TODO: revealed: str # TODO: revealed: str
reveal_type(takes_in_protocol(x)) # revealed: Unknown reveal_type(takes_in_protocol(x)) # revealed: Unknown
def deeper_list(x: list[set[str]]) -> None: def deeper_list(x: list[set[str]]) -> None:
# TODO: revealed: list[set[str]] reveal_type(takes_in_list(x)) # revealed: list[set[str]]
reveal_type(takes_in_list(x)) # revealed: list[Unknown]
# TODO: revealed: set[str] # TODO: revealed: set[str]
reveal_type(takes_in_protocol(x)) # revealed: Unknown reveal_type(takes_in_protocol(x)) # revealed: Unknown
@ -119,13 +117,11 @@ This also works when passing in arguments that are subclasses of the parameter t
class Sub(list[int]): ... class Sub(list[int]): ...
class GenericSub(list[T]): ... class GenericSub(list[T]): ...
# TODO: revealed: list[int] reveal_type(takes_in_list(Sub())) # revealed: list[int]
reveal_type(takes_in_list(Sub())) # revealed: list[Unknown]
# TODO: revealed: int # TODO: revealed: int
reveal_type(takes_in_protocol(Sub())) # revealed: Unknown reveal_type(takes_in_protocol(Sub())) # revealed: Unknown
# TODO: revealed: list[str] reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[str]
reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[Unknown]
# TODO: revealed: str # TODO: revealed: str
reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown

View file

@ -83,14 +83,12 @@ def takes_in_protocol[T](x: CanIndex[T]) -> T:
return x[0] return x[0]
def deep_list(x: list[str]) -> None: def deep_list(x: list[str]) -> None:
# TODO: revealed: list[str] reveal_type(takes_in_list(x)) # revealed: list[str]
reveal_type(takes_in_list(x)) # revealed: list[Unknown]
# TODO: revealed: str # TODO: revealed: str
reveal_type(takes_in_protocol(x)) # revealed: Unknown reveal_type(takes_in_protocol(x)) # revealed: Unknown
def deeper_list(x: list[set[str]]) -> None: def deeper_list(x: list[set[str]]) -> None:
# TODO: revealed: list[set[str]] reveal_type(takes_in_list(x)) # revealed: list[set[str]]
reveal_type(takes_in_list(x)) # revealed: list[Unknown]
# TODO: revealed: set[str] # TODO: revealed: set[str]
reveal_type(takes_in_protocol(x)) # revealed: Unknown reveal_type(takes_in_protocol(x)) # revealed: Unknown
@ -114,13 +112,11 @@ This also works when passing in arguments that are subclasses of the parameter t
class Sub(list[int]): ... class Sub(list[int]): ...
class GenericSub[T](list[T]): ... class GenericSub[T](list[T]): ...
# TODO: revealed: list[int] reveal_type(takes_in_list(Sub())) # revealed: list[int]
reveal_type(takes_in_list(Sub())) # revealed: list[Unknown]
# TODO: revealed: int # TODO: revealed: int
reveal_type(takes_in_protocol(Sub())) # revealed: Unknown reveal_type(takes_in_protocol(Sub())) # revealed: Unknown
# TODO: revealed: list[str] reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[str]
reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[Unknown]
# TODO: revealed: str # TODO: revealed: str
reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown

View file

@ -562,25 +562,22 @@ impl<'db> Type<'db> {
fn is_none(&self, db: &'db dyn Db) -> bool { fn is_none(&self, db: &'db dyn Db) -> bool {
self.into_nominal_instance() self.into_nominal_instance()
.is_some_and(|instance| instance.class().is_known(db, KnownClass::NoneType)) .is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType))
} }
fn is_bool(&self, db: &'db dyn Db) -> bool { fn is_bool(&self, db: &'db dyn Db) -> bool {
self.into_nominal_instance() self.into_nominal_instance()
.is_some_and(|instance| instance.class().is_known(db, KnownClass::Bool)) .is_some_and(|instance| instance.class.is_known(db, KnownClass::Bool))
} }
pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool {
self.into_nominal_instance().is_some_and(|instance| { self.into_nominal_instance()
instance .is_some_and(|instance| instance.class.is_known(db, KnownClass::NotImplementedType))
.class()
.is_known(db, KnownClass::NotImplementedType)
})
} }
pub fn is_object(&self, db: &'db dyn Db) -> bool { pub fn is_object(&self, db: &'db dyn Db) -> bool {
self.into_nominal_instance() self.into_nominal_instance()
.is_some_and(|instance| instance.class().is_object(db)) .is_some_and(|instance| instance.class.is_object(db))
} }
pub const fn is_todo(&self) -> bool { pub const fn is_todo(&self) -> bool {
@ -1063,7 +1060,7 @@ impl<'db> Type<'db> {
(_, Type::Never) => false, (_, Type::Never) => false,
// Everything is a subtype of `object`. // Everything is a subtype of `object`.
(_, Type::NominalInstance(instance)) if instance.class().is_object(db) => true, (_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true,
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied: // In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`. // 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
@ -1373,7 +1370,7 @@ impl<'db> Type<'db> {
// All types are assignable to `object`. // All types are assignable to `object`.
// TODO this special case might be removable once the below cases are comprehensive // TODO this special case might be removable once the below cases are comprehensive
(_, Type::NominalInstance(instance)) if instance.class().is_object(db) => true, (_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true,
// In general, a TypeVar `T` is not assignable to a type `S` unless one of the two conditions is satisfied: // In general, a TypeVar `T` is not assignable to a type `S` unless one of the two conditions is satisfied:
// 1. `T` is a bound TypeVar and `T`'s upper bound is assignable to `S`. // 1. `T` is a bound TypeVar and `T`'s upper bound is assignable to `S`.
@ -1547,7 +1544,7 @@ impl<'db> Type<'db> {
} }
(Type::NominalInstance(instance), Type::Callable(_)) (Type::NominalInstance(instance), Type::Callable(_))
if instance.class().is_subclass_of_any_or_unknown(db) => if instance.class.is_subclass_of_any_or_unknown(db) =>
{ {
true true
} }
@ -1616,7 +1613,7 @@ impl<'db> Type<'db> {
} }
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
n.class().is_object(db) && protocol.normalized(db) == nominal n.class.is_object(db) && protocol.normalized(db) == nominal
} }
_ => self == other && self.is_fully_static(db) && other.is_fully_static(db), _ => self == other && self.is_fully_static(db) && other.is_fully_static(db),
} }
@ -1671,7 +1668,7 @@ impl<'db> Type<'db> {
} }
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
n.class().is_object(db) && protocol.normalized(db) == nominal n.class.is_object(db) && protocol.normalized(db) == nominal
} }
_ => false, _ => false,
} }
@ -1883,7 +1880,7 @@ impl<'db> Type<'db> {
// member on `protocol`. // member on `protocol`.
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
n.class().is_final(db) && !nominal.satisfies_protocol(db, protocol) n.class.is_final(db) && !nominal.satisfies_protocol(db, protocol)
} }
( (
@ -1948,7 +1945,7 @@ impl<'db> Type<'db> {
(Type::KnownInstance(known_instance), Type::NominalInstance(instance)) (Type::KnownInstance(known_instance), Type::NominalInstance(instance))
| (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => { | (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => {
!known_instance.is_instance_of(db, instance.class()) !known_instance.is_instance_of(db, instance.class)
} }
(known_instance_ty @ Type::KnownInstance(_), Type::Tuple(tuple)) (known_instance_ty @ Type::KnownInstance(_), Type::Tuple(tuple))
@ -1960,7 +1957,7 @@ impl<'db> Type<'db> {
| (Type::NominalInstance(instance), Type::BooleanLiteral(..)) => { | (Type::NominalInstance(instance), Type::BooleanLiteral(..)) => {
// A `Type::BooleanLiteral()` must be an instance of exactly `bool` // A `Type::BooleanLiteral()` must be an instance of exactly `bool`
// (it cannot be an instance of a `bool` subclass) // (it cannot be an instance of a `bool` subclass)
!KnownClass::Bool.is_subclass_of(db, instance.class()) !KnownClass::Bool.is_subclass_of(db, instance.class)
} }
(Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true, (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true,
@ -1969,7 +1966,7 @@ impl<'db> Type<'db> {
| (Type::NominalInstance(instance), Type::IntLiteral(..)) => { | (Type::NominalInstance(instance), Type::IntLiteral(..)) => {
// A `Type::IntLiteral()` must be an instance of exactly `int` // A `Type::IntLiteral()` must be an instance of exactly `int`
// (it cannot be an instance of an `int` subclass) // (it cannot be an instance of an `int` subclass)
!KnownClass::Int.is_subclass_of(db, instance.class()) !KnownClass::Int.is_subclass_of(db, instance.class)
} }
(Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true, (Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true,
@ -1981,7 +1978,7 @@ impl<'db> Type<'db> {
| (Type::NominalInstance(instance), Type::StringLiteral(..) | Type::LiteralString) => { | (Type::NominalInstance(instance), Type::StringLiteral(..) | Type::LiteralString) => {
// A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str` // A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str`
// (it cannot be an instance of a `str` subclass) // (it cannot be an instance of a `str` subclass)
!KnownClass::Str.is_subclass_of(db, instance.class()) !KnownClass::Str.is_subclass_of(db, instance.class)
} }
(Type::LiteralString, Type::LiteralString) => false, (Type::LiteralString, Type::LiteralString) => false,
@ -1991,7 +1988,7 @@ impl<'db> Type<'db> {
| (Type::NominalInstance(instance), Type::BytesLiteral(..)) => { | (Type::NominalInstance(instance), Type::BytesLiteral(..)) => {
// A `Type::BytesLiteral()` must be an instance of exactly `bytes` // A `Type::BytesLiteral()` must be an instance of exactly `bytes`
// (it cannot be an instance of a `bytes` subclass) // (it cannot be an instance of a `bytes` subclass)
!KnownClass::Bytes.is_subclass_of(db, instance.class()) !KnownClass::Bytes.is_subclass_of(db, instance.class)
} }
// A class-literal type `X` is always disjoint from an instance type `Y`, // A class-literal type `X` is always disjoint from an instance type `Y`,
@ -2012,7 +2009,7 @@ impl<'db> Type<'db> {
| (Type::NominalInstance(instance), Type::FunctionLiteral(..)) => { | (Type::NominalInstance(instance), Type::FunctionLiteral(..)) => {
// A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType`
// (it cannot be an instance of a `types.FunctionType` subclass) // (it cannot be an instance of a `types.FunctionType` subclass)
!KnownClass::FunctionType.is_subclass_of(db, instance.class()) !KnownClass::FunctionType.is_subclass_of(db, instance.class)
} }
(Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType
@ -2440,7 +2437,7 @@ impl<'db> Type<'db> {
// i.e. Type::NominalInstance(type). So looking up a name in the MRO of // i.e. Type::NominalInstance(type). So looking up a name in the MRO of
// `Type::NominalInstance(type)` is equivalent to looking up the name in the // `Type::NominalInstance(type)` is equivalent to looking up the name in the
// MRO of the class `object`. // MRO of the class `object`.
Type::NominalInstance(instance) if instance.class().is_known(db, KnownClass::Type) => { Type::NominalInstance(instance) if instance.class.is_known(db, KnownClass::Type) => {
KnownClass::Object KnownClass::Object
.to_class_literal(db) .to_class_literal(db)
.find_name_in_mro_with_policy(db, name, policy) .find_name_in_mro_with_policy(db, name, policy)
@ -2530,7 +2527,7 @@ impl<'db> Type<'db> {
Type::Dynamic(_) | Type::Never => Symbol::bound(self).into(), Type::Dynamic(_) | Type::Never => Symbol::bound(self).into(),
Type::NominalInstance(instance) => instance.class().instance_member(db, name), Type::NominalInstance(instance) => instance.class.instance_member(db, name),
Type::ProtocolInstance(protocol) => protocol.instance_member(db, name), Type::ProtocolInstance(protocol) => protocol.instance_member(db, name),
@ -2978,7 +2975,7 @@ impl<'db> Type<'db> {
Type::NominalInstance(instance) Type::NominalInstance(instance)
if matches!(name.as_str(), "major" | "minor") if matches!(name.as_str(), "major" | "minor")
&& instance.class().is_known(db, KnownClass::VersionInfo) => && instance.class.is_known(db, KnownClass::VersionInfo) =>
{ {
let python_version = Program::get(db).python_version(db); let python_version = Program::get(db).python_version(db);
let segment = if name == "major" { let segment = if name == "major" {
@ -3050,7 +3047,7 @@ impl<'db> Type<'db> {
// resolve the attribute. // resolve the attribute.
if matches!( if matches!(
self.into_nominal_instance() self.into_nominal_instance()
.and_then(|instance| instance.class().known(db)), .and_then(|instance| instance.class.known(db)),
Some(KnownClass::ModuleType | KnownClass::GenericAlias) Some(KnownClass::ModuleType | KnownClass::GenericAlias)
) { ) {
return Symbol::Unbound.into(); return Symbol::Unbound.into();
@ -3309,7 +3306,7 @@ impl<'db> Type<'db> {
} }
}, },
Type::NominalInstance(instance) => match instance.class().known(db) { Type::NominalInstance(instance) => match instance.class.known(db) {
Some(known_class) => known_class.bool(), Some(known_class) => known_class.bool(),
None => try_dunder_bool()?, None => try_dunder_bool()?,
}, },
@ -4863,7 +4860,7 @@ impl<'db> Type<'db> {
Type::Dynamic(_) => Ok(*self), Type::Dynamic(_) => Ok(*self),
Type::NominalInstance(instance) => match instance.class().known(db) { Type::NominalInstance(instance) => match instance.class.known(db) {
Some(KnownClass::TypeVar) => Ok(todo_type!( Some(KnownClass::TypeVar) => Ok(todo_type!(
"Support for `typing.TypeVar` instances in type expressions" "Support for `typing.TypeVar` instances in type expressions"
)), )),
@ -5291,7 +5288,7 @@ impl<'db> Type<'db> {
} }
Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))), Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))),
Self::NominalInstance(instance) => { Self::NominalInstance(instance) => {
Some(TypeDefinition::Class(instance.class().definition(db))) Some(TypeDefinition::Class(instance.class.definition(db)))
} }
Self::KnownInstance(instance) => match instance { Self::KnownInstance(instance) => match instance {
KnownInstanceType::TypeVar(var) => { KnownInstanceType::TypeVar(var) => {
@ -8046,7 +8043,7 @@ impl<'db> SuperOwnerKind<'db> {
Either::Left(ClassBase::Dynamic(dynamic).mro(db, None)) Either::Left(ClassBase::Dynamic(dynamic).mro(db, None))
} }
SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)), SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)),
SuperOwnerKind::Instance(instance) => Either::Right(instance.class().iter_mro(db)), SuperOwnerKind::Instance(instance) => Either::Right(instance.class.iter_mro(db)),
} }
} }
@ -8062,7 +8059,7 @@ impl<'db> SuperOwnerKind<'db> {
match self { match self {
SuperOwnerKind::Dynamic(_) => None, SuperOwnerKind::Dynamic(_) => None,
SuperOwnerKind::Class(class) => Some(class), SuperOwnerKind::Class(class) => Some(class),
SuperOwnerKind::Instance(instance) => Some(instance.class()), SuperOwnerKind::Instance(instance) => Some(instance.class),
} }
} }
@ -8240,7 +8237,7 @@ impl<'db> BoundSuperType<'db> {
.expect("Calling `find_name_in_mro` on dynamic type should return `Some`") .expect("Calling `find_name_in_mro` on dynamic type should return `Some`")
} }
SuperOwnerKind::Class(class) => class, SuperOwnerKind::Class(class) => class,
SuperOwnerKind::Instance(instance) => instance.class(), SuperOwnerKind::Instance(instance) => instance.class,
}; };
let (class_literal, _) = class.class_literal(db); let (class_literal, _) = class.class_literal(db);

View file

@ -614,7 +614,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
_ => { _ => {
let known_instance = new_positive let known_instance = new_positive
.into_nominal_instance() .into_nominal_instance()
.and_then(|instance| instance.class().known(db)); .and_then(|instance| instance.class.known(db));
if known_instance == Some(KnownClass::Object) { if known_instance == Some(KnownClass::Object) {
// `object & T` -> `T`; it is always redundant to add `object` to an intersection // `object & T` -> `T`; it is always redundant to add `object` to an intersection
@ -634,7 +634,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
new_positive = Type::BooleanLiteral(false); new_positive = Type::BooleanLiteral(false);
} }
Type::NominalInstance(instance) Type::NominalInstance(instance)
if instance.class().is_known(db, KnownClass::Bool) => if instance.class.is_known(db, KnownClass::Bool) =>
{ {
match new_positive { match new_positive {
// `bool & AlwaysTruthy` -> `Literal[True]` // `bool & AlwaysTruthy` -> `Literal[True]`
@ -728,7 +728,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
self.positive self.positive
.iter() .iter()
.filter_map(|ty| ty.into_nominal_instance()) .filter_map(|ty| ty.into_nominal_instance())
.filter_map(|instance| instance.class().known(db)) .filter_map(|instance| instance.class.known(db))
.any(KnownClass::is_bool) .any(KnownClass::is_bool)
}; };
@ -744,7 +744,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
Type::Never => { Type::Never => {
// Adding ~Never to an intersection is a no-op. // Adding ~Never to an intersection is a no-op.
} }
Type::NominalInstance(instance) if instance.class().is_object(db) => { Type::NominalInstance(instance) if instance.class.is_object(db) => {
// Adding ~object to an intersection results in Never. // Adding ~object to an intersection results in Never.
*self = Self::default(); *self = Self::default();
self.positive.insert(Type::Never); self.positive.insert(Type::Never);

View file

@ -2733,7 +2733,7 @@ impl<'db> Type<'db> {
/// The type must be a specialization of the `slice` builtin type, where the specialized /// The type must be a specialization of the `slice` builtin type, where the specialized
/// typevars are statically known integers or `None`. /// typevars are statically known integers or `None`.
pub(crate) fn slice_literal(self, db: &'db dyn Db) -> Option<SliceLiteral> { pub(crate) fn slice_literal(self, db: &'db dyn Db) -> Option<SliceLiteral> {
let ClassType::Generic(alias) = self.into_nominal_instance()?.class() else { let ClassType::Generic(alias) = self.into_nominal_instance()?.class else {
return None; return None;
}; };
if !alias.origin(db).is_known(db, KnownClass::Slice) { if !alias.origin(db).is_known(db, KnownClass::Slice) {
@ -2747,7 +2747,7 @@ impl<'db> Type<'db> {
Type::IntLiteral(n) => i32::try_from(*n).map(Some).ok(), Type::IntLiteral(n) => i32::try_from(*n).map(Some).ok(),
Type::BooleanLiteral(b) => Some(Some(i32::from(*b))), Type::BooleanLiteral(b) => Some(Some(i32::from(*b))),
Type::NominalInstance(instance) Type::NominalInstance(instance)
if instance.class().is_known(db, KnownClass::NoneType) => if instance.class.is_known(db, KnownClass::NoneType) =>
{ {
Some(None) Some(None)
} }

View file

@ -103,7 +103,7 @@ impl<'db> ClassBase<'db> {
} }
Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))),
Type::NominalInstance(instance) Type::NominalInstance(instance)
if instance.class().is_known(db, KnownClass::GenericAlias) => if instance.class.is_known(db, KnownClass::GenericAlias) =>
{ {
Self::try_from_type(db, todo_type!("GenericAlias instance")) Self::try_from_type(db, todo_type!("GenericAlias instance"))
} }

View file

@ -69,7 +69,7 @@ impl Display for DisplayRepresentation<'_> {
Type::Dynamic(dynamic) => dynamic.fmt(f), Type::Dynamic(dynamic) => dynamic.fmt(f),
Type::Never => f.write_str("Never"), Type::Never => f.write_str("Never"),
Type::NominalInstance(instance) => { Type::NominalInstance(instance) => {
match (instance.class(), instance.class().known(self.db)) { match (instance.class, instance.class.known(self.db)) {
(_, Some(KnownClass::NoneType)) => f.write_str("None"), (_, Some(KnownClass::NoneType)) => f.write_str("None"),
(_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"),
(ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)), (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)),

View file

@ -2,6 +2,9 @@ use ruff_python_ast as ast;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use crate::semantic_index::SemanticIndex; use crate::semantic_index::SemanticIndex;
use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::instance::NominalInstanceType;
use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::{ use crate::types::{
declaration_type, todo_type, KnownInstanceType, Type, TypeVarBoundOrConstraints, declaration_type, todo_type, KnownInstanceType, Type, TypeVarBoundOrConstraints,
@ -671,6 +674,35 @@ impl<'db> SpecializationBuilder<'db> {
} }
} }
(
Type::NominalInstance(NominalInstanceType {
class: ClassType::Generic(formal_alias),
..
}),
Type::NominalInstance(NominalInstanceType {
class: actual_class,
..
}),
) => {
let formal_origin = formal_alias.origin(self.db);
for base in actual_class.iter_mro(self.db) {
let ClassBase::Class(ClassType::Generic(base_alias)) = base else {
continue;
};
if formal_origin != base_alias.origin(self.db) {
continue;
}
let formal_specialization = formal_alias.specialization(self.db).types(self.db);
let base_specialization = base_alias.specialization(self.db).types(self.db);
for (formal_ty, base_ty) in
formal_specialization.iter().zip(base_specialization)
{
self.infer(*formal_ty, *base_ty)?;
}
return Ok(());
}
}
(Type::Union(formal), _) => { (Type::Union(formal), _) => {
// TODO: We haven't implemented a full unification solver yet. If typevars appear // TODO: We haven't implemented a full unification solver yet. If typevars appear
// in multiple union elements, we ideally want to express that _only one_ of them // in multiple union elements, we ideally want to express that _only one_ of them

View file

@ -1384,7 +1384,7 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} Type::BooleanLiteral(_) | Type::IntLiteral(_) => {}
Type::NominalInstance(instance) Type::NominalInstance(instance)
if matches!( if matches!(
instance.class().known(self.db()), instance.class.known(self.db()),
Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool)
) => {} ) => {}
_ => return false, _ => return false,
@ -2477,7 +2477,7 @@ impl<'db> TypeInferenceBuilder<'db> {
definition: Definition<'db>, definition: Definition<'db>,
) { ) {
fn extract_tuple_specialization<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Type<'db>> { fn extract_tuple_specialization<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Type<'db>> {
let class = ty.into_nominal_instance()?.class(); let class = ty.into_nominal_instance()?.class;
if !class.is_known(db, KnownClass::Tuple) { if !class.is_known(db, KnownClass::Tuple) {
return None; return None;
} }
@ -2933,7 +2933,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
// Super instances do not allow attribute assignment // Super instances do not allow attribute assignment
Type::NominalInstance(instance) if instance.class().is_known(db, KnownClass::Super) => { Type::NominalInstance(instance) if instance.class.is_known(db, KnownClass::Super) => {
if emit_diagnostics { if emit_diagnostics {
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) {
builder.into_diagnostic(format_args!( builder.into_diagnostic(format_args!(
@ -3462,10 +3462,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// Handle various singletons. // Handle various singletons.
if let Type::NominalInstance(instance) = declared_ty.inner_type() { if let Type::NominalInstance(instance) = declared_ty.inner_type() {
if instance if instance.class.is_known(self.db(), KnownClass::SpecialForm) {
.class()
.is_known(self.db(), KnownClass::SpecialForm)
{
if let Some(name_expr) = target.as_name_expr() { if let Some(name_expr) = target.as_name_expr() {
if let Some(known_instance) = KnownInstanceType::try_from_file_and_name( if let Some(known_instance) = KnownInstanceType::try_from_file_and_name(
self.db(), self.db(),
@ -6593,9 +6590,7 @@ impl<'db> TypeInferenceBuilder<'db> {
range, range,
), ),
(Type::Tuple(_), Type::NominalInstance(instance)) (Type::Tuple(_), Type::NominalInstance(instance))
if instance if instance.class.is_known(self.db(), KnownClass::VersionInfo) =>
.class()
.is_known(self.db(), KnownClass::VersionInfo) =>
{ {
self.infer_binary_type_comparison( self.infer_binary_type_comparison(
left, left,
@ -6605,9 +6600,7 @@ impl<'db> TypeInferenceBuilder<'db> {
) )
} }
(Type::NominalInstance(instance), Type::Tuple(_)) (Type::NominalInstance(instance), Type::Tuple(_))
if instance if instance.class.is_known(self.db(), KnownClass::VersionInfo) =>
.class()
.is_known(self.db(), KnownClass::VersionInfo) =>
{ {
self.infer_binary_type_comparison( self.infer_binary_type_comparison(
Type::version_info_tuple(self.db()), Type::version_info_tuple(self.db()),
@ -6999,9 +6992,7 @@ impl<'db> TypeInferenceBuilder<'db> {
) -> Type<'db> { ) -> Type<'db> {
match (value_ty, slice_ty, slice_ty.slice_literal(self.db())) { match (value_ty, slice_ty, slice_ty.slice_literal(self.db())) {
(Type::NominalInstance(instance), _, _) (Type::NominalInstance(instance), _, _)
if instance if instance.class.is_known(self.db(), KnownClass::VersionInfo) =>
.class()
.is_known(self.db(), KnownClass::VersionInfo) =>
{ {
self.infer_subscript_expression_types( self.infer_subscript_expression_types(
value_node, value_node,
@ -7362,7 +7353,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let type_to_slice_argument = |ty: Option<Type<'db>>| match ty { let type_to_slice_argument = |ty: Option<Type<'db>>| match ty {
Some(ty @ (Type::IntLiteral(_) | Type::BooleanLiteral(_))) => SliceArg::Arg(ty), Some(ty @ (Type::IntLiteral(_) | Type::BooleanLiteral(_))) => SliceArg::Arg(ty),
Some(ty @ Type::NominalInstance(instance)) Some(ty @ Type::NominalInstance(instance))
if instance.class().is_known(self.db(), KnownClass::NoneType) => if instance.class.is_known(self.db(), KnownClass::NoneType) =>
{ {
SliceArg::Arg(ty) SliceArg::Arg(ty)
} }

View file

@ -1,5 +1,7 @@
//! Instance types: both nominal and structural. //! Instance types: both nominal and structural.
use std::marker::PhantomData;
use super::protocol_class::ProtocolInterface; use super::protocol_class::ProtocolInterface;
use super::{ClassType, KnownClass, SubclassOfType, Type}; use super::{ClassType, KnownClass, SubclassOfType, Type};
use crate::symbol::{Symbol, SymbolAndQualifiers}; use crate::symbol::{Symbol, SymbolAndQualifiers};
@ -14,7 +16,10 @@ impl<'db> Type<'db> {
if class.class_literal(db).0.is_protocol(db) { if class.class_literal(db).0.is_protocol(db) {
Self::ProtocolInstance(ProtocolInstanceType(Protocol::FromClass(class))) Self::ProtocolInstance(ProtocolInstanceType(Protocol::FromClass(class)))
} else { } else {
Self::NominalInstance(NominalInstanceType { class }) Self::NominalInstance(NominalInstanceType {
class,
_phantom: PhantomData,
})
} }
} }
@ -56,16 +61,14 @@ impl<'db> Type<'db> {
/// A type representing the set of runtime objects which are instances of a certain nominal class. /// A type representing the set of runtime objects which are instances of a certain nominal class.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)]
pub struct NominalInstanceType<'db> { pub struct NominalInstanceType<'db> {
pub(super) class: ClassType<'db>,
// Keep this field private, so that the only way of constructing `NominalInstanceType` instances // Keep this field private, so that the only way of constructing `NominalInstanceType` instances
// is through the `Type::instance` constructor function. // is through the `Type::instance` constructor function.
class: ClassType<'db>, _phantom: PhantomData<()>,
} }
impl<'db> NominalInstanceType<'db> { impl<'db> NominalInstanceType<'db> {
pub(super) fn class(self) -> ClassType<'db> {
self.class
}
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
// N.B. The subclass relation is fully static // N.B. The subclass relation is fully static
self.class.is_subclass_of(db, other.class) self.class.is_subclass_of(db, other.class)
@ -130,6 +133,7 @@ impl<'db> NominalInstanceType<'db> {
) -> Self { ) -> Self {
Self { Self {
class: self.class.apply_type_mapping(db, type_mapping), class: self.class.apply_type_mapping(db, type_mapping),
_phantom: PhantomData,
} }
} }

View file

@ -474,7 +474,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
} }
// Treat `bool` as `Literal[True, False]`. // Treat `bool` as `Literal[True, False]`.
Type::NominalInstance(instance) Type::NominalInstance(instance)
if instance.class().is_known(db, KnownClass::Bool) => if instance.class.is_known(db, KnownClass::Bool) =>
{ {
UnionType::from_elements( UnionType::from_elements(
db, db,
@ -505,7 +505,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
fn evaluate_expr_ne(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option<Type<'db>> { fn evaluate_expr_ne(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option<Type<'db>> {
match (lhs_ty, rhs_ty) { match (lhs_ty, rhs_ty) {
(Type::NominalInstance(instance), Type::IntLiteral(i)) (Type::NominalInstance(instance), Type::IntLiteral(i))
if instance.class().is_known(self.db, KnownClass::Bool) => if instance.class.is_known(self.db, KnownClass::Bool) =>
{ {
if i == 0 { if i == 0 {
Some(Type::BooleanLiteral(false).negate(self.db)) Some(Type::BooleanLiteral(false).negate(self.db))

View file

@ -126,9 +126,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::SubclassOf(_), _) => Ordering::Less, (Type::SubclassOf(_), _) => Ordering::Less,
(_, Type::SubclassOf(_)) => Ordering::Greater, (_, Type::SubclassOf(_)) => Ordering::Greater,
(Type::NominalInstance(left), Type::NominalInstance(right)) => { (Type::NominalInstance(left), Type::NominalInstance(right)) => left.class.cmp(&right.class),
left.class().cmp(&right.class())
}
(Type::NominalInstance(_), _) => Ordering::Less, (Type::NominalInstance(_), _) => Ordering::Less,
(_, Type::NominalInstance(_)) => Ordering::Greater, (_, Type::NominalInstance(_)) => Ordering::Greater,
@ -171,7 +169,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(SuperOwnerKind::Class(_), _) => Ordering::Less, (SuperOwnerKind::Class(_), _) => Ordering::Less,
(_, SuperOwnerKind::Class(_)) => Ordering::Greater, (_, SuperOwnerKind::Class(_)) => Ordering::Greater,
(SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => { (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => {
left.class().cmp(&right.class()) left.class.cmp(&right.class)
} }
(SuperOwnerKind::Instance(_), _) => Ordering::Less, (SuperOwnerKind::Instance(_), _) => Ordering::Less,
(_, SuperOwnerKind::Instance(_)) => Ordering::Greater, (_, SuperOwnerKind::Instance(_)) => Ordering::Greater,