Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
```

Expand Down
27 changes: 17 additions & 10 deletions crates/red_knot_python_semantic/resources/mdtest/type_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +30 to +34
Copy link
Member

@AlexWaygood AlexWaygood Apr 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment doesn't seem to reflect the code immediately below it anymore. The comment says "these are both boolean-literal types", but the assertion reveals bool, not Literal[True] or Literal[False]?

bool isn't incorrect here, but it would be great if we could fix this comment to something like

Suggested change
# 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
# TODO: `bool` is not incorrect, but these would ideally be `Literal[True]` and `Literal[False]`
# respectively, 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# 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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 5 additions & 6 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5264,12 +5264,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));
}
Comment on lines -5267 to -5272
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think this removal is correct. I guess to address the TODO comments added above (where we intersect with LiteralString, we would need to introduce some Type method which would be true for LiteralString, which would mean "inhabitants of this type can never be equal to any object that doesn't inhabit this type, and all proper subtypes are single-valued." And then here, we could apply these removed cases only if all positive members of the intersection are such a type.

But I think maybe LiteralString is the only such type we need to care about, so a Type method might be overkill, we could also just special-case LiteralString here for now.

Copy link
Member

@AlexWaygood AlexWaygood Apr 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I think maybe LiteralString is the only such type we need to care about, so a Type method might be overkill, we could also just special-case LiteralString here for now.

No, I don't think so. bool can be safely narrowed to Literal[True] using an == True comparison. And in due course we'll want to be able to narrow <instance of enum class> to <Literal member of that enum> using equality as well (assuming all enum members have unique values, and the the enum class doesn't have a custom __eq__ method -- either case would mean such narrowing would not be sound).

(ast::CmpOp::Is, Some(Type::BooleanLiteral(true))) => {
return Ok(Type::BooleanLiteral(false));
}
Expand Down Expand Up @@ -5319,6 +5313,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 => {
Expand Down Expand Up @@ -5393,6 +5390,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()))
Expand Down
Loading