diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md new file mode 100644 index 0000000000..9e391e4ef5 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md @@ -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 +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 19d645800b..cf5e0833d4 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -431,6 +431,11 @@ pub enum Type<'db> { Union(UnionType<'db>), /// The set of objects in all of the types in the intersection 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 IntLiteral(i64), /// 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)) } + // 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`. (Type::StringLiteral(_), Type::LiteralString) => true, @@ -1105,6 +1119,16 @@ impl<'db> Type<'db> { 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) => { left.instance_fallback(db).is_disjoint_from(db, right) } @@ -1238,7 +1262,9 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(_) | Type::SliceLiteral(_) - | Type::KnownInstance(_) => true, + | Type::KnownInstance(_) + | Type::AlwaysFalsy + | Type::AlwaysTruthy => true, Type::SubclassOf(SubclassOfType { base }) => matches!(base, ClassBase::Class(_)), Type::ClassLiteral(_) | Type::Instance(_) => { // TODO: Ideally, we would iterate over the MRO of the class, check if all @@ -1340,6 +1366,7 @@ impl<'db> Type<'db> { // false } + Type::AlwaysTruthy | Type::AlwaysFalsy => false, } } @@ -1410,7 +1437,9 @@ impl<'db> Type<'db> { | Type::Todo(_) | Type::Union(..) | 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_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(), } } @@ -1600,6 +1633,8 @@ impl<'db> Type<'db> { // TODO: see above Truthiness::Ambiguous } + Type::AlwaysTruthy => Truthiness::AlwaysTrue, + Type::AlwaysFalsy => Truthiness::AlwaysFalse, instance_ty @ Type::Instance(InstanceType { class }) => { if class.is_known(db, KnownClass::NoneType) { Truthiness::AlwaysFalse @@ -1912,7 +1947,9 @@ impl<'db> Type<'db> { | Type::StringLiteral(_) | Type::SliceLiteral(_) | 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")) .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)), } } @@ -3558,6 +3596,8 @@ pub(crate) mod tests { SubclassOfAbcClass(&'static str), StdlibModule(CoreStdlibModule), SliceLiteral(i32, i32, i32), + AlwaysTruthy, + AlwaysFalsy, } impl Ty { @@ -3625,6 +3665,8 @@ pub(crate) mod tests { Some(stop), 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::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) { let db = setup_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::AbcInstance("ABCMeta"), Ty::SubclassOfBuiltinClass("type"))] #[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) { let db = setup_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::SubclassOfBuiltinClass("object"), Ty::None)] #[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::LiteralString)] + #[test_case(Ty::AlwaysFalsy, Ty::AlwaysTruthy)] fn is_disjoint_from(a: Ty, b: Ty) { let db = setup_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::SubclassOfAny)] #[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) { let db = setup_db(); let a = a.into_type(&db); diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 9972451955..8d5a559f86 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -30,6 +30,8 @@ use crate::types::{InstanceType, IntersectionType, KnownClass, Type, UnionType}; use crate::{Db, FxOrderSet}; use smallvec::SmallVec; +use super::Truthiness; + pub(crate) struct UnionBuilder<'db> { elements: Vec>, db: &'db dyn Db, @@ -243,15 +245,22 @@ impl<'db> InnerIntersectionBuilder<'db> { } } else { // ~Literal[True] & bool = Literal[False] + // ~AlwaysTruthy & bool = Literal[False] if let Type::Instance(InstanceType { class }) = new_positive { if class.is_known(db, KnownClass::Bool) { - if let Some(&Type::BooleanLiteral(value)) = self + if let Some(new_type) = self .negative .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.positive.insert(Type::BooleanLiteral(!value)); + self.positive.insert(new_type); return; } } @@ -318,15 +327,15 @@ impl<'db> InnerIntersectionBuilder<'db> { // simplify the representation. self.add_positive(db, ty); } - // ~Literal[True] & bool = Literal[False] - Type::BooleanLiteral(bool) - if self - .positive - .iter() - .any(|pos| *pos == KnownClass::Bool.to_instance(db)) => + // bool & ~Literal[True] = Literal[False] + // bool & ~AlwaysTruthy = Literal[False] + Type::BooleanLiteral(_) | Type::AlwaysFalsy | Type::AlwaysTruthy + if self.positive.contains(&KnownClass::Bool.to_instance(db)) => { *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(); @@ -380,7 +389,7 @@ mod tests { use super::{IntersectionBuilder, IntersectionType, Type, UnionType}; 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::system::DbWithTestSystem; @@ -997,42 +1006,43 @@ mod tests { assert_eq!(ty, expected); } - #[test_case(true)] - #[test_case(false)] - fn build_intersection_simplify_split_bool(bool_value: bool) { + #[test_case(Type::BooleanLiteral(true))] + #[test_case(Type::BooleanLiteral(false))] + #[test_case(Type::AlwaysTruthy)] + #[test_case(Type::AlwaysFalsy)] + fn build_intersection_simplify_split_bool(t_splitter: Type) { let db = setup_db(); - - let t_bool = KnownClass::Bool.to_instance(&db); - let t_boolean_literal = Type::BooleanLiteral(bool_value); + let bool_value = t_splitter.bool(&db) == Truthiness::AlwaysTrue; // We add t_object in various orders (in first or second position) in // the tests below to ensure that the boolean simplification eliminates // everything from the intersection, not just `bool`. let t_object = KnownClass::Object.to_instance(&db); + let t_bool = KnownClass::Bool.to_instance(&db); let ty = IntersectionBuilder::new(&db) .add_positive(t_object) .add_positive(t_bool) - .add_negative(t_boolean_literal) + .add_negative(t_splitter) .build(); assert_eq!(ty, Type::BooleanLiteral(!bool_value)); let ty = IntersectionBuilder::new(&db) .add_positive(t_bool) .add_positive(t_object) - .add_negative(t_boolean_literal) + .add_negative(t_splitter) .build(); assert_eq!(ty, Type::BooleanLiteral(!bool_value)); let ty = IntersectionBuilder::new(&db) .add_positive(t_object) - .add_negative(t_boolean_literal) + .add_negative(t_splitter) .add_positive(t_bool) .build(); assert_eq!(ty, Type::BooleanLiteral(!bool_value)); let ty = IntersectionBuilder::new(&db) - .add_negative(t_boolean_literal) + .add_negative(t_splitter) .add_positive(t_object) .add_positive(t_bool) .build(); diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index ede1e5e1c9..4bd2fea206 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -70,7 +70,9 @@ impl<'db> ClassBase<'db> { | Type::Tuple(_) | Type::SliceLiteral(_) | Type::ModuleLiteral(_) - | Type::SubclassOf(_) => None, + | Type::SubclassOf(_) + | Type::AlwaysFalsy + | Type::AlwaysTruthy => None, Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::TypeVar(_) | KnownInstanceType::TypeAliasType(_) diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 55c24a27f3..bec476d725 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -140,6 +140,8 @@ impl Display for DisplayRepresentation<'_> { } f.write_str("]") } + Type::AlwaysTruthy => f.write_str("AlwaysTruthy"), + Type::AlwaysFalsy => f.write_str("AlwaysFalsy"), } } } diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index 69513ccfba..53476cdbb4 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -196,6 +196,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> { is_positive: bool, ) -> Option> { match expression_node { + ast::Expr::Name(name) => Some(self.evaluate_expr_name(name, is_positive)), ast::Expr::Compare(expr_compare) => { 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( &mut self, expr_compare: &ast::ExprCompare, diff --git a/crates/red_knot_python_semantic/src/types/property_tests.rs b/crates/red_knot_python_semantic/src/types/property_tests.rs index 83f49ea220..584a9fc793 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests.rs +++ b/crates/red_knot_python_semantic/src/types/property_tests.rs @@ -75,6 +75,8 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty { Ty::AbcClassLiteral("ABCMeta"), Ty::SubclassOfAbcClass("ABC"), Ty::SubclassOfAbcClass("ABCMeta"), + Ty::AlwaysTruthy, + Ty::AlwaysFalsy, ]) .unwrap() .clone()