-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Fix stack overflows when attempting to infer the truthiness of a TypeVar with an invalid recursive bound or constraints #21833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…TypeVar with an invalid recursive bound
Diagnostic diff on typing conformance testsNo changes detected when running ty on typing conformance tests ✅ |
| Some(TypeVarBoundOrConstraints::Constraints(_)) => { | ||
| visitor.visit(*self, || try_union(Either::Right(*bound_typevar)))? | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I first naively tried doing this
| Some(TypeVarBoundOrConstraints::Constraints(_)) => { | |
| visitor.visit(*self, || try_union(Either::Right(*bound_typevar)))? | |
| } | |
| Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { | |
| visitor.visit(*self, || try_union(constraints.as_type(db)))? | |
| } |
but that's no good -- the TypeVarConstraints::as_type() call itself causes a stack overflow if one of the TypeVar constraints is recursive. So I had to refactor try_union so that it could take a UnionType or a TypeVar.
|
|
#21840 also fixes this issue and looks like it might do so in a better way (though I haven't studied that PR in detail yet) |
What #21840 does is report the type variable that creates the "false" recursion as an invalid type variable and mark its specialization as Neither of the panic cases detected by the fuzzer is a valid recursive type variable. # name_2[0] => Unknown
class name_1[name_2: name_2[0]]:
def name_4(name_3: name_2, /):
if name_3:
pass
# (name_5 if unique_name_0 else name_1)[0] => Unknown
def name_4[name_5: (name_5 if unique_name_0 else name_1)[0], **name_1](): ...Therefore, it cannot handle stack overflows that occur with truly valid recursive type variables. |
Right, but I'm struggling to come up with any examples of stack overflows with truly valid recursive type variables 😆 are you able to find any? |
Nah, I think the stack overflow is probably caused by an incorrect specialization. The specialization def f[T: (int, list[T])](x: T):
if not isinstance(x, int):
reveal_type(x) # list[typing.TypeVar]
def f[T: (int, list[T[0]])](x: T):
if not isinstance(x, int):
reveal_type(x) # list[T] |
|
I re-read PEP-695, and it appears that creating recursive type variables is not permitted by the type system in the first place (even though it's possible at runtime). https://peps.python.org/pep-0695/#type-parameter-scopes
The requirement that the bound of a type variable be concrete seems to imply that it is forbidden to use itself within it. Therefore, I think it would be compliant to treat recursions found in type variables as errors and replace them with |
I think the reason for that is that it opens the door to higher-kinded types, which would be a very significant development in the Python type system and would require a lot of design work. Thank you! |
Summary
This PR fixes the first stack overflow MRE given in astral-sh/ty#1794.
Test Plan
I added two corpus test cases that cause us to overflow our stack on
main: one with a recursive upper bound, and a slightly trickier one with recursive constraints. I also added mdtests and snapshots.