[ty] use Type::Divergent to avoid panic in infinitely-nested-tuple implicit attribute (#20333)

## Summary

Use `Type::Divergent` to avoid "too many iterations" panic on an
infinitely-nested tuple in an implicit instance attribute.

The regression here is from checking all tuple elements to see if they
contain a Divergent type. It's 5% on one project, 1% on another, and
zero on the rest. I spent some time looking into eliminating this
regression by tracking a flag on inference results to note if they could
possibly contain any Divergent type, but this doesn't really work --
there are too many different ways a type containing a Divergent type
could enter an inference result. Still thinking about whether there are
other ways to reduce this. One option is if we see certain kinds of
non-atomic types that are commonly expensive to check for Divergent, we
could make `has_divergent_type` a Salsa query on those types.

## Test Plan

Added mdtest.

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Carl Meyer 2025-09-11 06:51:22 -07:00 committed by GitHub
parent 89f17467ef
commit ffd4340dce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 142 additions and 79 deletions

View file

@ -762,6 +762,10 @@ impl<'db> Type<'db> {
Self::Dynamic(DynamicType::Unknown)
}
pub(crate) fn divergent(scope: ScopeId<'db>) -> Self {
Self::Dynamic(DynamicType::Divergent(DivergentType { scope }))
}
pub(crate) fn object(db: &'db dyn Db) -> Self {
KnownClass::Object.to_instance(db)
}
@ -6516,7 +6520,6 @@ impl<'db> Type<'db> {
}
}
#[allow(unused)]
pub(super) fn has_divergent_type(self, db: &'db dyn Db, div: Type<'db>) -> bool {
any_over_type(db, self, &|ty| match ty {
Type::Dynamic(DynamicType::Divergent(_)) => ty == div,

View file

@ -85,7 +85,7 @@ fn scope_cycle_recover<'db>(
}
fn scope_cycle_initial<'db>(_db: &'db dyn Db, scope: ScopeId<'db>) -> ScopeInference<'db> {
ScopeInference::cycle_fallback(scope)
ScopeInference::cycle_initial(scope)
}
/// Infer all types for a [`Definition`] (including sub-expressions).
@ -123,7 +123,7 @@ fn definition_cycle_initial<'db>(
db: &'db dyn Db,
definition: Definition<'db>,
) -> DefinitionInference<'db> {
DefinitionInference::cycle_fallback(definition.scope(db))
DefinitionInference::cycle_initial(definition.scope(db))
}
/// Infer types for all deferred type expressions in a [`Definition`].
@ -164,7 +164,7 @@ fn deferred_cycle_initial<'db>(
db: &'db dyn Db,
definition: Definition<'db>,
) -> DefinitionInference<'db> {
DefinitionInference::cycle_fallback(definition.scope(db))
DefinitionInference::cycle_initial(definition.scope(db))
}
/// Infer all types for an [`Expression`] (including sub-expressions).
@ -192,20 +192,29 @@ pub(crate) fn infer_expression_types<'db>(
.finish_expression()
}
/// How many fixpoint iterations to allow before falling back to Divergent type.
const ITERATIONS_BEFORE_FALLBACK: u32 = 10;
fn expression_cycle_recover<'db>(
_db: &'db dyn Db,
db: &'db dyn Db,
_value: &ExpressionInference<'db>,
_count: u32,
_expression: Expression<'db>,
count: u32,
expression: Expression<'db>,
) -> salsa::CycleRecoveryAction<ExpressionInference<'db>> {
salsa::CycleRecoveryAction::Iterate
if count == ITERATIONS_BEFORE_FALLBACK {
salsa::CycleRecoveryAction::Fallback(ExpressionInference::cycle_fallback(
expression.scope(db),
))
} else {
salsa::CycleRecoveryAction::Iterate
}
}
fn expression_cycle_initial<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
) -> ExpressionInference<'db> {
ExpressionInference::cycle_fallback(expression.scope(db))
ExpressionInference::cycle_initial(expression.scope(db))
}
/// Infers the type of an `expression` that is guaranteed to be in the same file as the calling query.
@ -324,7 +333,7 @@ fn unpack_cycle_recover<'db>(
}
fn unpack_cycle_initial<'db>(_db: &'db dyn Db, _unpack: Unpack<'db>) -> UnpackResult<'db> {
UnpackResult::cycle_fallback(Type::Never)
UnpackResult::cycle_initial(Type::Never)
}
/// Returns the type of the nearest enclosing class for the given scope.
@ -378,6 +387,35 @@ impl<'db> InferenceRegion<'db> {
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, get_size2::GetSize, salsa::Update)]
enum CycleRecovery<'db> {
/// An initial-value for fixpoint iteration; all types are `Type::Never`.
Initial,
/// A divergence-fallback value for fixpoint iteration; all types are `Divergent`.
Divergent(ScopeId<'db>),
}
impl<'db> CycleRecovery<'db> {
fn merge(self, other: Option<CycleRecovery<'db>>) -> Self {
if let Some(other) = other {
match (self, other) {
// It's important here that we keep the scope of `self` if merging two `Divergent`.
(Self::Divergent(scope), _) | (_, Self::Divergent(scope)) => Self::Divergent(scope),
_ => Self::Initial,
}
} else {
self
}
}
fn fallback_type(self) -> Type<'db> {
match self {
Self::Initial => Type::Never,
Self::Divergent(scope) => Type::divergent(scope),
}
}
}
/// The inferred types for a scope region.
#[derive(Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)]
pub(crate) struct ScopeInference<'db> {
@ -385,27 +423,25 @@ pub(crate) struct ScopeInference<'db> {
expressions: FxHashMap<ExpressionNodeKey, Type<'db>>,
/// The extra data that is only present for few inference regions.
extra: Option<Box<ScopeInferenceExtra>>,
extra: Option<Box<ScopeInferenceExtra<'db>>>,
}
#[derive(Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update, Default)]
struct ScopeInferenceExtra {
/// The fallback type for missing expressions/bindings/declarations.
///
/// This is used only when constructing a cycle-recovery `TypeInference`.
cycle_fallback: bool,
struct ScopeInferenceExtra<'db> {
/// Is this a cycle-recovery inference result, and if so, what kind?
cycle_recovery: Option<CycleRecovery<'db>>,
/// The diagnostics for this region.
diagnostics: TypeCheckDiagnostics,
}
impl<'db> ScopeInference<'db> {
fn cycle_fallback(scope: ScopeId<'db>) -> Self {
fn cycle_initial(scope: ScopeId<'db>) -> Self {
let _ = scope;
Self {
extra: Some(Box::new(ScopeInferenceExtra {
cycle_fallback: true,
cycle_recovery: Some(CycleRecovery::Initial),
..ScopeInferenceExtra::default()
})),
expressions: FxHashMap::default(),
@ -431,14 +467,10 @@ impl<'db> ScopeInference<'db> {
.or_else(|| self.fallback_type())
}
fn is_cycle_callback(&self) -> bool {
fn fallback_type(&self) -> Option<Type<'db>> {
self.extra
.as_ref()
.is_some_and(|extra| extra.cycle_fallback)
}
fn fallback_type(&self) -> Option<Type<'db>> {
self.is_cycle_callback().then_some(Type::Never)
.and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type))
}
}
@ -471,10 +503,8 @@ pub(crate) struct DefinitionInference<'db> {
#[derive(Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update, Default)]
struct DefinitionInferenceExtra<'db> {
/// The fallback type for missing expressions/bindings/declarations.
///
/// This is used only when constructing a cycle-recovery `TypeInference`.
cycle_fallback: bool,
/// Is this a cycle-recovery inference result, and if so, what kind?
cycle_recovery: Option<CycleRecovery<'db>>,
/// The definitions that are deferred.
deferred: Box<[Definition<'db>]>,
@ -487,7 +517,7 @@ struct DefinitionInferenceExtra<'db> {
}
impl<'db> DefinitionInference<'db> {
fn cycle_fallback(scope: ScopeId<'db>) -> Self {
fn cycle_initial(scope: ScopeId<'db>) -> Self {
let _ = scope;
Self {
@ -497,7 +527,7 @@ impl<'db> DefinitionInference<'db> {
#[cfg(debug_assertions)]
scope,
extra: Some(Box::new(DefinitionInferenceExtra {
cycle_fallback: true,
cycle_recovery: Some(CycleRecovery::Initial),
..DefinitionInferenceExtra::default()
})),
}
@ -566,14 +596,10 @@ impl<'db> DefinitionInference<'db> {
self.declarations.iter().map(|(_, qualifiers)| *qualifiers)
}
fn is_cycle_callback(&self) -> bool {
fn fallback_type(&self) -> Option<Type<'db>> {
self.extra
.as_ref()
.is_some_and(|extra| extra.cycle_fallback)
}
fn fallback_type(&self) -> Option<Type<'db>> {
self.is_cycle_callback().then_some(Type::Never)
.and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type))
}
pub(crate) fn undecorated_type(&self) -> Option<Type<'db>> {
@ -605,21 +631,34 @@ struct ExpressionInferenceExtra<'db> {
/// The diagnostics for this region.
diagnostics: TypeCheckDiagnostics,
/// `true` if this region is part of a cycle-recovery `TypeInference`.
///
/// Falls back to `Type::Never` if an expression is missing.
cycle_fallback: bool,
/// Is this a cycle recovery inference result, and if so, what kind?
cycle_recovery: Option<CycleRecovery<'db>>,
/// `true` if all places in this expression are definitely bound
all_definitely_bound: bool,
}
impl<'db> ExpressionInference<'db> {
fn cycle_initial(scope: ScopeId<'db>) -> Self {
let _ = scope;
Self {
extra: Some(Box::new(ExpressionInferenceExtra {
cycle_recovery: Some(CycleRecovery::Initial),
all_definitely_bound: true,
..ExpressionInferenceExtra::default()
})),
expressions: FxHashMap::default(),
#[cfg(debug_assertions)]
scope,
}
}
fn cycle_fallback(scope: ScopeId<'db>) -> Self {
let _ = scope;
Self {
extra: Some(Box::new(ExpressionInferenceExtra {
cycle_fallback: true,
cycle_recovery: Some(CycleRecovery::Divergent(scope)),
all_definitely_bound: true,
..ExpressionInferenceExtra::default()
})),
@ -645,14 +684,10 @@ impl<'db> ExpressionInference<'db> {
.unwrap_or_else(Type::unknown)
}
fn is_cycle_callback(&self) -> bool {
fn fallback_type(&self) -> Option<Type<'db>> {
self.extra
.as_ref()
.is_some_and(|extra| extra.cycle_fallback)
}
fn fallback_type(&self) -> Option<Type<'db>> {
self.is_cycle_callback().then_some(Type::Never)
.and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type))
}
/// Returns true if all places in this expression are definitely bound.

