[ty] Record predicate truthiness alongside expression inference (#29706)

## Summary

A full ty check of isort went from 204 to 172 ms in the ecosystem run.
Across all 163 projects, the average timing factor was 1.00×. Local
checks of two isort revisions on macOS took 16–23% less time. [Ecosystem
timings](https://dqhfo45wdv0ek.cloudfront.net/ruff/pull-requests/29706/ty-ecosystem/40342132072/1/timing.html)

In `if bool(stop()):`, the call to `bool` has type `bool`, but neither
branch can run if `stop()` returns `Never`. Ty uses the operand types to
work this out. In a loop, those types can depend on an assignment
guarded by the same condition. Computing types and truthiness in
separate queries can make ty revisit that loop repeatedly.

We now record each predicate’s evaluation context when we index it.
Expression inference computes the predicate’s truthiness alongside its
operand types and recomputes it if cycle recovery widens them. Ordinary
conditions and match guards both read that result.

The only warning that changed consistently across ecosystem runs was in
psycopg. It now lists the `int` class separately even though `type[str |
Any | int]` already includes it. [Ecosystem
diff](https://dqhfo45wdv0ek.cloudfront.net/ruff/pull-requests/29706/ty-ecosystem/40342132072/1/diff.html)
This commit is contained in:
Charlie Marsh authored and GitHub committed 2026-10-26 15:11:17 -07:00
1 parent 60f104a1a2
commit 2f0a29ece1
11 files changed
+150 -165

No files matched your search

+42 -37
View File
@@ -2209,7 +2209,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
&mut self,
predicate_node: &'ast ast::Expr,
) -> (PredicateOrLiteral<'db>, ScopedPredicateId) {
let predicate = self.build_predicate(predicate_node, ExpressionContext::Condition);
let predicate = self.build_predicate(predicate_node, ExpressionContext::Condition, None);
let predicate_id = self.record_narrowing_constraint(predicate);
(predicate, predicate_id)
}
@@ -2218,6 +2218,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
&mut self,
predicate_node: &'ast ast::Expr,
context: ExpressionContext,
registered: Option<Expression<'db>>,
) -> PredicateOrLiteral<'db> {
// Some commonly used test expressions are eagerly evaluated as `true`
// or `false` here for performance reasons. This list does not need to
@@ -2245,35 +2246,19 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
self.register_narrowing_alias_predicates(predicate_node);
let expression = self.add_standalone_expression(predicate_node);
match resolve_to_literal(predicate_node) {
Some(literal) => PredicateOrLiteral::Literal(literal),
None => PredicateOrLiteral::Predicate(Predicate {
node: match (context, predicate_node) {
(
ExpressionContext::Condition,
ast::Expr::BoolOp(_)
| ast::Expr::If(_)
| ast::Expr::UnaryOp(ast::ExprUnaryOp {
op: ast::UnaryOp::Not,
..
}),
) => PredicateNode::Condition(expression),
(ExpressionContext::Condition, ast::Expr::Compare(compare))
if compare.ops.len() > 1 =>
{
PredicateNode::Condition(expression)
}
_ => PredicateNode::Expression {
expression,
truthiness_from_type: predicate_node.is_name_expr()
|| predicate_node.is_literal_expr(),
},
},
is_positive: true,
}),
if let Some(literal) = resolve_to_literal(predicate_node) {
if registered.is_none() {
self.add_standalone_expression(predicate_node);
}
return PredicateOrLiteral::Literal(literal);
}
let expression =
registered.unwrap_or_else(|| self.add_predicate_expression(predicate_node, context));
PredicateOrLiteral::Predicate(Predicate {
node: PredicateNode::Expression(expression),
is_positive: true,
})
}
fn visit_condition(&mut self, test: &'ast ast::Expr) {
@@ -2405,8 +2390,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
let place_table = self.current_place_table();
match pred.node {
PredicateNode::Expression { expression, .. }
| PredicateNode::Condition(expression) => {
PredicateNode::Expression(expression) => {
let expression_node = expression.node_ref(self.db).node(self.module);
let mut places = PossiblyNarrowedPlacesBuilder::new(self.db, place_table)
.expression(expression_node);
@@ -2838,7 +2822,8 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
// See the comment in TypeInferenceBuilder::infer_match_pattern for more details.
let kind = self.predicate_kind(pattern);
let guard = guard.map(|guard| self.add_standalone_expression(guard));
let guard =
guard.map(|guard| self.add_predicate_expression(guard, ExpressionContext::Condition));
PatternPredicate::new(
self.db,
@@ -2908,6 +2893,22 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
self.add_standalone_expression_impl(expression_node, ExpressionKind::Normal, None)
}
/// Record a predicate in the context in which Python tests it.
fn add_predicate_expression(
&mut self,
node: &ast::Expr,
context: ExpressionContext,
) -> Expression<'db> {
self.add_standalone_expression_impl(
node,
ExpressionKind::Predicate {
context,
in_loop: self.in_loop_context(),
},
None,
)
}
/// Record an expression that is immediately assigned to a target, and that needs to be a Salsa
/// ingredient, because we need to infer its type standalone (type narrowing tests, RHS of an
/// assignment.)
@@ -3755,7 +3756,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
value,
));
let condition_flow_snapshots = self.take_condition_flow_snapshots(value);
let predicate = self.build_predicate(value, context);
let predicate = self.build_predicate(value, context, None);
let possibly_narrowed = self.compute_possibly_narrowed_places(&predicate);
let predicate_id = match op {
ast::BoolOp::And => self.add_predicate(predicate),
@@ -4297,7 +4298,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
self.visit_condition(test);
let condition_flow_snapshot = self.flow_snapshot_for_condition(test);
let predicate = self.build_predicate(test, ExpressionContext::Condition);
let predicate = self.build_predicate(test, ExpressionContext::Condition, None);
if msg.is_some()
|| self
@@ -4615,6 +4616,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
// Visit the test expression after creating loop headers, so that loop-back values
// are visible.
let outer_loop = self.push_loop();
self.visit_condition(test);
let condition_flow_snapshot = self.flow_snapshot_for_condition(test);
@@ -4628,7 +4630,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
let (predicate, predicate_id) = self.record_expression_narrowing_constraint(test);
self.record_reachability_constraint_id(predicate_id);
let outer_loop = self.push_loop();
self.visit_body(body);
let this_loop = self.pop_loop(outer_loop);
@@ -5003,8 +5004,12 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
self.flow_snapshot()
};
let (guard_predicate, guard_predicate_id) =
self.record_expression_narrowing_constraint(guard);
let guard_predicate = self.build_predicate(
guard,
ExpressionContext::Condition,
match_pattern_predicate.guard(self.db),
);
let guard_predicate_id = self.record_narrowing_constraint(guard_predicate);
let guard_reachability_constraint =
self.record_reachability_constraint_id(guard_predicate_id);
let guard_success = self.flow_snapshot();
+6 -1
View File
@@ -78,7 +78,7 @@ use salsa;
/// Condition context does not propagate through calls or assignment expressions: in
/// `if f(x and False)`, the call's result controls the branch, but its argument is evaluated in
/// value context.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)]
pub enum ExpressionContext {
/// Produce the expression's result object for the enclosing code to use.
Value,
@@ -91,6 +91,11 @@ pub enum ExpressionContext {
pub enum ExpressionKind {
/// An ordinary value expression, such as `1` in `self.x: int = 1`.
Normal,
/// A predicate whose truthiness is recorded in its evaluation context.
Predicate {
context: ExpressionContext,
in_loop: bool,
},
/// The callable part of a call, such as `list[T]` in `list[T]()`.
///
/// Type variables used to specialize the callable must already be bound. A constructor call
+2 -12
View File
@@ -160,18 +160,8 @@ impl<'ast> StatementCall<'ast> {
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)]
pub enum PredicateNode<'db> {
/// The truthiness of an expression's resulting value.
Expression {
expression: Expression<'db>,
/// Names and literals need only their inferred type, avoiding a separate truthiness query.
truthiness_from_type: bool,
},
/// A boolean operation, `not`, conditional expression, or chained comparison evaluated
/// directly as a condition.
///
/// In `if x and False`, the truthy branch is unreachable. But after `y = x and False`,
/// `if y` may be truthy: it can call `x.__bool__` a second time and get a different result.
Condition(Expression<'db>),
/// The truthiness of an expression in its evaluation context.
Expression(Expression<'db>),
/// Whether a context manager's exit return type allows an exception to be suppressed.
///
/// Resolved during type inference because the context manager's type is unavailable during
+8 -90
View File
@@ -202,14 +202,13 @@ use crate::{
place::{DefinedPlace, Definedness, Place, RequiresExplicitReExport, imported_symbol},
types::{
CallableType, ComparisonSoundnessPolicy, EnumClassLiteral, KnownInstanceType,
NarrowingConstraint, SpecialFormType, TruthinessAnalyzer, Type, TypeContext, UnionType,
NarrowingConstraint, SpecialFormType, Type, TypeContext, UnionType,
definite_match_pattern_type, definite_match_pattern_type_for_subject, equality_truthiness,
expand_type, infer_expression_types, infer_narrowing_constraints,
expand_type, infer_narrowing_constraints, infer_predicate_truthiness,
infer_same_file_expression_type, mapping_pattern_type, pattern_binding_fallthrough_type,
sequence_pattern_type_builder, singleton_pattern_type,
},
};
use ruff_db::parsed::parsed_module;
use ruff_index::{Idx, IndexSlice};
use ruff_python_ast::name::Name;
use ruff_text_size::TextRange;
@@ -220,7 +219,7 @@ use ty_python_core::{
FileScopeId, NarrowingEvaluator, PredicateNarrowingTargets, ScopedDefinitionId, SemanticIndex,
Truthiness, UseDefMap,
definition::{DefinitionKind, DefinitionState},
expression::{Expression, ExpressionContext},
expression::Expression,
narrowing_constraints::{NarrowingConstraints, ScopedNarrowingConstraint},
place::ScopedPlaceId,
place_table,
@@ -313,7 +312,7 @@ fn pattern_guard_allows_all_matches(db: &dyn Db, predicate: PatternPredicate<'_>
// Condition analysis recovers cycles as ambiguous, so an unresolved guard cannot exclude values.
predicate
.guard(db)
.is_none_or(|guard| condition_truthiness(db, guard).is_always_true())
.is_none_or(|guard| infer_predicate_truthiness(db, guard).is_always_true())
}
/// Narrow `subject_ty` by a match pattern.
@@ -595,8 +594,7 @@ const CONTROL_FLOW_REACHABILITY_CHECKPOINT_INTERVAL: usize = 16;
const NARROWING_EVALUATION_CHECKPOINT_INTERVAL: usize = 8;
fn predicate_scope<'db>(db: &'db dyn Db, predicate: &Predicate<'db>) -> ScopeId<'db> {
match predicate.node {
PredicateNode::Expression { expression, .. }
| PredicateNode::Condition(expression)
PredicateNode::Expression(expression)
| PredicateNode::ContextManagerSuppresses { expression, .. } => expression.scope(db),
PredicateNode::IsNonTerminalCall(call) => call.callable(db).scope(db),
PredicateNode::Pattern(pattern) => pattern.scope(db),
@@ -1946,62 +1944,6 @@ fn context_manager_suppresses<'db>(
)
}
/// Test an expression's result object, accounting for operands that cannot complete.
/// Separate value and condition queries use the expression directly as their Salsa key, avoiding
/// an additional interner for pairs of expressions and evaluation contexts.
#[salsa::tracked(
returns(copy),
cycle_initial = |_, _, _| Truthiness::Ambiguous,
cycle_fn = |_, cycle: &salsa::Cycle, previous: &Truthiness, result: Truthiness, _| widen_truthiness(cycle, *previous, result),
heap_size = get_size2::GetSize::get_heap_size
)]
fn value_truthiness<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Truthiness {
expression_truthiness(db, expression, ExpressionContext::Value)
}
/// Follow short-circuit conditions without testing their result objects again.
#[salsa::tracked(
returns(copy),
cycle_initial = |_, _, _| Truthiness::Ambiguous,
cycle_fn = |_, cycle: &salsa::Cycle, previous: &Truthiness, result: Truthiness, _| widen_truthiness(cycle, *previous, result),
heap_size = get_size2::GetSize::get_heap_size
)]
fn condition_truthiness<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Truthiness {
expression_truthiness(db, expression, ExpressionContext::Condition)
}
fn widen_truthiness(cycle: &salsa::Cycle, previous: Truthiness, result: Truthiness) -> Truthiness {
// A condition can control whether one of its own inputs is reachable. Expression inference
// can lose its previous result when it ceases to be a cycle head, so its type widening alone
// does not ensure that the condition's truthiness converges. Delay widening here to avoid
// retaining imprecise results from the first few iterations.
if cycle.iteration() > crate::TAINTED_CYCLES {
previous.union(result)
} else {
result
}
}
/// Keep this separate from type inference: `bool(never)` has type `bool`, but cannot produce a
/// boolean outcome. The calling query caches that distinction and widens it during inference cycles.
fn expression_truthiness<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
context: ExpressionContext,
) -> Truthiness {
let env = ProgramEnvironment::from_scope(expression.scope(db));
let module = parsed_module(db, expression.python_file(db)).load(db);
let inference = infer_expression_types(db, expression, TypeContext::default());
let node = expression.node_ref(db).node(&module);
TruthinessAnalyzer::new(
db,
&env,
|node| inference.expression_type(node),
|node| inference.comparison_truthiness(node),
)
.truthiness(node, context)
}
/// Evaluate a predicate, returning `Uninhabited` when it cannot produce a boolean outcome.
///
/// Unlike ambiguous truthiness, an uninhabited condition does not make either branch reachable.
@@ -2011,24 +1953,8 @@ fn analyze_single(db: &dyn Db, env: &ProgramEnvironment<'_>, predicate: &Predica
let _span = tracing::trace_span!("analyze_single", ?predicate).entered();
match predicate.node {
PredicateNode::Expression {
expression: test_expr,
truthiness_from_type: true,
} => {
let inference = infer_expression_types(db, test_expr, TypeContext::default());
let ty = inference.expression_type(test_expr.node_ref(db));
if ty.is_equivalent_to(db, env, Type::Never) {
Truthiness::Uninhabited
} else {
ty.bool(db, env).negate_if(!predicate.is_positive)
}
}
PredicateNode::Expression {
expression: test_expr,
truthiness_from_type: false,
} => value_truthiness(db, test_expr).negate_if(!predicate.is_positive),
PredicateNode::Condition(test_expr) => {
condition_truthiness(db, test_expr).negate_if(!predicate.is_positive)
PredicateNode::Expression(expression) => {
infer_predicate_truthiness(db, expression).negate_if(!predicate.is_positive)
}
PredicateNode::ContextManagerSuppresses {
expression,
@@ -2460,15 +2386,7 @@ class TargetB:
let predicate = use_def
.predicates()
.iter()
.find(|predicate| {
matches!(
predicate.node,
PredicateNode::Expression {
truthiness_from_type: true,
..
}
)
})
.find(|predicate| matches!(predicate.node, PredicateNode::Expression(_)))
.unwrap();
let predicates: Predicates = std::iter::repeat_n(*predicate, DEPTH).collect();
+4 -3
View File
@@ -38,9 +38,10 @@ pub(crate) use self::diagnostic::TypeCheckDiagnostics;
pub(crate) use self::diagnostic::register_lints;
pub use self::diagnostic::{UNDEFINED_REVEAL, UNRESOLVED_REFERENCE};
pub(crate) use self::infer::{
InferredDeclaration, TruthinessAnalyzer, TypeContext, infer_complete_scope_types,
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types,
infer_same_file_expression_type, infer_scope_types, is_discarded_dict_key_assignment,
InferredDeclaration, TypeContext, infer_complete_scope_types, infer_deferred_types,
infer_definition_types, infer_expression_type, infer_expression_types,
infer_predicate_truthiness, infer_same_file_expression_type, infer_scope_types,
is_discarded_dict_key_assignment,
};
use self::infer::{
infer_function_default_types, infer_legacy_type_alias, legacy_type_alias_parameters,
+77 -6
View File
@@ -45,7 +45,7 @@
use crate::ProgramEnvironment;
use itertools::Either;
use ruff_db::parsed::parsed_module;
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashMap;
@@ -68,7 +68,7 @@ use crate::{Db, FxIndexSet};
use builder::TypeInferenceBuilder;
pub(super) use comparisons::UnsupportedComparisonError;
use ty_python_core::definition::{Definition, DefinitionKind};
use ty_python_core::expression::Expression;
use ty_python_core::expression::{Expression, ExpressionKind};
use ty_python_core::scope::ScopeId;
use ty_python_core::statement::StatementInner;
use ty_python_core::unpack::Unpack;
@@ -578,13 +578,43 @@ pub(crate) fn infer_expression_types<'db>(
infer_expression_types_impl(db, InferExpression::new(db, expression, tcx))
}
/// A loop predicate can depend on bindings it guards in the previous iteration.
/// Reading inference directly keeps the extra truthiness query out of that cycle.
pub(crate) fn infer_predicate_truthiness<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
) -> Truthiness {
if matches!(
expression.kind(db),
ExpressionKind::Predicate { in_loop: true, .. }
) {
recorded_predicate_truthiness(db, expression)
} else {
cached_predicate_truthiness(db, expression)
}
}
/// Keep reachability queries cached when an operand's type changes but its truthiness stays the same.
#[salsa::tracked(
returns(copy),
cycle_initial = |_, _, _| Truthiness::Ambiguous,
heap_size = get_size2::GetSize::get_heap_size
)]
fn cached_predicate_truthiness<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Truthiness {
recorded_predicate_truthiness(db, expression)
}
fn recorded_predicate_truthiness<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Truthiness {
infer_expression_types_impl(db, InferExpression::Bare(expression)).predicate_truthiness
}
#[salsa::tracked(
returns(ref),
cycle_initial=expression_cycle_initial,
cycle_fn=|db, cycle, previous: &ExpressionInference<'db>, inference: ExpressionInference<'db>, input: InferExpression<'db>| {
let (expression, _) = input.into_inner(db);
let env = ProgramEnvironment::from_scope(expression.scope(db));
inference.cycle_normalized(db, &env, previous, cycle)
inference.cycle_normalized(db, &env, previous, cycle, input)
},
heap_size=ruff_memory_usage::heap_size
)]
@@ -609,7 +639,7 @@ pub(super) fn infer_expression_types_impl<'db>(
let env = ProgramEnvironment::from_file(program_file);
TypeInferenceBuilder::new(
let mut inference = TypeInferenceBuilder::new(
db,
&env,
InferenceRegion::Expression(expression, tcx),
@@ -618,7 +648,9 @@ pub(super) fn infer_expression_types_impl<'db>(
index,
&module,
)
.finish_expression()
.finish_expression();
inference.record_predicate_truthiness(db, &env, input, Some(module));
inference
}
fn expression_cycle_initial<'db>(
@@ -1995,6 +2027,9 @@ pub(crate) struct ExpressionInference<'db> {
/// The types of every expression in this region.
expressions: FrozenMap<ExpressionNodeKey, Type<'db>>,
/// The predicate's possible outcomes in its evaluation context.
predicate_truthiness: Truthiness,
extra: Option<Box<ExpressionInferenceExtra<'db>>>,
/// The scope this region is part of.
@@ -2063,6 +2098,31 @@ struct ExpressionInferenceExtra<'db> {
}
impl<'db> ExpressionInference<'db> {
/// Reachability always asks for a predicate's truthiness without an expected type.
fn record_predicate_truthiness(
&mut self,
db: &'db dyn Db,
env: &ProgramEnvironment<'db>,
input: InferExpression<'db>,
module: Option<ParsedModuleRef>,
) {
let InferExpression::Bare(expression) = input else {
return;
};
let ExpressionKind::Predicate { context, .. } = expression.kind(db) else {
return;
};
let module =
module.unwrap_or_else(|| parsed_module(db, expression.python_file(db)).load(db));
self.predicate_truthiness = TruthinessAnalyzer::new(
db,
env,
|node| self.expression_type(node),
|node| self.comparison_truthiness(node),
)
.truthiness(expression.node_ref(db).node(&module), context);
}
fn cycle_initial(scope: ScopeId<'db>, cycle_recovery: Type<'db>) -> Self {
let _ = scope;
Self {
@@ -2070,6 +2130,7 @@ impl<'db> ExpressionInference<'db> {
cycle_recovery: Some(cycle_recovery),
..ExpressionInferenceExtra::default()
})),
predicate_truthiness: Truthiness::Ambiguous,
expressions: FrozenMap::default(),
#[cfg(debug_assertions)]
scope,
@@ -2082,6 +2143,7 @@ impl<'db> ExpressionInference<'db> {
env: &ProgramEnvironment<'db>,
previous: &ExpressionInference<'db>,
cycle: &salsa::Cycle,
input: InferExpression<'db>,
) -> ExpressionInference<'db> {
if let Some(extra) = self.extra.as_mut() {
for (binding, binding_ty) in &mut extra.bindings {
@@ -2107,6 +2169,15 @@ impl<'db> ExpressionInference<'db> {
*ty = ty.cycle_normalized(db, env, previous_ty, cycle);
}
// Normalizing the operands can change whether the predicate completes. Once the first
// provisional results have cleared, retain earlier outcomes so the predicate converges.
self.record_predicate_truthiness(db, env, input, None);
if cycle.iteration() > crate::TAINTED_CYCLES {
self.predicate_truthiness = previous
.predicate_truthiness
.union(self.predicate_truthiness);
}
if cycle.iteration() > crate::TAINTED_CYCLES
&& let Some(previous_extra) = previous.extra.as_deref()
&& !previous_extra.collection_use_constraints.is_empty()
@@ -2164,7 +2235,7 @@ impl<'db> ExpressionInference<'db> {
.unwrap_or_else(Type::unknown)
}
pub(crate) fn comparison_truthiness(
fn comparison_truthiness(
&self,
expression: impl Into<ExpressionNodeKey>,
) -> Option<Truthiness> {
@@ -1515,7 +1515,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.context.inference_flags |= InferenceFlags::CHECK_UNBOUND_TYPEVARS;
self.infer_expression_impl(expression.node_ref(self.db()).node(self.module()), tcx);
}
ExpressionKind::Normal => {
ExpressionKind::Normal | ExpressionKind::Predicate { .. } => {
self.infer_expression_impl(expression.node_ref(self.db()).node(self.module()), tcx);
}
ExpressionKind::TypeExpression => {
@@ -12949,6 +12949,7 @@ impl<'db> FullExpressionCacheEntry<'db> {
});
ExpressionInference {
predicate_truthiness: Truthiness::Ambiguous,
expressions: FrozenMap::from(self.expressions),
extra,
#[cfg(debug_assertions)]
@@ -1509,10 +1509,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let places = self.index.place_table(self.scope().file_scope_id(db));
let predicate = Predicate {
node: PredicateNode::Expression {
expression: self.index.expression(test),
truthiness_from_type: test.is_name_expr() || test.is_literal_expr(),
},
node: PredicateNode::Expression(self.index.expression(test)),
is_positive: false,
};
@@ -876,9 +876,9 @@ fn predicate_contains_special_cased_condition<'db>(
predicate: ScopedPredicateId,
) -> bool {
let expression = match use_def_map(db, scope).predicates()[predicate].node {
PredicateNode::Expression { expression, .. }
| PredicateNode::Condition(expression)
| PredicateNode::IsNonEmptyIterable(expression) => expression,
PredicateNode::Expression(expression) | PredicateNode::IsNonEmptyIterable(expression) => {
expression
}
PredicateNode::Pattern(pattern) => pattern.subject(db),
PredicateNode::SubjectElementPattern(element) => element.pattern.subject(db),
// These predicates describe implicit control flow rather than a tested value. In
@@ -574,6 +574,7 @@ fn comparison_truthiness_widens_across_sparse_cycle_results() -> anyhow::Result<
let inference = |ty, truthiness: Option<Truthiness>| {
(
ExpressionInference {
predicate_truthiness: Truthiness::Ambiguous,
expressions: [(expression, ty)].into_iter().collect(),
extra: truthiness.map(|truthiness| {
Box::new(ExpressionInferenceExtra {
@@ -88,7 +88,7 @@ pub(crate) fn infer_narrowing_constraints<'db>(
Option<NarrowingConstraint<'db>>,
) {
let constraints = match predicate.node {
PredicateNode::Expression { expression, .. } | PredicateNode::Condition(expression) => {
PredicateNode::Expression(expression) => {
let constraints = all_narrowing_constraints_for_expression(db, expression);
(constraints.get(place, true), constraints.get(place, false))
}
@@ -153,10 +153,7 @@ fn all_narrowing_constraints_for_expression<'db>(
let python_file = program_file.python_file(db);
let env = ProgramEnvironment::from_file(program_file);
let module = parsed_module(db, python_file).load(db);
let predicate = PredicateNode::Expression {
expression,
truthiness_from_type: false,
};
let predicate = PredicateNode::Expression(expression);
let mut positive = NarrowingConstraintsBuilder::new(db, &env, &module, predicate, true);
let positive_constraints = positive.finish();
let mut negative = NarrowingConstraintsBuilder::new(db, &env, &module, predicate, false);
@@ -1720,7 +1717,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
fn finish(&mut self) -> Option<FrozenNarrowingConstraints<'db>> {
let constraints: Option<NarrowingConstraints<'db>> = match self.predicate {
PredicateNode::Expression { expression, .. } | PredicateNode::Condition(expression) => {
PredicateNode::Expression(expression) => {
self.evaluate_expression_predicate(expression, self.is_positive)
}
PredicateNode::Pattern(pattern) => {
@@ -3484,8 +3481,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> {
fn scope(&self) -> ScopeId<'db> {
let db = self.db;
match self.predicate {
PredicateNode::Expression { expression, .. }
| PredicateNode::Condition(expression)
PredicateNode::Expression(expression)
| PredicateNode::ContextManagerSuppresses { expression, .. } => expression.scope(db),
PredicateNode::Pattern(pattern) => pattern.scope(db),
PredicateNode::FinallyNormalPathImpossible { scope, .. } => scope,