mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 20:09:22 +00:00
[red-knot] fix collapsing literal and its negation to object (#17605)
## Summary Another follow-up to the unions-of-large-literals optimization. Restore the behavior that e.g. `Literal[""] | ~Literal[""]` collapses to `object`. ## Test Plan Added mdtests.
This commit is contained in:
parent
e93fa7062c
commit
ac6219ec38
2 changed files with 125 additions and 44 deletions
|
@ -165,26 +165,39 @@ def _(flag: bool):
|
||||||
## Unions with literals and negations
|
## Unions with literals and negations
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import Literal, Union
|
from typing import Literal
|
||||||
from knot_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to
|
from knot_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to
|
||||||
|
|
||||||
static_assert(is_subtype_of(Literal["a", ""], Union[Literal["a", ""], Not[AlwaysFalsy]]))
|
static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[AlwaysFalsy]))
|
||||||
static_assert(is_subtype_of(Not[AlwaysFalsy], Union[Literal["", "a"], Not[AlwaysFalsy]]))
|
static_assert(is_subtype_of(Not[AlwaysFalsy], Literal["", "a"] | Not[AlwaysFalsy]))
|
||||||
static_assert(is_subtype_of(Literal["a", ""], Union[Not[AlwaysFalsy], Literal["a", ""]]))
|
static_assert(is_subtype_of(Literal["a", ""], Not[AlwaysFalsy] | Literal["a", ""]))
|
||||||
static_assert(is_subtype_of(Not[AlwaysFalsy], Union[Not[AlwaysFalsy], Literal["a", ""]]))
|
static_assert(is_subtype_of(Not[AlwaysFalsy], Not[AlwaysFalsy] | Literal["a", ""]))
|
||||||
|
|
||||||
static_assert(is_subtype_of(Literal["a", ""], Union[Literal["a", ""], Not[Literal[""]]]))
|
static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[Literal[""]]))
|
||||||
static_assert(is_subtype_of(Not[Literal[""]], Union[Literal["a", ""], Not[Literal[""]]]))
|
static_assert(is_subtype_of(Not[Literal[""]], Literal["a", ""] | Not[Literal[""]]))
|
||||||
static_assert(is_subtype_of(Literal["a", ""], Union[Not[Literal[""]], Literal["a", ""]]))
|
static_assert(is_subtype_of(Literal["a", ""], Not[Literal[""]] | Literal["a", ""]))
|
||||||
static_assert(is_subtype_of(Not[Literal[""]], Union[Not[Literal[""]], Literal["a", ""]]))
|
static_assert(is_subtype_of(Not[Literal[""]], Not[Literal[""]] | Literal["a", ""]))
|
||||||
|
|
||||||
def _(
|
def _(
|
||||||
x: Union[Literal["a", ""], Not[AlwaysFalsy]],
|
a: Literal["a", ""] | Not[AlwaysFalsy],
|
||||||
y: Union[Literal["a", ""], Not[Literal[""]]],
|
b: Literal["a", ""] | Not[Literal[""]],
|
||||||
|
c: Literal[""] | Not[Literal[""]],
|
||||||
|
d: Not[Literal[""]] | Literal[""],
|
||||||
|
e: Literal["a"] | Not[Literal["a"]],
|
||||||
|
f: Literal[b"b"] | Not[Literal[b"b"]],
|
||||||
|
g: Not[Literal[b"b"]] | Literal[b"b"],
|
||||||
|
h: Literal[42] | Not[Literal[42]],
|
||||||
|
i: Not[Literal[42]] | Literal[42],
|
||||||
):
|
):
|
||||||
reveal_type(x) # revealed: Literal[""] | ~AlwaysFalsy
|
reveal_type(a) # revealed: Literal[""] | ~AlwaysFalsy
|
||||||
# TODO should be `object`
|
reveal_type(b) # revealed: object
|
||||||
reveal_type(y) # revealed: Literal[""] | ~Literal[""]
|
reveal_type(c) # revealed: object
|
||||||
|
reveal_type(d) # revealed: object
|
||||||
|
reveal_type(e) # revealed: object
|
||||||
|
reveal_type(f) # revealed: object
|
||||||
|
reveal_type(g) # revealed: object
|
||||||
|
reveal_type(h) # revealed: object
|
||||||
|
reveal_type(i) # revealed: object
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cannot use an argument as both a value and a type form
|
## Cannot use an argument as both a value and a type form
|
||||||
|
|
|
@ -97,37 +97,70 @@ impl<'db> UnionElement<'db> {
|
||||||
fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> {
|
fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> {
|
||||||
match self {
|
match self {
|
||||||
UnionElement::IntLiterals(literals) => {
|
UnionElement::IntLiterals(literals) => {
|
||||||
ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::Int) {
|
if other_type.splits_literals(db, LiteralKind::Int) {
|
||||||
|
let mut collapse = false;
|
||||||
|
let negated = other_type.negate(db);
|
||||||
literals.retain(|literal| {
|
literals.retain(|literal| {
|
||||||
!Type::IntLiteral(*literal).is_subtype_of(db, other_type)
|
let ty = Type::IntLiteral(*literal);
|
||||||
|
if negated.is_subtype_of(db, ty) {
|
||||||
|
collapse = true;
|
||||||
|
}
|
||||||
|
!ty.is_subtype_of(db, other_type)
|
||||||
});
|
});
|
||||||
!literals.is_empty()
|
if collapse {
|
||||||
|
ReduceResult::CollapseToObject
|
||||||
|
} else {
|
||||||
|
ReduceResult::KeepIf(!literals.is_empty())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// SAFETY: All `UnionElement` literal kinds must always be non-empty
|
ReduceResult::KeepIf(
|
||||||
!Type::IntLiteral(literals[0]).is_subtype_of(db, other_type)
|
!Type::IntLiteral(literals[0]).is_subtype_of(db, other_type),
|
||||||
})
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UnionElement::StringLiterals(literals) => {
|
UnionElement::StringLiterals(literals) => {
|
||||||
ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::String) {
|
if other_type.splits_literals(db, LiteralKind::String) {
|
||||||
|
let mut collapse = false;
|
||||||
|
let negated = other_type.negate(db);
|
||||||
literals.retain(|literal| {
|
literals.retain(|literal| {
|
||||||
!Type::StringLiteral(*literal).is_subtype_of(db, other_type)
|
let ty = Type::StringLiteral(*literal);
|
||||||
|
if negated.is_subtype_of(db, ty) {
|
||||||
|
collapse = true;
|
||||||
|
}
|
||||||
|
!ty.is_subtype_of(db, other_type)
|
||||||
});
|
});
|
||||||
!literals.is_empty()
|
if collapse {
|
||||||
|
ReduceResult::CollapseToObject
|
||||||
|
} else {
|
||||||
|
ReduceResult::KeepIf(!literals.is_empty())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// SAFETY: All `UnionElement` literal kinds must always be non-empty
|
ReduceResult::KeepIf(
|
||||||
!Type::StringLiteral(literals[0]).is_subtype_of(db, other_type)
|
!Type::StringLiteral(literals[0]).is_subtype_of(db, other_type),
|
||||||
})
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UnionElement::BytesLiterals(literals) => {
|
UnionElement::BytesLiterals(literals) => {
|
||||||
ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::Bytes) {
|
if other_type.splits_literals(db, LiteralKind::Bytes) {
|
||||||
|
let mut collapse = false;
|
||||||
|
let negated = other_type.negate(db);
|
||||||
literals.retain(|literal| {
|
literals.retain(|literal| {
|
||||||
!Type::BytesLiteral(*literal).is_subtype_of(db, other_type)
|
let ty = Type::BytesLiteral(*literal);
|
||||||
|
if negated.is_subtype_of(db, ty) {
|
||||||
|
collapse = true;
|
||||||
|
}
|
||||||
|
!ty.is_subtype_of(db, other_type)
|
||||||
});
|
});
|
||||||
!literals.is_empty()
|
if collapse {
|
||||||
|
ReduceResult::CollapseToObject
|
||||||
|
} else {
|
||||||
|
ReduceResult::KeepIf(!literals.is_empty())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// SAFETY: All `UnionElement` literal kinds must always be non-empty
|
ReduceResult::KeepIf(
|
||||||
!Type::BytesLiteral(literals[0]).is_subtype_of(db, other_type)
|
!Type::BytesLiteral(literals[0]).is_subtype_of(db, other_type),
|
||||||
})
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UnionElement::Type(existing) => ReduceResult::Type(*existing),
|
UnionElement::Type(existing) => ReduceResult::Type(*existing),
|
||||||
}
|
}
|
||||||
|
@ -138,6 +171,8 @@ enum ReduceResult<'db> {
|
||||||
/// Reduction of this `UnionElement` is complete; keep it in the union if the nested
|
/// Reduction of this `UnionElement` is complete; keep it in the union if the nested
|
||||||
/// boolean is true, eliminate it from the union if false.
|
/// boolean is true, eliminate it from the union if false.
|
||||||
KeepIf(bool),
|
KeepIf(bool),
|
||||||
|
/// Collapse this entire union to `object`.
|
||||||
|
CollapseToObject,
|
||||||
/// The given `Type` can stand-in for the entire `UnionElement` for further union
|
/// The given `Type` can stand-in for the entire `UnionElement` for further union
|
||||||
/// simplification checks.
|
/// simplification checks.
|
||||||
Type(Type<'db>),
|
Type(Type<'db>),
|
||||||
|
@ -195,6 +230,7 @@ impl<'db> UnionBuilder<'db> {
|
||||||
// containing it.
|
// containing it.
|
||||||
Type::StringLiteral(literal) => {
|
Type::StringLiteral(literal) => {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
|
let ty_negated = ty.negate(self.db);
|
||||||
for element in &mut self.elements {
|
for element in &mut self.elements {
|
||||||
match element {
|
match element {
|
||||||
UnionElement::StringLiterals(literals) => {
|
UnionElement::StringLiterals(literals) => {
|
||||||
|
@ -207,8 +243,16 @@ impl<'db> UnionBuilder<'db> {
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => {
|
UnionElement::Type(existing) => {
|
||||||
return;
|
if ty.is_subtype_of(self.db, *existing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ty_negated.is_subtype_of(self.db, *existing) {
|
||||||
|
// The type that includes both this new element, and its negation
|
||||||
|
// (or a supertype of its negation), must be simply `object`.
|
||||||
|
self.collapse_to_object();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -223,6 +267,7 @@ impl<'db> UnionBuilder<'db> {
|
||||||
// Same for bytes literals as for string literals, above.
|
// Same for bytes literals as for string literals, above.
|
||||||
Type::BytesLiteral(literal) => {
|
Type::BytesLiteral(literal) => {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
|
let ty_negated = ty.negate(self.db);
|
||||||
for element in &mut self.elements {
|
for element in &mut self.elements {
|
||||||
match element {
|
match element {
|
||||||
UnionElement::BytesLiterals(literals) => {
|
UnionElement::BytesLiterals(literals) => {
|
||||||
|
@ -235,8 +280,16 @@ impl<'db> UnionBuilder<'db> {
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => {
|
UnionElement::Type(existing) => {
|
||||||
return;
|
if ty.is_subtype_of(self.db, *existing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ty_negated.is_subtype_of(self.db, *existing) {
|
||||||
|
// The type that includes both this new element, and its negation
|
||||||
|
// (or a supertype of its negation), must be simply `object`.
|
||||||
|
self.collapse_to_object();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -251,6 +304,7 @@ impl<'db> UnionBuilder<'db> {
|
||||||
// And same for int literals as well.
|
// And same for int literals as well.
|
||||||
Type::IntLiteral(literal) => {
|
Type::IntLiteral(literal) => {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
|
let ty_negated = ty.negate(self.db);
|
||||||
for element in &mut self.elements {
|
for element in &mut self.elements {
|
||||||
match element {
|
match element {
|
||||||
UnionElement::IntLiterals(literals) => {
|
UnionElement::IntLiterals(literals) => {
|
||||||
|
@ -263,8 +317,16 @@ impl<'db> UnionBuilder<'db> {
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => {
|
UnionElement::Type(existing) => {
|
||||||
return;
|
if ty.is_subtype_of(self.db, *existing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ty_negated.is_subtype_of(self.db, *existing) {
|
||||||
|
// The type that includes both this new element, and its negation
|
||||||
|
// (or a supertype of its negation), must be simply `object`.
|
||||||
|
self.collapse_to_object();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -298,6 +360,10 @@ impl<'db> UnionBuilder<'db> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ReduceResult::Type(ty) => ty,
|
ReduceResult::Type(ty) => ty,
|
||||||
|
ReduceResult::CollapseToObject => {
|
||||||
|
self.collapse_to_object();
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if Some(element_type) == bool_pair {
|
if Some(element_type) == bool_pair {
|
||||||
to_add = KnownClass::Bool.to_instance(self.db);
|
to_add = KnownClass::Bool.to_instance(self.db);
|
||||||
|
@ -317,12 +383,14 @@ impl<'db> UnionBuilder<'db> {
|
||||||
} else if element_type.is_subtype_of(self.db, ty) {
|
} else if element_type.is_subtype_of(self.db, ty) {
|
||||||
to_remove.push(index);
|
to_remove.push(index);
|
||||||
} else if ty_negated.is_subtype_of(self.db, element_type) {
|
} else if ty_negated.is_subtype_of(self.db, element_type) {
|
||||||
// We add `ty` to the union. We just checked that `~ty` is a subtype of an existing `element`.
|
// We add `ty` to the union. We just checked that `~ty` is a subtype of an
|
||||||
// This also means that `~ty | ty` is a subtype of `element | ty`, because both elements in the
|
// existing `element`. This also means that `~ty | ty` is a subtype of
|
||||||
// first union are subtypes of the corresponding elements in the second union. But `~ty | ty` is
|
// `element | ty`, because both elements in the first union are subtypes of
|
||||||
// just `object`. Since `object` is a subtype of `element | ty`, we can only conclude that
|
// the corresponding elements in the second union. But `~ty | ty` is just
|
||||||
// `element | ty` must be `object` (object has no other supertypes). This means we can simplify
|
// `object`. Since `object` is a subtype of `element | ty`, we can only
|
||||||
// the whole union to just `object`, since all other potential elements would also be subtypes of
|
// conclude that `element | ty` must be `object` (object has no other
|
||||||
|
// supertypes). This means we can simplify the whole union to just
|
||||||
|
// `object`, since all other potential elements would also be subtypes of
|
||||||
// `object`.
|
// `object`.
|
||||||
self.collapse_to_object();
|
self.collapse_to_object();
|
||||||
return;
|
return;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue