[red-knot] Do not assume that x != 0 if x inhabits ~Literal[0] (#17370)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[Knot Playground] Release / publish (push) Waiting to run

## Summary

Fixes incorrect negated type eq and ne assertions in
infer_binary_intersection_type_comparison

fixes #17360

## Test Plan

Remove and update some now incorrect tests
This commit is contained in:
Matthew Mckee 2025-04-16 06:27:27 +01:00 committed by GitHub
parent 1dedcb9e0d
commit a2a7b1e268
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 32 additions and 22 deletions

View file

@ -50,13 +50,17 @@ reveal_type(x) # revealed: LiteralString
if x != "abc":
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
reveal_type(x == "abc") # revealed: Literal[False]
reveal_type("abc" == x) # revealed: Literal[False]
# TODO: This should be `Literal[False]`
reveal_type(x == "abc") # revealed: bool
# TODO: This should be `Literal[False]`
reveal_type("abc" == x) # revealed: bool
reveal_type(x == "something else") # revealed: bool
reveal_type("something else" == x) # revealed: bool
reveal_type(x != "abc") # revealed: Literal[True]
reveal_type("abc" != x) # revealed: Literal[True]
# TODO: This should be `Literal[True]`
reveal_type(x != "abc") # revealed: bool
# TODO: This should be `Literal[True]`
reveal_type("abc" != x) # revealed: bool
reveal_type(x != "something else") # revealed: bool
reveal_type("something else" != x) # revealed: bool
@ -79,10 +83,10 @@ def _(x: int):
if x != 1:
reveal_type(x) # revealed: int & ~Literal[1]
reveal_type(x != 1) # revealed: Literal[True]
reveal_type(x != 1) # revealed: bool
reveal_type(x != 2) # revealed: bool
reveal_type(x == 1) # revealed: Literal[False]
reveal_type(x == 1) # revealed: bool
reveal_type(x == 2) # revealed: bool
```

View file

@ -23,12 +23,21 @@ def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None:
reveal_type(n2) # revealed: int
reveal_type(n3) # revealed: ~int
def static_truthiness(not_one: Not[Literal[1]]) -> None:
static_assert(not_one != 1)
static_assert(not (not_one == 1))
# error: "Special form `knot_extensions.Not` expected exactly one type parameter"
n: Not[int, str]
def static_truthiness(not_one: Not[Literal[1]]) -> None:
# these are both boolean-literal types,
# since all possible runtime objects that are created by the literal syntax `1`
# are members of the type `Literal[1]`
reveal_type(not_one is not 1) # revealed: bool
reveal_type(not_one is 1) # revealed: bool
# But these are both `bool`, rather than `Literal[True]` or `Literal[False]`
# as there are many runtime objects that inhabit the type `~Literal[1]`
# but still compare equal to `1`. Two examples are `1.0` and `True`.
reveal_type(not_one != 1) # revealed: bool
reveal_type(not_one == 1) # revealed: bool
```
### Intersection
@ -170,13 +179,11 @@ Static assertions can be used to enforce narrowing constraints:
```py
from knot_extensions import static_assert
def f(x: int) -> None:
if x != 0:
static_assert(x != 0)
def f(x: int | None) -> None:
if x is not None:
static_assert(x is not None)
else:
# `int` can be subclassed, so we cannot assert that `x == 0` here:
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
static_assert(x == 0)
static_assert(x is None)
```
### Truthy expressions

View file

@ -5283,12 +5283,6 @@ impl<'db> TypeInferenceBuilder<'db> {
};
match (op, result) {
(ast::CmpOp::Eq, Some(Type::BooleanLiteral(true))) => {
return Ok(Type::BooleanLiteral(false));
}
(ast::CmpOp::NotEq, Some(Type::BooleanLiteral(false))) => {
return Ok(Type::BooleanLiteral(true));
}
(ast::CmpOp::Is, Some(Type::BooleanLiteral(true))) => {
return Ok(Type::BooleanLiteral(false));
}
@ -5338,6 +5332,9 @@ impl<'db> TypeInferenceBuilder<'db> {
// we would get a result type `Literal[True]` which is too narrow.
//
let mut builder = IntersectionBuilder::new(self.db());
builder = builder.add_positive(KnownClass::Bool.to_instance(self.db()));
for pos in intersection.positive(self.db()) {
let result = match intersection_on {
IntersectionOn::Left => {
@ -5412,6 +5409,8 @@ impl<'db> TypeInferenceBuilder<'db> {
ast::CmpOp::LtE => Ok(Type::BooleanLiteral(n <= m)),
ast::CmpOp::Gt => Ok(Type::BooleanLiteral(n > m)),
ast::CmpOp::GtE => Ok(Type::BooleanLiteral(n >= m)),
// We cannot say that two equal int Literals will return True from an `is` or `is not` comparison.
// Even if they are the same value, they may not be the same object.
ast::CmpOp::Is => {
if n == m {
Ok(KnownClass::Bool.to_instance(self.db()))