Skip to content

Commit 27ada26

Browse files
carljmAlexWaygood
andauthored
[red-knot] fix unions of literals, again (#17534)
## Summary #17451 was incomplete. `AlwaysFalsy` and `AlwaysTruthy` are not the only two types that are super-types of some literals (of a given kind) and not others. That set also includes intersections containing `AlwaysTruthy` or `AlwaysFalsy`, and intersections containing literal types of the same kind. Cover these cases as well. Fixes #17478. ## Test Plan Added mdtests. `QUICKCHECK_TESTS=1000000 cargo test -p red_knot_python_semantic -- --ignored types::property_tests::stable` failed on both `all_fully_static_type_pairs_are_subtypes_of_their_union` and `all_type_pairs_are_assignable_to_their_union` prior to this PR, passes after it. --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
1 parent 810478f commit 27ada26

File tree

2 files changed

+62
-6
lines changed

2 files changed

+62
-6
lines changed

crates/red_knot_python_semantic/resources/mdtest/call/union.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,31 @@ def _(flag: bool):
162162
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
163163
```
164164

165+
## Unions with literals and negations
166+
167+
```py
168+
from typing import Literal, Union
169+
from knot_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to
170+
171+
static_assert(is_subtype_of(Literal["a", ""], Union[Literal["a", ""], Not[AlwaysFalsy]]))
172+
static_assert(is_subtype_of(Not[AlwaysFalsy], Union[Literal["", "a"], Not[AlwaysFalsy]]))
173+
static_assert(is_subtype_of(Literal["a", ""], Union[Not[AlwaysFalsy], Literal["a", ""]]))
174+
static_assert(is_subtype_of(Not[AlwaysFalsy], Union[Not[AlwaysFalsy], Literal["a", ""]]))
175+
176+
static_assert(is_subtype_of(Literal["a", ""], Union[Literal["a", ""], Not[Literal[""]]]))
177+
static_assert(is_subtype_of(Not[Literal[""]], Union[Literal["a", ""], Not[Literal[""]]]))
178+
static_assert(is_subtype_of(Literal["a", ""], Union[Not[Literal[""]], Literal["a", ""]]))
179+
static_assert(is_subtype_of(Not[Literal[""]], Union[Not[Literal[""]], Literal["a", ""]]))
180+
181+
def _(
182+
x: Union[Literal["a", ""], Not[AlwaysFalsy]],
183+
y: Union[Literal["a", ""], Not[Literal[""]]],
184+
):
185+
reveal_type(x) # revealed: Literal[""] | ~AlwaysFalsy
186+
# TODO should be `object`
187+
reveal_type(y) # revealed: Literal[""] | ~Literal[""]
188+
```
189+
165190
## Cannot use an argument as both a value and a type form
166191

167192
```py

crates/red_knot_python_semantic/src/types/builder.rs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,40 @@ use crate::types::{
4444
use crate::{Db, FxOrderSet};
4545
use smallvec::SmallVec;
4646

47+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48+
enum LiteralKind {
49+
Int,
50+
String,
51+
Bytes,
52+
}
53+
54+
impl<'db> Type<'db> {
55+
/// Return `true` if this type can be a supertype of some literals of `kind` and not others.
56+
fn splits_literals(self, db: &'db dyn Db, kind: LiteralKind) -> bool {
57+
match (self, kind) {
58+
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => true,
59+
(Type::StringLiteral(_), LiteralKind::String) => true,
60+
(Type::BytesLiteral(_), LiteralKind::Bytes) => true,
61+
(Type::IntLiteral(_), LiteralKind::Int) => true,
62+
(Type::Intersection(intersection), _) => {
63+
intersection
64+
.positive(db)
65+
.iter()
66+
.any(|ty| ty.splits_literals(db, kind))
67+
|| intersection
68+
.negative(db)
69+
.iter()
70+
.any(|ty| ty.splits_literals(db, kind))
71+
}
72+
(Type::Union(union), _) => union
73+
.elements(db)
74+
.iter()
75+
.any(|ty| ty.splits_literals(db, kind)),
76+
_ => false,
77+
}
78+
}
79+
}
80+
4781
enum UnionElement<'db> {
4882
IntLiterals(FxOrderSet<i64>),
4983
StringLiterals(FxOrderSet<StringLiteralType<'db>>),
@@ -61,12 +95,9 @@ impl<'db> UnionElement<'db> {
6195
/// If this `UnionElement` is some other type, return `ReduceResult::Type` so `UnionBuilder`
6296
/// can perform more complex checks on it.
6397
fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> {
64-
// `AlwaysTruthy` and `AlwaysFalsy` are the only types which can be a supertype of only
65-
// _some_ literals of the same kind, so we need to walk the full set in this case.
66-
let needs_filter = matches!(other_type, Type::AlwaysTruthy | Type::AlwaysFalsy);
6798
match self {
6899
UnionElement::IntLiterals(literals) => {
69-
ReduceResult::KeepIf(if needs_filter {
100+
ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::Int) {
70101
literals.retain(|literal| {
71102
!Type::IntLiteral(*literal).is_subtype_of(db, other_type)
72103
});
@@ -77,7 +108,7 @@ impl<'db> UnionElement<'db> {
77108
})
78109
}
79110
UnionElement::StringLiterals(literals) => {
80-
ReduceResult::KeepIf(if needs_filter {
111+
ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::String) {
81112
literals.retain(|literal| {
82113
!Type::StringLiteral(*literal).is_subtype_of(db, other_type)
83114
});
@@ -88,7 +119,7 @@ impl<'db> UnionElement<'db> {
88119
})
89120
}
90121
UnionElement::BytesLiterals(literals) => {
91-
ReduceResult::KeepIf(if needs_filter {
122+
ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::Bytes) {
92123
literals.retain(|literal| {
93124
!Type::BytesLiteral(*literal).is_subtype_of(db, other_type)
94125
});

0 commit comments

Comments
 (0)