Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion crates/red_knot_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/call/union.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]))
```

Expand Down
18 changes: 0 additions & 18 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1425,24 +1425,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
Expand Down
8 changes: 5 additions & 3 deletions crates/red_knot_python_semantic/src/types/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down
Loading