Skip to content

Conversation

@mtshiba
Copy link
Contributor

@mtshiba mtshiba commented Sep 25, 2025

Summary

Derived from #17371

Fixes astral-sh/ty#256
Fixes astral-sh/ty#1415
Fixes astral-sh/ty#1433

Properly handles any kind of recursive inference and prevents panics.
The discussion in #17371 (comment) revealed that additional changes to the salsa API are required to complete this PR, and I will update this PR as soon as those changes are made.


Let me explain techniques for converging fixed-point iterations during recursive type inference.
There are two types of type inference that naively don't converge (causing salsa to panic): divergent type inference and oscillating type inference.

Divergent type inference

Divergent type inference occurs when eagerly expanding a recursive type. A typical example is this:

class C:
    def f(self, other: "C"):
        self.x = (other.x, 1)

reveal_type(C().x) # revealed: Unknown | tuple[Unknown | tuple[Unknown | tuple[..., Literal[1]], Literal[1]], Literal[1]]

To solve this problem, we have already introduced Divergent types (#20312). Divergent types are treated as a kind of dynamic type 1.

Unknown | tuple[Unknown | tuple[Unknown | tuple[..., Literal[1]], Literal[1]], Literal[1]]
=> Unknown | tuple[Divergent, Literal[1]]

When a query function that returns a type enters a cycle, it sets Divergent as the cycle initial value (instead of Never). Then, in the cycle recovery function, it reduces the nesting of types containing Divergent to converge.

0th: Divergent
1st: Unknown | tuple[Divergent, Literal[1]]
2nd: Unknown | tuple[Unknown | tuple[Divergent, Literal[1]], Literal[1]]
=> Unknown | tuple[Divergent, Literal[1]]

Each cycle recovery function for each query should operate only on the Divergent type originating from that query.
For this reason, while Divergent appears the same as Any to the user, it internally carries some information: the location where the cycle occurred. Previously, we roughly identified this by having the scope where the cycle occurred, but with the update to salsa, functions that create cycle initial values ​​can now receive a salsa::Id (salsa-rs/salsa#1012). This is an opaque ID that uniquely identifies the cycle head (the query that is the starting point for the fixed-point iteration). Divergent now has this salsa::Id.

Oscillating type inference

Now, another thing to consider is oscillating type inference. Oscillating type inference arises from the fact that monotonicity is broken. Monotonicity here means that for a query function, if it enters a cycle, the calculation must start from a "bottom value" and progress towards the final result with each cycle. Monotonicity breaks down in type systems that have features like overloading and overriding.

class Base:
    def flip(self) -> "Sub":
        return Sub()

class Sub(Base):
    def flip(self) -> "Base":
        return Base()

class C:
    def __init__(self, x: Sub):
        self.x = x

    def replace_with(self, other: "C"):
        self.x = other.x.flip()

reveal_type(C(Sub()).x)

Naive fixed-point iteration results in Divergent -> Sub -> Base -> Sub -> ..., which oscillates forever without diverging or converging. To address this, the salsa API has been modified so that the cycle recovery function receives the value of the previous revision (salsa-rs/salsa#1012).
The cycle recovery function returns the union of the type of the current revision and the previous revision. Therefore, the value for each cycle is Divergent -> Sub -> Base (= Sub | Base) -> Base, which converges.
The final result of oscillating type inference does not contain Divergent because Divergent that appears in a union type can be removed, as is clear from the expansion. This simplification is performed at the same time as nesting reduction.

T | Divergent = T | (T | (T | ...)) = T

Test Plan

All samples listed in astral-sh/ty#256 are tested and passed without any panic!

Footnotes

  1. In theory, it may be possible to strictly treat types containing Divergent types as recursive types, but we probably shouldn't go that deep yet. (AFAIK, there are no PEPs that specify how to handle implicitly recursive types that aren't named by type aliases)

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Sep 25, 2025

#[allow(private_interfaces)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)]
pub enum DivergenceKind<'db> {
Copy link
Member

Choose a reason for hiding this comment

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

It seems we only use the identity of the divergent type and don't care that much about about its content. Could we use a u64 instead (with a global counter of the last issued id)?

@MichaReiser
Copy link
Member

What are the salsa features that are required for this PR to land?

@MichaReiser
Copy link
Member

MichaReiser commented Oct 22, 2025

I put up a PR that implements the extensions that I think are needed for this to work:

  • Pass the query id to the cycle recovery function. It can be used as a cheap identifier of the divergent value
  • Pass the last provisional value to the cycle recovery function

See salsa-rs/salsa#1012

Therefore, I think it's necessary to specify an additional value joining operation in the tracked function. For example, like this:

I didn't understand this part or the semantics of the join operation. Specifically, how the join operation is different from fallback because cycle_recovery already runs after the query. It can override the value from this iteration. That's why it isn't clear to me why the fallback implementation can't call the join internally when necessary

With those features in place, is this PR something you could drive forward? I'm asking because we're most likely going to land #20988 as is because it unblocks type of self with a minimal regression but it would be great to have a proper fix in place.

@MichaReiser
Copy link
Member

One thing I wonder. Should queries be allowed to customize converged so that we can use an operation other than Eq. That would allow us to return list[Diverged] as fallback from the cycle recovery function, it would lead to list[list[Diverged]] in the next iteration when we have while True: x = [x], but the has_converged function could decide to still return true in that case, saying that the query converged. We would then still need to replace the query result with list[Converged] to ensure the last provisional value and the new value are indeed equal.

So maybe, cycle_recovery should be called for all values and it returns:

  • Converged(T): The query has convered, use T as the final query result (allows customizing equal and overriding the final value to go from list[list[Converged]] back to list[Converged])
  • Iterate(T): Keep iterating, use the given value (should be new_value in the common case)

@mtshiba
Copy link
Contributor Author

mtshiba commented Oct 22, 2025

I put up a PR that implements the extensions that I think are needed for this to work:

Thanks, it'll be helpful, but I'll have to elaborate on my thoughts.

I didn't understand this part or the semantics of the join operation. Specifically, how the join operation is different from fallback because cycle_recovery already runs after the query. It can override the value from this iteration. That's why it isn't clear to me why the fallback implementation can't call the join internally when necessary

@carljm had a similar question, and my answer is given here.

With those features in place, is this PR something you could drive forward?

For the reason stated above, I believe that simply providing a "last provisional value" in the cycle_recovery function will not achieve fully-converged type inference.

I'm asking because we're most likely going to land #20988 as is because it unblocks type of self with a minimal regression but it would be great to have a proper fix in place.

In my opinion, there is no need to set a "threshold" at which to give up on further type inference and switch to Divergent (for reasons other than performance). That's the purpose of #20566.
However, I think it's OK to merge the PR as a provisional implementation to push forward with the implementation of self-related features, as I can create a follow-up PR.

@mtshiba
Copy link
Contributor Author

mtshiba commented Oct 22, 2025

One thing I wonder. Should queries be allowed to customize converged so that we can use an operation other than Eq. That would allow us to return list[Diverged] as fallback from the cycle recovery function, it would lead to list[list[Diverged]] in the next iteration when we have while True: x = [x], but the has_converged function could decide to still return true in that case, saying that the query converged. We would then still need to replace the query result with list[Converged] to ensure the last provisional value and the new value are indeed equal.

So maybe, cycle_recovery should be called for all values and it returns:

  • Converged(T): The query has convered, use T as the final query result (allows customizing equal and overriding the final value to go from list[list[Converged]] back to list[Converged])
  • Iterate(T): Keep iterating, use the given value (should be new_value in the common case)

I think that leaving the decision of convergence to the user risks making inference unstable: that is, salsa users may force (or erroneously) declare convergence when it has not actually converged, leading to non-determinism.

@MichaReiser
Copy link
Member

MichaReiser commented Oct 22, 2025

I think that leaving the decision of convergence to the user risks making inference unstable: that is, salsa users may force (or erroneously) declare convergence when it has not actually converged, leading to non-determinism.

Yeah, agree. But I think we can enable this functionality by comparing if the value returned by Fallback is identical with the last provisional and, if that's the case, consider the cycle as converged (which is true, because all queries would read exactly the same value when we iterate again).

@carljm had a similar question, and my answer is given #17371 (comment).

I read that conversation and it's the part I don't understand. It also seems specific to return type inference because the lattice isn't monotonic. Which seems different from diverging cases that continue going forever.

Edit: Instead of having specific handling in many places. Could the cycle function instead:

If the previous value contains Divergent:

Replace all instance of previous_value in new_value with Divergent

This is a no-op for Divergent, but should reduce list[list[Divergent]] to list[Divergent] if the previous_type was list[Divergent] and the new type is list[list[Divergent]].

The change I proposed in Salsa upstream to consider a cycle head as converged when the last_provisional_value == Fallback_value should then mark this cycle head as converged (which might result in the entire cycle to converge, if all other cycle heads have converged too)

@mtshiba
Copy link
Contributor Author

mtshiba commented Oct 23, 2025

I read that conversation and it's the part I don't understand. It also seems specific to return type inference because the lattice isn't monotonic. Which seems different from diverging cases that continue going forever.

Edit: Instead of having specific handling in many places. Could the cycle function instead:

If the previous value contains Divergent:

Replace all instance of previous_value in new_value with Divergent

This is a no-op for Divergent, but should reduce list[list[Divergent]] to list[Divergent] if the previous_type was list[Divergent] and the new type is list[list[Divergent]].

The change I proposed in Salsa upstream to consider a cycle head as converged when the last_provisional_value == Fallback_value should then mark this cycle head as converged (which might result in the entire cycle to converge, if all other cycle heads have converged too)

Consider inferring the return type of the following function:

# 0th: Divergent (cycle_initial)
# 1st: None | tuple[Divergent]
# 2nd: None | tuple[None | tuple[Divergent]]
# ...
def div(x: int):
    if x == 0:
        return None
    else:
        return (div(x-1),)

If the values ​​from the previous and current cycles do not match, then the cycle_recovery function is called. However, no matter what replacement we make to the current value (the type cached for the next cycle) within cycle_recovery, it will not match the result of the next cycle. This is because if the type after replacement is T, the type for the next cycle will be None | tuple[T]. Therefore, any manipulation of the type to converge type inference must be done before the equality test.
This is why cycle_recovery handling cannot be integrated with divergence suppression handling. As already explained, suppressing oscillating type inference also needs to be done separately from cycle_recovery. Oscillating type inference can occur not only in function return type inference as in #17371, but also in implicit instance attribute type inference. For example, this type inference should oscillate if self is typed and overload resolution is implemented correctly:

from typing import overload

class A: ...
class B(A): ...

@overload
def flip(x: B) -> A: ...
@overload
def flip(x: A) -> B: ...
def flip(x): ...

class C:
    def __init__(self, x: B):
        self.x = x

    def flip(self):
        # 0th: Divergent
        # 1st: B
        # 2nd: A
        # 3rd: B
        # ...
        self.x = flip(self.x)

reveal_type(C(B()).x)

Therefore, I believe the correct approach here is to allow salsa::tracked to specify a cycle_join function as an argument, as explained in #17371 (comment). This function joins the previous and current values ​​before the equality check.

@MichaReiser
Copy link
Member

MichaReiser commented Oct 23, 2025

Therefore, I believe the correct approach here is to allow salsa::tracked to specify a cycle_join function as an argument, as explained in #17371 (comment). This function joins the previous and current values ​​before the equality check.

I think the new salsa version supports this now. The cycle_fn gets the old and new value and it can return Fallback(V). If V == last_provisional, then salsa considers the cycle as converged (which I think is the same as you want with your join function)

https://github.com/salsa-rs/salsa/blob/d38145c29574758de7ffbe8a13cd4584c3b09161/src/function/execute.rs#L350-L360

@mtshiba
Copy link
Contributor Author

mtshiba commented Oct 23, 2025

Therefore, I believe the correct approach here is to allow salsa::tracked to specify a cycle_join function as an argument, as explained in #17371 (comment). This function joins the previous and current values ​​before the equality check.

I think the new salsa version supports this now. The cycle_fn gets the old and new value and it can return Fallback(V). If V == last_provisional, then salsa considers the cycle as converged (which I think is the same as you want with your join function)

https://github.com/salsa-rs/salsa/blob/d38145c29574758de7ffbe8a13cd4584c3b09161/src/function/execute.rs#L350-L360

Ah, so you've made it so that the equality test is performed again after the fallback. I overlooked that. So, I think there's no problem and and I can move this PR forward.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 27, 2025

Diagnostic diff on typing conformance tests

Changes were detected when running ty on typing conformance tests
--- old-output.txt	2025-11-07 06:03:21.954184401 +0000
+++ new-output.txt	2025-11-07 06:03:24.974194889 +0000
@@ -1,4 +1,3 @@
-fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/05a9af7/src/function/execute.rs:451:17 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(18b71)): execute: too many cycle iterations`
 _directives_deprecated_library.py:15:31: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
 _directives_deprecated_library.py:30:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `str`
 _directives_deprecated_library.py:36:41: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Self@__add__`
@@ -74,6 +73,23 @@
 aliases_type_statement.py:79:7: error[too-many-positional-arguments] Too many positional arguments: expected 2, got 3
 aliases_type_statement.py:80:7: error[too-many-positional-arguments] Too many positional arguments: expected 2, got 3
 aliases_type_statement.py:80:37: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?
+aliases_typealiastype.py:32:7: error[unresolved-attribute] Object of type `typing.TypeAliasType` has no attribute `other_attrib`
+aliases_typealiastype.py:39:26: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?
+aliases_typealiastype.py:52:40: error[invalid-type-form] Function calls are not allowed in type expressions
+aliases_typealiastype.py:53:40: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?
+aliases_typealiastype.py:54:42: error[invalid-type-form] Tuple literals are not allowed in this context in a type expression
+aliases_typealiastype.py:54:43: error[invalid-type-form] Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?
+aliases_typealiastype.py:55:42: error[invalid-type-form] List comprehensions are not allowed in type expressions
+aliases_typealiastype.py:56:42: error[invalid-type-form] Dict literals are not allowed in type expressions
+aliases_typealiastype.py:57:42: error[invalid-type-form] Function calls are not allowed in type expressions
+aliases_typealiastype.py:58:48: error[invalid-type-form] Int literals are not allowed in this context in a type expression
+aliases_typealiastype.py:59:42: error[invalid-type-form] `if` expressions are not allowed in type expressions
+aliases_typealiastype.py:60:42: error[invalid-type-form] Variable of type `Literal[3]` is not allowed in a type expression
+aliases_typealiastype.py:61:42: error[invalid-type-form] Boolean literals are not allowed in this context in a type expression
+aliases_typealiastype.py:62:42: error[invalid-type-form] Int literals are not allowed in this context in a type expression
+aliases_typealiastype.py:63:42: error[invalid-type-form] Boolean operations are not allowed in type expressions
+aliases_typealiastype.py:64:42: error[invalid-type-form] F-strings are not allowed in type expressions
+aliases_typealiastype.py:66:47: error[unresolved-reference] Name `BadAlias21` used when not defined
 aliases_variance.py:18:24: error[non-subscriptable] Cannot subscript object of type `<class 'ClassA[typing.TypeVar]'>` with no `__class_getitem__` method
 aliases_variance.py:28:16: error[non-subscriptable] Cannot subscript object of type `<class 'ClassA[typing.TypeVar]'>` with no `__class_getitem__` method
 aliases_variance.py:44:16: error[non-subscriptable] Cannot subscript object of type `<class 'ClassB[typing.TypeVar, typing.TypeVar]'>` with no `__class_getitem__` method
@@ -1002,5 +1018,4 @@
 typeddicts_usage.py:28:17: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
 typeddicts_usage.py:28:18: error[invalid-key] Invalid key for TypedDict `Movie`: Unknown key "title"
 typeddicts_usage.py:40:24: error[invalid-type-form] The special form `typing.TypedDict` is not allowed in type expressions. Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?
-Found 1004 diagnostics
-WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.
+Found 1020 diagnostics

@mtshiba mtshiba force-pushed the recursive-inference branch from 82d9580 to d9eac53 Compare October 27, 2025 05:18
@codspeed-hq
Copy link

codspeed-hq bot commented Oct 27, 2025

CodSpeed Performance Report

Merging #20566 will improve performances by 9.32%

Comparing mtshiba:recursive-inference (2393f6a) with main (c7ff982)

Summary

⚡ 2 improvements
✅ 50 untouched

Benchmarks breakdown

Mode Benchmark BASE HEAD Change
WallTime large[sympy] 52 s 49.8 s +4.4%
WallTime medium[static-frame] 12 s 11 s +9.32%

@MichaReiser
Copy link
Member

Ohh... do we also need to pass the id to the initial function?

@mtshiba mtshiba force-pushed the recursive-inference branch from d9eac53 to 041bb24 Compare October 27, 2025 07:58
@mtshiba
Copy link
Contributor Author

mtshiba commented Oct 27, 2025

Ohh... do we also need to pass the id to the initial function?

Yes. For now I'm using https://github.com/mtshiba/salsa/tree/input_id?rev=9ea5289bc6a87943b8a8620df8ff429062c56af0, but is there a better way? There will be a lot of changes.

@MichaReiser
Copy link
Member

Yes. For now I'm using mtshiba/salsa@input_id?rev=9ea5289bc6a87943b8a8620df8ff429062c56af0, but is there a better way? There will be a lot of changes.

No, I don't think there's a better way. It's a breaking change that requires updating all cycle initial functions. Do you want to put up a Salsa PR (Claude's really good at updating the function signatures, but not very fast :))

