mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
[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:
1 parent
8aeb8e2ebc
commit
fbf83e2268
2 files changed
+20
-16
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user