diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index ffec37148d..37f0961461 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -302,7 +302,7 @@ class C: c_instance = C() reveal_type(c_instance.a) # revealed: Unknown | Literal[1] -reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking) +reveal_type(c_instance.b) # revealed: Unknown ``` #### Attributes defined in for-loop (unpacking) @@ -1892,6 +1892,17 @@ reveal_type(B().x) # revealed: Unknown | Literal[1] reveal_type(A().x) # revealed: Unknown | Literal[1] ``` +This case additionally tests our union/intersection simplification logic: + +```py +class H: + def __init__(self): + self.x = 1 + + def copy(self, other: "H"): + self.x = other.x or self.x +``` + ### Builtin types attributes This test can probably be removed eventually, but we currently include it because we do not yet diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/union.md b/crates/red_knot_python_semantic/resources/mdtest/call/union.md index b3615496c1..88d6ad9c37 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/union.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/union.md @@ -201,3 +201,15 @@ def _(literals_2: Literal[0, 1], b: bool, flag: bool): # Now union the two: reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int ``` + +## Simplifying gradually-equivalent types + +If two types are gradually equivalent, we can keep just one of them in a union: + +```py +from typing import Any, Union +from knot_extensions import Intersection, Not + +def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]): + reveal_type(x) # revealed: Any & ~int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md b/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md index 6aed38694a..51bf653ef6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md @@ -842,7 +842,7 @@ def unknown( ### Mixed dynamic types -We currently do not simplify mixed dynamic types, but might consider doing so in the future: +Gradually-equivalent types can be simplified out of intersections: ```py from typing import Any @@ -854,10 +854,10 @@ def mixed( i3: Intersection[Not[Any], Unknown], i4: Intersection[Not[Any], Not[Unknown]], ) -> None: - reveal_type(i1) # revealed: Any & Unknown - reveal_type(i2) # revealed: Any & Unknown - reveal_type(i3) # revealed: Any & Unknown - reveal_type(i4) # revealed: Any & Unknown + reveal_type(i1) # revealed: Any + reveal_type(i2) # revealed: Any + reveal_type(i3) # revealed: Any + reveal_type(i4) # revealed: Any ``` ## Invalid diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md index 9bb53d4e67..d074d1b826 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md @@ -12,7 +12,7 @@ x = [1, 2, 3] reveal_type(x) # revealed: list # TODO reveal int -reveal_type(x[0]) # revealed: Unknown | @Todo(Support for `typing.TypeVar` instances in type expressions) +reveal_type(x[0]) # revealed: Unknown # TODO reveal list reveal_type(x[0:1]) # revealed: @Todo(specialized non-generic class) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md index e3e46a96e6..98061b3103 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md @@ -47,10 +47,7 @@ static_assert(is_gradual_equivalent_to(Intersection[str | int, Not[type[Any]]], static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes)) static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict)) -# TODO: No errors -# error: [static-assert-error] static_assert(is_gradual_equivalent_to(Unknown, Unknown | Any)) -# error: [static-assert-error] static_assert(is_gradual_equivalent_to(Unknown, Intersection[Unknown, Any])) ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index aeb0c67b8e..b57443a5e4 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1441,24 +1441,6 @@ impl<'db> Type<'db> { } } - /// Returns true if both `self` and `other` are the same gradual form - /// (limited to `Any`, `Unknown`, or `Todo`). - pub(crate) fn is_same_gradual_form(self, other: Type<'db>) -> bool { - matches!( - (self, other), - ( - Type::Dynamic(DynamicType::Any), - Type::Dynamic(DynamicType::Any) - ) | ( - Type::Dynamic(DynamicType::Unknown), - Type::Dynamic(DynamicType::Unknown) - ) | ( - Type::Dynamic(DynamicType::Todo(_)), - Type::Dynamic(DynamicType::Todo(_)) - ) - ) - } - /// Returns true if this type and `other` are gradual equivalent. /// /// > Two gradual types `A` and `B` are equivalent diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index f51f59b871..21171f11fb 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -278,7 +278,7 @@ impl<'db> UnionBuilder<'db> { break; } - if ty.is_same_gradual_form(element_type) + if ty.is_gradual_equivalent_to(self.db, element_type) || ty.is_subtype_of(self.db, element_type) || element_type.is_object(self.db) { @@ -560,7 +560,7 @@ impl<'db> InnerIntersectionBuilder<'db> { for (index, existing_positive) in self.positive.iter().enumerate() { // S & T = S if S <: T if existing_positive.is_subtype_of(db, new_positive) - || existing_positive.is_same_gradual_form(new_positive) + || existing_positive.is_gradual_equivalent_to(db, new_positive) { return; } @@ -656,7 +656,9 @@ impl<'db> InnerIntersectionBuilder<'db> { 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) { + if existing_negative.is_subtype_of(db, new_negative) + || existing_negative.is_gradual_equivalent_to(db, new_negative) + { to_remove.push(index); } // same rule, reverse order