Skip to content

Commit

Permalink
[red-knot] Eagerly normalize type[] types
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood committed Jan 5, 2025
1 parent eb01dfa commit 6662dc4
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def _(t: type[object]):
if issubclass(t, B):
reveal_type(t) # revealed: type[A] & type[B]
else:
reveal_type(t) # revealed: type[object] & ~type[A]
reveal_type(t) # revealed: type & ~type[A]
```

### Handling of `None`
Expand Down
22 changes: 22 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,25 @@ class Foo(type[int]): ...
# TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
```

## `@final` classes

`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is
used as the type argument. This applies to standard-library classes and user-defined classes:

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

```py
from types import EllipsisType
from typing import final

@final
class Foo: ...

def _(x: type[Foo], y: type[EllipsisType]):
reveal_type(x) # revealed: Literal[Foo]
reveal_type(y) # revealed: Literal[EllipsisType]
```
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ x: type = A() # error: [invalid-assignment]

```py
def f(x: type[object]):
reveal_type(x) # revealed: type[object]
# TODO: bound method types
reveal_type(x.__repr__) # revealed: Literal[__repr__]
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)

class A: ...

Expand Down
229 changes: 84 additions & 145 deletions crates/red_knot_python_semantic/src/types.rs

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions crates/red_knot_python_semantic/src/types/class_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ pub enum ClassBase<'db> {
}

impl<'db> ClassBase<'db> {
pub const fn is_dynamic(self) -> bool {
match self {
ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_) => true,
ClassBase::Class(_) => false,
}
}

pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
struct Display<'db> {
base: ClassBase<'db>,
Expand Down
18 changes: 8 additions & 10 deletions crates/red_knot_python_semantic/src/types/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use ruff_python_literal::escape::AsciiEscape;

use crate::types::class_base::ClassBase;
use crate::types::{
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
SubclassOfType, Type, UnionType,
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, Type,
UnionType,
};
use crate::Db;
use rustc_hash::FxHashMap;
Expand Down Expand Up @@ -84,16 +84,14 @@ impl Display for DisplayRepresentation<'_> {
}
// TODO functions and classes should display using a fully qualified name
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(class),
}) => {
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
// Only show the bare class name here; ClassBase::display would render this as
// type[<class 'Foo'>] instead of type[Foo].
write!(f, "type[{}]", class.name(self.db))
}
Type::SubclassOf(SubclassOfType { base }) => {
write!(f, "type[{}]", base.display(self.db))
}
ClassBase::Class(class) => write!(f, "type[{}]", class.name(self.db)),
base @ (ClassBase::Any | ClassBase::Todo(_) | ClassBase::Unknown) => {
write!(f, "type[{}]", base.display(self.db))
}
},
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
Type::Union(union) => union.display(self.db).fmt(f),
Expand Down
8 changes: 5 additions & 3 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ use crate::types::{
typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType, FunctionType,
InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass,
KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType,
Symbol, Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay,
SubclassOfType, Symbol, Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay,
TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
};
use crate::unpack::Unpack;
Expand Down Expand Up @@ -4870,9 +4870,11 @@ impl<'db> TypeInferenceBuilder<'db> {
ast::Expr::Name(_) | ast::Expr::Attribute(_) => {
let name_ty = self.infer_expression(slice);
match name_ty {
Type::ClassLiteral(ClassLiteralType { class }) => Type::subclass_of(class),
Type::ClassLiteral(ClassLiteralType { class }) => {
SubclassOfType::from(self.db(), class)
}
Type::KnownInstance(KnownInstanceType::Any) => {
Type::subclass_of_base(ClassBase::Any)
SubclassOfType::from(self.db(), ClassBase::Any)
}
_ => todo_type!("unsupported type[X] special form"),
}
Expand Down
2 changes: 1 addition & 1 deletion crates/red_knot_python_semantic/src/types/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ fn generate_classinfo_constraint<'db>(
Some(builder.build())
}
Type::ClassLiteral(ClassLiteralType { class }) => {
Some(constraint_fn.apply_constraint(*class))
Some(constraint_fn.apply_constraint(db, *class))
}
_ => None,
}
Expand Down
61 changes: 61 additions & 0 deletions crates/red_knot_python_semantic/src/types/subclass_of.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use super::{ClassBase, ClassLiteralType, Db, KnownClass, Symbol, Type};

/// A type that represents `type[C]`, i.e. the class literal `C` and class literals that are subclasses of `C`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub struct SubclassOfType<'db> {
subclass_of: ClassBase<'db>,
}

impl<'db> SubclassOfType<'db> {
pub fn from(db: &'db dyn Db, subclass_of: impl Into<ClassBase<'db>>) -> Type<'db> {
let subclass_of = subclass_of.into();
match subclass_of {
ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_) => {
Type::SubclassOf(Self { subclass_of })
}
ClassBase::Class(class) => {
if class.is_final(db) {
Type::ClassLiteral(ClassLiteralType { class })
} else if class.is_known(db, KnownClass::Object) {
KnownClass::Type.to_instance(db)
} else {
Type::SubclassOf(Self { subclass_of })
}
}
}
}

pub const fn subclass_of(self) -> ClassBase<'db> {
self.subclass_of
}

pub const fn is_dynamic(self) -> bool {
// Unpack `self` so that we're forced to update this method if any more fields are added in the future.
let Self { subclass_of } = self;
subclass_of.is_dynamic()
}

pub const fn is_fully_static(self) -> bool {
!self.is_dynamic()
}

pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
Type::from(self.subclass_of).member(db, name)
}

pub fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool {
match (self.subclass_of, other.subclass_of) {
// Non-fully-static types do not participate in subtyping
(ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_), _)
| (_, ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_)) => false,

// For example, `type[bool]` describes all possible runtime subclasses of the class `bool`,
// and `type[int]` describes all possible runtime subclasses of the class `int`.
// The first set is a subset of the second set, because `bool` is itself a subclass of `int`.
(ClassBase::Class(self_class), ClassBase::Class(other_class)) => {
// N.B. The subclass relation is fully static
self_class.is_subclass_of(db, other_class)
}
}
}
}

0 comments on commit 6662dc4

Please sign in to comment.