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-11 08:07:16.181643243 +0000
+++ new-output.txt	2025-11-11 08:07:19.267655072 +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(19384)): 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__`
@@ -79,6 +78,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
@@ -1006,5 +1022,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 1008 diagnostics
-WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.
+Found 1024 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 7.34%

Comparing mtshiba:recursive-inference (e879f8b) with main (33b942c)

Summary

⚡ 1 improvement
✅ 51 untouched

Benchmarks breakdown

Mode Benchmark BASE HEAD Change
WallTime medium[static-frame] 13.4 s 12.4 s +7.34%

@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 :))

@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.

@carljm
Copy link
Contributor

carljm commented Nov 11, 2025

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.

It's true that all Divergent types must originate from cycles, but not necessarily from this cycle. There are recursive-type cases where the final converged type includes a Divergent type, and that's the best we can do (the alternative would be an infinitely-nested type.) So we can easily have, for example, a list[Divergent] type as the final result of some recursive list type unrelated to the current cycle, and then a query in the current cycle could index into that list and obtain the unrelated Divergent type, which the current cycle should not treat specially or eliminate. The Divergent types that should be treated specially are only those originating from the current cycle.

It makes sense to me (unfortunately) that we need the information from CycleHeads. I agree we don't want to expose too much Salsa internals, but I note that most of CycleHeads is only pub(crate), so isn't exposed. The only thing exposed is the new ids method, which really is the minimum information we need to expose, I think.

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?

It has to be removed on any iteration, this is how we get to the right converged value. You can see this happening in #20566 (comment)

@mtshiba
Copy link
Contributor Author

mtshiba commented Nov 11, 2025

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.

In salsa's execution model since salsa-rs/salsa#999, queries that are inner cycle heads never see the converged values ​​of outer cycle heads. Therefore, to remove Divergent from the output of all queries, we need to perform convergence steps on the values ​​from outer cycle heads as well.

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?

cycle_fn sees the provisional value, and if it looks like T | Divergent(ID associated with this cycle), it means the value has not converged (note that this "convergence" is a subtly different meaning from the convergence determined by salsa; what it means here is that it is possible to get closer to a more stable/simplified value from the type system's perspective. If it "converges" in this sense, it will also converge in the salsa sense), so we set it to T.
This is an operation that always guides the calculation toward convergence, and does not change the handling depending on whether or not the provisional value 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.

I don't think this is a problem. Roughly speaking, cycle_fn performs cycle value normalization that is stronger than the convergence criteria required by salsa, so once salsa determines that the query has converged, cycle_fn does not need to be run again in the next iteration. This means that the change in salsa-rs/salsa#1021 (performing recover_from_cycle regardless of whether the values ​​have converged) was not actually necessary in this case...

@mtshiba mtshiba force-pushed the recursive-inference branch from 70ceed0 to e7b0dc5 Compare November 11, 2025 02:58
reflects the change in mtshiba/salsa@9333447
Comment on lines +270 to +277
fn visit(&self, item: Type<'db>, func: impl FnOnce() -> Type<'db>) -> Type<'db> {
self.transformer.visit(item, func)
}

fn visit_no_shift(&self, item: Type<'db>, func: impl FnOnce() -> Type<'db>) -> Type<'db> {
self.transformer.visit_no_shift(item, func)
}

Copy link
Member

@MichaReiser MichaReiser Nov 11, 2025

Choose a reason for hiding this comment

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

I think we can track level in this visitor (we also don't need level, a single bool if nested shouold be enough)

Suggested change
fn visit(&self, item: Type<'db>, func: impl FnOnce() -> Type<'db>) -> Type<'db> {
self.transformer.visit(item, func)
}
fn visit_no_shift(&self, item: Type<'db>, func: impl FnOnce() -> Type<'db>) -> Type<'db> {
self.transformer.visit_no_shift(item, func)
}
fn visit(&self, item: Type<'db>, func: impl FnOnce() -> Type<'db>) -> Type<'db> {
self.transformer.visit(item, || {
self.nested = true;
let res = func();
self.nested = false;
res
})
}
fn visit_no_shift(&self, item: Type<'db>, func: impl FnOnce() -> Type<'db>) -> Type<'db> {
self.transformer.visit_no_shift(item, || {
self.nested = true;
let res = func();
self.nested = false;
res
})
}

We could probably even get away by simply always setting nested to true (outside the callback)

if visitor.level() == 0 && self == visitor.div {
// int | Divergent = int | (int | (int | ...)) = int
return Type::Never;
} else if visitor.level() >= 1 && self.has_divergent_type(db, visitor.div) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we limit this to level == 1 to avoid recursively nested types or should has_divergent_type be limited to only traverse one level deep?

@MichaReiser
Copy link
Member

In salsa's execution model since salsa-rs/salsa#999, queries that are inner cycle heads never see the converged values ​​of outer cycle heads. Therefore, to remove Divergent from the output of all queries, we need to perform convergence steps on the values ​​from outer cycle heads as well.

Inner cycles should see the converged value from the outer-most cycle in the very last iteration.

I'm not able to come up with an example from the top of my head but I wonder if it's possible to end up with Divergent | Divergent types similar to #20566 (comment), for example, when a variable depends on the output of two separate queries that both are divergent (but aren't part of the same cycle). I guess that's more an UX issue than a convergence issue because the result converges, it just looks silly to have two Divergent in the output.

Which is related to

The Divergent types that should be treated specially are only those originating from the current cycle.

My assumption was that we eliminate all Divergent types when the cycle converges but I think that's simply not possible because Salsa has no hook to post-process all memos after a cycle converged.

It makes sense to me (unfortunately) that we need the information from CycleHeads. I agree we don't want to expose too much Salsa internals, but I note that most of CycleHeads is only pub(crate), so isn't exposed. The only thing exposed is the new ids method, which really is the minimum information we need to expose, I think.

I can see how this is useful but we should maybe reconsider the cycle_fn API overall. Its ergonomics is very poor with the amount of arguments it has and I still don't think we should expose the CycleHeads type. I don't mind exposing the ids but I rather not expose CycleHeads directly.

pub(crate) fn join_with_previous_cycle_place<'db>(
db: &'db dyn Db,
previous_place: &Place<'db>,
place: &Place<'db>,
Copy link
Member

Choose a reason for hiding this comment

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

This should remove the need to suppress needless_pass_by_value at all call sites

Suggested change
place: &Place<'db>,
place: Place<'db>,`

Place::bound(Type::divergent(id)).into()
}

pub(crate) fn join_with_previous_cycle_place<'db>(
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Should we make this a method on Place instead (and move it to place.rs to not add more to types.rs) and maybe even add it to PlaceAndQualifiers to reduce some of the boilerplate?

Comment on lines +314 to 317
for (expr, ty) in &mut inference.expressions {
let previous_ty = previous_inference.expression_type(*expr);
*ty = ty.cycle_normalized(db, previous_ty, cycle_heads);
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Consider making this a method on ExpressionInference

Comment on lines +1589 to +1590
// int | Divergent = int | (int | (int | ...)) = int
return Type::Never;
Copy link
Member

Choose a reason for hiding this comment

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

I don't think I understand this comment, specifically how it is related to union handling, given that level == 0

return Type::Never;
} else if visitor.level() >= 1 && self.has_divergent_type(db, visitor.div) {
// G[G[Divergent]] = G[Divergent]
return visitor.div;
Copy link
Member

Choose a reason for hiding this comment

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

I think this works well for the above mentioned case but what if we have list[list[Divergent]] after the first iteration and list[list[list[list[Divergent]]]] after the second. Wouldn't it be better to return the previous_type in that case?

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

4 participants