Skip to content

Commit bf03068

Browse files
authored
[red-knot] don't remove negations when simplifying constrained typevars (#17189)
For two non-disjoint types `P` and `Q`, the simplification of `(P | Q) & ~Q` is not `P`, but `P & ~Q`. In other words, the non-empty set `P & Q` is also excluded from the type. The same applies for a constrained typevar `[T: (P, Q)]`: `T & ~Q` should simplify to `P & ~Q`, not just `P`. Implementing this is actually purely a matter of removing code from the constrained typevar simplification logic; we just need to not bother removing the negations. If the negations are actually redundant (because the constraint types are disjoint), normal intersection simplification will already eliminate them (as shown in the added test.)
1 parent 4f924bb commit bf03068

File tree

2 files changed

+16
-12
lines changed

2 files changed

+16
-12
lines changed

crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ from knot_extensions import Not
488488

489489
def remove_constraint[T: (int, str, bool)](t: T) -> None:
490490
def _(x: Intersection[T, Not[int]]) -> None:
491-
reveal_type(x) # revealed: str
491+
reveal_type(x) # revealed: str & ~int
492492

493493
def _(x: Intersection[T, Not[str]]) -> None:
494494
# With OneOf this would be OneOf[int, bool]
@@ -521,14 +521,14 @@ def f[T: (P, Q)](t: T) -> None:
521521
reveal_type(t) # revealed: P
522522
p: P = t
523523
else:
524-
reveal_type(t) # revealed: Q
524+
reveal_type(t) # revealed: Q & ~P
525525
q: Q = t
526526

527527
if isinstance(t, Q):
528528
reveal_type(t) # revealed: Q
529529
q: Q = t
530530
else:
531-
reveal_type(t) # revealed: P
531+
reveal_type(t) # revealed: P & ~Q
532532
p: P = t
533533

534534
def g[T: (P, Q, R)](t: T) -> None:
@@ -539,7 +539,7 @@ def g[T: (P, Q, R)](t: T) -> None:
539539
reveal_type(t) # revealed: Q & ~P
540540
q: Q = t
541541
else:
542-
reveal_type(t) # revealed: R
542+
reveal_type(t) # revealed: R & ~P & ~Q
543543
r: R = t
544544

545545
if isinstance(t, P):
@@ -555,4 +555,16 @@ def g[T: (P, Q, R)](t: T) -> None:
555555
reveal_type(t) # revealed: Never
556556
```
557557

558+
If the constraints are disjoint, simplification does eliminate the redundant negative:
559+
560+
```py
561+
def h[T: (P, None)](t: T) -> None:
562+
if t is None:
563+
reveal_type(t) # revealed: None
564+
p: None = t
565+
else:
566+
reveal_type(t) # revealed: P
567+
p: P = t
568+
```
569+
558570
[pep 695]: https://peps.python.org/pep-0695/

crates/red_knot_python_semantic/src/types/builder.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,6 @@ impl<'db> InnerIntersectionBuilder<'db> {
499499
fn simplify_constrained_typevars(&mut self, db: &'db dyn Db) {
500500
let mut to_add = SmallVec::<[Type<'db>; 1]>::new();
501501
let mut positive_to_remove = SmallVec::<[usize; 1]>::new();
502-
let mut negative_to_remove = Vec::new();
503502

504503
for (typevar_index, ty) in self.positive.iter().enumerate() {
505504
let Type::TypeVar(typevar) = ty else {
@@ -567,20 +566,13 @@ impl<'db> InnerIntersectionBuilder<'db> {
567566
// replace the typevar itself with the remaining positive constraint.
568567
to_add.push(remaining_constraint);
569568
positive_to_remove.push(typevar_index);
570-
negative_to_remove.extend(to_remove);
571569
}
572570

573571
// We don't need to sort the positive list, since we only append to it in increasing order.
574572
for index in positive_to_remove.into_iter().rev() {
575573
self.positive.swap_remove_index(index);
576574
}
577575

578-
negative_to_remove.sort_unstable();
579-
negative_to_remove.dedup();
580-
for index in negative_to_remove.into_iter().rev() {
581-
self.negative.swap_remove_index(index);
582-
}
583-
584576
for remaining_constraint in to_add {
585577
self.add_positive(db, remaining_constraint);
586578
}

0 commit comments

Comments
 (0)