@github-actions
Copy link
Contributor

github-actions bot commented Nov 3, 2025

mypy_primer results

Changes were detected when running on open source projects
parso (https://github.com/davidhalter/parso)
- parso/python/errors.py:415:18: warning[possibly-missing-attribute] Attribute `add_block` may be missing on object of type `Unknown | None | _Context`
- parso/python/errors.py:416:24: warning[possibly-missing-attribute] Attribute `blocks` may be missing on object of type `Unknown | None | _Context`
- parso/python/errors.py:431:28: warning[possibly-missing-attribute] Attribute `parent_context` may be missing on object of type `Unknown | None | _Context`
- parso/python/errors.py:432:13: warning[possibly-missing-attribute] Attribute `close_child_context` may be missing on object of type `Unknown | None`
- parso/python/errors.py:470:32: warning[possibly-missing-attribute] Attribute `add_context` may be missing on object of type `Unknown | None | _Context`
- parso/python/errors.py:489:9: warning[possibly-missing-attribute] Attribute `finalize` may be missing on object of type `Unknown | None | _Context`
- Found 183 diagnostics
+ Found 177 diagnostics

spack (https://github.com/spack/spack)
- lib/spack/spack/mirrors/mirror.py:114:16: error[invalid-return-type] Return type does not match returned value: expected `bool`, found `Literal[True] | Unknown | str`
+ lib/spack/spack/mirrors/mirror.py:114:16: error[invalid-return-type] Return type does not match returned value: expected `bool`, found `Literal[True] | Unknown | str | Divergent`
- lib/spack/spack/mirrors/mirror.py:120:16: error[invalid-return-type] Return type does not match returned value: expected `bool`, found `Unknown | str`
+ lib/spack/spack/mirrors/mirror.py:120:16: error[invalid-return-type] Return type does not match returned value: expected `bool`, found `Unknown | str | Divergent`
- lib/spack/spack/mirrors/mirror.py:263:45: error[invalid-argument-type] Argument to bound method `_update_connection_dict` is incorrect: Expected `dict[Unknown, Unknown]`, found `@Todo | str`
+ lib/spack/spack/mirrors/mirror.py:263:45: error[invalid-argument-type] Argument to bound method `_update_connection_dict` is incorrect: Expected `dict[Unknown, Unknown]`, found `@Todo | str | Divergent`

dragonchain (https://github.com/dragonchain/dragonchain)
- dragonchain/job_processor/contract_job.py:127:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None` may be missing
+ dragonchain/job_processor/contract_job.py:127:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None | (Divergent & ~AlwaysFalsy)` may be missing
- dragonchain/job_processor/contract_job.py:128:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None` may be missing
+ dragonchain/job_processor/contract_job.py:128:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None | (Divergent & ~AlwaysFalsy)` may be missing
- dragonchain/job_processor/contract_job.py:129:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None` may be missing
+ dragonchain/job_processor/contract_job.py:129:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None | (Divergent & ~AlwaysFalsy)` may be missing
- dragonchain/job_processor/contract_job.py:130:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None` may be missing
+ dragonchain/job_processor/contract_job.py:130:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None | (Divergent & ~AlwaysFalsy)` may be missing
- dragonchain/job_processor/contract_job.py:131:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None` may be missing
+ dragonchain/job_processor/contract_job.py:131:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None | (Divergent & ~AlwaysFalsy)` may be missing
- dragonchain/job_processor/contract_job.py:132:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None` may be missing
+ dragonchain/job_processor/contract_job.py:132:9: warning[possibly-missing-implicit-call] Method `__setitem__` of type `Unknown | None | (Divergent & ~AlwaysFalsy)` may be missing
- dragonchain/job_processor/contract_job.py:167:24: warning[possibly-missing-attribute] Attribute `split` may be missing on object of type `Unknown | None`
+ dragonchain/job_processor/contract_job.py:167:24: warning[possibly-missing-attribute] Attribute `split` may be missing on object of type `Unknown | None | (Divergent & ~AlwaysFalsy)`
- dragonchain/job_processor/contract_job.py:198:60: error[not-iterable] Object of type `Unknown | None` may not be iterable
+ dragonchain/job_processor/contract_job.py:198:60: error[not-iterable] Object of type `Unknown | None | (Divergent & ~AlwaysFalsy)` may not be iterable
- dragonchain/job_processor/contract_job.py:250:29: error[invalid-argument-type] Argument to bound method `pull_image` is incorrect: Expected `str`, found `Unknown | None`
+ dragonchain/job_processor/contract_job.py:250:29: error[invalid-argument-type] Argument to bound method `pull_image` is incorrect: Expected `str`, found `Unknown | None | (Divergent & ~AlwaysFalsy)`
- dragonchain/job_processor/contract_job.py:327:23: error[not-iterable] Object of type `Unknown | None` may not be iterable
+ dragonchain/job_processor/contract_job.py:327:23: error[not-iterable] Object of type `Unknown | None | (Divergent & ~AlwaysFalsy)` may not be iterable
- dragonchain/job_processor/contract_job.py:419:13: warning[possibly-missing-attribute] Attribute `update` may be missing on object of type `Unknown | None`
+ dragonchain/job_processor/contract_job.py:419:13: warning[possibly-missing-attribute] Attribute `update` may be missing on object of type `Unknown | None | (Divergent & ~AlwaysFalsy)`
- dragonchain/job_processor/contract_job_utest.py:431:9: warning[possibly-missing-attribute] Attribute `update` may be missing on object of type `Unknown | None`
+ dragonchain/job_processor/contract_job_utest.py:431:9: warning[possibly-missing-attribute] Attribute `update` may be missing on object of type `Unknown | None | (Divergent & ~AlwaysFalsy)`

strawberry (https://github.com/strawberry-graphql/strawberry)
+ strawberry/channels/handlers/ws_handler.py:69:5: warning[unsupported-base] Unsupported class base with type `<class 'AsyncBaseHTTPView[GraphQLWSConsumer, GraphQLWSConsumer, GraphQLWSConsumer, GraphQLWSConsumer, GraphQLWSConsumer, Context@GraphQLWSConsumer, RootValue@GraphQLWSConsumer]'> | <class 'AsyncBaseHTTPView[GraphQLWSConsumer[None, None], GraphQLWSConsumer[None, None], GraphQLWSConsumer[None, None], GraphQLWSConsumer[None, None], GraphQLWSConsumer[None, None], Context@GraphQLWSConsumer, RootValue@GraphQLWSConsumer]'>`
- Found 376 diagnostics
+ Found 377 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
+ src/scikit_build_core/_logging.py:153:13: warning[unsupported-base] Unsupported class base with type `<class 'Mapping[str, Style]'> | <class 'Mapping[str, Divergent]'>`
- Found 49 diagnostics
+ Found 50 diagnostics

setuptools (https://github.com/pypa/setuptools)
- setuptools/_distutils/command/build_ext.py:270:23: warning[possibly-missing-attribute] Attribute `split` may be missing on object of type `(Unknown & ~AlwaysFalsy) | (list[tuple[Unknown, str] | Unknown] & ~AlwaysFalsy)`
+ setuptools/_distutils/command/build_ext.py:270:23: warning[possibly-missing-attribute] Attribute `split` may be missing on object of type `(Unknown & ~AlwaysFalsy) | (list[tuple[Unknown, str] | Unknown] & ~AlwaysFalsy) | (list[Divergent] & ~AlwaysFalsy)`
- setuptools/_distutils/text_file.py:232:44: error[unsupported-operator] Operator `+` is unsupported between objects of type `@Todo | int | None` and `Literal[1]`
+ setuptools/_distutils/text_file.py:232:44: error[unsupported-operator] Operator `+` is unsupported between objects of type `@Todo | int | None | Divergent` and `Literal[1]`
- setuptools/_distutils/text_file.py:242:41: error[unsupported-operator] Operator `+` is unsupported between objects of type `@Todo | int | None` and `Literal[1]`
+ setuptools/_distutils/text_file.py:242:41: error[unsupported-operator] Operator `+` is unsupported between objects of type `@Todo | int | None | Divergent` and `Literal[1]`

jax (https://github.com/google/jax)
- jax/_src/pallas/mosaic/sc_core.py:369:13: error[invalid-argument-type] Argument expression after ** must be a mapping with `str` key type: Found `object`
- jax/_src/pallas/mosaic_gpu/core.py:285:13: error[invalid-argument-type] Argument expression after ** must be a mapping with `str` key type: Found `object`
- Found 2588 diagnostics
+ Found 2586 diagnostics

pandas (https://github.com/pandas-dev/pandas)
- pandas/core/reshape/merge.py:2434:41: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Unknown | None | list[Divergent]`
+ pandas/core/reshape/merge.py:2434:41: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Unknown | None | list[Divergent] | list[Unknown | None | list[Divergent]]`
- pandas/core/reshape/merge.py:2438:24: error[unsupported-operator] Operator `+` is unsupported between objects of type `Unknown | None | list[Divergent]` and `list[Unknown]`
+ pandas/core/reshape/merge.py:2438:24: error[unsupported-operator] Operator `+` is unsupported between objects of type `Unknown | None | list[Divergent] | list[Unknown | None | list[Divergent]]` and `list[Unknown]`

scipy (https://github.com/scipy/scipy)
- scipy/linalg/blas.py:228:5: error[invalid-assignment] Object of type `None` is not assignable to `Never`
- scipy/linalg/lapack.py:876:5: error[invalid-assignment] Object of type `None` is not assignable to `Never`
- scipy/linalg/tests/test_blas.py:21:5: error[invalid-assignment] Object of type `None` is not assignable to `Never`
- scipy/linalg/tests/test_lapack.py:29:5: error[invalid-assignment] Object of type `None` is not assignable to `Never`
- Found 9083 diagnostics
+ Found 9079 diagnostics
Memory usage changes were detected when running on open source projects
trio (https://github.com/python-trio/trio)
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
-     struct metadata = ~9MB
+     struct metadata = ~10MB
-     struct fields = ~10MB
+     struct fields = ~11MB

sphinx (https://github.com/sphinx-doc/sphinx)
+ WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`

prefect (https://github.com/PrefectHQ/prefect)
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
-     struct metadata = ~35MB
+     struct metadata = ~36MB
-     struct fields = ~38MB
+     struct fields = ~40MB

@MichaReiser MichaReiser mentioned this pull request Nov 5, 2025
@mtshiba
Copy link
Contributor Author

mtshiba commented Nov 5, 2025

Hmm, it looks like the problem is still not solved...

MRE:

class ManyCycles:
    def __init__(self: "ManyCycles"):
        self.x1 = 0
        self.x2 = 0
        self.x3 = 1

    def f(self: "ManyCycles"):
        self.x1 = self.x2 + self.x3 + 1
        self.x2 = self.x1 + self.x3 + 2
        self.x3 = self.x1 + self.x2 + 3

m = ManyCycles()
reveal_type(m.x1)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
reveal_type(m.x2)  # should be: Unknown | int, but: Unknown | int | Divergent
reveal_type(m.x3)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
tracing output
INFO Defaulting to python-platform `win32`
INFO Python version: Python 3.13, platform: win32
INFO new_revision: R1 -> R2
INFO Indexed 1 file(s) in 0.007s
INFO check_file_impl(Id(c00)): executing query
INFO source_text(Id(c00)): executing query
INFO parsed_module(Id(c00)): executing query
INFO semantic_index(Id(c00)): executing query
INFO infer_scope_types(Id(1000)): executing query
INFO infer_definition_types(Id(140a)): executing query
INFO infer_definition_types(Id(140b)): executing query
INFO module_type_symbols(Id(2000)): executing query
INFO resolve_module_query(Id(2400)): executing query
INFO global_scope(Id(c01)): executing query
INFO semantic_index(Id(c01)): executing query
INFO parsed_module(Id(c01)): executing query
INFO source_text(Id(c01)): executing query
INFO place_table(Id(1004)): executing query
INFO place_by_id(Id(3000)): executing query
INFO use_def_map(Id(1004)): executing query
INFO infer_definition_types(Id(156f)): executing query
INFO infer_definition_types(Id(1429)): executing query
INFO resolve_module_query(Id(2401)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3800)): executing query
INFO imported_relative_submodules_of_stub_package(Id(2c01)): executing query
INFO global_scope(Id(c02)): executing query
INFO semantic_index(Id(c02)): executing query
INFO parsed_module(Id(c02)): executing query
INFO source_text(Id(c02)): executing query
INFO place_table(Id(10ee)): executing query
INFO place_by_id(Id(3001)): executing query
INFO use_def_map(Id(10ee)): executing query
INFO infer_definition_types(Id(1767)): executing query
INFO file_to_module(Id(c02)): executing query
INFO FunctionLiteral < 'db >::overloads_and_implementation_(Id(4400)): executing query
INFO file_to_module(Id(c01)): executing query
INFO place_table(Id(104d)): executing query
INFO module_type_symbols(Id(2000)): execute: iterate again (IterationCount(1))...
INFO place_by_id(Id(3000)): executing query
INFO infer_definition_types(Id(156f)): executing query
INFO infer_definition_types(Id(1429)): executing query
INFO ClassLiteral < 'db >::explicit_bases_(Id(1c00)): executing query
INFO resolve_module_query(Id(2402)): executing query
INFO global_scope(Id(c0e)): executing query
INFO semantic_index(Id(c0e)): executing query
INFO parsed_module(Id(c0e)): executing query
INFO source_text(Id(c0e)): executing query
INFO place_table(Id(117e)): executing query
INFO place_by_id(Id(3002)): executing query
INFO use_def_map(Id(117e)): executing query
INFO infer_definition_types(Id(3df1)): executing query
INFO file_to_module(Id(c0e)): executing query
INFO infer_definition_types(Id(3dba)): executing query
INFO ClassLiteral < 'db >::pep695_generic_context_(Id(1c02)): executing query
INFO ClassLiteral < 'db >::explicit_bases_(Id(1c02)): executing query
INFO ClassLiteral < 'db >::inherited_legacy_generic_context_(Id(1c02)): executing query
INFO ClassLiteral < 'db >::pep695_generic_context_(Id(1c00)): executing query
INFO ClassLiteral < 'db >::inherited_legacy_generic_context_(Id(1c00)): executing query
INFO ClassLiteral < 'db >::try_mro_(Id(4c00)): executing query
INFO resolve_module_query(Id(2403)): executing query
INFO global_scope(Id(c0f)): executing query
INFO semantic_index(Id(c0f)): executing query
INFO parsed_module(Id(c0f)): executing query
INFO source_text(Id(c0f)): executing query
INFO place_table(Id(11ea)): executing query
INFO place_by_id(Id(3003)): executing query
INFO use_def_map(Id(11ea)): executing query
INFO infer_definition_types(Id(3f3c)): executing query
INFO infer_definition_types(Id(3eef)): executing query
INFO file_to_module(Id(c0f)): executing query
INFO ClassLiteral < 'db >::pep695_generic_context_(Id(1c04)): executing query
INFO ClassLiteral < 'db >::explicit_bases_(Id(1c04)): executing query
INFO ClassLiteral < 'db >::inherited_legacy_generic_context_(Id(1c04)): executing query
INFO lookup_dunder_new_inner(Id(6000)): executing query
INFO place_by_id(Id(3004)): executing query
INFO infer_definition_types(Id(3fb5)): executing query
INFO ClassLiteral < 'db >::pep695_generic_context_(Id(1c05)): executing query
INFO ClassLiteral < 'db >::explicit_bases_(Id(1c05)): executing query
INFO ClassLiteral < 'db >::inherited_legacy_generic_context_(Id(1c05)): executing query
INFO ClassLiteral < 'db >::is_typed_dict_(Id(1c05)): executing query
INFO ClassLiteral < 'db >::try_metaclass_(Id(1c00)): executing query
INFO ClassLiteral < 'db >::is_typed_dict_(Id(1c00)): executing query
INFO code_generator_of_class(Id(6400)): executing query
INFO place_table(Id(1001)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6800)): executing query
INFO use_def_map(Id(1001)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3801)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c00)): executing query
INFO ClassLiteral < 'db >::decorators_(Id(1c00)): executing query
INFO enum_metadata(Id(1c00)): executing query
INFO ClassLiteral < 'db >::decorators_(Id(1c02)): executing query
INFO enum_metadata(Id(1c02)): executing query
INFO place_by_id(Id(3005)): executing query
INFO static_expression_truthiness(Id(1898)): executing query
INFO infer_expression_types_impl(Id(1898)): executing query
INFO infer_definition_types(Id(3d36)): executing query
INFO resolve_module_query(Id(2404)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3802)): executing query
INFO imported_relative_submodules_of_stub_package(Id(2c04)): executing query
INFO semantic_index(Id(c10)): executing query
INFO parsed_module(Id(c10)): executing query
INFO source_text(Id(c10)): executing query
INFO file_to_module(Id(c10)): executing query
INFO resolve_module_query(Id(2405)): executing query
INFO imported_modules(Id(c0e)): executing query
INFO global_scope(Id(c10)): executing query
INFO place_table(Id(59be)): executing query
INFO place_by_id(Id(3006)): executing query
INFO use_def_map(Id(59be)): executing query
INFO infer_definition_types(Id(5f9c)): executing query
INFO infer_definition_types(Id(5f9b)): executing query
INFO infer_definition_types(Id(5ede)): executing query
INFO resolve_module_query(Id(2406)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3803)): executing query
INFO imported_relative_submodules_of_stub_package(Id(2c06)): executing query
INFO global_scope(Id(c24)): executing query
INFO semantic_index(Id(c24)): executing query
INFO parsed_module(Id(c24)): executing query
INFO source_text(Id(c24)): executing query
INFO place_table(Id(5a46)): executing query
INFO place_by_id(Id(3007)): executing query
INFO use_def_map(Id(5a46)): executing query
INFO infer_definition_types(Id(704b)): executing query
INFO file_to_module(Id(c24)): executing query
INFO FunctionLiteral < 'db >::overloads_and_implementation_(Id(4401)): executing query
INFO infer_definition_types(Id(5edf)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3804)): executing query
INFO place_by_id(Id(3008)): executing query
INFO infer_definition_types(Id(7148)): executing query
INFO FunctionLiteral < 'db >::overloads_and_implementation_(Id(4402)): executing query
INFO ClassLiteral < 'db >::is_typed_dict_(Id(1c06)): executing query
INFO place_by_id(Id(3009)): executing query
INFO infer_definition_types(Id(5088)): executing query
INFO ClassLiteral < 'db >::pep695_generic_context_(Id(1c07)): executing query
INFO ClassLiteral < 'db >::explicit_bases_(Id(1c07)): executing query
INFO ClassLiteral < 'db >::inherited_legacy_generic_context_(Id(1c07)): executing query
INFO ClassLiteral < 'db >::is_typed_dict_(Id(1c07)): executing query
INFO infer_definition_types(Id(3dbb)): executing query
INFO all_narrowing_constraints_for_expression(Id(1898)): executing query
INFO ClassLiteral < 'db >::pep695_generic_context_(Id(1c03)): executing query
INFO ClassLiteral < 'db >::explicit_bases_(Id(1c03)): executing query
INFO infer_deferred_types(Id(3dba)): executing query
INFO ClassLiteral < 'db >::inherited_legacy_generic_context_(Id(1c03)): executing query
INFO ClassLiteral < 'db >::decorators_(Id(1c03)): executing query
INFO enum_metadata(Id(1c03)): executing query
INFO ClassLiteral < 'db >::try_mro_(Id(4c01)): executing query
INFO place_by_id(Id(300a)): executing query
INFO infer_definition_types(Id(1404)): executing query
INFO global_scope(Id(c00)): executing query
INFO place_table(Id(1000)): executing query
INFO place_by_id(Id(300b)): executing query
INFO use_def_map(Id(1000)): executing query
INFO Type < 'db >::try_call_dunder_get_(Id(8800)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c01)): executing query
INFO place_by_id(Id(300c)): executing query
INFO infer_definition_types(Id(145e)): executing query
INFO infer_definition_types(Id(1422)): executing query
INFO ClassLiteral < 'db >::is_typed_dict_(Id(1c08)): executing query
INFO resolve_module_query(Id(2407)): executing query
INFO global_scope(Id(c2f)): executing query
INFO semantic_index(Id(c2f)): executing query
INFO parsed_module(Id(c2f)): executing query
INFO source_text(Id(c2f)): executing query
INFO place_table(Id(5ba1)): executing query
INFO place_by_id(Id(300d)): executing query
INFO use_def_map(Id(5ba1)): executing query
INFO static_expression_truthiness(Id(1a3b)): executing query
INFO infer_expression_types_impl(Id(1a3b)): executing query
INFO infer_definition_types(Id(7437)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3805)): executing query
INFO imported_modules(Id(c2f)): executing query
INFO infer_definition_types(Id(751f)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3806)): executing query
INFO imported_relative_submodules_of_stub_package(Id(2c00)): executing query
INFO place_by_id(Id(300e)): executing query
INFO static_expression_truthiness(Id(184d)): executing query
INFO infer_expression_types_impl(Id(184d)): executing query
INFO infer_definition_types(Id(140c)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3807)): executing query
INFO imported_modules(Id(c01)): executing query
INFO infer_definition_types(Id(16f8)): executing query
INFO all_narrowing_constraints_for_expression(Id(184d)): executing query
INFO ClassLiteral < 'db >::pep695_generic_context_(Id(1c09)): executing query
INFO ClassLiteral < 'db >::explicit_bases_(Id(1c09)): executing query
INFO ClassLiteral < 'db >::inherited_legacy_generic_context_(Id(1c09)): executing query
INFO ClassLiteral < 'db >::is_typed_dict_(Id(1c09)): executing query
INFO ClassLiteral < 'db >::pep695_generic_context_(Id(1c08)): executing query
INFO ClassLiteral < 'db >::explicit_bases_(Id(1c08)): executing query
INFO ClassLiteral < 'db >::inherited_legacy_generic_context_(Id(1c08)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c00)): executing query
INFO ClassLiteral < 'db >::try_mro_(Id(4c02)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c01)): executing query
INFO FunctionLiteral < 'db >::overloads_and_implementation_(Id(4403)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c02)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c03)): executing query
INFO FunctionType < 'db >::signature_(Id(4803)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3808)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c04)): executing query
INFO file_settings(Id(c00)): executing query
INFO suppressions(Id(c00)): executing query
INFO place_by_id(Id(300f)): executing query
INFO static_expression_truthiness(Id(1863)): executing query
INFO infer_expression_types_impl(Id(1863)): executing query
INFO infer_definition_types(Id(1712)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3809)): executing query
INFO imported_modules(Id(c02)): executing query
INFO infer_definition_types(Id(17d0)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380a)): executing query
INFO place_by_id(Id(3010)): executing query
INFO static_expression_truthiness(Id(1a01)): executing query
INFO infer_expression_types_impl(Id(1a01)): executing query
INFO infer_definition_types(Id(7022)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380b)): executing query
INFO imported_modules(Id(c24)): executing query
INFO infer_definition_types(Id(7381)): executing query
INFO FunctionType < 'db >::signature_(Id(4804)): executing query
INFO FunctionLiteral < 'db >::overloads_and_implementation_(Id(4404)): executing query
INFO infer_deferred_types(Id(7381)): executing query
INFO infer_definition_types(Id(7138)): executing query
INFO infer_definition_types(Id(708e)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380c)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6801)): executing query
INFO place_by_id(Id(3011)): executing query
INFO infer_definition_types(Id(1409)): executing query
INFO FunctionLiteral < 'db >::overloads_and_implementation_(Id(4405)): executing query
INFO infer_expression_type_impl(Id(1800)): executing query
INFO infer_expression_types_impl(Id(1800)): executing query
INFO infer_expression_type_impl(Id(1803)): executing query
INFO infer_expression_types_impl(Id(1803)): executing query
INFO infer_definition_types(Id(1405)): executing query
INFO infer_scope_types(Id(1001)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380d)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6802)): executing query
INFO infer_expression_type_impl(Id(1801)): executing query
INFO infer_expression_types_impl(Id(1801)): executing query
INFO infer_expression_type_impl(Id(1804)): executing query
INFO infer_expression_types_impl(Id(1804)): executing query
INFO infer_definition_types(Id(1406)): executing query
INFO infer_expression_types_impl(Id(a400)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380e)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6803)): executing query
INFO infer_expression_type_impl(Id(1802)): executing query
INFO infer_expression_types_impl(Id(1802)): executing query
INFO infer_expression_type_impl(Id(1805)): executing query
INFO infer_expression_types_impl(Id(1805)): executing query
INFO infer_definition_types(Id(1407)): executing query
INFO infer_expression_types_impl(Id(a401)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c05)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6804)): executing query
INFO code_generator_of_class(Id(6401)): executing query
INFO ClassLiteral < 'db >::try_metaclass_(Id(1c04)): executing query
INFO ClassLiteral < 'db >::try_mro_(Id(4c03)): executing query
INFO ClassLiteral < 'db >::is_typed_dict_(Id(1c04)): executing query
INFO place_table(Id(11eb)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6805)): executing query
INFO use_def_map(Id(11eb)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c02)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c03)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6806)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c06)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6807)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6808)): executing query
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380e)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Type < 'db >::class_member_with_policy_(Id(6c07)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6809)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(680a)): executing query
INFO Detected nested cycle infer_definition_types(Id(1406)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Type < 'db >::is_redundant_with_(Id(8c04)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c05)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(680b)): executing query
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380d)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(680c)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380c)): execute: iterate again (IterationCount(1))...
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6801)): executing query
INFO infer_expression_type_impl(Id(1803)): executing query
INFO infer_expression_types_impl(Id(1803)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380d)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6802)): executing query
INFO infer_expression_type_impl(Id(1804)): executing query
INFO infer_expression_types_impl(Id(1804)): executing query
INFO infer_definition_types(Id(1406)): executing query
INFO infer_expression_types_impl(Id(a402)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380e)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6803)): executing query
INFO infer_expression_type_impl(Id(1805)): executing query
INFO infer_expression_types_impl(Id(1805)): executing query
INFO infer_definition_types(Id(1407)): executing query
INFO infer_expression_types_impl(Id(a403)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c06)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c07)): executing query
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380e)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle infer_definition_types(Id(1406)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380d)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Type < 'db >::member_lookup_with_policy_(Id(380c)): execute: iterate again (IterationCount(2))...
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6801)): executing query
INFO infer_expression_type_impl(Id(1803)): executing query
INFO infer_expression_types_impl(Id(1803)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380d)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6802)): executing query
INFO infer_expression_type_impl(Id(1804)): executing query
INFO infer_expression_types_impl(Id(1804)): executing query
INFO infer_definition_types(Id(1406)): executing query
INFO infer_expression_types_impl(Id(a404)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380e)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6803)): executing query
INFO infer_expression_type_impl(Id(1805)): executing query
INFO infer_expression_types_impl(Id(1805)): executing query
INFO infer_definition_types(Id(1407)): executing query
INFO infer_expression_types_impl(Id(a405)): executing query
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380e)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle infer_definition_types(Id(1406)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380d)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Type < 'db >::member_lookup_with_policy_(Id(380c)): execute: iterate again (IterationCount(3))...
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6801)): executing query
INFO infer_expression_type_impl(Id(1803)): executing query
INFO infer_expression_types_impl(Id(1803)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380d)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6802)): executing query
INFO infer_expression_type_impl(Id(1804)): executing query
INFO infer_expression_types_impl(Id(1804)): executing query
INFO infer_definition_types(Id(1406)): executing query
INFO infer_expression_types_impl(Id(a406)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380e)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6803)): executing query
INFO infer_expression_type_impl(Id(1805)): executing query
INFO infer_expression_types_impl(Id(1805)): executing query
INFO infer_definition_types(Id(1407)): executing query
INFO infer_expression_types_impl(Id(a407)): executing query
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380e)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle infer_definition_types(Id(1406)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380d)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Type < 'db >::member_lookup_with_policy_(Id(380c)): execute: iterate again (IterationCount(4))...
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6801)): executing query
INFO infer_expression_type_impl(Id(1803)): executing query
INFO infer_expression_types_impl(Id(1803)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380d)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6802)): executing query
INFO infer_expression_type_impl(Id(1804)): executing query
INFO infer_expression_types_impl(Id(1804)): executing query
INFO infer_definition_types(Id(1406)): executing query
INFO infer_expression_types_impl(Id(a408)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380e)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6803)): executing query
INFO infer_expression_type_impl(Id(1805)): executing query
INFO infer_expression_types_impl(Id(1805)): executing query
INFO infer_definition_types(Id(1407)): executing query
INFO infer_expression_types_impl(Id(a409)): executing query
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380e)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Type < 'db >::is_redundant_with_(Id(8c08)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c09)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c0a)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c0b)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c0c)): executing query
INFO Type < 'db >::is_redundant_with_(Id(8c0d)): executing query
INFO ClassLiteral < 'db >::decorators_(Id(1c07)): executing query
INFO enum_metadata(Id(1c07)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380f)): executing query
INFO code_generator_of_class(Id(6402)): executing query
INFO ClassLiteral < 'db >::try_metaclass_(Id(1c07)): executing query
INFO ClassLiteral < 'db >::try_mro_(Id(4c04)): executing query
INFO place_table(Id(122b)): executing query
INFO place_by_id(Id(3012)): executing query
INFO use_def_map(Id(122b)): executing query
INFO infer_definition_types(Id(5007)): executing query
INFO Type < 'db >::try_call_dunder_get_(Id(8801)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c08)): executing query
INFO FunctionLiteral < 'db >::overloads_and_implementation_(Id(4406)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c09)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c0a)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c0b)): executing query
INFO code_generator_of_class(Id(6403)): executing query
INFO ClassLiteral < 'db >::try_metaclass_(Id(1c05)): executing query
INFO place_table(Id(1211)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(680d)): executing query
INFO use_def_map(Id(1211)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(680e)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3810)): executing query
INFO Type < 'db >::try_call_dunder_get_(Id(8802)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c0c)): executing query
INFO ClassLiteral < 'db >::decorators_(Id(1c05)): executing query
INFO enum_metadata(Id(1c05)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3811)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c0d)): executing query
INFO place_by_id(Id(3013)): executing query
INFO infer_definition_types(Id(3ff2)): executing query
INFO Type < 'db >::try_call_dunder_get_(Id(8803)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c0e)): executing query
INFO FunctionLiteral < 'db >::overloads_and_implementation_(Id(4407)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c0f)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c10)): executing query
INFO FunctionType < 'db >::signature_(Id(4807)): executing query
INFO infer_deferred_types(Id(3ff2)): executing query
INFO place_by_id(Id(3014)): executing query
INFO FunctionType < 'db >::signature_(Id(4807)): execute: iterate again (IterationCount(1))...
INFO Detected nested cycle infer_definition_types(Id(1406)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380d)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Type < 'db >::member_lookup_with_policy_(Id(380c)): execute: iterate again (IterationCount(5))...
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6801)): executing query
INFO infer_expression_type_impl(Id(1803)): executing query
INFO infer_expression_types_impl(Id(1803)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380d)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6802)): executing query
INFO infer_expression_type_impl(Id(1804)): executing query
INFO infer_expression_types_impl(Id(1804)): executing query
INFO infer_definition_types(Id(1406)): executing query
INFO infer_expression_types_impl(Id(a40a)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380e)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6803)): executing query
INFO infer_expression_type_impl(Id(1805)): executing query
INFO infer_expression_types_impl(Id(1805)): executing query
INFO infer_definition_types(Id(1407)): executing query
INFO infer_expression_types_impl(Id(a40b)): executing query
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380e)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle infer_definition_types(Id(1406)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380d)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Type < 'db >::member_lookup_with_policy_(Id(380c)): execute: iterate again (IterationCount(6))...
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6801)): executing query
INFO infer_expression_type_impl(Id(1803)): executing query
INFO infer_expression_types_impl(Id(1803)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380d)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6802)): executing query
INFO infer_expression_type_impl(Id(1804)): executing query
INFO infer_expression_types_impl(Id(1804)): executing query
INFO infer_definition_types(Id(1406)): executing query
INFO infer_expression_types_impl(Id(a40a)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(380e)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(6803)): executing query
INFO infer_expression_type_impl(Id(1805)): executing query
INFO infer_expression_types_impl(Id(1805)): executing query
INFO infer_definition_types(Id(1407)): executing query
INFO infer_expression_types_impl(Id(a40b)): executing query
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380e)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle infer_definition_types(Id(1406)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO Detected nested cycle Type < 'db >::member_lookup_with_policy_(Id(380d)), iterate it as part of the outer cycle Type < 'db >::member_lookup_with_policy_(Id(380c))
INFO inferable_typevars_inner(Id(a000)): executing query
INFO Type < 'db >::apply_specialization_(Id(ac00)): executing query
INFO Type < 'db >::apply_specialization_(Id(ac01)): executing query
INFO Type < 'db >::apply_specialization_(Id(ac02)): executing query
INFO Type < 'db >::apply_specialization_(Id(ac03)): executing query
INFO ClassLiteral < 'db >::inheritance_cycle_(Id(1c00)): executing query
INFO infer_scope_types(Id(1002)): executing query
INFO infer_definition_types(Id(1400)): executing query
INFO Type < 'db >::member_lookup_with_policy_(Id(3812)): executing query
INFO Type < 'db >::class_member_with_policy_(Id(6c11)): executing query
INFO ClassLiteral < 'db >::implicit_attribute_inner_(Id(680f)): executing query
INFO infer_expression_types_impl(Id(a40c)): executing query
INFO infer_expression_types_impl(Id(a40d)): executing query
INFO infer_expression_types_impl(Id(a40e)): executing query
INFO infer_scope_types(Id(1003)): executing query
INFO infer_expression_types_impl(Id(a40f)): executing query
INFO infer_expression_types_impl(Id(a410)): executing query
INFO Checking file `C:\Users\sbym8\Desktop\typepy\cycle.py` took more than 100ms (734.6105ms)
INFO line_index(Id(c00)): executing query
warning[undefined-reveal]: `reveal_type` used without importing it
  --> cycle.py:13:1
   |
12 | m = ManyCycles()
13 | reveal_type(m.x1)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
   | ^^^^^^^^^^^
14 | reveal_type(m.x2)  # should be: Unknown | int, but: Unknown | int | Divergent
15 | reveal_type(m.x3)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
   |
info: This is allowed for debugging convenience but will fail at runtime
info: rule `undefined-reveal` is enabled by default

info[revealed-type]: Revealed type
  --> cycle.py:13:13
   |
12 | m = ManyCycles()
13 | reveal_type(m.x1)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
   |             ^^^^ `Unknown | int | Divergent(Id(380d)) | Divergent(Id(1406))`
14 | reveal_type(m.x2)  # should be: Unknown | int, but: Unknown | int | Divergent
15 | reveal_type(m.x3)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
   |

warning[undefined-reveal]: `reveal_type` used without importing it
  --> cycle.py:14:1
   |
12 | m = ManyCycles()
13 | reveal_type(m.x1)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
14 | reveal_type(m.x2)  # should be: Unknown | int, but: Unknown | int | Divergent
   | ^^^^^^^^^^^
15 | reveal_type(m.x3)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
   |
info: This is allowed for debugging convenience but will fail at runtime
info: rule `undefined-reveal` is enabled by default

info[revealed-type]: Revealed type
  --> cycle.py:14:13
   |
12 | m = ManyCycles()
13 | reveal_type(m.x1)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
14 | reveal_type(m.x2)  # should be: Unknown | int, but: Unknown | int | Divergent
   |             ^^^^ `Unknown | int | Divergent(Id(1406))`
15 | reveal_type(m.x3)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
   |

warning[undefined-reveal]: `reveal_type` used without importing it
  --> cycle.py:15:1
   |
13 | reveal_type(m.x1)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
14 | reveal_type(m.x2)  # should be: Unknown | int, but: Unknown | int | Divergent
15 | reveal_type(m.x3)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
   | ^^^^^^^^^^^
   |
info: This is allowed for debugging convenience but will fail at runtime
info: rule `undefined-reveal` is enabled by default

info[revealed-type]: Revealed type
  --> cycle.py:15:13
   |
13 | reveal_type(m.x1)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
14 | reveal_type(m.x2)  # should be: Unknown | int, but: Unknown | int | Divergent
15 | reveal_type(m.x3)  # should be: Unknown | int, but: Unknown | int | Divergent | Divergent
   |             ^^^^ `Unknown | int | Divergent(Id(380d)) | Divergent(Id(1406))`
   |

Found 6 diagnostics

@MichaReiser
Copy link
Member

Hmm, it looks like the problem is still not solved...

Were you able to figure out what's happening? Is the issue that the Divergent types remain in the final output?

@mtshiba
Copy link
Contributor Author

mtshiba commented Nov 6, 2025

This example might make it easier to see what's happening.

class A: ...
class B: ...
class C: ...

class Foo:
    def __init__(self):
        self.a = A()
        self.b = B()
        self.c = C()

    def f1(self):
        self.a = self.b
    def f2(self, cond: bool):
        self.b = self.c if cond else self.a
    def f3(self):
        self.c = self.b

# outermost cycle head
#   0th: Divergent(Foo.a)
#   1st: Unknown | A | B | C | Divergent(Foo.a) ->(strip divergent) Unknown | A | B | C
#   2nd: Unknown | A | B | C | Divergent(Foo.a) -> Unknown | A | B | C
reveal_type(Foo().a)
# iterated as part of the outer cycle implicit_attribute_inner(Foo.a)
#   0th: Divergent(Foo.b)
#   1st: Unknown | B | C | Divergent(Foo.b) | Divergent(Foo.a) -> Unknown | B | C | Divergent(Foo.a)
#   2nd: Unknown | B | C | Divergent(Foo.a) | A -> Unknown | B | C | Divergent(Foo.a) | A
#   3rd: Unknown | B | C | Divergent(Foo.a) | A -> Unknown | B | C | Divergent(Foo.a) | A
reveal_type(Foo().b)
# not a cycle head
#   0th: Unknown | C | Divergent(Foo.b)
#   1st: Unknown | C | B | Divergent(Foo.a)
#   2nd: Unknown | C | B | Divergent(Foo.a) | A
#   3rd: Unknown | C | B | Divergent(Foo.a) | A
reveal_type(Foo().c)
graph LR
	Foo.a-->Foo.b
	Foo.b-->Foo.c
	Foo.b-->Foo.a
    Foo.c-->Foo.b
Loading

The cycle recovery function for implicit_attribute_inner(Foo.a) can remove Divergent(Foo.a), but before that, Divergent(Foo.a) has infected Foo.b and Foo.c.

@mtshiba mtshiba force-pushed the recursive-inference branch from 915dd92 to 682a2e8 Compare November 6, 2025 15:46
@MichaReiser
Copy link
Member

The cycle recovery function for implicit_attribute_inner(Foo.a) can remove Divergent(Foo.a), but before that, Divergent(Foo.a) has infected Foo.b and Foo.c.

Can't we replace all Divergent types when finalizing?

@mtshiba
Copy link
Contributor Author

mtshiba commented Nov 6, 2025

Can't we replace all Divergent types when finalizing?

The cycle recovery function cannot remove Divergent types originating from other queries, as this would prevent it from detecting divergence. Also, when a query converges, if the result type contains a Divergent type originating from another query, it must not be removed either, as queries dependent on that query may not yet have converged.

@mtshiba
Copy link
Contributor Author

mtshiba commented Nov 6, 2025

As a PoC, I modified salsa so that the cycle recovery function receives CycleHeads, and removes the Divergent types originating from all outer cycles d0b68f9.

This is something we wanted to avoid, but it seems to work.

@MichaReiser
Copy link
Member

As a PoC, I modified salsa so that the cycle recovery function receives CycleHeads, and removes the Divergent types originating from all outer cycles d0b68f9.

It's not clear to me how this is different from removing all divergent types. As in, you can only see Divergent types for queries participating in a cycle, which is the same as the queries in CycleHeads.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 6, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@mtshiba mtshiba force-pushed the recursive-inference branch from ff59ea0 to 2393f6a Compare November 7, 2025 06:01
@MichaReiser
Copy link
Member

For my understanding. Are we now removing all instances of Divergent in cycle_fn (or of the cycle heads) regardless if this head has converged (last_provisional == value) or do we only remove the Diverged when the head has converged?

I'm asking because there are instances where a cycle head in iteration N becomes a normal cycle participant (not a head) in N+1. In which case cycle_fn is never called with two equal values.

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

3 participants