From 80136d81f680ec82e331fa419c850e178b91d1ea Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 28 Jan 2025 12:15:34 +0000 Subject: [PATCH] take 7? --- .../resources/mdtest/comparison/tuples.md | 17 +---- .../mdtest/exception/control_flow.md | 6 +- .../resources/mdtest/narrow/truthiness.md | 8 +-- .../type_properties/is_assignable_to.md | 8 +++ crates/red_knot_python_semantic/src/types.rs | 70 ++++++++----------- .../src/types/builder.rs | 31 +++++++- .../src/types/display.rs | 26 +------ 7 files changed, 76 insertions(+), 90 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md index 250ded168f..8fe7f29541 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md @@ -58,22 +58,7 @@ reveal_type(c >= d) # revealed: Literal[True] #### Results with Ambiguity ```py -class P: - def __lt__(self, other: "P") -> bool: - return True - - def __le__(self, other: "P") -> bool: - return True - - def __gt__(self, other: "P") -> bool: - return True - - def __ge__(self, other: "P") -> bool: - return True - -class Q(P): ... - -def _(x: P, y: Q): +def _(x: bool, y: int): a = (x,) b = (y,) diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md b/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md index a6e703dff5..284b0f24d5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md @@ -455,9 +455,9 @@ else: reveal_type(x) # revealed: slice finally: # TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice` - reveal_type(x) # revealed: bool | slice | float + reveal_type(x) # revealed: bool | float | slice -reveal_type(x) # revealed: bool | slice | float +reveal_type(x) # revealed: bool | float | slice ``` ## Nested `try`/`except` blocks @@ -534,7 +534,7 @@ try: reveal_type(x) # revealed: slice finally: # TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice` - reveal_type(x) # revealed: bool | slice | float + reveal_type(x) # revealed: bool | float | slice x = 2 reveal_type(x) # revealed: Literal[2] reveal_type(x) # revealed: Literal[2] diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md index b1129498ce..b3975c1a81 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md @@ -21,22 +21,22 @@ else: if x and not x: reveal_type(x) # revealed: Never else: - reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | tuple[()] | None + reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] if not (x and not x): - reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | tuple[()] | None + reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] else: reveal_type(x) # revealed: Never if x or not x: - reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | tuple[()] | None + reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] else: reveal_type(x) # revealed: Never if not (x or not x): reveal_type(x) # revealed: Never else: - reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | tuple[()] | None + reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] if (isinstance(x, int) or isinstance(x, str)) and x: reveal_type(x) # revealed: Literal[-1, True, "foo"] diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index e51cc4dcaa..318af24247 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -358,4 +358,12 @@ from knot_extensions import is_assignable_to, static_assert static_assert(is_assignable_to(bool, str | bool)) ``` +### `bool` is assignable to `AlwaysTruthy | AlwaysFalsy` + +```py +from knot_extensions import static_assert, is_assignable_to, AlwaysTruthy, AlwaysFalsy + +static_assert(is_assignable_to(bool, AlwaysTruthy | AlwaysFalsy)) +``` + [typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 893ad9f9f7..2b87e14b8c 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -824,11 +824,9 @@ impl<'db> Type<'db> { /// - Literal[True, False] | T <: bool | T #[must_use] pub fn with_normalized_bools(self, db: &'db dyn Db) -> Self { - const LITERAL_BOOLS: [Type; 2] = [Type::BooleanLiteral(false), Type::BooleanLiteral(true)]; - match self { Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Bool) => { - Type::Union(UnionType::new(db, Box::from(LITERAL_BOOLS))) + Type::normalized_bool(db) } // TODO: decompose `LiteralString` into `Literal[""] | TruthyLiteralString`? // We'd need to rename this method... --Alex @@ -884,12 +882,6 @@ impl<'db> Type<'db> { return true; } - let normalized_self = self.with_normalized_bools(db); - let normalized_target = target.with_normalized_bools(db); - if normalized_self != self || normalized_target != target { - return normalized_self.is_subtype_of(db, normalized_target); - } - // Non-fully-static types do not participate in subtyping. // // Type `A` can only be a subtype of type `B` if the set of possible runtime objects @@ -912,6 +904,13 @@ impl<'db> Type<'db> { (Type::Never, _) => true, (_, Type::Never) => false, + (Type::Instance(InstanceType { class }), _) if class.is_known(db, KnownClass::Bool) => { + Type::normalized_bool(db).is_subtype_of(db, target) + } + (_, Type::Instance(InstanceType { class })) if class.is_known(db, KnownClass::Bool) => { + self.is_boolean_literal() + } + (Type::Union(union), _) => union .elements(db) .iter() @@ -1108,11 +1107,7 @@ impl<'db> Type<'db> { if self.is_gradual_equivalent_to(db, target) { return true; } - let normalized_self = self.with_normalized_bools(db); - let normalized_target = target.with_normalized_bools(db); - if normalized_self != self || normalized_target != target { - return normalized_self.is_assignable_to(db, normalized_target); - } + match (self, target) { // Never can be assigned to any type. (Type::Never, _) => true, @@ -1129,6 +1124,13 @@ impl<'db> Type<'db> { true } + (Type::Instance(InstanceType { class }), _) if class.is_known(db, KnownClass::Bool) => { + Type::normalized_bool(db).is_assignable_to(db, target) + } + (_, Type::Instance(InstanceType { class })) if class.is_known(db, KnownClass::Bool) => { + self.is_assignable_to(db, Type::normalized_bool(db)) + } + // A union is assignable to a type T iff every element of the union is assignable to T. (Type::Union(union), ty) => union .elements(db) @@ -1213,13 +1215,6 @@ impl<'db> Type<'db> { pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { // TODO equivalent but not identical types: TypedDicts, Protocols, type aliases, etc. - let normalized_self = self.with_normalized_bools(db); - let normalized_other = other.with_normalized_bools(db); - - if normalized_self != self || normalized_other != other { - return normalized_self.is_equivalent_to(db, normalized_other); - } - match (self, other) { (Type::Union(left), Type::Union(right)) => left.is_equivalent_to(db, right), (Type::Intersection(left), Type::Intersection(right)) => { @@ -1261,13 +1256,6 @@ impl<'db> Type<'db> { /// /// [Summary of type relations]: https://typing.readthedocs.io/en/latest/spec/concepts.html#summary-of-type-relations pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { - let normalized_self = self.with_normalized_bools(db); - let normalized_other = other.with_normalized_bools(db); - - if normalized_self != self || normalized_other != other { - return normalized_self.is_gradual_equivalent_to(db, normalized_other); - } - if self == other { return true; } @@ -1300,12 +1288,6 @@ impl<'db> Type<'db> { /// 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 { - let normalized_self = self.with_normalized_bools(db); - let normalized_other = other.with_normalized_bools(db); - if normalized_self != self || normalized_other != other { - return normalized_self.is_disjoint_from(db, normalized_other); - } - match (self, other) { (Type::Never, _) | (_, Type::Never) => true, @@ -2427,6 +2409,13 @@ impl<'db> Type<'db> { KnownClass::NoneType.to_instance(db) } + /// The type `Literal[True, False]`, which is exactly equivalent to `bool` + /// (and which `bool` is eagerly normalized to in several situations) + pub fn normalized_bool(db: &'db dyn Db) -> Type<'db> { + const LITERAL_BOOLS: [Type; 2] = [Type::BooleanLiteral(false), Type::BooleanLiteral(true)]; + Type::Union(UnionType::new(db, Box::from(LITERAL_BOOLS))) + } + /// Return the type of `tuple(sys.version_info)`. /// /// This is not exactly the type that `sys.version_info` has at runtime, @@ -4698,19 +4687,18 @@ pub struct TupleType<'db> { } impl<'db> TupleType<'db> { - pub fn from_elements(db: &'db dyn Db, types: I) -> Type<'db> - where - I: IntoIterator, - T: Into>, - { + pub fn from_elements>>( + db: &'db dyn Db, + types: impl IntoIterator, + ) -> Type<'db> { let mut elements = vec![]; for ty in types { - let ty: Type<'db> = ty.into(); + let ty = ty.into(); if ty.is_never() { return Type::Never; } - elements.push(ty.with_normalized_bools(db)); + elements.push(ty); } Type::Tuple(Self::new(db, elements.into_boxed_slice())) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 51cefb7db3..193a63a1a3 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -103,10 +103,35 @@ impl<'db> UnionBuilder<'db> { } pub(crate) fn build(self) -> Type<'db> { - match self.elements.len() { + let UnionBuilder { elements, db } = self; + + match elements.len() { 0 => Type::Never, - 1 => self.elements[0], - _ => Type::Union(UnionType::new(self.db, self.elements.into_boxed_slice())), + 1 => elements[0], + _ => { + let mut normalized_elements = Vec::with_capacity(elements.len()); + let mut first_bool_literal_pos = None; + let mut seen_two_bool_literals = false; + for (i, element) in elements.into_iter().enumerate() { + if element.is_boolean_literal() { + if first_bool_literal_pos.is_none() { + first_bool_literal_pos = Some(i); + } else { + seen_two_bool_literals = true; + continue; + } + } + normalized_elements.push(element); + } + if let (Some(pos), true) = (first_bool_literal_pos, seen_two_bool_literals) { + // If we have two boolean literals, we can merge them to `bool`. + if normalized_elements.len() == 1 { + return KnownClass::Bool.to_instance(db); + } + normalized_elements[pos] = KnownClass::Bool.to_instance(db); + } + Type::Union(UnionType::new(db, normalized_elements.into_boxed_slice())) + } } } } diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 8769c8e021..50c3ae1fff 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -1,6 +1,5 @@ //! Display implementations for types. -use std::borrow::Cow; use std::fmt::{self, Display, Formatter, Write}; use ruff_db::display::FormatterJoinExtension; @@ -152,31 +151,12 @@ struct DisplayUnionType<'db> { impl Display for DisplayUnionType<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut elements = Cow::Borrowed(self.ty.elements(self.db)); - - if let Some(literal_false_pos) = elements - .iter() - .position(|ty| matches!(ty, Type::BooleanLiteral(false))) - { - if let Some(literal_true_pos) = elements - .iter() - .position(|ty| matches!(ty, Type::BooleanLiteral(true))) - { - let (min, max) = if literal_false_pos < literal_true_pos { - (literal_false_pos, literal_true_pos) - } else { - (literal_true_pos, literal_false_pos) - }; - let mutable_elements = elements.to_mut(); - mutable_elements.swap_remove(max); - mutable_elements[min] = KnownClass::Bool.to_instance(self.db); - } - } + let elements = self.ty.elements(self.db); // Group condensed-display types by kind. let mut grouped_condensed_kinds = FxHashMap::default(); - for element in &*elements { + for element in elements { if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) { grouped_condensed_kinds .entry(kind) @@ -187,7 +167,7 @@ impl Display for DisplayUnionType<'_> { let mut join = f.join(" | "); - for element in &*elements { + for element in elements { if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) { let Some(condensed_kind) = grouped_condensed_kinds.remove(&kind) else { continue;