Skip to content

[ty] Normalize divergent types with oscillating cycle heads#22818

Closed
ibraheemdev wants to merge 2 commits intoibraheem/lambda-tcxfrom
ibraheem/lambda-tcx-cycle-panic
Closed

[ty] Normalize divergent types with oscillating cycle heads#22818
ibraheemdev wants to merge 2 commits intoibraheem/lambda-tcxfrom
ibraheem/lambda-tcx-cycle-panic

Conversation

@ibraheemdev
Copy link
Copy Markdown
Member

@ibraheemdev ibraheemdev commented Jan 23, 2026

Fixes the panic in #22633, but I'm not entirely sure this is the correct fix.

@ibraheemdev ibraheemdev added the ty Multi-file analysis & type inference label Jan 23, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Jan 23, 2026

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Jan 23, 2026

mypy_primer results

Changes were detected when running on open source projects
prefect (https://github.com/PrefectHQ/prefect)
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any]` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `dict[str, Any] | int | T@resolve_block_document_references | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `T@resolve_variables | dict[str, Any]` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `int | T@resolve_variables | float | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any]` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `dict[str, Any] | int | T@resolve_block_document_references | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `T@resolve_variables` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `int | T@resolve_variables | float | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/deployments/runner.py:795:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | (((...) -> Any) & ((*args: object, **kwargs: object) -> object))`
+ src/prefect/deployments/runner.py:795:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | ((...) -> Any)`
- src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any]`
+ src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `dict[str, Any] | int | T@resolve_block_document_references | ... omitted 4 union elements`
+ src/prefect/flow_engine.py:989:32: error[invalid-await] `Unknown | R@FlowRunEngine | Coroutine[Any, Any, R@FlowRunEngine]` is not awaitable
+ src/prefect/flow_engine.py:1580:24: error[invalid-await] `Unknown | R@AsyncFlowRunEngine | Coroutine[Any, Any, R@AsyncFlowRunEngine]` is not awaitable
+ src/prefect/flow_engine.py:1661:43: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Unknown | R@run_generator_flow_sync`
+ src/prefect/flow_engine.py:1669:21: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_sync`
+ src/prefect/flow_engine.py:1703:44: warning[possibly-missing-attribute] Attribute `__anext__` may be missing on object of type `Unknown | R@run_generator_flow_async`
+ src/prefect/flow_engine.py:1710:25: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_async`
- src/prefect/flows.py:286:34: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
+ src/prefect/flows.py:286:34: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
- src/prefect/flows.py:404:68: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
+ src/prefect/flows.py:404:68: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
- src/prefect/flows.py:1749:53: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ src/prefect/flows.py:1809:21: error[no-matching-overload] No overload of function `run_coro_as_sync` matches arguments
- src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | dict[str, Any]` on object of type `dict[str, Any]`
+ src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `dict[str, Any] | int | T@resolve_block_document_references | ... omitted 4 union elements` on object of type `dict[str, Any]`
- src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[Unknown | T@resolve_block_document_references | dict[str, Any]]`
+ src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[Unknown | dict[str, Any] | int | ... omitted 5 union elements]`
- src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, Unknown | T@resolve_variables]`
+ src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, Unknown | int | T@resolve_variables | ... omitted 5 union elements]`
- src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[Unknown | T@resolve_variables]`
+ src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[Unknown | int | T@resolve_variables | ... omitted 5 union elements]`
- src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any]`
+ src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `dict[str, Any] | int | T@resolve_block_document_references | ... omitted 4 union elements`
- src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `T@resolve_variables`
+ src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `int | T@resolve_variables | float | ... omitted 4 union elements`
- Found 5378 diagnostics
+ Found 5384 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
+ src/scikit_build_core/build/wheel.py:99:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 46 diagnostics
+ Found 47 diagnostics

No memory usage changes detected ✅