View file

@ -9,10 +9,10 @@ use ruff_text_size::{Ranged, TextRange};
use rustc_hash::{FxHashMap, FxHashSet};
use super::{
DefinitionInference, DefinitionInferenceExtra, ExpressionInference, ExpressionInferenceExtra,
InferenceRegion, ScopeInference, ScopeInferenceExtra, infer_deferred_types,
infer_definition_types, infer_expression_types, infer_same_file_expression_type,
infer_scope_types, infer_unpack_types,
CycleRecovery, DefinitionInference, DefinitionInferenceExtra, ExpressionInference,
ExpressionInferenceExtra, InferenceRegion, ScopeInference, ScopeInferenceExtra,
infer_deferred_types, infer_definition_types, infer_expression_types,
infer_same_file_expression_type, infer_scope_types, infer_unpack_types,
};
use crate::module_name::{ModuleName, ModuleNameResolutionError};
use crate::module_resolver::{
@ -251,10 +251,8 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> {
/// For function definitions, the undecorated type of the function.
undecorated_type: Option<Type<'db>>,
/// The fallback type for missing expressions/bindings/declarations.
///
/// This is used only when constructing a cycle-recovery `TypeInference`.
cycle_fallback: bool,
/// Did we merge in a sub-region with a cycle-recovery fallback, and if so, what kind?
cycle_recovery: Option<CycleRecovery<'db>>,
/// `true` if all places in this expression are definitely bound
all_definitely_bound: bool,
@ -290,11 +288,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
typevar_binding_context: None,
deferred: VecSet::default(),
undecorated_type: None,
cycle_fallback: false,
cycle_recovery: None,
all_definitely_bound: true,
}
}
fn extend_cycle_recovery(&mut self, other_recovery: Option<CycleRecovery<'db>>) {
match &mut self.cycle_recovery {
Some(recovery) => *recovery = recovery.merge(other_recovery),
recovery @ None => *recovery = other_recovery,
}
}
fn fallback_type(&self) -> Option<Type<'db>> {
self.cycle_recovery.map(CycleRecovery::fallback_type)
}
fn extend_definition(&mut self, inference: &DefinitionInference<'db>) {
#[cfg(debug_assertions)]
assert_eq!(self.scope, inference.scope);
@ -307,7 +316,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
if let Some(extra) = &inference.extra {
self.cycle_fallback |= extra.cycle_fallback;
self.extend_cycle_recovery(extra.cycle_recovery);
self.context.extend(&extra.diagnostics);
self.deferred.extend(extra.deferred.iter().copied());
}
@ -325,7 +334,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Some(extra) = &inference.extra {
self.context.extend(&extra.diagnostics);
self.cycle_fallback |= extra.cycle_fallback;
self.extend_cycle_recovery(extra.cycle_recovery);
if !matches!(self.region, InferenceRegion::Scope(..)) {
self.bindings.extend(extra.bindings.iter().copied());
@ -399,7 +408,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.expressions
.get(&expr.into())
.copied()
.or(self.cycle_fallback.then_some(Type::Never))
.or(self.fallback_type())
}
/// Get the type of an expression from any scope in the same file.
@ -5189,12 +5198,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
parenthesized: _,
} = tuple;
// Collecting all elements is necessary to infer all sub-expressions even if some
// element types are `Never` (which leads `from_elements` to return early without
// consuming the whole iterator).
let element_types: Vec<_> = elts.iter().map(|elt| self.infer_expression(elt)).collect();
let db = self.db();
let divergent = Type::divergent(self.scope());
let element_types = elts.iter().map(|element| {
let element_type = self.infer_expression(element);
if element_type.has_divergent_type(self.db(), divergent) {
divergent
} else {
element_type
}
});
Type::heterogeneous_tuple(self.db(), element_types)
Type::heterogeneous_tuple(db, element_types)
}
fn infer_list_expression(&mut self, list: &ast::ExprList) -> Type<'db> {
@ -8808,7 +8823,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
bindings,
declarations,
deferred,
cycle_fallback,
cycle_recovery,
all_definitely_bound,
// Ignored; only relevant to definition regions
@ -8836,7 +8851,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
let extra =
(cycle_fallback || !bindings.is_empty() || !diagnostics.is_empty() || !all_definitely_bound).then(|| {
(cycle_recovery.is_some() || !bindings.is_empty() || !diagnostics.is_empty() || !all_definitely_bound).then(|| {
if bindings.len() > 20 {
tracing::debug!(
"Inferred expression region `{:?}` contains {} bindings. Lookups by linear scan might be slow.",
@ -8848,7 +8863,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Box::new(ExpressionInferenceExtra {
bindings: bindings.into_boxed_slice(),
diagnostics,
cycle_fallback,
cycle_recovery,
all_definitely_bound,
})
});
@ -8873,7 +8888,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
bindings,
declarations,
deferred,
cycle_fallback,
cycle_recovery,
undecorated_type,
all_definitely_bound: _,
// builder only state
@ -8889,12 +8904,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let diagnostics = context.finish();
let extra = (!diagnostics.is_empty()
|| cycle_fallback
|| cycle_recovery.is_some()
|| undecorated_type.is_some()
|| !deferred.is_empty())
.then(|| {
Box::new(DefinitionInferenceExtra {
cycle_fallback,
cycle_recovery,
deferred: deferred.into_boxed_slice(),
diagnostics,
undecorated_type,
@ -8936,7 +8951,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
context,
mut expressions,
scope,
cycle_fallback,
cycle_recovery,
// Ignored, because scope types are never extended into other scopes.
deferred: _,
@ -8959,9 +8974,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let _ = scope;
let diagnostics = context.finish();
let extra = (!diagnostics.is_empty() || cycle_fallback).then(|| {
let extra = (!diagnostics.is_empty() || cycle_recovery.is_some()).then(|| {
Box::new(ScopeInferenceExtra {
cycle_fallback,
cycle_recovery,
diagnostics,
})
});

View file

@ -224,7 +224,7 @@ impl<'db> UnpackResult<'db> {
&self.diagnostics
}
pub(crate) fn cycle_fallback(cycle_fallback_type: Type<'db>) -> Self {
pub(crate) fn cycle_initial(cycle_fallback_type: Type<'db>) -> Self {
Self {
targets: FxHashMap::default(),
diagnostics: TypeCheckDiagnostics::default(),