From 319a6193cab980264172aaa157e08d135bd56bbf Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Oct 2026 07:02:03 -0700 Subject: [PATCH] [ty] Evaluate single-predicate narrowing directly --- crates/ty_python_core/src/use_def.rs | 5 ++ crates/ty_python_semantic/src/place.rs | 3 +- crates/ty_python_semantic/src/reachability.rs | 61 +++++++++++++++++-- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_core/src/use_def.rs b/crates/ty_python_core/src/use_def.rs index 185101117f..dc659bbc86 100644 --- a/crates/ty_python_core/src/use_def.rs +++ b/crates/ty_python_core/src/use_def.rs @@ -1447,6 +1447,11 @@ pub struct BindingWithConstraintsIterator<'map, 'db> { } impl<'map, 'db> BindingWithConstraintsIterator<'map, 'db> { + /// Whether only one live binding remains, apart from an initial unbound sentinel. + pub fn has_single_live_binding(&self) -> bool { + self.inner.len() == 1 + } + pub const fn predicates(&self) -> &'map Predicates<'db> { &self.constraint_tables.predicates } diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 060e211c97..93d7aa5f39 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1830,6 +1830,7 @@ fn place_from_bindings_impl<'db>( let predicates = bindings_with_constraints.predicates(); let reachability_constraints = bindings_with_constraints.reachability_constraints(); let boundness_analysis = bindings_with_constraints.boundness_analysis(); + let single_binding = bindings_with_constraints.has_single_live_binding(); let mut bindings_with_constraints = bindings_with_constraints.peekable(); let is_non_exported = |binding: Definition<'db>| { @@ -2006,7 +2007,7 @@ fn place_from_bindings_impl<'db>( binding_ty, ) }) - .narrow(constraint, binding_ty), + .narrow(constraint, binding_ty, single_binding), }; Some((narrowed, static_reachability)) }, diff --git a/crates/ty_python_semantic/src/reachability.rs b/crates/ty_python_semantic/src/reachability.rs index 3565701c67..8023adfdf7 100644 --- a/crates/ty_python_semantic/src/reachability.rs +++ b/crates/ty_python_semantic/src/reachability.rs @@ -592,6 +592,13 @@ const NON_TERMINAL_CALL_CHUNK_SIZE: usize = 16; const REACHABILITY_EVALUATION_CHUNK_SIZE: usize = 256; const CONTROL_FLOW_REACHABILITY_CHECKPOINT_INTERVAL: usize = 16; const NARROWING_EVALUATION_CHECKPOINT_INTERVAL: usize = 8; + +fn is_narrowing_checkpoint_position(predicate: ScopedPredicateId) -> bool { + let index = predicate.index(); + let position = index ^ (index / NARROWING_EVALUATION_CHECKPOINT_INTERVAL); + (position + 1).is_multiple_of(NARROWING_EVALUATION_CHECKPOINT_INTERVAL) +} + fn predicate_scope<'db>(db: &'db dyn Db, predicate: &Predicate<'db>) -> ScopeId<'db> { match predicate.node { PredicateNode::Expression(expression) @@ -978,7 +985,7 @@ pub(crate) fn narrow_type_by_constraint<'db>( place, base_ty, ) - .narrow(id, base_ty) + .narrow(id, base_ty, true) } fn apply_accumulated_narrowing<'db>( @@ -1173,6 +1180,7 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { &mut self, constraint: ScopedNarrowingConstraint, base_ty: Type<'db>, + single_binding: bool, ) -> Type<'db> { self.base_ty = base_ty; match constraint { @@ -1187,10 +1195,55 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { return base_ty; } + // With multiple bindings, registering each root can turn a suffix of another + // binding's graph into a join. Replacement narrowing depends on those boundaries. + if single_binding && let Some(ty) = self.narrow_single_predicate(constraint) { + return ty; + } + let root = self.project(constraint, true); self.narrow_projected(root, base_ty) } + /// Evaluates a single positive or negative expression predicate without projecting a graph. + fn narrow_single_predicate(&self, root: ScopedNarrowingConstraint) -> Option> { + type Id = ScopedNarrowingConstraint; + + let node = self.constraints.get_interior_node(root); + let positive = match (node.if_true, node.if_uncertain, node.if_false) { + (Id::ALWAYS_TRUE, Id::ALWAYS_FALSE, Id::ALWAYS_FALSE) => true, + (Id::ALWAYS_FALSE, Id::ALWAYS_FALSE, Id::ALWAYS_TRUE) => false, + _ => return None, + }; + let predicate = self.predicates[node.atom]; + if !matches!(predicate.node, PredicateNode::Expression(_)) { + return None; + } + + if self + .predicate_narrowing_targets + .contains(node.atom, self.place) + { + if is_narrowing_checkpoint_position(node.atom) { + return None; + } + let (pos, neg) = infer_narrowing_constraints(self.db, predicate, self.place); + if pos.is_some() || neg.is_some() { + return Some(apply_accumulated_narrowing( + self.db, + self.env, + self.base_ty, + if positive { pos } else { neg }, + )); + } + } + + let reachable = analyze_single(self.db, self.env, &predicate) + .negate_if(!positive) + .may_be_true(); + Some(if reachable { self.base_ty } else { Type::Never }) + } + /// Narrows a projected constraint while reusing suffix results for its original binding type. /// /// Registering each root lets the graph recognize shared joins incrementally. @@ -1437,12 +1490,8 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { let node = self.constraints.get_interior_node(id); let predicate = self.predicates[node.atom]; - let index = node.atom.index(); - let checkpoint_position = - index ^ (index / NARROWING_EVALUATION_CHECKPOINT_INTERVAL); if (id != root || use_root_checkpoint) - && (checkpoint_position + 1) - .is_multiple_of(NARROWING_EVALUATION_CHECKPOINT_INTERVAL) + && is_narrowing_checkpoint_position(node.atom) && (self .predicate_narrowing_targets .contains(node.atom, self.place)