@ibraheemdev ibraheemdev force-pushed the ibraheem/lambda-tcx-cycle-panic branch from 8bbd73c to e6e9f14 Compare January 23, 2026 01:50
@ibraheemdev ibraheemdev force-pushed the ibraheem/lambda-tcx-cycle-panic branch from e6e9f14 to 5654adf Compare January 23, 2026 01:53
@ibraheemdev
Copy link
Copy Markdown
Member Author

Looks like this causes some new panics...

@ibraheemdev ibraheemdev force-pushed the ibraheem/lambda-tcx-cycle-panic branch from 74fee24 to f0b06c5 Compare January 23, 2026 04:01
@ibraheemdev ibraheemdev marked this pull request as ready for review January 23, 2026 05:09
@ibraheemdev
Copy link
Copy Markdown
Member Author

This seems to resolve the panic. There is a still a hang that I'm looking into, but that's unrelated (and also present on #22633). Note that this PR is stacked on #22633 so it can be reviewed separately (and because I'm not very confident in the changes).

@MichaReiser
Copy link
Copy Markdown
Member

MichaReiser commented Jan 23, 2026

Can you say a bit more about your reasoning here for this fix? What was the issue and why do you think this is correct (even if you don't feel sure about it). What are the parts that you feel unsure about?

Copy link
Copy Markdown
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not an expert on our cycle normalization, but here's two nits 😄

@@ -0,0 +1,25 @@
# This test would previous panic with: `infer_definition_types(Id(1406)): execute: too many cycle iterations`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# This test would previous panic with: `infer_definition_types(Id(1406)): execute: too many cycle iterations`.
# This test would previously panic with: `infer_definition_types(Id(1406)): execute: too many cycle iterations`.

})
}

pub(super) fn any_over_type_mut<'db>(
Copy link
Copy Markdown
Member

@AlexWaygood AlexWaygood Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we just keep it so that there's only one any_over_type* function? Your branch seems to compile fine with this patch applied; I don't think it's too burdensome to say that you always have to pass in a mutable reference to any_over_type:

Details
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index 996270b511..e58c67800c 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -76,7 +76,7 @@ use crate::types::typed_dict::TypedDictField;
 pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
 pub use crate::types::variance::TypeVarVariance;
 use crate::types::variance::VarianceInferable;
-use crate::types::visitor::{any_over_type, any_over_type_mut};
+use crate::types::visitor::any_over_type;
 use crate::unpack::EvaluationMode;
 use crate::{Db, FxOrderSet, Program};
 pub use class::KnownClass;
@@ -914,7 +914,7 @@ impl<'db> Type<'db> {
             db,
             *self,
             false,
-            &|ty| matches!(ty, Type::TypeVar(tv) if tv.typevar(db).is_self(db)),
+            &mut |ty| matches!(ty, Type::TypeVar(tv) if tv.typevar(db).is_self(db)),
         )
     }
 
