[ty] Avoid enum literal panic during cycle recovery#25237
Conversation
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 89.36%. The percentage of expected errors that received a diagnostic held steady at 85.49%. The number of fully passing files held steady at 88/134. |
Memory usage reportMemory usage unchanged ✅ |
|
c4a70f1 to
2e671bf
Compare
| // Metadata is only needed for enum-wide simplifications; class identity | ||
| // is enough to preserve a precise set of enum literals during cycle | ||
| // recovery. |
There was a problem hiding this comment.
I don't understand what this comment is trying to say. This function isn't specific to cycle recovery at all; it's a core part of the union builder. I think future readers of this code will probably be pretty confused by seeing a reference to cycle recovery here.
There was a problem hiding this comment.
I will fix this! On my TODO list.
There was a problem hiding this comment.
If we're revisiting this comment, I do wish we had a clearer idea why the example causes the previous expect to fail, and precisely what happens in cycle recovery that causes us to have a LiteralValueTypeKind::Enum wrapping a non-enum, and that were documented here and in the added test.
There was a problem hiding this comment.
Sure, I can add that. I looked at all of it when fixing the issue.
There was a problem hiding this comment.
It feels quite reminiscent to me of the situation discussed in astral-sh/ty#1587 (comment) and following comments on that issue thread.
There was a problem hiding this comment.
Yeah that looks very similar (I hadn't seen that issue before!). Although the more time I sink into this, the less I'm certain about what's going on. It's something like the following though.
On Python 3.14:
from enum import Enum
from typing import TypeVar
def fn1(x: T):
pass
def fn2(x: T):
pass
fn1()
fn2()
class C(Enum):
a = 1
b = 2
match m:
case C.a:
_()
case _:
_()
T = TypeVarWe hit a cycle around case C.a whereby we need to determine whether class C is reachable, which means we need to figure out whether fn1 and fn2 terminate, which means we need their signatures, which means we need to resolve T, which means we need to determine whether T = TypeVar is reachable, which means we go back into evaluating the match and re-infer infer_expression_type(C.a).
Then, across cycle iterations, we see enum_metadata return Some(...) in some case and None later during recovery. This last part I don't have a great grasp on, though it's similar to the comments in that linked issue.
Summary
Closes astral-sh/ty#3481.