Skip to content

Commit af43bd4

Browse files
authored
[red-knot] Gradual forms do not participate in equivalence/subtyping (#14758)
## Summary This changeset contains various improvements concerning non-fully-static types and their relationships: - Make sure that non-fully-static types do not participate in equivalence or subtyping. - Clarify what `Type::is_equivalent_to` actually implements. - Introduce `Type::is_fully_static` - New tests making sure that multiple `Any`/`Unknown`s inside unions and intersections are collapsed. closes #14524 ## Test Plan - Added new unit tests for union and intersection builder - Added new unit tests for `Type::is_equivalent_to` - Added new unit tests for `Type::is_subtype_of` - Added new property test making sure that non-fully-static types do not participate in subtyping
1 parent 6149177 commit af43bd4

File tree

3 files changed

+190
-24
lines changed

3 files changed

+190
-24
lines changed

crates/red_knot_python_semantic/src/types.rs

+132-22
Original file line numberDiff line numberDiff line change
@@ -302,13 +302,7 @@ fn declarations_ty<'db>(
302302
let declared_ty = if let Some(second) = all_types.next() {
303303
let mut builder = UnionBuilder::new(db).add(first);
304304
for other in [second].into_iter().chain(all_types) {
305-
// Make sure not to emit spurious errors relating to `Type::Todo`,
306-
// since we only infer this type due to a limitation in our current model.
307-
//
308-
// `Unknown` is different here, since we might infer `Unknown`
309-
// for one of these due to a variable being defined in one possible
310-
// control-flow branch but not another one.
311-
if !first.is_equivalent_to(db, other) && !first.is_todo() && !other.is_todo() {
305+
if !first.is_equivalent_to(db, other) {
312306
conflicting.push(other);
313307
}
314308
builder = builder.add(other);
@@ -600,11 +594,16 @@ impl<'db> Type<'db> {
600594

601595
/// Return true if this type is a [subtype of] type `target`.
602596
///
597+
/// This method returns `false` if either `self` or `other` is not fully static.
598+
///
603599
/// [subtype of]: https://typing.readthedocs.io/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
604600
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
605601
if self.is_equivalent_to(db, target) {
606602
return true;
607603
}
604+
if !self.is_fully_static(db) || !target.is_fully_static(db) {
605+
return false;
606+
}
608607
match (self, target) {
609608
(Type::Unknown | Type::Any | Type::Todo(_), _) => false,
610609
(_, Type::Unknown | Type::Any | Type::Todo(_)) => false,
@@ -764,8 +763,16 @@ impl<'db> Type<'db> {
764763
}
765764
}
766765

767-
/// Return true if this type is equivalent to type `other`.
766+
/// Return true if this type is [equivalent to] type `other`.
767+
///
768+
/// This method returns `false` if either `self` or `other` is not fully static.
769+
///
770+
/// [equivalent to]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent
768771
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
772+
if !(self.is_fully_static(db) && other.is_fully_static(db)) {
773+
return false;
774+
}
775+
769776
// TODO equivalent but not identical structural types, differently-ordered unions and
770777
// intersections, other cases?
771778

@@ -776,7 +783,6 @@ impl<'db> Type<'db> {
776783
// of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once
777784
// we understand `sys.version_info` branches.
778785
self == other
779-
|| matches!((self, other), (Type::Todo(_), Type::Todo(_)))
780786
|| matches!((self, other),
781787
(
782788
Type::Instance(InstanceType { class: self_class }),
@@ -790,6 +796,17 @@ impl<'db> Type<'db> {
790796
)
791797
}
792798

799+
/// Returns true if both `self` and `other` are the same gradual form
800+
/// (limited to `Any`, `Unknown`, or `Todo`).
801+
pub(crate) fn is_same_gradual_form(self, other: Type<'db>) -> bool {
802+
matches!(
803+
(self, other),
804+
(Type::Unknown, Type::Unknown)
805+
| (Type::Any, Type::Any)
806+
| (Type::Todo(_), Type::Todo(_))
807+
)
808+
}
809+
793810
/// Return true if this type and `other` have no common elements.
794811
///
795812
/// Note: This function aims to have no false positives, but might return
@@ -996,6 +1013,63 @@ impl<'db> Type<'db> {
9961013
}
9971014
}
9981015

1016+
/// Returns true if the type does not contain any gradual forms (as a sub-part).
1017+
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
1018+
match self {
1019+
Type::Any | Type::Unknown | Type::Todo(_) => false,
1020+
Type::Never
1021+
| Type::FunctionLiteral(..)
1022+
| Type::ModuleLiteral(..)
1023+
| Type::IntLiteral(_)
1024+
| Type::BooleanLiteral(_)
1025+
| Type::StringLiteral(_)
1026+
| Type::LiteralString
1027+
| Type::BytesLiteral(_)
1028+
| Type::SliceLiteral(_)
1029+
| Type::KnownInstance(_) => true,
1030+
Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::Instance(_) => {
1031+
// TODO: Ideally, we would iterate over the MRO of the class, check if all
1032+
// bases are fully static, and only return `true` if that is the case.
1033+
//
1034+
// This does not work yet, because we currently infer `Unknown` for some
1035+
// generic base classes that we don't understand yet. For example, `str`
1036+
// is defined as `class str(Sequence[str])` in typeshed and we currently
1037+
// compute its MRO as `(str, Unknown, object)`. This would make us think
1038+
// that `str` is a gradual type, which causes all sorts of downstream
1039+
// issues because it does not participate in equivalence/subtyping etc.
1040+
//
1041+
// Another problem is that we run into problems if we eagerly query the
1042+
// MRO of class literals here. I have not fully investigated this, but
1043+
// iterating over the MRO alone, without even acting on it, causes us to
1044+
// infer `Unknown` for many classes.
1045+
1046+
true
1047+
}
1048+
Type::Union(union) => union
1049+
.elements(db)
1050+
.iter()
1051+
.all(|elem| elem.is_fully_static(db)),
1052+
Type::Intersection(intersection) => {
1053+
intersection
1054+
.positive(db)
1055+
.iter()
1056+
.all(|elem| elem.is_fully_static(db))
1057+
&& intersection
1058+
.negative(db)
1059+
.iter()
1060+
.all(|elem| elem.is_fully_static(db))
1061+
}
1062+
Type::Tuple(tuple) => tuple
1063+
.elements(db)
1064+
.iter()
1065+
.all(|elem| elem.is_fully_static(db)),
1066+
// TODO: Once we support them, make sure that we return `false` for other types
1067+
// containing gradual forms such as `tuple[Any, ...]` or `Callable[..., str]`.
1068+
// Conversely, make sure to return `true` for homogeneous tuples such as
1069+
// `tuple[int, ...]`, once we add support for them.
1070+
}
1071+
}
1072+
9991073
/// Return true if there is just a single inhabitant for this type.
10001074
///
10011075
/// Note: This function aims to have no false positives, but might return `false`
@@ -3261,7 +3335,9 @@ pub(crate) mod tests {
32613335
}
32623336

32633337
#[test_case(Ty::BuiltinInstance("object"), Ty::BuiltinInstance("int"))]
3338+
#[test_case(Ty::Unknown, Ty::Unknown)]
32643339
#[test_case(Ty::Unknown, Ty::IntLiteral(1))]
3340+
#[test_case(Ty::Any, Ty::Any)]
32653341
#[test_case(Ty::Any, Ty::IntLiteral(1))]
32663342
#[test_case(Ty::IntLiteral(1), Ty::Unknown)]
32673343
#[test_case(Ty::IntLiteral(1), Ty::Any)]
@@ -3369,6 +3445,18 @@ pub(crate) mod tests {
33693445
assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
33703446
}
33713447

3448+
#[test_case(Ty::Any, Ty::Any)]
3449+
#[test_case(Ty::Any, Ty::None)]
3450+
#[test_case(Ty::Unknown, Ty::Unknown)]
3451+
#[test_case(Ty::Todo, Ty::Todo)]
3452+
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(0)]))]
3453+
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2), Ty::IntLiteral(3)]))]
3454+
fn is_not_equivalent_to(from: Ty, to: Ty) {
3455+
let db = setup_db();
3456+
3457+
assert!(!from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
3458+
}
3459+
33723460
#[test_case(Ty::Never, Ty::Never)]
33733461
#[test_case(Ty::Never, Ty::None)]
33743462
#[test_case(Ty::Never, Ty::BuiltinInstance("int"))]
@@ -3597,6 +3685,41 @@ pub(crate) mod tests {
35973685
assert!(!from.into_type(&db).is_singleton(&db));
35983686
}
35993687

3688+
#[test_case(Ty::Never)]
3689+
#[test_case(Ty::None)]
3690+
#[test_case(Ty::IntLiteral(1))]
3691+
#[test_case(Ty::BooleanLiteral(true))]
3692+
#[test_case(Ty::StringLiteral("abc"))]
3693+
#[test_case(Ty::LiteralString)]
3694+
#[test_case(Ty::BytesLiteral("abc"))]
3695+
#[test_case(Ty::KnownClassInstance(KnownClass::Str))]
3696+
#[test_case(Ty::KnownClassInstance(KnownClass::Object))]
3697+
#[test_case(Ty::KnownClassInstance(KnownClass::Type))]
3698+
#[test_case(Ty::BuiltinClassLiteral("str"))]
3699+
#[test_case(Ty::TypingLiteral)]
3700+
#[test_case(Ty::Union(vec![Ty::KnownClassInstance(KnownClass::Str), Ty::None]))]
3701+
#[test_case(Ty::Intersection{pos: vec![Ty::KnownClassInstance(KnownClass::Str)], neg: vec![Ty::LiteralString]})]
3702+
#[test_case(Ty::Tuple(vec![]))]
3703+
#[test_case(Ty::Tuple(vec![Ty::KnownClassInstance(KnownClass::Int), Ty::KnownClassInstance(KnownClass::Object)]))]
3704+
fn is_fully_static(from: Ty) {
3705+
let db = setup_db();
3706+
3707+
assert!(from.into_type(&db).is_fully_static(&db));
3708+
}
3709+
3710+
#[test_case(Ty::Any)]
3711+
#[test_case(Ty::Unknown)]
3712+
#[test_case(Ty::Todo)]
3713+
#[test_case(Ty::Union(vec![Ty::Any, Ty::KnownClassInstance(KnownClass::Str)]))]
3714+
#[test_case(Ty::Union(vec![Ty::KnownClassInstance(KnownClass::Str), Ty::Unknown]))]
3715+
#[test_case(Ty::Intersection{pos: vec![Ty::Any], neg: vec![Ty::LiteralString]})]
3716+
#[test_case(Ty::Tuple(vec![Ty::KnownClassInstance(KnownClass::Int), Ty::Any]))]
3717+
fn is_not_fully_static(from: Ty) {
3718+
let db = setup_db();
3719+
3720+
assert!(!from.into_type(&db).is_fully_static(&db));
3721+
}
3722+
36003723
#[test_case(Ty::IntLiteral(1); "is_int_literal_truthy")]
36013724
#[test_case(Ty::IntLiteral(-1))]
36023725
#[test_case(Ty::StringLiteral("foo"))]
@@ -3771,19 +3894,6 @@ pub(crate) mod tests {
37713894
let todo3 = todo_type!();
37723895
let todo4 = todo_type!();
37733896

3774-
assert!(todo1.is_equivalent_to(&db, todo2));
3775-
assert!(todo3.is_equivalent_to(&db, todo4));
3776-
assert!(todo1.is_equivalent_to(&db, todo3));
3777-
3778-
assert!(todo1.is_subtype_of(&db, todo2));
3779-
assert!(todo2.is_subtype_of(&db, todo1));
3780-
3781-
assert!(todo3.is_subtype_of(&db, todo4));
3782-
assert!(todo4.is_subtype_of(&db, todo3));
3783-
3784-
assert!(todo1.is_subtype_of(&db, todo3));
3785-
assert!(todo3.is_subtype_of(&db, todo1));
3786-
37873897
let int = KnownClass::Int.to_instance(&db);
37883898

37893899
assert!(int.is_assignable_to(&db, todo1));

crates/red_knot_python_semantic/src/types/builder.rs

+52-2
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ impl<'db> UnionBuilder<'db> {
7373
// supertype of bool. Therefore, we are done.
7474
break;
7575
}
76-
if ty.is_subtype_of(self.db, *element) {
76+
77+
if ty.is_same_gradual_form(*element) || ty.is_subtype_of(self.db, *element) {
7778
return self;
7879
} else if element.is_subtype_of(self.db, ty) {
7980
to_remove.push(index);
@@ -259,7 +260,9 @@ impl<'db> InnerIntersectionBuilder<'db> {
259260
let mut to_remove = SmallVec::<[usize; 1]>::new();
260261
for (index, existing_positive) in self.positive.iter().enumerate() {
261262
// S & T = S if S <: T
262-
if existing_positive.is_subtype_of(db, new_positive) {
263+
if existing_positive.is_subtype_of(db, new_positive)
264+
|| existing_positive.is_same_gradual_form(new_positive)
265+
{
263266
return;
264267
}
265268
// same rule, reverse order
@@ -496,6 +499,17 @@ mod tests {
496499
assert_eq!(u1.expect_union().elements(&db), &[t1, t0]);
497500
}
498501

502+
#[test]
503+
fn build_union_simplify_multiple_unknown() {
504+
let db = setup_db();
505+
let t0 = KnownClass::Str.to_instance(&db);
506+
let t1 = Type::Unknown;
507+
508+
let u = UnionType::from_elements(&db, [t0, t1, t1]);
509+
510+
assert_eq!(u.expect_union().elements(&db), &[t0, t1]);
511+
}
512+
499513
#[test]
500514
fn build_union_subsume_multiple() {
501515
let db = setup_db();
@@ -603,6 +617,42 @@ mod tests {
603617
assert_eq!(ty, Type::Never);
604618
}
605619

620+
#[test]
621+
fn build_intersection_simplify_multiple_unknown() {
622+
let db = setup_db();
623+
624+
let ty = IntersectionBuilder::new(&db)
625+
.add_positive(Type::Unknown)
626+
.add_positive(Type::Unknown)
627+
.build();
628+
assert_eq!(ty, Type::Unknown);
629+
630+
let ty = IntersectionBuilder::new(&db)
631+
.add_positive(Type::Unknown)
632+
.add_negative(Type::Unknown)
633+
.build();
634+
assert_eq!(ty, Type::Unknown);
635+
636+
let ty = IntersectionBuilder::new(&db)
637+
.add_negative(Type::Unknown)
638+
.add_negative(Type::Unknown)
639+
.build();
640+
assert_eq!(ty, Type::Unknown);
641+
642+
let ty = IntersectionBuilder::new(&db)
643+
.add_positive(Type::Unknown)
644+
.add_positive(Type::IntLiteral(0))
645+
.add_negative(Type::Unknown)
646+
.build();
647+
assert_eq!(
648+
ty,
649+
IntersectionBuilder::new(&db)
650+
.add_positive(Type::Unknown)
651+
.add_positive(Type::IntLiteral(0))
652+
.build()
653+
);
654+
}
655+
606656
#[test]
607657
fn intersection_distributes_over_union() {
608658
let db = setup_db();

crates/red_knot_python_semantic/src/types/property_tests.rs

+6
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ mod stable {
213213
singleton_implies_single_valued, db,
214214
forall types t. t.is_singleton(db) => t.is_single_valued(db)
215215
);
216+
217+
// If `T` contains a gradual form, it should not participate in subtyping
218+
type_property_test!(
219+
non_fully_static_types_do_not_participate_in_subtyping, db,
220+
forall types s, t. !s.is_fully_static(db) => !s.is_subtype_of(db, t) && !t.is_subtype_of(db, s)
221+
);
216222
}
217223

218224
/// This module contains property tests that currently lead to many false positives.

0 commit comments

Comments
 (0)