[ty] Avoid replaying conditions in long boolean expressions (#29754)

## Summary

Checking Home Assistant’s Anthropic integration took 283.1 ms before
this change and 252.6 ms after (10.8% less time, with a 95% paired
interval of 7.8–13.7% less). Each of 24 blocks ran `ty check
homeassistant/components/anthropic` twice with the baseline build and
once with the changed build, in balanced, randomized order. All runs
emitted the same 41 lines of diagnostics. The full Home Assistant
timings did not resolve this patch’s individual contribution.

The Anthropic SDK’s synchronous and asynchronous streaming functions
each test 53 event names in one `or` expression. At each operand, ty
used to reapply every condition leading up to it, recording 1,378
conditions per expression. The continuation already carries those
earlier conditions, so adding just the preceding operand’s condition
brings the count down to 52.

Checking a file that imports the two original function bodies went from
95.9 ms to 19.3 ms. Changing just those two `or` chains to tuple
membership checks made both builds take about 12 ms. Toggling prefix
replay in a single release executable also reproduced the integration
improvement (270.1 ms to 237.5 ms).

An assignment expression (`:=`) can introduce a new binding partway
through a chain. Those chains keep the earlier conditions so the
reassigned name gets the same reachability information.
This commit is contained in:
Charlie Marsh authored and GitHub committed 2026-10-26 23:14:59 -07:00
1 parent 8aeb8e2ebc
commit fbf83e2268
2 files changed
+20 -16

No files matched your search

+11 -16
View File
@@ -3771,7 +3771,10 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
fn visit_bool_expression(&mut self, node: &'ast ast::ExprBoolOp, context: ExpressionContext) {
let ast::ExprBoolOp { values, op, .. } = node;
let mut snapshots = vec![];
let mut reachability_constraints = vec![];
let has_named_expression = values
.iter()
.any(|value| any_over_expr(value, &ast::Expr::is_named_expr));
let mut reachability_constraints = SmallVec::<[ScopedReachabilityConstraintId; 1]>::new();
let mut last_condition_flow_snapshots = None;
for (index, value) in values.iter().enumerate() {
@@ -3812,19 +3815,17 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
self.flow_snapshot()
};
// We first model the short-circuiting behavior. We take the short-circuit
// path here if all of the previous short-circuit paths were not taken, so
// we record all previously existing reachability constraints, and negate the
// one for the current expression.
// This path reaches the current operand and then short-circuits.
self.record_negated_reachability_constraint(reachability_constraint);
snapshots.push(self.flow_snapshot());
// Then we model the non-short-circuiting behavior. Here, we need to delay
// the application of the reachability constraint until after the expression
// has been evaluated, so we only push it onto the stack here.
self.flow_restore(continuation);
self.record_narrowing_constraint_id_for_places(predicate_id, &possibly_narrowed);
// The continuation carries the conditions that led to this operand. Named
// expressions can introduce new bindings that still need the earlier gates.
if !has_named_expression {
reachability_constraints.clear();
}
reachability_constraints.push(reachability_constraint);
} else {
last_condition_flow_snapshots = self.take_condition_flow_snapshots(value);
@@ -3838,13 +3839,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
condition_flow_snapshots.into_short_circuit_and_continuation(*op);
(Some(short_circuit), Some(no_short_circuit))
} else {
(
None,
values
.iter()
.any(|value| any_over_expr(value, &ast::Expr::is_named_expr))
.then(|| self.flow_snapshot()),
)
(None, has_named_expression.then(|| self.flow_snapshot()))
};
if let Some(last_short_circuit) = last_short_circuit {
@@ -319,6 +319,15 @@ def nested_impossible_operands(other: object, value: bool, marker: int):
reveal_type(marker) # revealed: Never
```
An assignment expression can rebind a name tested earlier in the chain. Reaching the final operand
requires the new value to be `None`, so the branch is unreachable.
```py
def reassigned_operand(value: int | None, replacement: int | None, marker: int):
if value is not None and (value := replacement) is None and value is None and isinstance(value, int):
reveal_type(marker) # revealed: Never
```
## Conditions with aliased `Never` operands
A call cannot produce a result when its return type is an alias of `Never`. Only the preceding