@@ -960,7 +960,7 @@ impl<'db> Type<'db> {
 
             _ => {
                 let has_divergent_type_in_cycle = |ty| {
-                    any_over_type(db, ty, false, &|ty| {
+                    any_over_type(db, ty, false, &mut |ty| {
                         ty.as_divergent()
                             .is_some_and(|DivergentType { id }| cycle.head_ids().contains(&id))
                     })
@@ -979,7 +979,7 @@ impl<'db> Type<'db> {
                     //
                     // Without this, we may encounter oscillation as the cycle heads oscillate,
                     // despite the same divergent types being inferred in each iteration.
-                    any_over_type_mut(db, self, false, &mut |ty| {
+                    any_over_type(db, self, false, &mut |ty| {
                         if let Some(DivergentType { id }) = ty.as_divergent() {
                             cycle_heads.insert(id);
                         }
@@ -1254,7 +1254,7 @@ impl<'db> Type<'db> {
     }
 
     pub(crate) fn has_typevar(self, db: &'db dyn Db) -> bool {
-        any_over_type(db, self, false, &|ty| matches!(ty, Type::TypeVar(_)))
+        any_over_type(db, self, false, &mut |ty| matches!(ty, Type::TypeVar(_)))
     }
 
     pub(crate) const fn as_special_form(self) -> Option<SpecialFormType> {
@@ -6196,7 +6196,7 @@ impl<'db> Type<'db> {
                     }
                 });
 
-                let is_recursive = any_over_type(db, alias.raw_value_type(db).expand_eagerly(db), false, &|ty| ty.is_divergent());
+                let is_recursive = any_over_type(db, alias.raw_value_type(db).expand_eagerly(db), false, &mut |ty| ty.is_divergent());
 
                 // If the type mapping does not result in any change to this (non-recursive) type alias, do not expand it.
                 //
@@ -8117,7 +8117,7 @@ impl<'db> TypeVarInstance<'db> {
 
     fn type_is_self_referential(self, db: &'db dyn Db, ty: Type<'db>) -> bool {
         let identity = self.identity(db);
-        any_over_type(db, ty, false, &|ty| match ty {
+        any_over_type(db, ty, false, &mut |ty| match ty {
             Type::TypeVar(bound_typevar) => identity == bound_typevar.typevar(db).identity(db),
             Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => {
                 identity == typevar.identity(db)
diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs
index d0cde1e3b1..d0a059b647 100644
--- a/crates/ty_python_semantic/src/types/constraints.rs
+++ b/crates/ty_python_semantic/src/types/constraints.rs
@@ -1995,7 +1995,7 @@ impl<'db> InteriorNode<'db> {
     #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
     fn exists_one(self, db: &'db dyn Db, bound_typevar: BoundTypeVarIdentity<'db>) -> Node<'db> {
         let mut path = self.path_assignments(db);
-        let mentions_typevar = |ty: Type<'db>| match ty {
+        let mut mentions_typevar = |ty: Type<'db>| match ty {
             Type::TypeVar(haystack) => haystack.identity(db) == bound_typevar,
             _ => false,
         };
@@ -2012,10 +2012,10 @@ impl<'db> InteriorNode<'db> {
                 if constraint.typevar(db).identity(db) == bound_typevar {
                     return true;
                 }
-                if any_over_type(db, constraint.lower(db), false, &mentions_typevar) {
+                if any_over_type(db, constraint.lower(db), false, &mut mentions_typevar) {
                     return true;
                 }
-                if any_over_type(db, constraint.upper(db), false, &mentions_typevar) {
+                if any_over_type(db, constraint.upper(db), false, &mut mentions_typevar) {
                     return true;
                 }
                 false
diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs
index 516bf600fc..d03729869b 100644
--- a/crates/ty_python_semantic/src/types/function.rs
+++ b/crates/ty_python_semantic/src/types/function.rs
@@ -1709,11 +1709,11 @@ impl KnownFunction {
                 let [Some(casted_type), Some(source_type)] = parameter_types else {
                     return;
                 };
-                let contains_unknown_or_todo =
+                let mut contains_unknown_or_todo =
                     |ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
                 if source_type.is_equivalent_to(db, *casted_type)
-                    && !any_over_type(db, *source_type, true, &contains_unknown_or_todo)
-                    && !any_over_type(db, *casted_type, true, &contains_unknown_or_todo)
+                    && !any_over_type(db, *source_type, true, &mut contains_unknown_or_todo)
+                    && !any_over_type(db, *casted_type, true, &mut contains_unknown_or_todo)
                 {
                     if let Some(builder) = context.report_lint(&REDUNDANT_CAST, call_expression) {
                         let source_display = source_type.display(db).to_string();
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 1206e928e0..f04dac2a5e 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -15193,7 +15193,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                 } else {
                     return Err(GenericContextError::InvalidArgument);
                 }
-            } else if any_over_type(db, *typevar, true, &|ty| match ty {
+            } else if any_over_type(db, *typevar, true, &mut |ty| match ty {
                 Type::Dynamic(DynamicType::TodoUnpack | DynamicType::TodoStarredExpression) => true,
                 Type::NominalInstance(nominal) => {
                     nominal.has_known_class(db, KnownClass::TypeVarTuple)
diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
index 81edb423d5..4a164725e3 100644
--- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
@@ -818,7 +818,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
         // instead of two. So until we properly support these, specialize all remaining type
         // variables with a `@Todo` type (since we don't know which of the type arguments
         // belongs to the remaining type variables).
-        if any_over_type(self.db(), value_ty, true, &|ty| ty.is_divergent()) {
+        if any_over_type(self.db(), value_ty, true, &mut |ty| ty.is_divergent()) {
             let value_ty = value_ty.apply_specialization(
                 db,
                 generic_context.specialize(
diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs
index c7b87c0706..6c2bbc602e 100644
--- a/crates/ty_python_semantic/src/types/visitor.rs
+++ b/crates/ty_python_semantic/src/types/visitor.rs
@@ -296,17 +296,6 @@ impl<'db> TypeCollector<'db> {
 /// (value of a type alias, attributes of a class-based protocol, bounds/constraints of a typevar)
 /// are visited or not.
 pub(super) fn any_over_type<'db>(
-    db: &'db dyn Db,
-    ty: Type<'db>,
-    should_visit_lazy_type_attributes: bool,
-    query: &dyn Fn(Type<'db>) -> bool,
-) -> bool {
-    any_over_type_mut(db, ty, should_visit_lazy_type_attributes, &mut |ty| {
-        query(ty)
-    })
-}
-
-pub(super) fn any_over_type_mut<'db>(
     db: &'db dyn Db,
     ty: Type<'db>,
     should_visit_lazy_type_attributes: bool,
~/dev/ruff (ibraheem/lambda-tcx-cycle-panic)⚡ % gd
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index 996270b511..e58c67800c 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -76,7 +76,7 @@ use crate::types::typed_dict::TypedDictField;
 pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
 pub use crate::types::variance::TypeVarVariance;
 use crate::types::variance::VarianceInferable;
-use crate::types::visitor::{any_over_type, any_over_type_mut};
+use crate::types::visitor::any_over_type;
 use crate::unpack::EvaluationMode;
 use crate::{Db, FxOrderSet, Program};
 pub use class::KnownClass;
@@ -914,7 +914,7 @@ impl<'db> Type<'db> {
             db,
             *self,
             false,
-            &|ty| matches!(ty, Type::TypeVar(tv) if tv.typevar(db).is_self(db)),
+            &mut |ty| matches!(ty, Type::TypeVar(tv) if tv.typevar(db).is_self(db)),
         )
     }
 
@@ -960,7 +960,7 @@ impl<'db> Type<'db> {
 
             _ => {
                 let has_divergent_type_in_cycle = |ty| {
-                    any_over_type(db, ty, false, &|ty| {
+                    any_over_type(db, ty, false, &mut |ty| {
                         ty.as_divergent()
                             .is_some_and(|DivergentType { id }| cycle.head_ids().contains(&id))
                     })
@@ -979,7 +979,7 @@ impl<'db> Type<'db> {
                     //
                     // Without this, we may encounter oscillation as the cycle heads oscillate,
                     // despite the same divergent types being inferred in each iteration.
-                    any_over_type_mut(db, self, false, &mut |ty| {
+                    any_over_type(db, self, false, &mut |ty| {
                         if let Some(DivergentType { id }) = ty.as_divergent() {
                             cycle_heads.insert(id);
                         }
@@ -1254,7 +1254,7 @@ impl<'db> Type<'db> {
     }
 
     pub(crate) fn has_typevar(self, db: &'db dyn Db) -> bool {
-        any_over_type(db, self, false, &|ty| matches!(ty, Type::TypeVar(_)))
+        any_over_type(db, self, false, &mut |ty| matches!(ty, Type::TypeVar(_)))
     }
 
     pub(crate) const fn as_special_form(self) -> Option<SpecialFormType> {
@@ -6196,7 +6196,7 @@ impl<'db> Type<'db> {
                     }
                 });
 
-                let is_recursive = any_over_type(db, alias.raw_value_type(db).expand_eagerly(db), false, &|ty| ty.is_divergent());
+                let is_recursive = any_over_type(db, alias.raw_value_type(db).expand_eagerly(db), false, &mut |ty| ty.is_divergent());
 
                 // If the type mapping does not result in any change to this (non-recursive) type alias, do not expand it.
                 //
@@ -8117,7 +8117,7 @@ impl<'db> TypeVarInstance<'db> {
 
     fn type_is_self_referential(self, db: &'db dyn Db, ty: Type<'db>) -> bool {
         let identity = self.identity(db);
-        any_over_type(db, ty, false, &|ty| match ty {
+        any_over_type(db, ty, false, &mut |ty| match ty {
             Type::TypeVar(bound_typevar) => identity == bound_typevar.typevar(db).identity(db),
             Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => {
                 identity == typevar.identity(db)
diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs
index d0cde1e3b1..d0a059b647 100644
--- a/crates/ty_python_semantic/src/types/constraints.rs
+++ b/crates/ty_python_semantic/src/types/constraints.rs
@@ -1995,7 +1995,7 @@ impl<'db> InteriorNode<'db> {
     #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
     fn exists_one(self, db: &'db dyn Db, bound_typevar: BoundTypeVarIdentity<'db>) -> Node<'db> {
         let mut path = self.path_assignments(db);
-        let mentions_typevar = |ty: Type<'db>| match ty {
+        let mut mentions_typevar = |ty: Type<'db>| match ty {
             Type::TypeVar(haystack) => haystack.identity(db) == bound_typevar,
             _ => false,
         };
@@ -2012,10 +2012,10 @@ impl<'db> InteriorNode<'db> {
                 if constraint.typevar(db).identity(db) == bound_typevar {
                     return true;
                 }
-                if any_over_type(db, constraint.lower(db), false, &mentions_typevar) {
+                if any_over_type(db, constraint.lower(db), false, &mut mentions_typevar) {
                     return true;
                 }
-                if any_over_type(db, constraint.upper(db), false, &mentions_typevar) {
+                if any_over_type(db, constraint.upper(db), false, &mut mentions_typevar) {
                     return true;
                 }
                 false
diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs
index 516bf600fc..d03729869b 100644
--- a/crates/ty_python_semantic/src/types/function.rs
+++ b/crates/ty_python_semantic/src/types/function.rs
@@ -1709,11 +1709,11 @@ impl KnownFunction {
                 let [Some(casted_type), Some(source_type)] = parameter_types else {
                     return;
                 };
-                let contains_unknown_or_todo =
+                let mut contains_unknown_or_todo =
                     |ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
                 if source_type.is_equivalent_to(db, *casted_type)
-                    && !any_over_type(db, *source_type, true, &contains_unknown_or_todo)
-                    && !any_over_type(db, *casted_type, true, &contains_unknown_or_todo)
+                    && !any_over_type(db, *source_type, true, &mut contains_unknown_or_todo)
+                    && !any_over_type(db, *casted_type, true, &mut contains_unknown_or_todo)
                 {
                     if let Some(builder) = context.report_lint(&REDUNDANT_CAST, call_expression) {
                         let source_display = source_type.display(db).to_string();
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 1206e928e0..f04dac2a5e 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -15193,7 +15193,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                 } else {
                     return Err(GenericContextError::InvalidArgument);
                 }
-            } else if any_over_type(db, *typevar, true, &|ty| match ty {
+            } else if any_over_type(db, *typevar, true, &mut |ty| match ty {
                 Type::Dynamic(DynamicType::TodoUnpack | DynamicType::TodoStarredExpression) => true,
                 Type::NominalInstance(nominal) => {
                     nominal.has_known_class(db, KnownClass::TypeVarTuple)
diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
index 81edb423d5..4a164725e3 100644
--- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
@@ -818,7 +818,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
         // instead of two. So until we properly support these, specialize all remaining type
         // variables with a `@Todo` type (since we don't know which of the type arguments
         // belongs to the remaining type variables).
-        if any_over_type(self.db(), value_ty, true, &|ty| ty.is_divergent()) {
+        if any_over_type(self.db(), value_ty, true, &mut |ty| ty.is_divergent()) {
             let value_ty = value_ty.apply_specialization(
                 db,
                 generic_context.specialize(
diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs
index c7b87c0706..6c2bbc602e 100644
--- a/crates/ty_python_semantic/src/types/visitor.rs
+++ b/crates/ty_python_semantic/src/types/visitor.rs
@@ -296,17 +296,6 @@ impl<'db> TypeCollector<'db> {
 /// (value of a type alias, attributes of a class-based protocol, bounds/constraints of a typevar)
 /// are visited or not.
 pub(super) fn any_over_type<'db>(
-    db: &'db dyn Db,
-    ty: Type<'db>,
-    should_visit_lazy_type_attributes: bool,
-    query: &dyn Fn(Type<'db>) -> bool,
-) -> bool {
-    any_over_type_mut(db, ty, should_visit_lazy_type_attributes, &mut |ty| {
-        query(ty)
-    })
-}
-
-pub(super) fn any_over_type_mut<'db>(
     db: &'db dyn Db,
     ty: Type<'db>,
     should_visit_lazy_type_attributes: bool,

//
// Without this, we may encounter oscillation as the cycle heads oscillate,
// despite the same divergent types being inferred in each iteration.
any_over_type_mut(db, self, false, &mut |ty| {
Copy link
Copy Markdown
Member

@MichaReiser MichaReiser Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this different from always replacing all Divergent types found in the type?

@mtshiba as our convergences expert: does this change make sense to you? Any idea what could be the reason for the too many iterations error on the lambda PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally thought we could only replace Divergent types that occur in the current cycle heads or the previous type, but that seems to cause more panics. This should be equivalent to replacing all Divergent types.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking into this...

This is just my impression at the moment, but I think there's another, more effective solution.

@ibraheemdev
Copy link
Copy Markdown
Member Author

ibraheemdev commented Jan 23, 2026

Can you say a bit more about your reasoning here for this fix? What was the issue and why do you think this is correct (even if you don't feel sure about it). What are the parts that you feel unsure about?

The comments explain my reasoning, the non-convergence on the lambda PR is caused by the cycle heads oscillating across iterations, while the inferred type remains identical, and so continuing to normalize divergent types from previous cycles allows us to converge. The reason I'm unsure is that I don't understand why the cycle heads are oscillating, and why we are able to infer a divergent type from a previous cycle head, but it seems plausible to me that that is possible and not the root of the issue.

I also don't exactly understand why we limited normalization to the current cycle heads in the first place.

@sharkdp sharkdp removed their request for review February 3, 2026 13:45
@carljm
Copy link
Copy Markdown
Contributor

carljm commented Feb 14, 2026

@mtshiba You mentioned you were looking into alternative approaches here; any findings?

@carljm
Copy link
Copy Markdown
Contributor

carljm commented Feb 14, 2026

I also don't exactly understand why we limited normalization to the current cycle heads in the first place.

It's because Divergent can sometimes be the valid final result of a resolved cycle (or part of a type that is the result of a resolved cycle.) So we shouldn't normalize away unrelated Divergent that aren't part of the current cycle.

If cycle heads can oscillate it seems we need a broader concept of "the current cycle", though :/

@mtshiba
Copy link
Copy Markdown
Collaborator

mtshiba commented Mar 6, 2026

Sorry for the delay.

I looked into it again and found a simpler method than #22818. The panic in #22633 is caused by oscillation of is_redundant_with. In other words, we can suppress the panic by setting cycle_fn to this. cycle_initial=false, cycle_fn=or would be the correct choice. Setting the initial value to false is the opposite of the current true, but it is a more conservative and safer decision. Upon double-checking, it seems this explanation was actually incorrect.
However, there is indeed a prescription to suppress panic! It's #23563.

@ibraheemdev
Copy link
Copy Markdown
Member Author

Going to close this because it's unlikely to be merged anymore.

@ibraheemdev ibraheemdev closed this Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants