-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[red-knot] Implement disjointness for Instance types where the underlying class is @final
#15539
Changes from 3 commits
1c77b78
099e3f7
7765965
1d78b74
dac2982
d6c4521
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Tests for disjointness | ||
|
||
If two types can be disjoint, it means that it is known that no possible runtime object could ever inhabit both types simultaneously. | ||
|
||
TODO: Most of our disjointness tests are still Rust tests; they should be moved to this file. | ||
|
||
## Instance types versus `type[T]` types | ||
|
||
An instance type is disjoint from a `type[T]` type if the instance type is `@final` | ||
and the associated class of the instance type is not a subclass of `T`'s metaclass. | ||
|
||
```py | ||
from typing import final | ||
from knot_extensions import is_disjoint_from, static_assert | ||
|
||
@final | ||
class Foo: ... | ||
|
||
static_assert(is_disjoint_from(Foo, type[int])) | ||
static_assert(is_disjoint_from(type[object], Foo)) | ||
static_assert(is_disjoint_from(type[dict], Foo)) | ||
|
||
# Instance types can be disjoint from `type[]` types | ||
# even if the instance type is a subtype of `type` | ||
|
||
@final | ||
class Meta1(type): ... | ||
class UsesMeta1(metaclass=Meta1): ... | ||
|
||
static_assert(not is_disjoint_from(Meta1, type[UsesMeta1])) | ||
|
||
class Meta2(type): ... | ||
class UsesMeta2(metaclass=Meta2): ... | ||
static_assert(not is_disjoint_from(Meta2, type[UsesMeta2])) | ||
static_assert(is_disjoint_from(Meta1, type[UsesMeta2])) | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1278,14 +1278,17 @@ impl<'db> Type<'db> { | |
|
||
(Type::SubclassOf(_), Type::SubclassOf(_)) => false, | ||
|
||
(Type::SubclassOf(_), Type::Instance(instance)) | ||
| (Type::Instance(instance), Type::SubclassOf(_)) => { | ||
// TODO this should be `true` if the instance is of a final type which is not a | ||
// subclass of type. (With non-final types, we never know whether a subclass might | ||
// multiply-inherit `type` or a subclass of it, and thus not be disjoint with | ||
// `type[...]`.) Until we support finality, hardcode None, which is known to be | ||
// final. | ||
instance.class.is_known(db, KnownClass::NoneType) | ||
(Type::SubclassOf(subclass_of_ty), instance @ Type::Instance(_)) | ||
| (instance @ Type::Instance(_), Type::SubclassOf(subclass_of_ty)) => { | ||
// `type[T]` is disjoint from `S`, where `S` is an instance type, | ||
// if `U` is disjoint from `S`, | ||
// where `U` represents all instances of `T`'s metaclass | ||
let metaclass_instance = subclass_of_ty | ||
.subclass_of() | ||
.into_class() | ||
.map(|class| class.metaclass(db).to_instance(db)) | ||
.unwrap_or_else(|| KnownClass::Type.to_instance(db)); | ||
instance.is_disjoint_from(db, metaclass_instance) | ||
} | ||
Comment on lines
+1281
to
1292
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @carljm I realised almost as soon as our pairing session here that we were missing some subtle edge cases where a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did a property test reveal this, or you just realized it looking at the code/tests? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just me reading the diff on github prior to filing the PR! |
||
|
||
( | ||
|
@@ -1339,24 +1342,6 @@ impl<'db> Type<'db> { | |
known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db)) | ||
} | ||
|
||
( | ||
Type::Instance(InstanceType { class: class_none }), | ||
Type::Instance(InstanceType { class: class_other }), | ||
) | ||
| ( | ||
Type::Instance(InstanceType { class: class_other }), | ||
Type::Instance(InstanceType { class: class_none }), | ||
) if class_none.is_known(db, KnownClass::NoneType) => { | ||
!class_none.is_subclass_of(db, class_other) | ||
} | ||
|
||
(Type::Instance(InstanceType { class: class_none }), _) | ||
| (_, Type::Instance(InstanceType { class: class_none })) | ||
if class_none.is_known(db, KnownClass::NoneType) => | ||
{ | ||
true | ||
} | ||
|
||
(Type::BooleanLiteral(..), Type::Instance(InstanceType { class })) | ||
| (Type::Instance(InstanceType { class }), Type::BooleanLiteral(..)) => { | ||
// A `Type::BooleanLiteral()` must be an instance of exactly `bool` | ||
|
@@ -1430,15 +1415,12 @@ impl<'db> Type<'db> { | |
other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db)) | ||
} | ||
|
||
(Type::Instance(..), Type::Instance(..)) => { | ||
// TODO: once we have support for `final`, there might be some cases where | ||
// we can determine that two types are disjoint. Once we do this, some cases | ||
// above (e.g. NoneType) can be removed. For non-final classes, we return | ||
// false (multiple inheritance). | ||
|
||
// TODO: is there anything specific to do for instances of KnownClass::Type? | ||
|
||
false | ||
( | ||
Type::Instance(InstanceType { class: left_class }), | ||
Type::Instance(InstanceType { class: right_class }), | ||
) => { | ||
(left_class.is_final(db) && !left_class.is_subclass_of(db, right_class)) | ||
|| (right_class.is_final(db) && !right_class.is_subclass_of(db, left_class)) | ||
} | ||
|
||
(Type::Tuple(tuple), Type::Tuple(other_tuple)) => { | ||
|
@@ -1451,7 +1433,8 @@ impl<'db> Type<'db> { | |
.any(|(e1, e2)| e1.is_disjoint_from(db, *e2)) | ||
} | ||
|
||
(Type::Tuple(..), Type::Instance(..)) | (Type::Instance(..), Type::Tuple(..)) => { | ||
(Type::Tuple(..), instance @ Type::Instance(_)) | ||
| (instance @ Type::Instance(_), Type::Tuple(..)) => { | ||
// We cannot be sure if the tuple is disjoint from the instance because: | ||
// - 'other' might be the homogeneous arbitrary-length tuple type | ||
// tuple[T, ...] (which we don't have support for yet); if all of | ||
|
@@ -1460,7 +1443,7 @@ impl<'db> Type<'db> { | |
// over the same or compatible *Ts, would overlap with tuple. | ||
// | ||
// TODO: add checks for the above cases once we support them | ||
false | ||
instance.is_disjoint_from(db, KnownClass::Tuple.to_instance(db)) | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I like that we can delegate to instance-vs-instance, rather than checking finality here also.
It seems right that
T
actually disappears from this check, apart from its metaclass. There's no difference betweentype[object]
andtype[OtherClass]
as regards disjointness from an instance type, as long as the metaclass ofOtherClass
is justtype
.