[red-knot] Narrowing For Truthiness Checks (if x or if not x) (#14687)

## Summary

Fixes #14550.

Add `AlwaysTruthy` and `AlwaysFalsy` types, representing the set of objects whose `__bool__` method can only ever return `True` or `False`, respectively, and narrow `if x` and `if not x` accordingly.


## Test Plan

- New Markdown test for truthiness narrowing `narrow/truthiness.md`
- unit tests in `types.rs` and `builders.rs` (`cargo test --package
red_knot_python_semantic --lib -- types`)
This commit is contained in:
cake-monotone 2024-12-18 01:37:07 +09:00 committed by GitHub
parent c3b6139f39
commit f463fa7b7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 343 additions and 25 deletions

View file

@ -0,0 +1,221 @@
# Narrowing For Truthiness Checks (`if x` or `if not x`)
## Value Literals
```py
def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[()]:
return 0
x = foo()
if x:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
else:
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
if not x:
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
else:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
if x and not x:
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
if not (x and not x):
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
else:
reveal_type(x) # revealed: Never
if x or not x:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
else:
reveal_type(x) # revealed: Never
if not (x or not x):
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
if (isinstance(x, int) or isinstance(x, str)) and x:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"]
else:
reveal_type(x) # revealed: Literal[b"", b"bar"] | None | tuple[()] | Literal[0] | Literal[False] | Literal[""]
```
## Function Literals
Basically functions are always truthy.
```py
def flag() -> bool:
return True
def foo(hello: int) -> bytes:
return b""
def bar(world: str, *args, **kwargs) -> float:
return 0.0
x = foo if flag() else bar
if x:
reveal_type(x) # revealed: Literal[foo, bar]
else:
reveal_type(x) # revealed: Never
```
## Mutable Truthiness
### Truthiness of Instances
The boolean value of an instance is not always consistent. For example, `__bool__` can be customized
to return random values, or in the case of a `list()`, the result depends on the number of elements
in the list. Therefore, these types should not be narrowed by `if x` or `if not x`.
```py
class A: ...
class B: ...
def f(x: A | B):
if x:
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy
if x and not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy | A & ~AlwaysFalsy | B & ~AlwaysFalsy
if x or not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy | A & ~AlwaysTruthy | B & ~AlwaysTruthy
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
```
### Truthiness of Types
Also, types may not be Truthy. This is because `__bool__` can be customized via a metaclass.
Although this is a very rare case, we may consider metaclass checks in the future to handle this
more accurately.
```py
def flag() -> bool:
return True
x = int if flag() else str
reveal_type(x) # revealed: Literal[int, str]
if x:
reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy
else:
reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy
```
## Determined Truthiness
Some custom classes can have a boolean value that is consistently determined as either `True` or
`False`, regardless of the instance's state. This is achieved by defining a `__bool__` method that
always returns a fixed value.
These types can always be fully narrowed in boolean contexts, as shown below:
```py
class T:
def __bool__(self) -> Literal[True]:
return True
class F:
def __bool__(self) -> Literal[False]:
return False
t = T()
if t:
reveal_type(t) # revealed: T
else:
reveal_type(t) # revealed: Never
f = F()
if f:
reveal_type(f) # revealed: Never
else:
reveal_type(f) # revealed: F
```
## Narrowing Complex Intersection and Union
```py
class A: ...
class B: ...
def flag() -> bool:
return True
def instance() -> A | B:
return A()
def literals() -> Literal[0, 42, "", "hello"]:
return 42
x = instance()
y = literals()
if isinstance(x, str) and not isinstance(x, B):
reveal_type(x) # revealed: A & str & ~B
reveal_type(y) # revealed: Literal[0, 42] | Literal["", "hello"]
z = x if flag() else y
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42] | Literal["", "hello"]
if z:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42] | Literal["hello"]
else:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0] | Literal[""]
```
## Narrowing Multiple Variables
```py
def f(x: Literal[0, 1], y: Literal["", "hello"]):
if x and y and not x and not y:
reveal_type(x) # revealed: Never
reveal_type(y) # revealed: Never
else:
# ~(x or not x) and ~(y or not y)
reveal_type(x) # revealed: Literal[0, 1]
reveal_type(y) # revealed: Literal["", "hello"]
if (x or not x) and (y and not y):
reveal_type(x) # revealed: Literal[0, 1]
reveal_type(y) # revealed: Never
else:
# ~(x or not x) or ~(y and not y)
reveal_type(x) # revealed: Literal[0, 1]
reveal_type(y) # revealed: Literal["", "hello"]
```
## ControlFlow Merging
After merging control flows, when we take the union of all constraints applied in each branch, we
should return to the original state.
```py
class A: ...
x = A()
if x and not x:
y = x
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
else:
y = x
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
# TODO: It should be A. We should improve UnionBuilder or IntersectionBuilder. (issue #15023)
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
```

View file

@ -431,6 +431,11 @@ pub enum Type<'db> {
Union(UnionType<'db>), Union(UnionType<'db>),
/// The set of objects in all of the types in the intersection /// The set of objects in all of the types in the intersection
Intersection(IntersectionType<'db>), Intersection(IntersectionType<'db>),
/// Represents objects whose `__bool__` method is deterministic:
/// - `AlwaysTruthy`: `__bool__` always returns `True`
/// - `AlwaysFalsy`: `__bool__` always returns `False`
AlwaysTruthy,
AlwaysFalsy,
/// An integer literal /// An integer literal
IntLiteral(i64), IntLiteral(i64),
/// A boolean literal, either `True` or `False`. /// A boolean literal, either `True` or `False`.
@ -717,6 +722,15 @@ impl<'db> Type<'db> {
.all(|&neg_ty| self.is_disjoint_from(db, neg_ty)) .all(|&neg_ty| self.is_disjoint_from(db, neg_ty))
} }
// Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`.
// If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively.
(left, Type::AlwaysFalsy) => matches!(left.bool(db), Truthiness::AlwaysFalse),
(left, Type::AlwaysTruthy) => matches!(left.bool(db), Truthiness::AlwaysTrue),
// Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance).
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => {
target.is_equivalent_to(db, KnownClass::Object.to_instance(db))
}
// All `StringLiteral` types are a subtype of `LiteralString`. // All `StringLiteral` types are a subtype of `LiteralString`.
(Type::StringLiteral(_), Type::LiteralString) => true, (Type::StringLiteral(_), Type::LiteralString) => true,
@ -1105,6 +1119,16 @@ impl<'db> Type<'db> {
false false
} }
(Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => {
// `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint.
// Thus, they are only disjoint if `ty.bool() == AlwaysFalse`.
matches!(ty.bool(db), Truthiness::AlwaysFalse)
}
(Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => {
// Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`.
matches!(ty.bool(db), Truthiness::AlwaysTrue)
}
(Type::KnownInstance(left), right) => { (Type::KnownInstance(left), right) => {
left.instance_fallback(db).is_disjoint_from(db, right) left.instance_fallback(db).is_disjoint_from(db, right)
} }
@ -1238,7 +1262,9 @@ impl<'db> Type<'db> {
| Type::LiteralString | Type::LiteralString
| Type::BytesLiteral(_) | Type::BytesLiteral(_)
| Type::SliceLiteral(_) | Type::SliceLiteral(_)
| Type::KnownInstance(_) => true, | Type::KnownInstance(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy => true,
Type::SubclassOf(SubclassOfType { base }) => matches!(base, ClassBase::Class(_)), Type::SubclassOf(SubclassOfType { base }) => matches!(base, ClassBase::Class(_)),
Type::ClassLiteral(_) | Type::Instance(_) => { Type::ClassLiteral(_) | Type::Instance(_) => {
// TODO: Ideally, we would iterate over the MRO of the class, check if all // TODO: Ideally, we would iterate over the MRO of the class, check if all
@ -1340,6 +1366,7 @@ impl<'db> Type<'db> {
// //
false false
} }
Type::AlwaysTruthy | Type::AlwaysFalsy => false,
} }
} }
@ -1410,7 +1437,9 @@ impl<'db> Type<'db> {
| Type::Todo(_) | Type::Todo(_)
| Type::Union(..) | Type::Union(..)
| Type::Intersection(..) | Type::Intersection(..)
| Type::LiteralString => false, | Type::LiteralString
| Type::AlwaysTruthy
| Type::AlwaysFalsy => false,
} }
} }
@ -1578,6 +1607,10 @@ impl<'db> Type<'db> {
// TODO: implement tuple methods // TODO: implement tuple methods
todo_type!().into() todo_type!().into()
} }
Type::AlwaysTruthy | Type::AlwaysFalsy => {
// TODO return `Callable[[], Literal[True/False]]` for `__bool__` access
KnownClass::Object.to_instance(db).member(db, name)
}
&todo @ Type::Todo(_) => todo.into(), &todo @ Type::Todo(_) => todo.into(),
} }
} }
@ -1600,6 +1633,8 @@ impl<'db> Type<'db> {
// TODO: see above // TODO: see above
Truthiness::Ambiguous Truthiness::Ambiguous
} }
Type::AlwaysTruthy => Truthiness::AlwaysTrue,
Type::AlwaysFalsy => Truthiness::AlwaysFalse,
instance_ty @ Type::Instance(InstanceType { class }) => { instance_ty @ Type::Instance(InstanceType { class }) => {
if class.is_known(db, KnownClass::NoneType) { if class.is_known(db, KnownClass::NoneType) {
Truthiness::AlwaysFalse Truthiness::AlwaysFalse
@ -1912,7 +1947,9 @@ impl<'db> Type<'db> {
| Type::StringLiteral(_) | Type::StringLiteral(_)
| Type::SliceLiteral(_) | Type::SliceLiteral(_)
| Type::Tuple(_) | Type::Tuple(_)
| Type::LiteralString => Type::Unknown, | Type::LiteralString
| Type::AlwaysTruthy
| Type::AlwaysFalsy => Type::Unknown,
} }
} }
@ -2074,6 +2111,7 @@ impl<'db> Type<'db> {
ClassBase::try_from_ty(db, todo_type!("Intersection meta-type")) ClassBase::try_from_ty(db, todo_type!("Intersection meta-type"))
.expect("Type::Todo should be a valid ClassBase"), .expect("Type::Todo should be a valid ClassBase"),
), ),
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
Type::Todo(todo) => Type::subclass_of_base(ClassBase::Todo(*todo)), Type::Todo(todo) => Type::subclass_of_base(ClassBase::Todo(*todo)),
} }
} }
@ -3558,6 +3596,8 @@ pub(crate) mod tests {
SubclassOfAbcClass(&'static str), SubclassOfAbcClass(&'static str),
StdlibModule(CoreStdlibModule), StdlibModule(CoreStdlibModule),
SliceLiteral(i32, i32, i32), SliceLiteral(i32, i32, i32),
AlwaysTruthy,
AlwaysFalsy,
} }
impl Ty { impl Ty {
@ -3625,6 +3665,8 @@ pub(crate) mod tests {
Some(stop), Some(stop),
Some(step), Some(step),
)), )),
Ty::AlwaysTruthy => Type::AlwaysTruthy,
Ty::AlwaysFalsy => Type::AlwaysFalsy,
} }
} }
} }
@ -3763,6 +3805,12 @@ pub(crate) mod tests {
)] )]
#[test_case(Ty::SliceLiteral(1, 2, 3), Ty::BuiltinInstance("slice"))] #[test_case(Ty::SliceLiteral(1, 2, 3), Ty::BuiltinInstance("slice"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::Intersection{pos: vec![], neg: vec![Ty::None]})] #[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::Intersection{pos: vec![], neg: vec![Ty::None]})]
#[test_case(Ty::IntLiteral(1), Ty::AlwaysTruthy)]
#[test_case(Ty::IntLiteral(0), Ty::AlwaysFalsy)]
#[test_case(Ty::AlwaysTruthy, Ty::BuiltinInstance("object"))]
#[test_case(Ty::AlwaysFalsy, Ty::BuiltinInstance("object"))]
#[test_case(Ty::Never, Ty::AlwaysTruthy)]
#[test_case(Ty::Never, Ty::AlwaysFalsy)]
fn is_subtype_of(from: Ty, to: Ty) { fn is_subtype_of(from: Ty, to: Ty) {
let db = setup_db(); let db = setup_db();
assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db))); assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
@ -3797,6 +3845,10 @@ pub(crate) mod tests {
#[test_case(Ty::BuiltinClassLiteral("str"), Ty::SubclassOfAny)] #[test_case(Ty::BuiltinClassLiteral("str"), Ty::SubclassOfAny)]
#[test_case(Ty::AbcInstance("ABCMeta"), Ty::SubclassOfBuiltinClass("type"))] #[test_case(Ty::AbcInstance("ABCMeta"), Ty::SubclassOfBuiltinClass("type"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::BuiltinClassLiteral("str"))] #[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::BuiltinClassLiteral("str"))]
#[test_case(Ty::IntLiteral(1), Ty::AlwaysFalsy)]
#[test_case(Ty::IntLiteral(0), Ty::AlwaysTruthy)]
#[test_case(Ty::BuiltinInstance("str"), Ty::AlwaysTruthy)]
#[test_case(Ty::BuiltinInstance("str"), Ty::AlwaysFalsy)]
fn is_not_subtype_of(from: Ty, to: Ty) { fn is_not_subtype_of(from: Ty, to: Ty) {
let db = setup_db(); let db = setup_db();
assert!(!from.into_type(&db).is_subtype_of(&db, to.into_type(&db))); assert!(!from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
@ -3931,6 +3983,7 @@ pub(crate) mod tests {
#[test_case(Ty::Tuple(vec![]), Ty::BuiltinClassLiteral("object"))] #[test_case(Ty::Tuple(vec![]), Ty::BuiltinClassLiteral("object"))]
#[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::None)] #[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::None)]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::LiteralString)] #[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::LiteralString)]
#[test_case(Ty::AlwaysFalsy, Ty::AlwaysTruthy)]
fn is_disjoint_from(a: Ty, b: Ty) { fn is_disjoint_from(a: Ty, b: Ty) {
let db = setup_db(); let db = setup_db();
let a = a.into_type(&db); let a = a.into_type(&db);
@ -3961,6 +4014,8 @@ pub(crate) mod tests {
#[test_case(Ty::BuiltinClassLiteral("str"), Ty::BuiltinInstance("type"))] #[test_case(Ty::BuiltinClassLiteral("str"), Ty::BuiltinInstance("type"))]
#[test_case(Ty::BuiltinClassLiteral("str"), Ty::SubclassOfAny)] #[test_case(Ty::BuiltinClassLiteral("str"), Ty::SubclassOfAny)]
#[test_case(Ty::AbcClassLiteral("ABC"), Ty::AbcInstance("ABCMeta"))] #[test_case(Ty::AbcClassLiteral("ABC"), Ty::AbcInstance("ABCMeta"))]
#[test_case(Ty::BuiltinInstance("str"), Ty::AlwaysTruthy)]
#[test_case(Ty::BuiltinInstance("str"), Ty::AlwaysFalsy)]
fn is_not_disjoint_from(a: Ty, b: Ty) { fn is_not_disjoint_from(a: Ty, b: Ty) {
let db = setup_db(); let db = setup_db();
let a = a.into_type(&db); let a = a.into_type(&db);

View file

@ -30,6 +30,8 @@ use crate::types::{InstanceType, IntersectionType, KnownClass, Type, UnionType};
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
use smallvec::SmallVec; use smallvec::SmallVec;
use super::Truthiness;
pub(crate) struct UnionBuilder<'db> { pub(crate) struct UnionBuilder<'db> {
elements: Vec<Type<'db>>, elements: Vec<Type<'db>>,
db: &'db dyn Db, db: &'db dyn Db,
@ -243,15 +245,22 @@ impl<'db> InnerIntersectionBuilder<'db> {
} }
} else { } else {
// ~Literal[True] & bool = Literal[False] // ~Literal[True] & bool = Literal[False]
// ~AlwaysTruthy & bool = Literal[False]
if let Type::Instance(InstanceType { class }) = new_positive { if let Type::Instance(InstanceType { class }) = new_positive {
if class.is_known(db, KnownClass::Bool) { if class.is_known(db, KnownClass::Bool) {
if let Some(&Type::BooleanLiteral(value)) = self if let Some(new_type) = self
.negative .negative
.iter() .iter()
.find(|element| element.is_boolean_literal()) .find(|element| {
element.is_boolean_literal()
| matches!(element, Type::AlwaysFalsy | Type::AlwaysTruthy)
})
.map(|element| {
Type::BooleanLiteral(element.bool(db) != Truthiness::AlwaysTrue)
})
{ {
*self = Self::default(); *self = Self::default();
self.positive.insert(Type::BooleanLiteral(!value)); self.positive.insert(new_type);
return; return;
} }
} }
@ -318,15 +327,15 @@ impl<'db> InnerIntersectionBuilder<'db> {
// simplify the representation. // simplify the representation.
self.add_positive(db, ty); self.add_positive(db, ty);
} }
// ~Literal[True] & bool = Literal[False] // bool & ~Literal[True] = Literal[False]
Type::BooleanLiteral(bool) // bool & ~AlwaysTruthy = Literal[False]
if self Type::BooleanLiteral(_) | Type::AlwaysFalsy | Type::AlwaysTruthy
.positive if self.positive.contains(&KnownClass::Bool.to_instance(db)) =>
.iter()
.any(|pos| *pos == KnownClass::Bool.to_instance(db)) =>
{ {
*self = Self::default(); *self = Self::default();
self.positive.insert(Type::BooleanLiteral(!bool)); self.positive.insert(Type::BooleanLiteral(
new_negative.bool(db) != Truthiness::AlwaysTrue,
));
} }
_ => { _ => {
let mut to_remove = SmallVec::<[usize; 1]>::new(); let mut to_remove = SmallVec::<[usize; 1]>::new();
@ -380,7 +389,7 @@ mod tests {
use super::{IntersectionBuilder, IntersectionType, Type, UnionType}; use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
use crate::db::tests::{setup_db, TestDb}; use crate::db::tests::{setup_db, TestDb};
use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder}; use crate::types::{global_symbol, todo_type, KnownClass, Truthiness, UnionBuilder};
use ruff_db::files::system_path_to_file; use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithTestSystem; use ruff_db::system::DbWithTestSystem;
@ -997,42 +1006,43 @@ mod tests {
assert_eq!(ty, expected); assert_eq!(ty, expected);
} }
#[test_case(true)] #[test_case(Type::BooleanLiteral(true))]
#[test_case(false)] #[test_case(Type::BooleanLiteral(false))]
fn build_intersection_simplify_split_bool(bool_value: bool) { #[test_case(Type::AlwaysTruthy)]
#[test_case(Type::AlwaysFalsy)]
fn build_intersection_simplify_split_bool(t_splitter: Type) {
let db = setup_db(); let db = setup_db();
let bool_value = t_splitter.bool(&db) == Truthiness::AlwaysTrue;
let t_bool = KnownClass::Bool.to_instance(&db);
let t_boolean_literal = Type::BooleanLiteral(bool_value);
// We add t_object in various orders (in first or second position) in // We add t_object in various orders (in first or second position) in
// the tests below to ensure that the boolean simplification eliminates // the tests below to ensure that the boolean simplification eliminates
// everything from the intersection, not just `bool`. // everything from the intersection, not just `bool`.
let t_object = KnownClass::Object.to_instance(&db); let t_object = KnownClass::Object.to_instance(&db);
let t_bool = KnownClass::Bool.to_instance(&db);
let ty = IntersectionBuilder::new(&db) let ty = IntersectionBuilder::new(&db)
.add_positive(t_object) .add_positive(t_object)
.add_positive(t_bool) .add_positive(t_bool)
.add_negative(t_boolean_literal) .add_negative(t_splitter)
.build(); .build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value)); assert_eq!(ty, Type::BooleanLiteral(!bool_value));
let ty = IntersectionBuilder::new(&db) let ty = IntersectionBuilder::new(&db)
.add_positive(t_bool) .add_positive(t_bool)
.add_positive(t_object) .add_positive(t_object)
.add_negative(t_boolean_literal) .add_negative(t_splitter)
.build(); .build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value)); assert_eq!(ty, Type::BooleanLiteral(!bool_value));
let ty = IntersectionBuilder::new(&db) let ty = IntersectionBuilder::new(&db)
.add_positive(t_object) .add_positive(t_object)
.add_negative(t_boolean_literal) .add_negative(t_splitter)
.add_positive(t_bool) .add_positive(t_bool)
.build(); .build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value)); assert_eq!(ty, Type::BooleanLiteral(!bool_value));
let ty = IntersectionBuilder::new(&db) let ty = IntersectionBuilder::new(&db)
.add_negative(t_boolean_literal) .add_negative(t_splitter)
.add_positive(t_object) .add_positive(t_object)
.add_positive(t_bool) .add_positive(t_bool)
.build(); .build();

View file

@ -70,7 +70,9 @@ impl<'db> ClassBase<'db> {
| Type::Tuple(_) | Type::Tuple(_)
| Type::SliceLiteral(_) | Type::SliceLiteral(_)
| Type::ModuleLiteral(_) | Type::ModuleLiteral(_)
| Type::SubclassOf(_) => None, | Type::SubclassOf(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy => None,
Type::KnownInstance(known_instance) => match known_instance { Type::KnownInstance(known_instance) => match known_instance {
KnownInstanceType::TypeVar(_) KnownInstanceType::TypeVar(_)
| KnownInstanceType::TypeAliasType(_) | KnownInstanceType::TypeAliasType(_)

View file

@ -140,6 +140,8 @@ impl Display for DisplayRepresentation<'_> {
} }
f.write_str("]") f.write_str("]")
} }
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
} }
} }
} }

View file

@ -196,6 +196,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
is_positive: bool, is_positive: bool,
) -> Option<NarrowingConstraints<'db>> { ) -> Option<NarrowingConstraints<'db>> {
match expression_node { match expression_node {
ast::Expr::Name(name) => Some(self.evaluate_expr_name(name, is_positive)),
ast::Expr::Compare(expr_compare) => { ast::Expr::Compare(expr_compare) => {
self.evaluate_expr_compare(expr_compare, expression, is_positive) self.evaluate_expr_compare(expr_compare, expression, is_positive)
} }
@ -254,6 +255,31 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
} }
} }
fn evaluate_expr_name(
&mut self,
expr_name: &ast::ExprName,
is_positive: bool,
) -> NarrowingConstraints<'db> {
let ast::ExprName { id, .. } = expr_name;
let symbol = self
.symbols()
.symbol_id_by_name(id)
.expect("Should always have a symbol for every Name node");
let mut constraints = NarrowingConstraints::default();
constraints.insert(
symbol,
if is_positive {
Type::AlwaysFalsy.negate(self.db)
} else {
Type::AlwaysTruthy.negate(self.db)
},
);
constraints
}
fn evaluate_expr_compare( fn evaluate_expr_compare(
&mut self, &mut self,
expr_compare: &ast::ExprCompare, expr_compare: &ast::ExprCompare,

View file

@ -75,6 +75,8 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
Ty::AbcClassLiteral("ABCMeta"), Ty::AbcClassLiteral("ABCMeta"),
Ty::SubclassOfAbcClass("ABC"), Ty::SubclassOfAbcClass("ABC"),
Ty::SubclassOfAbcClass("ABCMeta"), Ty::SubclassOfAbcClass("ABCMeta"),
Ty::AlwaysTruthy,
Ty::AlwaysFalsy,
]) ])
.unwrap() .unwrap()
.clone() .clone()