[red-knot] T | object == object (#16088)
Some checks are pending
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 build (msrv) (push) Blocked by required conditions
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 / 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 / benchmarks (push) Blocked by required conditions

## Summary

- Simplify unions with `object` to `object`.
- Add a new `Type::object(db)` constructor to abbreviate
`KnownClass::Object.to_instance(db)` in some places.
- Add a `Type::is_object` and `Class::is_object` function to make some
tests for a bit easier to read.

closes #16084

## Test Plan

New Markdown tests.
This commit is contained in:
David Peter 2025-02-10 23:07:06 +01:00 committed by GitHub
parent f30fac6326
commit 0019d39f6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 75 additions and 29 deletions

View file

@ -37,6 +37,31 @@ def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None:
reveal_type(u2) # revealed: int | str
```
## `object` subsumes everything
Unions with `object` can be simplified to `object`:
```py
from typing_extensions import Never, Any
def _(
u1: int | object,
u2: object | int,
u3: Any | object,
u4: object | Any,
u5: object | Never,
u6: Never | object,
u7: int | str | object | bytes | Any,
) -> None:
reveal_type(u1) # revealed: object
reveal_type(u2) # revealed: object
reveal_type(u3) # revealed: object
reveal_type(u4) # revealed: object
reveal_type(u5) # revealed: object
reveal_type(u6) # revealed: object
reveal_type(u7) # revealed: object
```
## Flattening of nested unions
```py
@ -120,8 +145,8 @@ Simplifications still apply when `Unknown` is present.
```py
from knot_extensions import Unknown
def _(u1: str | Unknown | int | object):
reveal_type(u1) # revealed: Unknown | object
def _(u1: int | Unknown | bool) -> None:
reveal_type(u1) # revealed: int | Unknown
```
## Union of intersections

View file

@ -631,6 +631,10 @@ impl<'db> Type<'db> {
Self::Dynamic(DynamicType::Unknown)
}
pub fn object(db: &'db dyn Db) -> Self {
KnownClass::Object.to_instance(db)
}
pub const fn is_unknown(&self) -> bool {
matches!(self, Type::Dynamic(DynamicType::Unknown))
}
@ -639,6 +643,11 @@ impl<'db> Type<'db> {
matches!(self, Type::Never)
}
pub fn is_object(&self, db: &'db dyn Db) -> bool {
self.into_instance()
.is_some_and(|instance| instance.class.is_object(db))
}
pub const fn is_todo(&self) -> bool {
matches!(self, Type::Dynamic(DynamicType::Todo(_)))
}
@ -895,7 +904,7 @@ impl<'db> Type<'db> {
// `object` is the only type that can be known to be a supertype of any intersection,
// even an intersection with no positive elements
(Type::Intersection(_), Type::Instance(InstanceType { class }))
if class.is_known(db, KnownClass::Object) =>
if class.is_object(db) =>
{
true
}
@ -949,7 +958,7 @@ impl<'db> Type<'db> {
(left, Type::AlwaysTruthy) => left.bool(db).is_always_true(),
// Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance).
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => {
target.is_equivalent_to(db, KnownClass::Object.to_instance(db))
target.is_equivalent_to(db, Type::object(db))
}
// All `StringLiteral` types are a subtype of `LiteralString`.
@ -1088,11 +1097,7 @@ impl<'db> Type<'db> {
// All types are assignable to `object`.
// TODO this special case might be removable once the below cases are comprehensive
(_, Type::Instance(InstanceType { class }))
if class.is_known(db, KnownClass::Object) =>
{
true
}
(_, Type::Instance(InstanceType { class })) if class.is_object(db) => true,
// A union is assignable to a type T iff every element of the union is assignable to T.
(Type::Union(union), ty) => union
@ -1801,7 +1806,7 @@ impl<'db> Type<'db> {
// TODO should be `Callable[[], Literal[True/False]]`
todo_type!("`__bool__` for `AlwaysTruthy`/`AlwaysFalsy` Type variants").into()
}
_ => KnownClass::Object.to_instance(db).member(db, name),
_ => Type::object(db).member(db, name),
},
}
}
@ -3853,6 +3858,11 @@ impl<'db> Class<'db> {
self.known(db) == Some(known_class)
}
/// Return `true` if this class represents the builtin class `object`
pub fn is_object(self, db: &'db dyn Db) -> bool {
self.is_known(db, KnownClass::Object)
}
/// Return an iterator over the inferred types of this class's *explicit* bases.
///
/// Note that any class (except for `object`) that has no explicit

View file

@ -43,6 +43,13 @@ impl<'db> UnionBuilder<'db> {
}
}
/// Collapse the union to a single type: `object`.
fn collapse_to_object(mut self) -> Self {
self.elements.clear();
self.elements.push(Type::object(self.db));
self
}
/// Adds a type to this union.
pub(crate) fn add(mut self, ty: Type<'db>) -> Self {
match ty {
@ -53,7 +60,12 @@ impl<'db> UnionBuilder<'db> {
self = self.add(*element);
}
}
// Adding `Never` to a union is a no-op.
Type::Never => {}
// Adding `object` to a union results in `object`.
ty if ty.is_object(self.db) => {
return self.collapse_to_object();
}
_ => {
let bool_pair = if let Type::BooleanLiteral(b) = ty {
Some(Type::BooleanLiteral(!b))
@ -76,7 +88,10 @@ impl<'db> UnionBuilder<'db> {
break;
}
if ty.is_same_gradual_form(*element) || ty.is_subtype_of(self.db, *element) {
if ty.is_same_gradual_form(*element)
|| ty.is_subtype_of(self.db, *element)
|| element.is_object(self.db)
{
return self;
} else if element.is_subtype_of(self.db, ty) {
to_remove.push(index);
@ -88,9 +103,7 @@ impl<'db> UnionBuilder<'db> {
// `element | ty` must be `object` (object has no other supertypes). This means we can simplify
// the whole union to just `object`, since all other potential elements would also be subtypes of
// `object`.
self.elements.clear();
self.elements.push(KnownClass::Object.to_instance(self.db));
return self;
return self.collapse_to_object();
}
}
match to_remove[..] {
@ -416,7 +429,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
Type::Never => {
// Adding ~Never to an intersection is a no-op.
}
Type::Instance(instance) if instance.class.is_known(db, KnownClass::Object) => {
Type::Instance(instance) if instance.class.is_object(db) => {
// Adding ~object to an intersection results in Never.
*self = Self::default();
self.positive.insert(Type::Never);
@ -481,7 +494,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
match (self.positive.len(), self.negative.len()) {
(0, 0) => KnownClass::Object.to_instance(db),
(0, 0) => Type::object(db),
(1, 0) => self.positive[0],
_ => {
self.positive.shrink_to_fit();
@ -534,7 +547,7 @@ mod tests {
let db = setup_db();
let intersection = IntersectionBuilder::new(&db).build();
assert_eq!(intersection, KnownClass::Object.to_instance(&db));
assert_eq!(intersection, Type::object(&db));
}
#[test_case(Type::BooleanLiteral(true))]
@ -548,7 +561,7 @@ mod tests {
// We add t_object in various orders (in first or second position) in
// the tests below to ensure that the boolean simplification eliminates
// everything from the intersection, not just `bool`.
let t_object = KnownClass::Object.to_instance(&db);
let t_object = Type::object(&db);
let t_bool = KnownClass::Bool.to_instance(&db);
let ty = IntersectionBuilder::new(&db)

View file

@ -4,7 +4,7 @@ use std::ops::Deref;
use rustc_hash::FxHashSet;
use crate::types::class_base::ClassBase;
use crate::types::{Class, KnownClass, Type};
use crate::types::{Class, Type};
use crate::Db;
/// The inferred method resolution order of a given class.
@ -52,9 +52,7 @@ impl<'db> Mro<'db> {
match class_bases {
// `builtins.object` is the special case:
// the only class in Python that has an MRO with length <2
[] if class.is_known(db, KnownClass::Object) => {
Ok(Self::from([ClassBase::Class(class)]))
}
[] if class.is_object(db) => Ok(Self::from([ClassBase::Class(class)])),
// All other classes in Python have an MRO with length >=2.
// Even if a class has no explicit base classes,

View file

@ -152,13 +152,13 @@ fn merge_constraints_or<'db>(
*entry.get_mut() = UnionBuilder::new(db).add(*entry.get()).add(*value).build();
}
Entry::Vacant(entry) => {
entry.insert(KnownClass::Object.to_instance(db));
entry.insert(Type::object(db));
}
}
}
for (key, value) in into.iter_mut() {
if !from.contains_key(key) {
*value = KnownClass::Object.to_instance(db);
*value = Type::object(db);
}
}
}

View file

@ -329,7 +329,7 @@ fn union<'db>(db: &'db TestDb, tys: impl IntoIterator<Item = Type<'db>>) -> Type
mod stable {
use super::union;
use crate::types::{KnownClass, Type};
use crate::types::Type;
// Reflexivity: `T` is equivalent to itself.
type_property_test!(
@ -419,13 +419,13 @@ mod stable {
// All types should be assignable to `object`
type_property_test!(
all_types_assignable_to_object, db,
forall types t. t.is_assignable_to(db, KnownClass::Object.to_instance(db))
forall types t. t.is_assignable_to(db, Type::object(db))
);
// And for fully static types, they should also be subtypes of `object`
type_property_test!(
all_fully_static_types_subtype_of_object, db,
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, KnownClass::Object.to_instance(db))
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, Type::object(db))
);
// Never should be assignable to every type

View file

@ -411,7 +411,7 @@ mod tests {
},
Parameter {
name: Some(Name::new_static("args")),
annotated_ty: Some(KnownClass::Object.to_instance(&db)),
annotated_ty: Some(Type::object(&db)),
kind: ParameterKind::Variadic,
},
Parameter {

View file

@ -26,7 +26,7 @@ impl<'db> SubclassOfType<'db> {
ClassBase::Class(class) => {
if class.is_final(db) {
Type::ClassLiteral(ClassLiteralType { class })
} else if class.is_known(db, KnownClass::Object) {
} else if class.is_object(db) {
KnownClass::Type.to_instance(db)
} else {
Type::SubclassOf(Self { subclass_of })