From 81157e0cbfe3668e6f10137bfa9de55bdc0e5982 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 16 Oct 2024 10:13:45 +0200 Subject: [PATCH 01/38] [red-knot] intersection type simplifications --- .../mdtest/narrow/conditionals_is.md | 6 +- .../resources/mdtest/narrow/match.md | 3 +- crates/red_knot_python_semantic/src/types.rs | 28 +++++ .../src/types/builder.rs | 107 ++++++++++++++---- 4 files changed, 119 insertions(+), 25 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is.md index 1f51771dc035c3..d215be4bc29953 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is.md @@ -6,8 +6,7 @@ x = None if flag else 1 if x is None: - # TODO the following should be simplified to 'None' - reveal_type(x) # revealed: None | Literal[1] & None + reveal_type(x) # revealed: None reveal_type(x) # revealed: None | Literal[1] ``` @@ -22,8 +21,7 @@ x = A() y = x if flag else None if y is x: - # TODO the following should be simplified to 'A' - reveal_type(y) # revealed: A | None & A + reveal_type(y) # revealed: A reveal_type(y) # revealed: A | None ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md index b3218d2c6ed1b1..0c8ea0e363cbe7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md @@ -12,6 +12,5 @@ match x: case None: y = x -# TODO intersection simplification: should be just Literal[0] | None -reveal_type(y) # revealed: Literal[0] | None | Literal[1] & None +reveal_type(y) # revealed: Literal[0] | None ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 067c5f4cad5998..3b72308cc28391 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -471,6 +471,34 @@ impl<'db> Type<'db> { self == other } + /// Return true if this type and `other` have no common elements. + pub(crate) fn is_disjoint_from(self, _db: &'db dyn Db, other: Type<'db>) -> bool { + match (self, other) { + (Type::Never, _) => true, + (_, Type::Never) => true, + ( + Type::None, + Type::BooleanLiteral(..) + | Type::BytesLiteral(..) + | Type::IntLiteral(..) + | Type::LiteralString + | Type::StringLiteral(..), + ) => true, + ( + Type::BooleanLiteral(..) + | Type::BytesLiteral(..) + | Type::IntLiteral(..) + | Type::LiteralString + | Type::StringLiteral(..), + Type::None, + ) => true, + (Type::None, Type::Instance(i)) | (Type::Instance(i), Type::None) => { + true // TODO not correct if i is an instance of NoneType + } + _ => false, + } + } + /// Return true if there is just a single inhabitant for this type. /// /// Note: This function aims to have no false positives, but might return `false` diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 013e6988cccbc0..7003554f3612cc 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -220,17 +220,36 @@ impl<'db> InnerIntersectionBuilder<'db> { // TODO `Any`/`Unknown`/`Todo` actually should not self-cancel match ty { Type::Intersection(inter) => { - let pos = inter.positive(db); - let neg = inter.negative(db); - self.positive.extend(pos.difference(&self.negative)); - self.negative.extend(neg.difference(&self.positive)); - self.positive.retain(|elem| !neg.contains(elem)); - self.negative.retain(|elem| !pos.contains(elem)); + for pos in inter.positive(db) { + self.add_positive(db, *pos); + } + for neg in inter.negative(db) { + self.add_negative(db, *neg); + } + + // self.positive.extend(pos.difference(&self.negative)); + // self.negative.extend(neg.difference(&self.positive)); + // self.positive.retain(|elem| !neg.contains(elem)); + // self.negative.retain(|elem| !pos.contains(elem)); } _ => { - if !self.negative.remove(&ty) { - self.positive.insert(ty); - }; + for pos in &self.positive { + if ty.is_disjoint_from(db, *pos) { + self.negative.clear(); + self.positive.clear(); + return; + } + } + for neg in &self.negative { + if ty.is_subtype_of(db, *neg) { + self.negative.clear(); + self.positive.clear(); + return; + } + } + self.positive.insert(ty); + // if !self.negative.remove(&ty) { + // }; } } } @@ -239,20 +258,40 @@ impl<'db> InnerIntersectionBuilder<'db> { fn add_negative(&mut self, db: &'db dyn Db, ty: Type<'db>) { // TODO `Any`/`Unknown`/`Todo` actually should not self-cancel match ty { - Type::Intersection(intersection) => { - let pos = intersection.negative(db); - let neg = intersection.positive(db); - self.positive.extend(pos.difference(&self.negative)); - self.negative.extend(neg.difference(&self.positive)); - self.positive.retain(|elem| !neg.contains(elem)); - self.negative.retain(|elem| !pos.contains(elem)); + Type::Intersection(inter) => { + // let pos = intersection.negative(db); + // let neg = intersection.positive(db); + // self.positive.extend(pos.difference(&self.negative)); + // self.negative.extend(neg.difference(&self.positive)); + // self.positive.retain(|elem| !neg.contains(elem)); + // self.negative.retain(|elem| !pos.contains(elem)); + + for pos in inter.positive(db) { + self.add_negative(db, *pos); + } + for neg in inter.negative(db) { + self.add_positive(db, *neg); + } } Type::Never => {} Type::Unbound => {} _ => { - if !self.positive.remove(&ty) { - self.negative.insert(ty); - }; + for pos in &self.positive { + if pos.is_subtype_of(db, ty) { + self.negative.clear(); + self.positive.clear(); + return; + } + } + for neg in &self.negative { + // TODO: what is the mirror-rule here? + // if ty.is_disjoint_from(db, neg) { + // self.negative.clear(); + // self.positive.clear(); + // return; + // } + } + self.negative.insert(ty); } } } @@ -581,4 +620,34 @@ mod tests { assert_eq!(ty, Type::IntLiteral(1)); } + + #[test] + fn build_intersection_simplify_positive_subtype() { + let db = setup_db(); + + let t = KnownClass::Str.to_instance(&db); + let s = Type::LiteralString; + + let ty = IntersectionBuilder::new(&db) + .add_negative(t) + .add_positive(s) + .build(); + + assert_eq!(ty, Type::Never); + } + + #[test] + fn build_intersection_simplify_positive_disjoint() { + let db = setup_db(); + + let t1 = Type::IntLiteral(1); + let t2 = Type::None; + + let ty = IntersectionBuilder::new(&db) + .add_positive(t1) + .add_positive(t2) + .build(); + + assert_eq!(ty, Type::Never); + } } From 14dee2ec556afa5381cd60051ce78d326c162aa5 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 16 Oct 2024 11:26:58 +0200 Subject: [PATCH 02/38] clean up --- .../src/types/builder.rs | 55 +++++++------------ 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 7003554f3612cc..11ea59e953af74 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -218,39 +218,31 @@ impl<'db> InnerIntersectionBuilder<'db> { /// Adds a positive type to this intersection. fn add_positive(&mut self, db: &'db dyn Db, ty: Type<'db>) { // TODO `Any`/`Unknown`/`Todo` actually should not self-cancel - match ty { - Type::Intersection(inter) => { - for pos in inter.positive(db) { - self.add_positive(db, *pos); - } - for neg in inter.negative(db) { - self.add_negative(db, *neg); + if let Type::Intersection(other) = ty { + for pos in other.positive(db) { + self.add_positive(db, *pos); + } + for neg in other.negative(db) { + self.add_negative(db, *neg); + } + } else { + for pos in &self.positive { + if ty.is_disjoint_from(db, *pos) { + self.negative.clear(); + self.positive.clear(); + return; } - // self.positive.extend(pos.difference(&self.negative)); - // self.negative.extend(neg.difference(&self.positive)); - // self.positive.retain(|elem| !neg.contains(elem)); - // self.negative.retain(|elem| !pos.contains(elem)); + // TODO add simplification for ty & pos != Never } - _ => { - for pos in &self.positive { - if ty.is_disjoint_from(db, *pos) { - self.negative.clear(); - self.positive.clear(); - return; - } - } - for neg in &self.negative { - if ty.is_subtype_of(db, *neg) { - self.negative.clear(); - self.positive.clear(); - return; - } + for neg in &self.negative { + if ty.is_subtype_of(db, *neg) { + self.negative.clear(); + self.positive.clear(); + return; } - self.positive.insert(ty); - // if !self.negative.remove(&ty) { - // }; } + self.positive.insert(ty); } } @@ -259,13 +251,6 @@ impl<'db> InnerIntersectionBuilder<'db> { // TODO `Any`/`Unknown`/`Todo` actually should not self-cancel match ty { Type::Intersection(inter) => { - // let pos = intersection.negative(db); - // let neg = intersection.positive(db); - // self.positive.extend(pos.difference(&self.negative)); - // self.negative.extend(neg.difference(&self.positive)); - // self.positive.retain(|elem| !neg.contains(elem)); - // self.negative.retain(|elem| !pos.contains(elem)); - for pos in inter.positive(db) { self.add_negative(db, *pos); } From e06687cf6e190ef6f63e2d69467d28f53b8f045c Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 16 Oct 2024 13:04:33 +0200 Subject: [PATCH 03/38] Handle more cases in disjoint_from --- crates/red_knot_python_semantic/src/types.rs | 93 ++++++++++++++++---- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 3b72308cc28391..4f2914f5664feb 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -472,29 +472,53 @@ impl<'db> Type<'db> { } /// Return true if this type and `other` have no common elements. - pub(crate) fn is_disjoint_from(self, _db: &'db dyn Db, other: Type<'db>) -> bool { + pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { match (self, other) { - (Type::Never, _) => true, - (_, Type::Never) => true, - ( - Type::None, - Type::BooleanLiteral(..) - | Type::BytesLiteral(..) - | Type::IntLiteral(..) + (Type::Never, _) | (_, Type::Never) => true, + (Type::Union(union), other) | (other, Type::Union(union)) => todo!(), + (Type::Intersection(intersection), other) + | (other, Type::Intersection(intersection)) => todo!(), + + // In all branches below, Never, Union, and Intersection are unreachable + // on both sides + (Type::None, other) | (other, Type::None) => match other { + Type::Never | Type::Union(..) | Type::Intersection(..) => unreachable!(), + Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(..) | Type::LiteralString - | Type::StringLiteral(..), - ) => true, - ( - Type::BooleanLiteral(..) | Type::BytesLiteral(..) - | Type::IntLiteral(..) + | Type::Function(..) + | Type::Module(..) + | Type::Class(..) + | Type::Tuple(..) => true, + Type::None => false, + Type::Instance(class_type) => { + !class_type.is_known(db, KnownClass::NoneType) // TODO: is this enough? + } + Type::Any | Type::Unknown | Type::Unbound | Type::Todo => todo!(), + }, + + // In all branches below, None is unreachable in addition + (Type::BooleanLiteral(b), other) | (other, Type::BooleanLiteral(b)) => match other { + Type::Never | Type::Union(..) | Type::Intersection(..) | Type::None => { + unreachable!() + } + Type::IntLiteral(_) + | Type::StringLiteral(..) | Type::LiteralString - | Type::StringLiteral(..), - Type::None, - ) => true, - (Type::None, Type::Instance(i)) | (Type::Instance(i), Type::None) => { - true // TODO not correct if i is an instance of NoneType - } + | Type::BytesLiteral(..) + | Type::Function(..) + | Type::Module(..) + | Type::Class(..) + | Type::Tuple(..) => true, + Type::BooleanLiteral(other_b) => b != other_b, + Type::Instance(class_type) => { + !class_type.is_known(db, KnownClass::Bool) // TODO: is this enough? + } + Type::Any | Type::Unknown | Type::Unbound | Type::Todo => todo!(), + }, + _ => false, } } @@ -1715,6 +1739,37 @@ mod tests { assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db))); } + #[test_case(Ty::Never, Ty::Never)] + #[test_case(Ty::Never, Ty::None)] + #[test_case(Ty::Never, Ty::BuiltinInstance("int"))] + #[test_case(Ty::None, Ty::StringLiteral("test"))] + #[test_case(Ty::None, Ty::BuiltinInstance("int"))] + #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("int"))] + #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(false))] + fn is_disjoint_from(a: Ty, b: Ty) { + let db = setup_db(); + let a = a.into_type(&db); + let b = b.into_type(&db); + + assert!(a.is_disjoint_from(&db, b)); + assert!(b.is_disjoint_from(&db, a)); + } + + #[test_case(Ty::None, Ty::None)] + #[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("int"))] + #[test_case(Ty::BuiltinInstance("str"), Ty::LiteralString)] + #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(true))] + #[test_case(Ty::BoolLiteral(false), Ty::BoolLiteral(false))] + #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("bool"))] + fn is_not_disjoint_from(a: Ty, b: Ty) { + let db = setup_db(); + let a = a.into_type(&db); + let b = b.into_type(&db); + + assert!(!a.is_disjoint_from(&db, b)); + assert!(!b.is_disjoint_from(&db, a)); + } + #[test_case(Ty::None)] #[test_case(Ty::BoolLiteral(true))] #[test_case(Ty::BoolLiteral(false))] From 5770be67eb8dfefa412b7eb829136a6c6a87dd5f Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 16 Oct 2024 13:48:48 +0200 Subject: [PATCH 04/38] Handle tuples, intersections --- crates/red_knot_python_semantic/src/types.rs | 145 +++++++++++++++--- .../src/types/builder.rs | 18 +-- 2 files changed, 132 insertions(+), 31 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 4f2914f5664feb..55999189ed71bd 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -472,17 +472,65 @@ impl<'db> Type<'db> { } /// Return true if this type and `other` have no common elements. + /// + /// Note: This function aims to have no false positives, but might return + /// wrong `false` answers in some cases. pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { match (self, other) { (Type::Never, _) | (_, Type::Never) => true, - (Type::Union(union), other) | (other, Type::Union(union)) => todo!(), + (Type::Any, _) | (_, Type::Any) => false, + (Type::Unknown, _) | (_, Type::Unknown) => false, + (Type::Unbound, _) | (_, Type::Unbound) => false, + (Type::Todo, _) | (_, Type::Todo) => false, + + (Type::Union(union), other) | (other, Type::Union(union)) => union + .elements(db) + .iter() + .all(|e| e.is_disjoint_from(db, other)), + (Type::Intersection(intersection), other) - | (other, Type::Intersection(intersection)) => todo!(), + | (other, Type::Intersection(intersection)) => { + if intersection + .positive(db) + .iter() + .any(|p| p.is_disjoint_from(db, other)) + { + true + } else { + // TODO we can do better here. For example: + // X & ~Literal[1] is disjoint from Literal[1] + false + } + } + + (Type::Tuple(tuple), other) | (other, Type::Tuple(tuple)) => { + if let Type::Tuple(other_tuple) = other { + if tuple.len(db) == other_tuple.len(db) { + tuple + .elements(db) + .iter() + .zip(other_tuple.elements(db).iter()) + .all(|(e1, e2)| e1.is_disjoint_from(db, *e2)) + } else { + true + } + } else { + // TODO: is this wrong because tuple can be subclassed? + true + } + } - // In all branches below, Never, Union, and Intersection are unreachable - // on both sides (Type::None, other) | (other, Type::None) => match other { - Type::Never | Type::Union(..) | Type::Intersection(..) => unreachable!(), + Type::Never + | Type::Any + | Type::Unknown + | Type::Unbound + | Type::Todo + | Type::Union(..) + | Type::Intersection(..) + | Type::Tuple(..) => { + unreachable!("handled above") + } Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::StringLiteral(..) @@ -490,35 +538,66 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::Function(..) | Type::Module(..) - | Type::Class(..) - | Type::Tuple(..) => true, + | Type::Class(..) => true, Type::None => false, Type::Instance(class_type) => { - !class_type.is_known(db, KnownClass::NoneType) // TODO: is this enough? + !class_type.is_known(db, KnownClass::NoneType) // TODO: is this enough since NoneType is final? } - Type::Any | Type::Unknown | Type::Unbound | Type::Todo => todo!(), }, - // In all branches below, None is unreachable in addition - (Type::BooleanLiteral(b), other) | (other, Type::BooleanLiteral(b)) => match other { - Type::Never | Type::Union(..) | Type::Intersection(..) | Type::None => { - unreachable!() + (Type::BooleanLiteral(bool), other) | (other, Type::BooleanLiteral(bool)) => { + match other { + Type::Never + | Type::Any + | Type::Unknown + | Type::Unbound + | Type::Todo + | Type::Union(..) + | Type::Intersection(..) + | Type::Tuple(..) + | Type::None => { + unreachable!("handled above") + } + Type::IntLiteral(_) + | Type::StringLiteral(..) + | Type::LiteralString + | Type::BytesLiteral(..) + | Type::Function(..) + | Type::Module(..) + | Type::Class(..) => true, + Type::BooleanLiteral(bool_other) => bool != bool_other, + Type::Instance(class_type) => { + !class_type.is_known(db, KnownClass::Bool) // TODO: is this enough since bool is final? + } } - Type::IntLiteral(_) - | Type::StringLiteral(..) + } + + (Type::IntLiteral(int), other) | (other, Type::IntLiteral(int)) => match other { + Type::Never + | Type::Any + | Type::Unknown + | Type::Unbound + | Type::Todo + | Type::Union(..) + | Type::Intersection(..) + | Type::Tuple(..) + | Type::None + | Type::BooleanLiteral(..) => { + unreachable!("handled above") + } + Type::StringLiteral(..) | Type::LiteralString | Type::BytesLiteral(..) | Type::Function(..) | Type::Module(..) - | Type::Class(..) - | Type::Tuple(..) => true, - Type::BooleanLiteral(other_b) => b != other_b, + | Type::Class(..) => true, + Type::IntLiteral(int_other) => int != int_other, Type::Instance(class_type) => { - !class_type.is_known(db, KnownClass::Bool) // TODO: is this enough? + !class_type.is_known(db, KnownClass::Int) // TODO: this is probably wrong since there could be subclasses of int? } - Type::Any | Type::Unknown | Type::Unbound | Type::Todo => todo!(), }, + // TODO: Handle, StringLiteral, LiteralString, BytesLiteral, Tuple _ => false, } } @@ -1600,8 +1679,8 @@ impl<'db> TupleType<'db> { #[cfg(test)] mod tests { use super::{ - builtins_symbol_ty, BytesLiteralType, StringLiteralType, Truthiness, TupleType, Type, - UnionType, + builtins_symbol_ty, BytesLiteralType, IntersectionBuilder, StringLiteralType, Truthiness, + TupleType, Type, UnionType, }; use crate::db::tests::TestDb; use crate::program::{Program, SearchPathSettings}; @@ -1645,6 +1724,7 @@ mod tests { BytesLiteral(&'static str), BuiltinInstance(&'static str), Union(Vec), + Intersection(Vec, Vec), Tuple(Vec), } @@ -1664,6 +1744,16 @@ mod tests { Ty::Union(tys) => { UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) } + Ty::Intersection(pos, neg) => { + let mut builder = IntersectionBuilder::new(db); + for p in pos { + builder = builder.add_positive(p.into_type(db)); + } + for n in neg { + builder = builder.add_negative(n.into_type(db)); + } + builder.build() + } Ty::Tuple(tys) => { let elements: Box<_> = tys.into_iter().map(|ty| ty.into_type(db)).collect(); Type::Tuple(TupleType::new(db, elements)) @@ -1746,6 +1836,12 @@ mod tests { #[test_case(Ty::None, Ty::BuiltinInstance("int"))] #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("int"))] #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(false))] + #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(3))] + #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(3), Ty::IntLiteral(4)]))] + #[test_case(Ty::Intersection(vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], vec![]), Ty::IntLiteral(2))] + #[test_case(Ty::Tuple(vec![Ty::BuiltinInstance("int")]), Ty::BuiltinInstance("int"))] + #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1)]), Ty::Tuple(vec![Ty::IntLiteral(2)]))] + #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1)]))] fn is_disjoint_from(a: Ty, b: Ty) { let db = setup_db(); let a = a.into_type(&db); @@ -1755,12 +1851,17 @@ mod tests { assert!(b.is_disjoint_from(&db, a)); } + #[test_case(Ty::Any, Ty::BuiltinInstance("int"))] #[test_case(Ty::None, Ty::None)] #[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("int"))] #[test_case(Ty::BuiltinInstance("str"), Ty::LiteralString)] #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(true))] #[test_case(Ty::BoolLiteral(false), Ty::BoolLiteral(false))] #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("bool"))] + #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(2))] + #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(2), Ty::IntLiteral(3)]))] + #[test_case(Ty::Intersection(vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(2)], vec![]), Ty::IntLiteral(2))] + #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1), Ty::BuiltinInstance("int")]))] fn is_not_disjoint_from(a: Ty, b: Ty) { let db = setup_db(); let a = a.into_type(&db); diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 11ea59e953af74..37ba5c163f4dd3 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -268,14 +268,14 @@ impl<'db> InnerIntersectionBuilder<'db> { return; } } - for neg in &self.negative { - // TODO: what is the mirror-rule here? - // if ty.is_disjoint_from(db, neg) { - // self.negative.clear(); - // self.positive.clear(); - // return; - // } - } + // TODO: what is the mirror-rule here? + // for neg in &self.negative { + // if ty.is_disjoint_from(db, neg) { + // self.negative.clear(); + // self.positive.clear(); + // return; + // } + // } self.negative.insert(ty); } } @@ -505,7 +505,7 @@ mod tests { let db = setup_db(); let ta = Type::Any; let t1 = Type::IntLiteral(1); - let t2 = Type::IntLiteral(2); + let t2 = KnownClass::Int.to_instance(&db); let i0 = IntersectionBuilder::new(&db) .add_positive(ta) .add_negative(t1) From ccb9b1ffa3943b0a9e721cccadb1d925161c788d Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 16 Oct 2024 13:57:50 +0200 Subject: [PATCH 05/38] Remove commented-out code --- crates/red_knot_python_semantic/src/types/builder.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 37ba5c163f4dd3..c956297ab190c5 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -268,14 +268,7 @@ impl<'db> InnerIntersectionBuilder<'db> { return; } } - // TODO: what is the mirror-rule here? - // for neg in &self.negative { - // if ty.is_disjoint_from(db, neg) { - // self.negative.clear(); - // self.positive.clear(); - // return; - // } - // } + // TODO: what is the analog for the negative part here? self.negative.insert(ty); } } From d0063ed39a7e00cca5eba705690b55cfaf135af2 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 16 Oct 2024 14:02:58 +0200 Subject: [PATCH 06/38] Fix tuples --- crates/red_knot_python_semantic/src/types.rs | 34 +++++++++++--------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 55999189ed71bd..2f70e39b2829ec 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -483,6 +483,11 @@ impl<'db> Type<'db> { (Type::Unbound, _) | (_, Type::Unbound) => false, (Type::Todo, _) | (_, Type::Todo) => false, + (ty @ (Type::Function(..) | Type::Module(..) | Type::Class(..)), other) + | (other, ty @ (Type::Function(..) | Type::Module(..) | Type::Class(..))) => { + ty != other + } + (Type::Union(union), other) | (other, Type::Union(union)) => union .elements(db) .iter() @@ -510,7 +515,7 @@ impl<'db> Type<'db> { .elements(db) .iter() .zip(other_tuple.elements(db).iter()) - .all(|(e1, e2)| e1.is_disjoint_from(db, *e2)) + .any(|(e1, e2)| e1.is_disjoint_from(db, *e2)) } else { true } @@ -526,6 +531,9 @@ impl<'db> Type<'db> { | Type::Unknown | Type::Unbound | Type::Todo + | Type::Function(..) + | Type::Module(..) + | Type::Class(..) | Type::Union(..) | Type::Intersection(..) | Type::Tuple(..) => { @@ -535,10 +543,7 @@ impl<'db> Type<'db> { | Type::BooleanLiteral(_) | Type::StringLiteral(..) | Type::LiteralString - | Type::BytesLiteral(..) - | Type::Function(..) - | Type::Module(..) - | Type::Class(..) => true, + | Type::BytesLiteral(..) => true, Type::None => false, Type::Instance(class_type) => { !class_type.is_known(db, KnownClass::NoneType) // TODO: is this enough since NoneType is final? @@ -552,6 +557,9 @@ impl<'db> Type<'db> { | Type::Unknown | Type::Unbound | Type::Todo + | Type::Function(..) + | Type::Module(..) + | Type::Class(..) | Type::Union(..) | Type::Intersection(..) | Type::Tuple(..) @@ -561,10 +569,7 @@ impl<'db> Type<'db> { Type::IntLiteral(_) | Type::StringLiteral(..) | Type::LiteralString - | Type::BytesLiteral(..) - | Type::Function(..) - | Type::Module(..) - | Type::Class(..) => true, + | Type::BytesLiteral(..) => true, Type::BooleanLiteral(bool_other) => bool != bool_other, Type::Instance(class_type) => { !class_type.is_known(db, KnownClass::Bool) // TODO: is this enough since bool is final? @@ -577,6 +582,9 @@ impl<'db> Type<'db> { | Type::Any | Type::Unknown | Type::Unbound + | Type::Function(..) + | Type::Module(..) + | Type::Class(..) | Type::Todo | Type::Union(..) | Type::Intersection(..) @@ -585,12 +593,7 @@ impl<'db> Type<'db> { | Type::BooleanLiteral(..) => { unreachable!("handled above") } - Type::StringLiteral(..) - | Type::LiteralString - | Type::BytesLiteral(..) - | Type::Function(..) - | Type::Module(..) - | Type::Class(..) => true, + Type::StringLiteral(..) | Type::LiteralString | Type::BytesLiteral(..) => true, Type::IntLiteral(int_other) => int != int_other, Type::Instance(class_type) => { !class_type.is_known(db, KnownClass::Int) // TODO: this is probably wrong since there could be subclasses of int? @@ -1842,6 +1845,7 @@ mod tests { #[test_case(Ty::Tuple(vec![Ty::BuiltinInstance("int")]), Ty::BuiltinInstance("int"))] #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1)]), Ty::Tuple(vec![Ty::IntLiteral(2)]))] #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1)]))] + #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(3)]))] fn is_disjoint_from(a: Ty, b: Ty) { let db = setup_db(); let a = a.into_type(&db); From 0f9ba92afcbc5650add586bf5992fc439c878560 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 16 Oct 2024 14:04:37 +0200 Subject: [PATCH 07/38] Fix TODO --- crates/red_knot_python_semantic/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 2f70e39b2829ec..5de55c673a93bb 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -600,7 +600,7 @@ impl<'db> Type<'db> { } }, - // TODO: Handle, StringLiteral, LiteralString, BytesLiteral, Tuple + // TODO: Handle, StringLiteral, LiteralString, BytesLiteral _ => false, } } From 68eae7ff98d9c6a402f1e41157f4c6f2167628a7 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 16 Oct 2024 14:45:35 +0200 Subject: [PATCH 08/38] Minor improvements --- crates/red_knot_python_semantic/src/types.rs | 2 +- .../src/types/builder.rs | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 5de55c673a93bb..3e805b7e7b5baf 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -478,6 +478,7 @@ impl<'db> Type<'db> { pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { match (self, other) { (Type::Never, _) | (_, Type::Never) => true, + (Type::Any, _) | (_, Type::Any) => false, (Type::Unknown, _) | (_, Type::Unknown) => false, (Type::Unbound, _) | (_, Type::Unbound) => false, @@ -520,7 +521,6 @@ impl<'db> Type<'db> { true } } else { - // TODO: is this wrong because tuple can be subclassed? true } } diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index c956297ab190c5..04bcc1908db442 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -216,9 +216,9 @@ impl<'db> InnerIntersectionBuilder<'db> { } /// Adds a positive type to this intersection. - fn add_positive(&mut self, db: &'db dyn Db, ty: Type<'db>) { + fn add_positive(&mut self, db: &'db dyn Db, new_positive: Type<'db>) { // TODO `Any`/`Unknown`/`Todo` actually should not self-cancel - if let Type::Intersection(other) = ty { + if let Type::Intersection(other) = new_positive { for pos in other.positive(db) { self.add_positive(db, *pos); } @@ -227,29 +227,32 @@ impl<'db> InnerIntersectionBuilder<'db> { } } else { for pos in &self.positive { - if ty.is_disjoint_from(db, *pos) { + if new_positive.is_disjoint_from(db, *pos) { self.negative.clear(); self.positive.clear(); return; } - // TODO add simplification for ty & pos != Never + // TODO if ty <: pos or pos <: ty, we can only + // keep the smaller of the two (the subtype). } + for neg in &self.negative { - if ty.is_subtype_of(db, *neg) { + if new_positive.is_subtype_of(db, *neg) { self.negative.clear(); self.positive.clear(); return; } } - self.positive.insert(ty); + + self.positive.insert(new_positive); } } /// Adds a negative type to this intersection. - fn add_negative(&mut self, db: &'db dyn Db, ty: Type<'db>) { + fn add_negative(&mut self, db: &'db dyn Db, new_negative: Type<'db>) { // TODO `Any`/`Unknown`/`Todo` actually should not self-cancel - match ty { + match new_negative { Type::Intersection(inter) => { for pos in inter.positive(db) { self.add_negative(db, *pos); @@ -262,14 +265,14 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::Unbound => {} _ => { for pos in &self.positive { - if pos.is_subtype_of(db, ty) { + if pos.is_subtype_of(db, new_negative) { self.negative.clear(); self.positive.clear(); return; } } - // TODO: what is the analog for the negative part here? - self.negative.insert(ty); + + self.negative.insert(new_negative); } } } @@ -600,7 +603,7 @@ mod tests { } #[test] - fn build_intersection_simplify_positive_subtype() { + fn build_intersection_simplify_negative_type_and_positive_subtype() { let db = setup_db(); let t = KnownClass::Str.to_instance(&db); @@ -615,7 +618,7 @@ mod tests { } #[test] - fn build_intersection_simplify_positive_disjoint() { + fn build_intersection_simplify_disjoint_positive_types() { let db = setup_db(); let t1 = Type::IntLiteral(1); From 68ab56e6edb15df4255f57967cca951b06b87e08 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 16 Oct 2024 15:10:58 +0200 Subject: [PATCH 09/38] Add new simplification --- .../src/types/builder.rs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 04bcc1908db442..1114dbe020f8bb 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -272,7 +272,18 @@ impl<'db> InnerIntersectionBuilder<'db> { } } - self.negative.insert(new_negative); + // This condition disregards irrelevant negative contributions. For example, + // we have A & ~B = A, if B is disjoint from A. So we only add negative + // elements if they overlap with any of the given positive contributions. + // Unless there are none yet. + if self.positive.is_empty() + || self + .positive + .iter() + .any(|pos| !new_negative.is_disjoint_from(db, *pos)) + { + self.negative.insert(new_negative); + } } } } @@ -631,4 +642,19 @@ mod tests { assert_eq!(ty, Type::Never); } + + #[test] + fn build_intersection_simplify_disregard_irrelevant_negative() { + let db = setup_db(); + + let t_p = KnownClass::Bool.to_instance(&db); + let t_n = Type::IntLiteral(1); + + let ty = IntersectionBuilder::new(&db) + .add_positive(t_p) + .add_negative(t_n) + .build(); + + assert_eq!(ty, t_p); + } } From 4b95c65f500b247f4ff1a12a316932a56eb7ccc2 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 16 Oct 2024 16:43:34 +0200 Subject: [PATCH 10/38] Add simplification for bools --- .../mdtest/narrow/conditionals_is_not.md | 3 +- .../src/types/builder.rs | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is_not.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is_not.md index b1c75d053c1d35..dc094096a94a55 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is_not.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is_not.md @@ -20,8 +20,7 @@ x = True if flag else False reveal_type(x) # revealed: bool if x is not False: - # TODO the following should be `Literal[True]` - reveal_type(x) # revealed: bool & ~Literal[False] + reveal_type(x) # revealed: Literal[True] ``` ## `is not` for non-singleton types diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 1114dbe020f8bb..63a983492d3501 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -263,7 +263,21 @@ impl<'db> InnerIntersectionBuilder<'db> { } Type::Never => {} Type::Unbound => {} + _ => { + if let Type::BooleanLiteral(bool) = new_negative { + if self + .positive + .iter() + .any(|pos| *pos == KnownClass::Bool.to_instance(db)) + { + self.positive.clear(); + self.negative.clear(); + self.positive.insert(Type::BooleanLiteral(!bool)); + return; + } + } + for pos in &self.positive { if pos.is_subtype_of(db, new_negative) { self.negative.clear(); @@ -657,4 +671,22 @@ mod tests { assert_eq!(ty, t_p); } + + #[test] + fn build_intersection_simplify_split_bool() { + let db = setup_db(); + + let t_p = KnownClass::Bool.to_instance(&db); + + for bool in [true, false] { + let t_n = Type::BooleanLiteral(bool); + + let ty = IntersectionBuilder::new(&db) + .add_positive(t_p) + .add_negative(t_n) + .build(); + + assert_eq!(ty, Type::BooleanLiteral(!bool)); + } + } } From 1917618b596291b04a72ed6116e6d772bb2e49f4 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 12:18:43 +0200 Subject: [PATCH 11/38] Add rule for S & T = S --- .../src/types/builder.rs | 105 ++++++++++++++---- 1 file changed, 85 insertions(+), 20 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 63a983492d3501..1030ce43fe6d30 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -226,21 +226,27 @@ impl<'db> InnerIntersectionBuilder<'db> { self.add_negative(db, *neg); } } else { - for pos in &self.positive { - if new_positive.is_disjoint_from(db, *pos) { - self.negative.clear(); - self.positive.clear(); + let mut to_remove = None; + for (index, existing_positive) in self.positive.iter().enumerate() { + if existing_positive.is_subtype_of(db, new_positive) { return; } + if new_positive.is_subtype_of(db, *existing_positive) { + to_remove = Some(index); + } - // TODO if ty <: pos or pos <: ty, we can only - // keep the smaller of the two (the subtype). + if new_positive.is_disjoint_from(db, *existing_positive) { + *self = Self::new(); + return; + } + } + if let Some(index) = to_remove { + self.positive.remove_index(index); } - for neg in &self.negative { - if new_positive.is_subtype_of(db, *neg) { - self.negative.clear(); - self.positive.clear(); + for existing_negative in &self.negative { + if new_positive.is_subtype_of(db, *existing_negative) { + *self = Self::new(); return; } } @@ -265,31 +271,43 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::Unbound => {} _ => { + // TODO: do we need the mirror-version of this above if let Type::BooleanLiteral(bool) = new_negative { if self .positive .iter() .any(|pos| *pos == KnownClass::Bool.to_instance(db)) { - self.positive.clear(); - self.negative.clear(); + *self = Self::new(); self.positive.insert(Type::BooleanLiteral(!bool)); return; } } - for pos in &self.positive { - if pos.is_subtype_of(db, new_negative) { - self.negative.clear(); - self.positive.clear(); + let mut to_remove = None; + for (index, existing_negative) in self.negative.iter().enumerate() { + if existing_negative.is_subtype_of(db, new_negative) { + to_remove = Some(index); + } + if new_negative.is_subtype_of(db, *existing_negative) { + return; + } + } + if let Some(index) = to_remove { + self.negative.remove_index(index); + } + + for existing_positive in &self.positive { + if existing_positive.is_subtype_of(db, new_negative) { + *self = Self::new(); return; } } // This condition disregards irrelevant negative contributions. For example, - // we have A & ~B = A, if B is disjoint from A. So we only add negative - // elements if they overlap with any of the given positive contributions. - // Unless there are none yet. + // A & ~B = A, if B is disjoint from A. So we only add negative elements if + // they overlap with any of the given positive contributions. Unless there + // are none yet. if self.positive.is_empty() || self .positive @@ -537,7 +555,7 @@ mod tests { .build() .expect_intersection(); - assert_eq!(intersection.pos_vec(&db), &[t2, t1]); + assert_eq!(intersection.pos_vec(&db), &[t1]); assert_eq!(intersection.neg_vec(&db), &[ta]); } @@ -627,6 +645,48 @@ mod tests { assert_eq!(ty, Type::IntLiteral(1)); } + #[test] + fn build_intersection_simplify_positive_type_and_positive_subtype() { + let db = setup_db(); + + let t = KnownClass::Str.to_instance(&db); + let s = Type::LiteralString; + + let ty = IntersectionBuilder::new(&db) + .add_positive(t) + .add_positive(s) + .build(); + assert_eq!(ty, s); + + let ty = IntersectionBuilder::new(&db) + .add_positive(s) + .add_positive(t) + .build(); + assert_eq!(ty, s); + } + + #[test] + fn build_intersection_simplify_negative_type_and_negative_subtype() { + let db = setup_db(); + + let t = KnownClass::Str.to_instance(&db); + let s = Type::LiteralString; + + let expected = IntersectionBuilder::new(&db).add_negative(t).build(); + + let ty = IntersectionBuilder::new(&db) + .add_negative(t) + .add_negative(s) + .build(); + assert_eq!(ty, expected); + + let ty = IntersectionBuilder::new(&db) + .add_negative(s) + .add_negative(t) + .build(); + assert_eq!(ty, expected); + } + #[test] fn build_intersection_simplify_negative_type_and_positive_subtype() { let db = setup_db(); @@ -638,7 +698,12 @@ mod tests { .add_negative(t) .add_positive(s) .build(); + assert_eq!(ty, Type::Never); + let ty = IntersectionBuilder::new(&db) + .add_positive(s) + .add_negative(t) + .build(); assert_eq!(ty, Type::Never); } From 8d78019d80b97e34b2714ae67d93950b3a1df17b Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 12:28:29 +0200 Subject: [PATCH 12/38] Remove/reduce simplify --- .../src/types/builder.rs | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 1030ce43fe6d30..4b820bf6a39e03 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -244,11 +244,19 @@ impl<'db> InnerIntersectionBuilder<'db> { self.positive.remove_index(index); } - for existing_negative in &self.negative { + let mut to_remove = None; + for (index, existing_negative) in self.negative.iter().enumerate() { if new_positive.is_subtype_of(db, *existing_negative) { *self = Self::new(); return; } + + if existing_negative.is_disjoint_from(db, new_positive) { + to_remove = Some(index); + } + } + if let Some(index) = to_remove { + self.negative.remove_index(index); } self.positive.insert(new_positive); @@ -320,33 +328,15 @@ impl<'db> InnerIntersectionBuilder<'db> { } } - fn simplify(&mut self) { - // TODO this should be generalized based on subtyping, for now we just handle a few cases - - // Never is a subtype of all types - if self.positive.contains(&Type::Never) { - self.positive.retain(Type::is_never); - self.negative.clear(); - } - + fn simplify_unbound(&mut self) { if self.positive.contains(&Type::Unbound) { self.positive.retain(Type::is_unbound); self.negative.clear(); } - - // None intersects only with object - for pos in &self.positive { - if let Type::Instance(_) = pos { - // could be `object` type - } else { - self.negative.remove(&Type::None); - break; - } - } } fn build(mut self, db: &'db dyn Db) -> Type<'db> { - self.simplify(); + self.simplify_unbound(); match (self.positive.len(), self.negative.len()) { (0, 0) => Type::Never, (1, 0) => self.positive[0], @@ -637,11 +627,17 @@ mod tests { #[test] fn build_intersection_simplify_negative_none() { let db = setup_db(); + let ty = IntersectionBuilder::new(&db) .add_negative(Type::None) .add_positive(Type::IntLiteral(1)) .build(); + assert_eq!(ty, Type::IntLiteral(1)); + let ty = IntersectionBuilder::new(&db) + .add_positive(Type::IntLiteral(1)) + .add_negative(Type::None) + .build(); assert_eq!(ty, Type::IntLiteral(1)); } From 1737dd8d93c3c23f4ca5578965f43bbb8d790a9f Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 12:39:38 +0200 Subject: [PATCH 13/38] Additional rules --- .../src/types/builder.rs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 4b820bf6a39e03..ddc03d19838fe6 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -228,13 +228,15 @@ impl<'db> InnerIntersectionBuilder<'db> { } else { let mut to_remove = None; for (index, existing_positive) in self.positive.iter().enumerate() { + // S & T = S if S <: T if existing_positive.is_subtype_of(db, new_positive) { return; } + // same rule, reverse order if new_positive.is_subtype_of(db, *existing_positive) { to_remove = Some(index); } - + // A & B = Never if A and B are disjoint if new_positive.is_disjoint_from(db, *existing_positive) { *self = Self::new(); return; @@ -246,11 +248,12 @@ impl<'db> InnerIntersectionBuilder<'db> { let mut to_remove = None; for (index, existing_negative) in self.negative.iter().enumerate() { + // S & ~T = Never if S <: T if new_positive.is_subtype_of(db, *existing_negative) { *self = Self::new(); return; } - + // A & ~B = A if A and B are disjoint if existing_negative.is_disjoint_from(db, new_positive) { to_remove = Some(index); } @@ -294,9 +297,11 @@ impl<'db> InnerIntersectionBuilder<'db> { let mut to_remove = None; for (index, existing_negative) in self.negative.iter().enumerate() { + // ~S & ~T = ~T if S <: T if existing_negative.is_subtype_of(db, new_negative) { to_remove = Some(index); } + // same rule, reverse order if new_negative.is_subtype_of(db, *existing_negative) { return; } @@ -306,24 +311,18 @@ impl<'db> InnerIntersectionBuilder<'db> { } for existing_positive in &self.positive { + // S & ~T = Never if S <: T if existing_positive.is_subtype_of(db, new_negative) { *self = Self::new(); return; } + // A & ~B = A if A and B are disjoint + if existing_positive.is_disjoint_from(db, new_negative) { + return; + } } - // This condition disregards irrelevant negative contributions. For example, - // A & ~B = A, if B is disjoint from A. So we only add negative elements if - // they overlap with any of the given positive contributions. Unless there - // are none yet. - if self.positive.is_empty() - || self - .positive - .iter() - .any(|pos| !new_negative.is_disjoint_from(db, *pos)) - { - self.negative.insert(new_negative); - } + self.negative.insert(new_negative); } } } @@ -526,7 +525,7 @@ mod tests { .expect_intersection(); assert_eq!(intersection.pos_vec(&db), &[t2, ta]); - assert_eq!(intersection.neg_vec(&db), &[t1]); + assert_eq!(intersection.neg_vec(&db), &[]); } #[test] From 98df52220bdf841280586041cb4e3e516d49c42e Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 13:01:55 +0200 Subject: [PATCH 14/38] Mirror version of the bool rule --- .../src/types/builder.rs | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index ddc03d19838fe6..3aff838933d6e7 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -226,6 +226,24 @@ impl<'db> InnerIntersectionBuilder<'db> { self.add_negative(db, *neg); } } else { + // ~Literal[True] & bool = Literal[False] + if let Type::Instance(instance) = new_positive { + if instance.is_known(db, KnownClass::Bool) { + let mut found_bool_literal = None; + for neg in &self.negative { + if let Type::BooleanLiteral(bool) = neg { + found_bool_literal = Some(*bool); + } + } + + if let Some(bool) = found_bool_literal { + *self = Self::new(); + self.positive.insert(Type::BooleanLiteral(!bool)); + return; + } + } + } + let mut to_remove = None; for (index, existing_positive) in self.positive.iter().enumerate() { // S & T = S if S <: T @@ -278,23 +296,18 @@ impl<'db> InnerIntersectionBuilder<'db> { self.add_positive(db, *neg); } } - Type::Never => {} Type::Unbound => {} - + // ~Literal[True] & bool = Literal[False] + Type::BooleanLiteral(bool) + if self + .positive + .iter() + .any(|pos| *pos == KnownClass::Bool.to_instance(db)) => + { + *self = Self::new(); + self.positive.insert(Type::BooleanLiteral(!bool)); + } _ => { - // TODO: do we need the mirror-version of this above - if let Type::BooleanLiteral(bool) = new_negative { - if self - .positive - .iter() - .any(|pos| *pos == KnownClass::Bool.to_instance(db)) - { - *self = Self::new(); - self.positive.insert(Type::BooleanLiteral(!bool)); - return; - } - } - let mut to_remove = None; for (index, existing_negative) in self.negative.iter().enumerate() { // ~S & ~T = ~T if S <: T @@ -745,7 +758,12 @@ mod tests { .add_positive(t_p) .add_negative(t_n) .build(); + assert_eq!(ty, Type::BooleanLiteral(!bool)); + let ty = IntersectionBuilder::new(&db) + .add_negative(t_n) + .add_positive(t_p) + .build(); assert_eq!(ty, Type::BooleanLiteral(!bool)); } } From 2bd1789d6b2c56ae7e68720e0f457dc89294a6f3 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 13:24:23 +0200 Subject: [PATCH 15/38] Handle more cases in disjoint_from --- crates/red_knot_python_semantic/src/types.rs | 105 +++++++++++++++++- .../src/types/builder.rs | 13 ++- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 3e805b7e7b5baf..a3b191fe4e124b 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -595,13 +595,109 @@ impl<'db> Type<'db> { } Type::StringLiteral(..) | Type::LiteralString | Type::BytesLiteral(..) => true, Type::IntLiteral(int_other) => int != int_other, - Type::Instance(class_type) => { - !class_type.is_known(db, KnownClass::Int) // TODO: this is probably wrong since there could be subclasses of int? + Type::Instance(..) => false, + }, + + (Type::StringLiteral(string), other) | (other, Type::StringLiteral(string)) => { + match other { + Type::Never + | Type::Any + | Type::Unknown + | Type::Unbound + | Type::Function(..) + | Type::Module(..) + | Type::Class(..) + | Type::Todo + | Type::Union(..) + | Type::Intersection(..) + | Type::Tuple(..) + | Type::None + | Type::BooleanLiteral(..) + | Type::IntLiteral(..) => { + unreachable!("handled above") + } + Type::StringLiteral(string_other) => string != string_other, + Type::LiteralString => false, + Type::BytesLiteral(..) => true, + Type::Instance(..) => false, + } + } + + (Type::LiteralString, other) | (other, Type::LiteralString) => match other { + Type::Never + | Type::Any + | Type::Unknown + | Type::Unbound + | Type::Function(..) + | Type::Module(..) + | Type::Class(..) + | Type::Todo + | Type::Union(..) + | Type::Intersection(..) + | Type::Tuple(..) + | Type::None + | Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) => { + unreachable!("handled above") } + Type::LiteralString => false, + Type::BytesLiteral(..) => true, + Type::Instance(..) => false, }, - // TODO: Handle, StringLiteral, LiteralString, BytesLiteral - _ => false, + (Type::BytesLiteral(bytes), other) | (other, Type::BytesLiteral(bytes)) => { + match other { + Type::Never + | Type::Any + | Type::Unknown + | Type::Unbound + | Type::Function(..) + | Type::Module(..) + | Type::Class(..) + | Type::Todo + | Type::Union(..) + | Type::Intersection(..) + | Type::Tuple(..) + | Type::None + | Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::LiteralString => { + unreachable!("handled above") + } + Type::BytesLiteral(bytes_other) => bytes != bytes_other, + Type::Instance(..) => false, + } + } + + (Type::Instance(..), other) => { + match other { + Type::Never + | Type::Any + | Type::Unknown + | Type::Unbound + | Type::Function(..) + | Type::Module(..) + | Type::Class(..) + | Type::Todo + | Type::Union(..) + | Type::Intersection(..) + | Type::Tuple(..) + | Type::None + | Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::LiteralString + | Type::BytesLiteral(..) => { + unreachable!("handled above") + } + Type::Instance(..) => { + // TODO + false + } + } + } } } @@ -1839,6 +1935,7 @@ mod tests { #[test_case(Ty::None, Ty::BuiltinInstance("int"))] #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("int"))] #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(false))] + #[test_case(Ty::IntLiteral(1), Ty::IntLiteral(2))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(3))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(3), Ty::IntLiteral(4)]))] #[test_case(Ty::Intersection(vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], vec![]), Ty::IntLiteral(2))] diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 3aff838933d6e7..fdaea3a0727d15 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -227,8 +227,8 @@ impl<'db> InnerIntersectionBuilder<'db> { } } else { // ~Literal[True] & bool = Literal[False] - if let Type::Instance(instance) = new_positive { - if instance.is_known(db, KnownClass::Bool) { + if let Type::Instance(class_type) = new_positive { + if class_type.is_known(db, KnownClass::Bool) { let mut found_bool_literal = None; for neg in &self.negative { if let Type::BooleanLiteral(bool) = neg { @@ -734,14 +734,19 @@ mod tests { fn build_intersection_simplify_disregard_irrelevant_negative() { let db = setup_db(); - let t_p = KnownClass::Bool.to_instance(&db); - let t_n = Type::IntLiteral(1); + let t_p = KnownClass::Int.to_instance(&db); + let t_n = Type::BooleanLiteral(false); let ty = IntersectionBuilder::new(&db) .add_positive(t_p) .add_negative(t_n) .build(); + assert_eq!(ty, t_p); + let ty = IntersectionBuilder::new(&db) + .add_negative(t_n) + .add_positive(t_p) + .build(); assert_eq!(ty, t_p); } From 8ff474faefae81983c7982fffe6d8a9e46d8b29f Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 13:28:54 +0200 Subject: [PATCH 16/38] Remove TODO --- crates/red_knot_python_semantic/src/types.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index a3b191fe4e124b..aff84dfab8cdf8 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -545,9 +545,7 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(..) => true, Type::None => false, - Type::Instance(class_type) => { - !class_type.is_known(db, KnownClass::NoneType) // TODO: is this enough since NoneType is final? - } + Type::Instance(class_type) => !class_type.is_known(db, KnownClass::NoneType), }, (Type::BooleanLiteral(bool), other) | (other, Type::BooleanLiteral(bool)) => { @@ -571,9 +569,7 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(..) => true, Type::BooleanLiteral(bool_other) => bool != bool_other, - Type::Instance(class_type) => { - !class_type.is_known(db, KnownClass::Bool) // TODO: is this enough since bool is final? - } + Type::Instance(class_type) => !class_type.is_known(db, KnownClass::Bool), } } From 63709692570eaffd22c624cc73099e9d0e92d8ae Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 14:00:54 +0200 Subject: [PATCH 17/38] Improve test coverage --- crates/red_knot_python_semantic/src/types.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index aff84dfab8cdf8..31cd349ed81667 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1932,6 +1932,7 @@ mod tests { #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("int"))] #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(false))] #[test_case(Ty::IntLiteral(1), Ty::IntLiteral(2))] + #[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("b"))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(3))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(3), Ty::IntLiteral(4)]))] #[test_case(Ty::Intersection(vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], vec![]), Ty::IntLiteral(2))] @@ -1955,6 +1956,10 @@ mod tests { #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(true))] #[test_case(Ty::BoolLiteral(false), Ty::BoolLiteral(false))] #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("bool"))] + #[test_case(Ty::IntLiteral(1), Ty::IntLiteral(1))] + #[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("a"))] + #[test_case(Ty::StringLiteral("a"), Ty::LiteralString)] + #[test_case(Ty::StringLiteral("a"), Ty::BuiltinInstance("str"))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(2))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(2), Ty::IntLiteral(3)]))] #[test_case(Ty::Intersection(vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(2)], vec![]), Ty::IntLiteral(2))] From efe64d81ad56a5b4e68a79dc78aa5f38d74240ff Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 14:07:53 +0200 Subject: [PATCH 18/38] Change test name --- crates/red_knot_python_semantic/src/types/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index fdaea3a0727d15..860b759092d227 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -731,7 +731,7 @@ mod tests { } #[test] - fn build_intersection_simplify_disregard_irrelevant_negative() { + fn build_intersection_simplify_disjoint_positive_and_negative_types() { let db = setup_db(); let t_p = KnownClass::Int.to_instance(&db); From bf4af40a3f4afa96b0c08e66e13de039b6e39303 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 14:36:51 +0200 Subject: [PATCH 19/38] Fix instance checks --- crates/red_knot_python_semantic/src/types.rs | 21 ++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 31cd349ed81667..a2fd505084d5fc 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -545,7 +545,10 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(..) => true, Type::None => false, - Type::Instance(class_type) => !class_type.is_known(db, KnownClass::NoneType), + Type::Instance(class_type) => { + !class_type.is_known(db, KnownClass::NoneType) + && !class_type.is_known(db, KnownClass::Object) + } }, (Type::BooleanLiteral(bool), other) | (other, Type::BooleanLiteral(bool)) => { @@ -569,7 +572,10 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(..) => true, Type::BooleanLiteral(bool_other) => bool != bool_other, - Type::Instance(class_type) => !class_type.is_known(db, KnownClass::Bool), + Type::Instance(class_type) => { + !class_type.is_known(db, KnownClass::Bool) + && !class_type.is_known(db, KnownClass::Object) + } } } @@ -591,7 +597,10 @@ impl<'db> Type<'db> { } Type::StringLiteral(..) | Type::LiteralString | Type::BytesLiteral(..) => true, Type::IntLiteral(int_other) => int != int_other, - Type::Instance(..) => false, + Type::Instance(class_type) => { + !class_type.is_known(db, KnownClass::Int) + && !class_type.is_known(db, KnownClass::Object) + } }, (Type::StringLiteral(string), other) | (other, Type::StringLiteral(string)) => { @@ -615,7 +624,10 @@ impl<'db> Type<'db> { Type::StringLiteral(string_other) => string != string_other, Type::LiteralString => false, Type::BytesLiteral(..) => true, - Type::Instance(..) => false, + Type::Instance(class_type) => { + !class_type.is_known(db, KnownClass::Str) + && !class_type.is_known(db, KnownClass::Object) + } } } @@ -1951,6 +1963,7 @@ mod tests { #[test_case(Ty::Any, Ty::BuiltinInstance("int"))] #[test_case(Ty::None, Ty::None)] + #[test_case(Ty::None, Ty::BuiltinInstance("object"))] #[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("int"))] #[test_case(Ty::BuiltinInstance("str"), Ty::LiteralString)] #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(true))] From 5dd8ab59b0357018267af746ec71095e0a486d3a Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 14:56:12 +0200 Subject: [PATCH 20/38] =?UTF-8?q?Use=20matches!(=E2=80=A6)=20for=20instanc?= =?UTF-8?q?e=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/red_knot_python_semantic/src/types.rs | 32 ++++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index a2fd505084d5fc..04e63682bd8f92 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -545,10 +545,10 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(..) => true, Type::None => false, - Type::Instance(class_type) => { - !class_type.is_known(db, KnownClass::NoneType) - && !class_type.is_known(db, KnownClass::Object) - } + Type::Instance(class_type) => !matches!( + class_type.known(db), + Some(KnownClass::NoneType | KnownClass::Object) + ), }, (Type::BooleanLiteral(bool), other) | (other, Type::BooleanLiteral(bool)) => { @@ -572,10 +572,10 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(..) => true, Type::BooleanLiteral(bool_other) => bool != bool_other, - Type::Instance(class_type) => { - !class_type.is_known(db, KnownClass::Bool) - && !class_type.is_known(db, KnownClass::Object) - } + Type::Instance(class_type) => !matches!( + class_type.known(db), + Some(KnownClass::Bool | KnownClass::Object) + ), } } @@ -597,10 +597,10 @@ impl<'db> Type<'db> { } Type::StringLiteral(..) | Type::LiteralString | Type::BytesLiteral(..) => true, Type::IntLiteral(int_other) => int != int_other, - Type::Instance(class_type) => { - !class_type.is_known(db, KnownClass::Int) - && !class_type.is_known(db, KnownClass::Object) - } + Type::Instance(class_type) => !matches!( + class_type.known(db), + Some(KnownClass::Int | KnownClass::Object) + ), }, (Type::StringLiteral(string), other) | (other, Type::StringLiteral(string)) => { @@ -624,10 +624,10 @@ impl<'db> Type<'db> { Type::StringLiteral(string_other) => string != string_other, Type::LiteralString => false, Type::BytesLiteral(..) => true, - Type::Instance(class_type) => { - !class_type.is_known(db, KnownClass::Str) - && !class_type.is_known(db, KnownClass::Object) - } + Type::Instance(class_type) => !matches!( + class_type.known(db), + Some(KnownClass::Str | KnownClass::Object) + ), } } From 08cc4ba0804283eea5dc4bb4ba394f17c919ef2d Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 17 Oct 2024 15:45:55 +0200 Subject: [PATCH 21/38] Flatten nested match statements --- crates/red_knot_python_semantic/src/types.rs | 254 ++++++------------- 1 file changed, 82 insertions(+), 172 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 04e63682bd8f92..748ae63e7a280d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -525,186 +525,96 @@ impl<'db> Type<'db> { } } - (Type::None, other) | (other, Type::None) => match other { - Type::Never - | Type::Any - | Type::Unknown - | Type::Unbound - | Type::Todo - | Type::Function(..) - | Type::Module(..) - | Type::Class(..) - | Type::Union(..) - | Type::Intersection(..) - | Type::Tuple(..) => { - unreachable!("handled above") - } + ( + Type::None, + Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(..) + | Type::LiteralString + | Type::BytesLiteral(..), + ) + | ( Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::StringLiteral(..) | Type::LiteralString - | Type::BytesLiteral(..) => true, - Type::None => false, - Type::Instance(class_type) => !matches!( + | Type::BytesLiteral(..), + Type::None, + ) => true, + (Type::None, Type::None) => false, + (Type::None, Type::Instance(class_type)) | (Type::Instance(class_type), Type::None) => { + !matches!( class_type.known(db), Some(KnownClass::NoneType | KnownClass::Object) - ), - }, - - (Type::BooleanLiteral(bool), other) | (other, Type::BooleanLiteral(bool)) => { - match other { - Type::Never - | Type::Any - | Type::Unknown - | Type::Unbound - | Type::Todo - | Type::Function(..) - | Type::Module(..) - | Type::Class(..) - | Type::Union(..) - | Type::Intersection(..) - | Type::Tuple(..) - | Type::None => { - unreachable!("handled above") - } - Type::IntLiteral(_) - | Type::StringLiteral(..) - | Type::LiteralString - | Type::BytesLiteral(..) => true, - Type::BooleanLiteral(bool_other) => bool != bool_other, - Type::Instance(class_type) => !matches!( - class_type.known(db), - Some(KnownClass::Bool | KnownClass::Object) - ), - } + ) } - (Type::IntLiteral(int), other) | (other, Type::IntLiteral(int)) => match other { - Type::Never - | Type::Any - | Type::Unknown - | Type::Unbound - | Type::Function(..) - | Type::Module(..) - | Type::Class(..) - | Type::Todo - | Type::Union(..) - | Type::Intersection(..) - | Type::Tuple(..) - | Type::None - | Type::BooleanLiteral(..) => { - unreachable!("handled above") - } - Type::StringLiteral(..) | Type::LiteralString | Type::BytesLiteral(..) => true, - Type::IntLiteral(int_other) => int != int_other, - Type::Instance(class_type) => !matches!( - class_type.known(db), - Some(KnownClass::Int | KnownClass::Object) - ), - }, + ( + Type::BooleanLiteral(..), + Type::IntLiteral(_) + | Type::StringLiteral(..) + | Type::LiteralString + | Type::BytesLiteral(..), + ) + | ( + Type::IntLiteral(_) + | Type::StringLiteral(..) + | Type::LiteralString + | Type::BytesLiteral(..), + Type::BooleanLiteral(..), + ) => true, + (Type::BooleanLiteral(left), Type::BooleanLiteral(right)) => left != right, + (Type::BooleanLiteral(..), Type::Instance(class_type)) + | (Type::Instance(class_type), Type::BooleanLiteral(..)) => !matches!( + class_type.known(db), + Some(KnownClass::Bool | KnownClass::Object) + ), - (Type::StringLiteral(string), other) | (other, Type::StringLiteral(string)) => { - match other { - Type::Never - | Type::Any - | Type::Unknown - | Type::Unbound - | Type::Function(..) - | Type::Module(..) - | Type::Class(..) - | Type::Todo - | Type::Union(..) - | Type::Intersection(..) - | Type::Tuple(..) - | Type::None - | Type::BooleanLiteral(..) - | Type::IntLiteral(..) => { - unreachable!("handled above") - } - Type::StringLiteral(string_other) => string != string_other, - Type::LiteralString => false, - Type::BytesLiteral(..) => true, - Type::Instance(class_type) => !matches!( - class_type.known(db), - Some(KnownClass::Str | KnownClass::Object) - ), - } - } + ( + Type::IntLiteral(..), + Type::StringLiteral(..) | Type::LiteralString | Type::BytesLiteral(..), + ) + | ( + Type::StringLiteral(..) | Type::LiteralString | Type::BytesLiteral(..), + Type::IntLiteral(..), + ) => true, + (Type::IntLiteral(left), Type::IntLiteral(right)) => left != right, + (Type::IntLiteral(..), Type::Instance(class_type)) + | (Type::Instance(class_type), Type::IntLiteral(..)) => !matches!( + class_type.known(db), + Some(KnownClass::Int | KnownClass::Object) + ), - (Type::LiteralString, other) | (other, Type::LiteralString) => match other { - Type::Never - | Type::Any - | Type::Unknown - | Type::Unbound - | Type::Function(..) - | Type::Module(..) - | Type::Class(..) - | Type::Todo - | Type::Union(..) - | Type::Intersection(..) - | Type::Tuple(..) - | Type::None - | Type::BooleanLiteral(..) - | Type::IntLiteral(..) - | Type::StringLiteral(..) => { - unreachable!("handled above") - } - Type::LiteralString => false, - Type::BytesLiteral(..) => true, - Type::Instance(..) => false, - }, + (Type::StringLiteral(left), Type::StringLiteral(right)) => left != right, + (Type::StringLiteral(..), Type::LiteralString) + | (Type::LiteralString, Type::StringLiteral(..)) => false, + (Type::StringLiteral(..), Type::BytesLiteral(..)) + | (Type::BytesLiteral(..), Type::StringLiteral(..)) => true, + (Type::StringLiteral(..), Type::Instance(class_type)) + | (Type::Instance(class_type), Type::StringLiteral(..)) => !matches!( + class_type.known(db), + Some(KnownClass::Str | KnownClass::Object) + ), - (Type::BytesLiteral(bytes), other) | (other, Type::BytesLiteral(bytes)) => { - match other { - Type::Never - | Type::Any - | Type::Unknown - | Type::Unbound - | Type::Function(..) - | Type::Module(..) - | Type::Class(..) - | Type::Todo - | Type::Union(..) - | Type::Intersection(..) - | Type::Tuple(..) - | Type::None - | Type::BooleanLiteral(..) - | Type::IntLiteral(..) - | Type::StringLiteral(..) - | Type::LiteralString => { - unreachable!("handled above") - } - Type::BytesLiteral(bytes_other) => bytes != bytes_other, - Type::Instance(..) => false, - } - } + (Type::LiteralString, Type::LiteralString) => false, + (Type::LiteralString, Type::BytesLiteral(..)) + | (Type::BytesLiteral(..), Type::LiteralString) => false, + (Type::LiteralString, Type::Instance(class_type)) + | (Type::Instance(class_type), Type::LiteralString) => !matches!( + class_type.known(db), + Some(KnownClass::Str | KnownClass::Object) + ), - (Type::Instance(..), other) => { - match other { - Type::Never - | Type::Any - | Type::Unknown - | Type::Unbound - | Type::Function(..) - | Type::Module(..) - | Type::Class(..) - | Type::Todo - | Type::Union(..) - | Type::Intersection(..) - | Type::Tuple(..) - | Type::None - | Type::BooleanLiteral(..) - | Type::IntLiteral(..) - | Type::StringLiteral(..) - | Type::LiteralString - | Type::BytesLiteral(..) => { - unreachable!("handled above") - } - Type::Instance(..) => { - // TODO - false - } - } + (Type::BytesLiteral(left), Type::BytesLiteral(right)) => left != right, + (Type::BytesLiteral(..), Type::Instance(class_type)) + | (Type::Instance(class_type), Type::BytesLiteral(..)) => !matches!( + class_type.known(db), + Some(KnownClass::Bytes | KnownClass::Object) + ), + + (Type::Instance(..), Type::Instance(..)) => { + // TODO + false } } } @@ -1831,7 +1741,7 @@ mod tests { BytesLiteral(&'static str), BuiltinInstance(&'static str), Union(Vec), - Intersection(Vec, Vec), + Intersection { pos: Vec, neg: Vec }, Tuple(Vec), } @@ -1851,7 +1761,7 @@ mod tests { Ty::Union(tys) => { UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) } - Ty::Intersection(pos, neg) => { + Ty::Intersection { pos, neg } => { let mut builder = IntersectionBuilder::new(db); for p in pos { builder = builder.add_positive(p.into_type(db)); @@ -1947,7 +1857,7 @@ mod tests { #[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("b"))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(3))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(3), Ty::IntLiteral(4)]))] - #[test_case(Ty::Intersection(vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], vec![]), Ty::IntLiteral(2))] + #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], neg: vec![]}, Ty::IntLiteral(2))] #[test_case(Ty::Tuple(vec![Ty::BuiltinInstance("int")]), Ty::BuiltinInstance("int"))] #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1)]), Ty::Tuple(vec![Ty::IntLiteral(2)]))] #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1)]))] @@ -1975,7 +1885,7 @@ mod tests { #[test_case(Ty::StringLiteral("a"), Ty::BuiltinInstance("str"))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(2))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(2), Ty::IntLiteral(3)]))] - #[test_case(Ty::Intersection(vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(2)], vec![]), Ty::IntLiteral(2))] + #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(2)], neg: vec![]}, Ty::IntLiteral(2))] #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1), Ty::BuiltinInstance("int")]))] fn is_not_disjoint_from(a: Ty, b: Ty) { let db = setup_db(); From 76787c8a09d0b73ab9898ac2b24a8daa7c8227a1 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 11:26:28 +0200 Subject: [PATCH 22/38] Use Iterator::find --- .../red_knot_python_semantic/src/types/builder.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 860b759092d227..e261545131f49a 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -229,16 +229,13 @@ impl<'db> InnerIntersectionBuilder<'db> { // ~Literal[True] & bool = Literal[False] if let Type::Instance(class_type) = new_positive { if class_type.is_known(db, KnownClass::Bool) { - let mut found_bool_literal = None; - for neg in &self.negative { - if let Type::BooleanLiteral(bool) = neg { - found_bool_literal = Some(*bool); - } - } - - if let Some(bool) = found_bool_literal { + if let Some(&Type::BooleanLiteral(value)) = self + .negative + .iter() + .find(|element| matches!(element, Type::BooleanLiteral(..))) + { *self = Self::new(); - self.positive.insert(Type::BooleanLiteral(!bool)); + self.positive.insert(Type::BooleanLiteral(!value)); return; } } From e242909828a352fd2055d5c88a85d01d16f4e64d Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 14:59:31 +0200 Subject: [PATCH 23/38] Fix is_disjoint_from(tuple, other) --- crates/red_knot_python_semantic/src/types.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 748ae63e7a280d..92f0b108ace8bb 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -521,7 +521,16 @@ impl<'db> Type<'db> { true } } else { - true + // We can not be sure if the tuple is disjoint from 'other' because: + // - 'other' might be the homogeneous arbitrary-length tuple type + // tuple[T, ...] (which we don't have support for yet); if all + // of our element types are subtypes of T, this is not disjoint + // - 'other' might be a user subtype of tuple, which, if generic + // over the same or compatible *Ts, would overlap with tuple. + // + // TODO: add checks for the above cases once we support them + + false } } @@ -1858,7 +1867,6 @@ mod tests { #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(3))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(3), Ty::IntLiteral(4)]))] #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], neg: vec![]}, Ty::IntLiteral(2))] - #[test_case(Ty::Tuple(vec![Ty::BuiltinInstance("int")]), Ty::BuiltinInstance("int"))] #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1)]), Ty::Tuple(vec![Ty::IntLiteral(2)]))] #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1)]))] #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(3)]))] From 28f7b46079673398acb578ff933443e68066f781 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 15:13:15 +0200 Subject: [PATCH 24/38] Handle None, BooleanLiteral, IntLiteral, StringLiteral, BytesLiteral in common way --- crates/red_knot_python_semantic/src/types.rs | 57 +++++--------------- 1 file changed, 13 insertions(+), 44 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 92f0b108ace8bb..3337a6b095c53a 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -535,22 +535,18 @@ impl<'db> Type<'db> { } ( - Type::None, - Type::IntLiteral(_) - | Type::BooleanLiteral(_) + left @ (Type::None + | Type::BooleanLiteral(..) + | Type::IntLiteral(..) | Type::StringLiteral(..) - | Type::LiteralString - | Type::BytesLiteral(..), - ) - | ( - Type::IntLiteral(_) - | Type::BooleanLiteral(_) + | Type::BytesLiteral(..)), + right @ (Type::None + | Type::BooleanLiteral(..) + | Type::IntLiteral(..) | Type::StringLiteral(..) - | Type::LiteralString - | Type::BytesLiteral(..), - Type::None, - ) => true, - (Type::None, Type::None) => false, + | Type::BytesLiteral(..)), + ) => left != right, + (Type::None, Type::Instance(class_type)) | (Type::Instance(class_type), Type::None) => { !matches!( class_type.known(db), @@ -558,47 +554,20 @@ impl<'db> Type<'db> { ) } - ( - Type::BooleanLiteral(..), - Type::IntLiteral(_) - | Type::StringLiteral(..) - | Type::LiteralString - | Type::BytesLiteral(..), - ) - | ( - Type::IntLiteral(_) - | Type::StringLiteral(..) - | Type::LiteralString - | Type::BytesLiteral(..), - Type::BooleanLiteral(..), - ) => true, - (Type::BooleanLiteral(left), Type::BooleanLiteral(right)) => left != right, (Type::BooleanLiteral(..), Type::Instance(class_type)) | (Type::Instance(class_type), Type::BooleanLiteral(..)) => !matches!( class_type.known(db), Some(KnownClass::Bool | KnownClass::Object) ), - ( - Type::IntLiteral(..), - Type::StringLiteral(..) | Type::LiteralString | Type::BytesLiteral(..), - ) - | ( - Type::StringLiteral(..) | Type::LiteralString | Type::BytesLiteral(..), - Type::IntLiteral(..), - ) => true, - (Type::IntLiteral(left), Type::IntLiteral(right)) => left != right, (Type::IntLiteral(..), Type::Instance(class_type)) | (Type::Instance(class_type), Type::IntLiteral(..)) => !matches!( class_type.known(db), Some(KnownClass::Int | KnownClass::Object) ), - (Type::StringLiteral(left), Type::StringLiteral(right)) => left != right, (Type::StringLiteral(..), Type::LiteralString) | (Type::LiteralString, Type::StringLiteral(..)) => false, - (Type::StringLiteral(..), Type::BytesLiteral(..)) - | (Type::BytesLiteral(..), Type::StringLiteral(..)) => true, (Type::StringLiteral(..), Type::Instance(class_type)) | (Type::Instance(class_type), Type::StringLiteral(..)) => !matches!( class_type.known(db), @@ -606,15 +575,13 @@ impl<'db> Type<'db> { ), (Type::LiteralString, Type::LiteralString) => false, - (Type::LiteralString, Type::BytesLiteral(..)) - | (Type::BytesLiteral(..), Type::LiteralString) => false, (Type::LiteralString, Type::Instance(class_type)) | (Type::Instance(class_type), Type::LiteralString) => !matches!( class_type.known(db), Some(KnownClass::Str | KnownClass::Object) ), + (Type::LiteralString, _) | (_, Type::LiteralString) => true, - (Type::BytesLiteral(left), Type::BytesLiteral(right)) => left != right, (Type::BytesLiteral(..), Type::Instance(class_type)) | (Type::Instance(class_type), Type::BytesLiteral(..)) => !matches!( class_type.known(db), @@ -1864,6 +1831,7 @@ mod tests { #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(false))] #[test_case(Ty::IntLiteral(1), Ty::IntLiteral(2))] #[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("b"))] + #[test_case(Ty::LiteralString, Ty::BytesLiteral("a"))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(3))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(3), Ty::IntLiteral(4)]))] #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], neg: vec![]}, Ty::IntLiteral(2))] @@ -1891,6 +1859,7 @@ mod tests { #[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("a"))] #[test_case(Ty::StringLiteral("a"), Ty::LiteralString)] #[test_case(Ty::StringLiteral("a"), Ty::BuiltinInstance("str"))] + #[test_case(Ty::LiteralString, Ty::LiteralString)] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(2))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(2), Ty::IntLiteral(3)]))] #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(2)], neg: vec![]}, Ty::IntLiteral(2))] From 06ecd888a8caff2fe5762e7425bd7dfca732bc05 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 15:23:48 +0200 Subject: [PATCH 25/38] Handle None in a more general way --- crates/red_knot_python_semantic/src/types.rs | 69 +++++++++++--------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 3337a6b095c53a..1e244f10863755 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -509,31 +509,6 @@ impl<'db> Type<'db> { } } - (Type::Tuple(tuple), other) | (other, Type::Tuple(tuple)) => { - if let Type::Tuple(other_tuple) = other { - if tuple.len(db) == other_tuple.len(db) { - tuple - .elements(db) - .iter() - .zip(other_tuple.elements(db).iter()) - .any(|(e1, e2)| e1.is_disjoint_from(db, *e2)) - } else { - true - } - } else { - // We can not be sure if the tuple is disjoint from 'other' because: - // - 'other' might be the homogeneous arbitrary-length tuple type - // tuple[T, ...] (which we don't have support for yet); if all - // of our element types are subtypes of T, this is not disjoint - // - 'other' might be a user subtype of tuple, which, if generic - // over the same or compatible *Ts, would overlap with tuple. - // - // TODO: add checks for the above cases once we support them - - false - } - } - ( left @ (Type::None | Type::BooleanLiteral(..) @@ -547,11 +522,15 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..)), ) => left != right, - (Type::None, Type::Instance(class_type)) | (Type::Instance(class_type), Type::None) => { - !matches!( - class_type.known(db), - Some(KnownClass::NoneType | KnownClass::Object) - ) + (Type::None, other) | (other, Type::None) => { + if let Type::Instance(class_type) = other { + !matches!( + class_type.known(db), + Some(KnownClass::NoneType | KnownClass::Object) + ) + } else { + true + } } (Type::BooleanLiteral(..), Type::Instance(class_type)) @@ -592,6 +571,31 @@ impl<'db> Type<'db> { // TODO false } + + (Type::Tuple(tuple), other) | (other, Type::Tuple(tuple)) => { + if let Type::Tuple(other_tuple) = other { + if tuple.len(db) == other_tuple.len(db) { + tuple + .elements(db) + .iter() + .zip(other_tuple.elements(db).iter()) + .any(|(e1, e2)| e1.is_disjoint_from(db, *e2)) + } else { + true + } + } else { + // We can not be sure if the tuple is disjoint from 'other' because: + // - 'other' might be the homogeneous arbitrary-length tuple type + // tuple[T, ...] (which we don't have support for yet); if all + // of our element types are subtypes of T, this is not disjoint + // - 'other' might be a user subtype of tuple, which, if generic + // over the same or compatible *Ts, would overlap with tuple. + // + // TODO: add checks for the above cases once we support them + + false + } + } } } @@ -1825,8 +1829,13 @@ mod tests { #[test_case(Ty::Never, Ty::Never)] #[test_case(Ty::Never, Ty::None)] #[test_case(Ty::Never, Ty::BuiltinInstance("int"))] + #[test_case(Ty::None, Ty::BoolLiteral(true))] + #[test_case(Ty::None, Ty::IntLiteral(1))] #[test_case(Ty::None, Ty::StringLiteral("test"))] + #[test_case(Ty::None, Ty::BytesLiteral("test"))] + #[test_case(Ty::None, Ty::LiteralString)] #[test_case(Ty::None, Ty::BuiltinInstance("int"))] + #[test_case(Ty::None, Ty::Tuple(vec![Ty::None]))] #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("int"))] #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(false))] #[test_case(Ty::IntLiteral(1), Ty::IntLiteral(2))] From 57b6949532c285a230cb665fde0ebb861526964e Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 15:39:42 +0200 Subject: [PATCH 26/38] Same for (Boolean|Int|String|Bytes)Literal --- crates/red_knot_python_semantic/src/types.rs | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1e244f10863755..c9e461e1a5fea9 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -522,28 +522,27 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..)), ) => left != right, - (Type::None, other) | (other, Type::None) => { - if let Type::Instance(class_type) = other { - !matches!( - class_type.known(db), - Some(KnownClass::NoneType | KnownClass::Object) - ) - } else { - true - } + (Type::None, Type::Instance(class_type)) | (Type::Instance(class_type), Type::None) => { + !matches!( + class_type.known(db), + Some(KnownClass::NoneType | KnownClass::Object) + ) } + (Type::None, _) | (_, Type::None) => true, (Type::BooleanLiteral(..), Type::Instance(class_type)) | (Type::Instance(class_type), Type::BooleanLiteral(..)) => !matches!( class_type.known(db), Some(KnownClass::Bool | KnownClass::Object) ), + (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true, (Type::IntLiteral(..), Type::Instance(class_type)) | (Type::Instance(class_type), Type::IntLiteral(..)) => !matches!( class_type.known(db), Some(KnownClass::Int | KnownClass::Object) ), + (Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true, (Type::StringLiteral(..), Type::LiteralString) | (Type::LiteralString, Type::StringLiteral(..)) => false, @@ -552,6 +551,7 @@ impl<'db> Type<'db> { class_type.known(db), Some(KnownClass::Str | KnownClass::Object) ), + (Type::StringLiteral(..), _) | (_, Type::StringLiteral(..)) => true, (Type::LiteralString, Type::LiteralString) => false, (Type::LiteralString, Type::Instance(class_type)) @@ -566,6 +566,7 @@ impl<'db> Type<'db> { class_type.known(db), Some(KnownClass::Bytes | KnownClass::Object) ), + (Type::BytesLiteral(..), _) | (_, Type::BytesLiteral(..)) => true, (Type::Instance(..), Type::Instance(..)) => { // TODO @@ -1838,9 +1839,14 @@ mod tests { #[test_case(Ty::None, Ty::Tuple(vec![Ty::None]))] #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("int"))] #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(false))] + #[test_case(Ty::BoolLiteral(true), Ty::Tuple(vec![Ty::None]))] #[test_case(Ty::IntLiteral(1), Ty::IntLiteral(2))] + #[test_case(Ty::IntLiteral(1), Ty::Tuple(vec![Ty::None]))] #[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("b"))] + #[test_case(Ty::StringLiteral("a"), Ty::Tuple(vec![Ty::None]))] #[test_case(Ty::LiteralString, Ty::BytesLiteral("a"))] + #[test_case(Ty::BytesLiteral("a"), Ty::BytesLiteral("b"))] + #[test_case(Ty::BytesLiteral("a"), Ty::Tuple(vec![Ty::None]))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(3))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(3), Ty::IntLiteral(4)]))] #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], neg: vec![]}, Ty::IntLiteral(2))] From 599c9d675e3284d9af235f32d674ffba5ade12ea Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 15:46:21 +0200 Subject: [PATCH 27/38] Add test for BytesLiteral vs StringLiteral --- crates/red_knot_python_semantic/src/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index c9e461e1a5fea9..77a9fffce56b9c 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1847,6 +1847,7 @@ mod tests { #[test_case(Ty::LiteralString, Ty::BytesLiteral("a"))] #[test_case(Ty::BytesLiteral("a"), Ty::BytesLiteral("b"))] #[test_case(Ty::BytesLiteral("a"), Ty::Tuple(vec![Ty::None]))] + #[test_case(Ty::BytesLiteral("a"), Ty::StringLiteral("a"))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(3))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(3), Ty::IntLiteral(4)]))] #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], neg: vec![]}, Ty::IntLiteral(2))] From 7992f1dc59c7e756cdbe76e115b32975eacf0f89 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 15:49:27 +0200 Subject: [PATCH 28/38] Add TODO comment --- crates/red_knot_python_semantic/src/types.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 77a9fffce56b9c..dfc3316df9553d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -569,7 +569,9 @@ impl<'db> Type<'db> { (Type::BytesLiteral(..), _) | (_, Type::BytesLiteral(..)) => true, (Type::Instance(..), Type::Instance(..)) => { - // TODO + // TODO: once we have support for `final`, there might be some cases where + // we can determine that two types are disjoint. For non-final classes, we + // return false (multiple inheritance). false } From ebd4b6e6a88010a185a4c267644c2c7ea811f3f5 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 15:54:37 +0200 Subject: [PATCH 29/38] Use test_cases instead of loop --- .../src/types/builder.rs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index e261545131f49a..c4e388a9a152d0 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -367,6 +367,7 @@ mod tests { use crate::types::{KnownClass, UnionBuilder}; use crate::ProgramSettings; use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; + use test_case::test_case; fn setup_db() -> TestDb { let db = TestDb::new(); @@ -747,26 +748,25 @@ mod tests { assert_eq!(ty, t_p); } - #[test] - fn build_intersection_simplify_split_bool() { + #[test_case(true)] + #[test_case(false)] + fn build_intersection_simplify_split_bool(bool_value: bool) { let db = setup_db(); let t_p = KnownClass::Bool.to_instance(&db); - for bool in [true, false] { - let t_n = Type::BooleanLiteral(bool); + let t_n = Type::BooleanLiteral(bool_value); - let ty = IntersectionBuilder::new(&db) - .add_positive(t_p) - .add_negative(t_n) - .build(); - assert_eq!(ty, Type::BooleanLiteral(!bool)); + let ty = IntersectionBuilder::new(&db) + .add_positive(t_p) + .add_negative(t_n) + .build(); + assert_eq!(ty, Type::BooleanLiteral(!bool_value)); - let ty = IntersectionBuilder::new(&db) - .add_negative(t_n) - .add_positive(t_p) - .build(); - assert_eq!(ty, Type::BooleanLiteral(!bool)); - } + let ty = IntersectionBuilder::new(&db) + .add_negative(t_n) + .add_positive(t_p) + .build(); + assert_eq!(ty, Type::BooleanLiteral(!bool_value)); } } From 8d0ff15803d67497251848bce5f19dd407519e59 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 16:13:32 +0200 Subject: [PATCH 30/38] Fix TODO regarding Any/Unknown/Todo --- .../src/types/builder.rs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index c4e388a9a152d0..a2691cdf5227e8 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -217,7 +217,6 @@ impl<'db> InnerIntersectionBuilder<'db> { /// Adds a positive type to this intersection. fn add_positive(&mut self, db: &'db dyn Db, new_positive: Type<'db>) { - // TODO `Any`/`Unknown`/`Todo` actually should not self-cancel if let Type::Intersection(other) = new_positive { for pos in other.positive(db) { self.add_positive(db, *pos); @@ -250,6 +249,7 @@ impl<'db> InnerIntersectionBuilder<'db> { // same rule, reverse order if new_positive.is_subtype_of(db, *existing_positive) { to_remove = Some(index); + // no break here, we want to apply the other rules as well. } // A & B = Never if A and B are disjoint if new_positive.is_disjoint_from(db, *existing_positive) { @@ -271,6 +271,7 @@ impl<'db> InnerIntersectionBuilder<'db> { // A & ~B = A if A and B are disjoint if existing_negative.is_disjoint_from(db, new_positive) { to_remove = Some(index); + break; } } if let Some(index) = to_remove { @@ -283,7 +284,6 @@ impl<'db> InnerIntersectionBuilder<'db> { /// Adds a negative type to this intersection. fn add_negative(&mut self, db: &'db dyn Db, new_negative: Type<'db>) { - // TODO `Any`/`Unknown`/`Todo` actually should not self-cancel match new_negative { Type::Intersection(inter) => { for pos in inter.positive(db) { @@ -294,6 +294,12 @@ impl<'db> InnerIntersectionBuilder<'db> { } } Type::Unbound => {} + ty @ (Type::Any | Type::Unknown | Type::Todo) => { + // Adding any of these types to the negative side of an intersection + // is equivalent to adding it to the positive side. We do this to + // simplify the representation. + self.positive.insert(ty); + } // ~Literal[True] & bool = Literal[False] Type::BooleanLiteral(bool) if self @@ -555,8 +561,8 @@ mod tests { .build() .expect_intersection(); - assert_eq!(intersection.pos_vec(&db), &[t1]); - assert_eq!(intersection.neg_vec(&db), &[ta]); + assert_eq!(intersection.pos_vec(&db), &[ta, t1]); + assert_eq!(intersection.neg_vec(&db), &[]); } #[test] @@ -769,4 +775,23 @@ mod tests { .build(); assert_eq!(ty, Type::BooleanLiteral(!bool_value)); } + + #[test_case(Type::Any)] + #[test_case(Type::Unknown)] + #[test_case(Type::Todo)] + fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) { + let db = setup_db(); + + let result = IntersectionBuilder::new(&db) + .add_positive(ty) + .add_negative(ty) + .build(); + assert_eq!(result, ty); + + let result = IntersectionBuilder::new(&db) + .add_negative(ty) + .add_positive(ty) + .build(); + assert_eq!(result, ty); + } } From 3282c0c803e31ee283e2b032af1e92da9de66c57 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 20:02:09 +0200 Subject: [PATCH 31/38] Fix comment regarding tuples --- crates/red_knot_python_semantic/src/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index dfc3316df9553d..181ade4ae6917e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -589,8 +589,8 @@ impl<'db> Type<'db> { } else { // We can not be sure if the tuple is disjoint from 'other' because: // - 'other' might be the homogeneous arbitrary-length tuple type - // tuple[T, ...] (which we don't have support for yet); if all - // of our element types are subtypes of T, this is not disjoint + // tuple[T, ...] (which we don't have support for yet); if all of + // our element types are not disjoint with T, this is not disjoint // - 'other' might be a user subtype of tuple, which, if generic // over the same or compatible *Ts, would overlap with tuple. // From 1b663581734f38f16c03f81bb64520df6badfb12 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 20:30:05 +0200 Subject: [PATCH 32/38] Add support for removing multiple elements in one go --- .../src/types/builder.rs | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index a2691cdf5227e8..f4eca865baaccb 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -240,7 +240,7 @@ impl<'db> InnerIntersectionBuilder<'db> { } } - let mut to_remove = None; + let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_positive) in self.positive.iter().enumerate() { // S & T = S if S <: T if existing_positive.is_subtype_of(db, new_positive) { @@ -248,8 +248,7 @@ impl<'db> InnerIntersectionBuilder<'db> { } // same rule, reverse order if new_positive.is_subtype_of(db, *existing_positive) { - to_remove = Some(index); - // no break here, we want to apply the other rules as well. + to_remove.push(index); } // A & B = Never if A and B are disjoint if new_positive.is_disjoint_from(db, *existing_positive) { @@ -257,11 +256,11 @@ impl<'db> InnerIntersectionBuilder<'db> { return; } } - if let Some(index) = to_remove { - self.positive.remove_index(index); + for index in to_remove.iter().rev() { + self.positive.swap_remove_index(*index); } - let mut to_remove = None; + let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_negative) in self.negative.iter().enumerate() { // S & ~T = Never if S <: T if new_positive.is_subtype_of(db, *existing_negative) { @@ -270,12 +269,11 @@ impl<'db> InnerIntersectionBuilder<'db> { } // A & ~B = A if A and B are disjoint if existing_negative.is_disjoint_from(db, new_positive) { - to_remove = Some(index); - break; + to_remove.push(index); } } - if let Some(index) = to_remove { - self.negative.remove_index(index); + for index in to_remove.iter().rev() { + self.negative.swap_remove_index(*index); } self.positive.insert(new_positive); @@ -311,19 +309,19 @@ impl<'db> InnerIntersectionBuilder<'db> { self.positive.insert(Type::BooleanLiteral(!bool)); } _ => { - let mut to_remove = None; + let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_negative) in self.negative.iter().enumerate() { // ~S & ~T = ~T if S <: T if existing_negative.is_subtype_of(db, new_negative) { - to_remove = Some(index); + to_remove.push(index); } // same rule, reverse order if new_negative.is_subtype_of(db, *existing_negative) { return; } } - if let Some(index) = to_remove { - self.negative.remove_index(index); + for index in to_remove.iter().rev() { + self.negative.swap_remove_index(*index); } for existing_positive in &self.positive { @@ -699,6 +697,24 @@ mod tests { assert_eq!(ty, expected); } + #[test] + fn build_intersection_simplify_negative_type_and_multiple_negative_subtypes() { + let db = setup_db(); + + let s1 = Type::IntLiteral(1); + let s2 = Type::IntLiteral(2); + let t = KnownClass::Int.to_instance(&db); + + let expected = IntersectionBuilder::new(&db).add_negative(t).build(); + + let ty = IntersectionBuilder::new(&db) + .add_negative(s1) + .add_negative(s2) + .add_negative(t) + .build(); + assert_eq!(ty, expected); + } + #[test] fn build_intersection_simplify_negative_type_and_positive_subtype() { let db = setup_db(); From 9eddcbecbe7fbf3ff7a08f82e24d9f8ef5a50d8c Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 21:13:39 +0200 Subject: [PATCH 33/38] Add 'object' as a third type to the boolean simplification tests --- .../src/types/builder.rs | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index f4eca865baaccb..b73915afd8077e 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -775,19 +775,39 @@ mod tests { fn build_intersection_simplify_split_bool(bool_value: bool) { let db = setup_db(); - let t_p = KnownClass::Bool.to_instance(&db); + let t_bool = KnownClass::Bool.to_instance(&db); + let t_bool_literal = Type::BooleanLiteral(bool_value); - let t_n = Type::BooleanLiteral(bool_value); + // 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 ty = IntersectionBuilder::new(&db) - .add_positive(t_p) - .add_negative(t_n) + .add_positive(t_object) + .add_positive(t_bool) + .add_negative(t_bool_literal) .build(); assert_eq!(ty, Type::BooleanLiteral(!bool_value)); let ty = IntersectionBuilder::new(&db) - .add_negative(t_n) - .add_positive(t_p) + .add_positive(t_bool) + .add_positive(t_object) + .add_negative(t_bool_literal) + .build(); + assert_eq!(ty, Type::BooleanLiteral(!bool_value)); + + let ty = IntersectionBuilder::new(&db) + .add_positive(t_object) + .add_negative(t_bool_literal) + .add_positive(t_bool) + .build(); + assert_eq!(ty, Type::BooleanLiteral(!bool_value)); + + let ty = IntersectionBuilder::new(&db) + .add_negative(t_bool_literal) + .add_positive(t_object) + .add_positive(t_bool) .build(); assert_eq!(ty, Type::BooleanLiteral(!bool_value)); } From e0cfef424e43f986835ba49ff256106c039dac67 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 21:49:10 +0200 Subject: [PATCH 34/38] Add 'third' type to simplification unit tests --- .../src/types/builder.rs | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index b73915afd8077e..b048516e1fd0d7 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -368,7 +368,7 @@ mod tests { use crate::db::tests::TestDb; use crate::program::{Program, SearchPathSettings}; use crate::python_version::PythonVersion; - use crate::types::{KnownClass, UnionBuilder}; + use crate::types::{KnownClass, StringLiteralType, UnionBuilder}; use crate::ProgramSettings; use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use test_case::test_case; @@ -673,6 +673,26 @@ mod tests { .add_positive(t) .build(); assert_eq!(ty, s); + + let literal = Type::StringLiteral(StringLiteralType::new(&db, "a")); + let expected = IntersectionBuilder::new(&db) + .add_positive(s) + .add_negative(literal) + .build(); + + let ty = IntersectionBuilder::new(&db) + .add_positive(t) + .add_negative(literal) + .add_positive(s) + .build(); + assert_eq!(ty, expected); + + let ty = IntersectionBuilder::new(&db) + .add_positive(s) + .add_negative(literal) + .add_positive(t) + .build(); + assert_eq!(ty, expected); } #[test] @@ -695,6 +715,19 @@ mod tests { .add_negative(t) .build(); assert_eq!(ty, expected); + + let object = KnownClass::Object.to_instance(&db); + let expected = IntersectionBuilder::new(&db) + .add_negative(t) + .add_positive(object) + .build(); + + let ty = IntersectionBuilder::new(&db) + .add_negative(t) + .add_positive(object) + .add_negative(s) + .build(); + assert_eq!(ty, expected); } #[test] @@ -733,6 +766,21 @@ mod tests { .add_negative(t) .build(); assert_eq!(ty, Type::Never); + + // This should also work in the presence of additional contributions: + let ty = IntersectionBuilder::new(&db) + .add_positive(KnownClass::Object.to_instance(&db)) + .add_negative(t) + .add_positive(s) + .build(); + assert_eq!(ty, Type::Never); + + let ty = IntersectionBuilder::new(&db) + .add_positive(s) + .add_negative(Type::StringLiteral(StringLiteralType::new(&db, "a"))) + .add_negative(t) + .build(); + assert_eq!(ty, Type::Never); } #[test] @@ -746,7 +794,15 @@ mod tests { .add_positive(t1) .add_positive(t2) .build(); + assert_eq!(ty, Type::Never); + // If there are any negative contributions, they should + // be removed too. + let ty = IntersectionBuilder::new(&db) + .add_positive(KnownClass::Str.to_instance(&db)) + .add_negative(Type::LiteralString) + .add_positive(t2) + .build(); assert_eq!(ty, Type::Never); } @@ -768,6 +824,26 @@ mod tests { .add_positive(t_p) .build(); assert_eq!(ty, t_p); + + let int_literal = Type::IntLiteral(1); + let expected = IntersectionBuilder::new(&db) + .add_positive(t_p) + .add_negative(int_literal) + .build(); + + let ty = IntersectionBuilder::new(&db) + .add_positive(t_p) + .add_negative(int_literal) + .add_negative(t_n) + .build(); + assert_eq!(ty, expected); + + let ty = IntersectionBuilder::new(&db) + .add_negative(t_n) + .add_negative(int_literal) + .add_positive(t_p) + .build(); + assert_eq!(ty, expected); } #[test_case(true)] From 34b03839f8dc9328a8305babb86577cde5bca3d3 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 22:15:13 +0200 Subject: [PATCH 35/38] Fix is_disjoint for function/class/module --- crates/red_knot_python_semantic/src/types.rs | 24 ++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 181ade4ae6917e..e415c3d9957e1f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -484,11 +484,6 @@ impl<'db> Type<'db> { (Type::Unbound, _) | (_, Type::Unbound) => false, (Type::Todo, _) | (_, Type::Todo) => false, - (ty @ (Type::Function(..) | Type::Module(..) | Type::Class(..)), other) - | (other, ty @ (Type::Function(..) | Type::Module(..) | Type::Class(..))) => { - ty != other - } - (Type::Union(union), other) | (other, Type::Union(union)) => union .elements(db) .iter() @@ -514,12 +509,18 @@ impl<'db> Type<'db> { | Type::BooleanLiteral(..) | Type::IntLiteral(..) | Type::StringLiteral(..) - | Type::BytesLiteral(..)), + | Type::BytesLiteral(..) + | Type::Function(..) + | Type::Module(..) + | Type::Class(..)), right @ (Type::None | Type::BooleanLiteral(..) | Type::IntLiteral(..) | Type::StringLiteral(..) - | Type::BytesLiteral(..)), + | Type::BytesLiteral(..) + | Type::Function(..) + | Type::Module(..) + | Type::Class(..)), ) => left != right, (Type::None, Type::Instance(class_type)) | (Type::Instance(class_type), Type::None) => { @@ -568,6 +569,15 @@ impl<'db> Type<'db> { ), (Type::BytesLiteral(..), _) | (_, Type::BytesLiteral(..)) => true, + ( + Type::Function(..) | Type::Module(..) | Type::Class(..), + Type::Instance(class_type), + ) + | ( + Type::Instance(class_type), + Type::Function(..) | Type::Module(..) | Type::Class(..), + ) => !class_type.is_known(db, KnownClass::Object), + (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. For non-final classes, we From eaa3d48ad9ee8f3cabfa3c192f52d044f08d1909 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 22:24:10 +0200 Subject: [PATCH 36/38] Add test for union of class types --- crates/red_knot_python_semantic/src/types.rs | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e415c3d9957e1f..e3e42e9eeacf6b 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1901,6 +1901,29 @@ mod tests { assert!(!b.is_disjoint_from(&db, a)); } + #[test] + fn is_disjoint_from_union_of_class_types() { + let mut db = setup_db(); + db.write_dedented( + "/src/module.py", + " + class A: ... + class B: ... + x = A if flag else B + ", + ) + .unwrap(); + let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap(); + + let type_a = super::global_symbol_ty(&db, module, "A"); + let type_x = super::global_symbol_ty(&db, module, "x"); + + assert!(matches!(type_a, Type::Class(_))); + assert!(matches!(type_x, Type::Union(_))); + + assert!(!type_a.is_disjoint_from(&db, type_x)); + } + #[test_case(Ty::None)] #[test_case(Ty::BoolLiteral(true))] #[test_case(Ty::BoolLiteral(false))] From 3d4bd5209e2efd57da77e0c444c9da2371cf053b Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 18 Oct 2024 22:26:03 +0200 Subject: [PATCH 37/38] Add TODO comment regarding KnownClass::Type --- crates/red_knot_python_semantic/src/types.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e3e42e9eeacf6b..49038596cf0e49 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -582,6 +582,9 @@ impl<'db> Type<'db> { // TODO: once we have support for `final`, there might be some cases where // we can determine that two types are disjoint. For non-final classes, we // return false (multiple inheritance). + + // TODO: is there anything specific to do for instances of KnownClass::Type? + false } From b3b0ffbe1cbac676be41f624bae9e1027f872b4e Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Oct 2024 14:26:17 -0700 Subject: [PATCH 38/38] bool and int are not disjoint --- crates/red_knot_python_semantic/src/types.rs | 6 ++++-- crates/red_knot_python_semantic/src/types/builder.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 49038596cf0e49..6db3cae79833ff 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -534,7 +534,7 @@ impl<'db> Type<'db> { (Type::BooleanLiteral(..), Type::Instance(class_type)) | (Type::Instance(class_type), Type::BooleanLiteral(..)) => !matches!( class_type.known(db), - Some(KnownClass::Bool | KnownClass::Object) + Some(KnownClass::Bool | KnownClass::Int | KnownClass::Object) ), (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true, @@ -1852,9 +1852,10 @@ mod tests { #[test_case(Ty::None, Ty::LiteralString)] #[test_case(Ty::None, Ty::BuiltinInstance("int"))] #[test_case(Ty::None, Ty::Tuple(vec![Ty::None]))] - #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("int"))] #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(false))] #[test_case(Ty::BoolLiteral(true), Ty::Tuple(vec![Ty::None]))] + #[test_case(Ty::BoolLiteral(true), Ty::IntLiteral(1))] + #[test_case(Ty::BoolLiteral(false), Ty::IntLiteral(0))] #[test_case(Ty::IntLiteral(1), Ty::IntLiteral(2))] #[test_case(Ty::IntLiteral(1), Ty::Tuple(vec![Ty::None]))] #[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("b"))] @@ -1886,6 +1887,7 @@ mod tests { #[test_case(Ty::BoolLiteral(true), Ty::BoolLiteral(true))] #[test_case(Ty::BoolLiteral(false), Ty::BoolLiteral(false))] #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("bool"))] + #[test_case(Ty::BoolLiteral(true), Ty::BuiltinInstance("int"))] #[test_case(Ty::IntLiteral(1), Ty::IntLiteral(1))] #[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("a"))] #[test_case(Ty::StringLiteral("a"), Ty::LiteralString)] diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index b048516e1fd0d7..4cf17ba4f6ddc2 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -811,7 +811,7 @@ mod tests { let db = setup_db(); let t_p = KnownClass::Int.to_instance(&db); - let t_n = Type::BooleanLiteral(false); + let t_n = Type::StringLiteral(StringLiteralType::new(&db, "t_n")); let ty = IntersectionBuilder::new(&db) .add_positive(t_p)