Skip to content
Merged
51 changes: 51 additions & 0 deletions crates/ty_python_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,27 @@ impl Truthiness {
if condition { self.negate() } else { self }
}

#[must_use]
pub fn and(self, other: Self) -> Self {
match self {
Truthiness::AlwaysTrue => other,
Truthiness::AlwaysFalse => self,
Truthiness::Ambiguous => match other {
Truthiness::AlwaysFalse => Truthiness::AlwaysFalse,
Truthiness::AlwaysTrue | Truthiness::Ambiguous => Truthiness::Ambiguous,
},
}
}

/// Like [`Truthiness::and`], but evaluates `other` only when `self` may be true.
#[must_use]
pub fn and_else(self, other: impl FnOnce() -> Self) -> Self {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the normal convention would be to call this .and_then(), similar to how Option has both .or_else() which only calls the closure if self is None and .and_then() which only does work when self is Some

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah shoot, makes sense, I'll push a follow-up

match self {
Truthiness::AlwaysFalse => self,
Truthiness::AlwaysTrue | Truthiness::Ambiguous => self.and(other()),
}
}

#[must_use]
pub fn or(self, other: Self) -> Self {
match self {
Expand Down Expand Up @@ -1078,6 +1099,7 @@ mod tests {
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextRange};

use super::Truthiness::{AlwaysFalse, AlwaysTrue, Ambiguous};
use super::*;

use crate::{
Expand Down Expand Up @@ -1137,6 +1159,35 @@ mod tests {
.collect()
}

#[test]
fn truthiness_and() {
for (left, right, expected) in [
(AlwaysTrue, AlwaysTrue, AlwaysTrue),
(AlwaysTrue, AlwaysFalse, AlwaysFalse),
(AlwaysTrue, Ambiguous, Ambiguous),
(AlwaysFalse, AlwaysTrue, AlwaysFalse),
(AlwaysFalse, AlwaysFalse, AlwaysFalse),
(AlwaysFalse, Ambiguous, AlwaysFalse),
(Ambiguous, AlwaysTrue, Ambiguous),
(Ambiguous, AlwaysFalse, AlwaysFalse),
(Ambiguous, Ambiguous, Ambiguous),
] {
assert_eq!(left.and(right), expected, "{left:?}.and({right:?})");

let mut calls = 0;
let lazy_result = left.and_else(|| {
calls += 1;
right
});
assert_eq!(lazy_result, expected, "{left:?}.and_else(|| {right:?})");
assert_eq!(
calls,
usize::from(left != AlwaysFalse),
"{left:?}.and_else call count"
);
}
}

#[test]
fn empty() {
let TestCase { db, file } = test_case("");
Expand Down
16 changes: 5 additions & 11 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11193,10 +11193,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
err.fallback_truthiness()
});
preceding_truthiness = match op {
ast::BoolOp::And => preceding_truthiness
.negate()
.or(truthiness.negate())
.negate(),
ast::BoolOp::And => preceding_truthiness.and(truthiness),
ast::BoolOp::Or => preceding_truthiness.or(truthiness),
};

Expand Down Expand Up @@ -11311,14 +11308,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);

if ops.len() > 1 {
// As a condition, the chain is truthy only if both its prefix and final comparison are
// truthy. Skip the final comparison's truthiness computation when the prefix is
// already always false.
let truthiness = preceding_truthiness
.negate()
.or_else(|| {
last_comparison_ty
.bool(db, self.program_environment())
.negate()
})
.negate();
.and_else(|| last_comparison_ty.bool(db, self.program_environment()));
let expression = ast::ExprRef::Compare(compare).into();
if truthiness != ty.bool(db, self.program_environment()) {
self.comparison_truthiness.insert(expression, truthiness);
Comment thread
carljm marked this conversation as resolved.
Expand Down