[ty] Protocols: Fixpoint iteration for fully-static check (#17880)

## Summary

A recursive protocol like the following would previously lead to stack
overflows when attempting to create the union type for the `P | None`
member, because `UnionBuilder` checks if element types are fully static,
and the fully-static check on `P` would in turn list all members and
check whether all of them were fully static, leading to a cycle.

```py
from __future__ import annotations

from typing import Protocol

class P(Protocol):
    parent: P | None
```

Here, we make the fully-static check on protocols a salsa query and add
fixpoint iteration, starting with `true` as the initial value (assume
that the recursive protocol is fully-static). If the recursive protocol
has any non-fully-static members, we still return `false` when
re-executing the query (see newly added tests).

closes #17861

## Test Plan

Added regression test
This commit is contained in:
David Peter 2025-05-07 08:55:21 +02:00 committed by GitHub
parent a33d0d4bf4
commit 04457f99b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 113 additions and 48 deletions

View file

@ -1558,7 +1558,43 @@ def two(some_list: list, some_tuple: tuple[int, str], some_sized: Sized):
c: Sized = some_sized
```
## Regression test: narrowing with self-referential protocols
## Recursive protocols
### Properties
```py
from __future__ import annotations
from typing import Protocol, Any
from ty_extensions import is_fully_static, static_assert, is_assignable_to, is_subtype_of, is_equivalent_to
class RecursiveFullyStatic(Protocol):
parent: RecursiveFullyStatic | None
x: int
class RecursiveNonFullyStatic(Protocol):
parent: RecursiveNonFullyStatic | None
x: Any
static_assert(is_fully_static(RecursiveFullyStatic))
static_assert(not is_fully_static(RecursiveNonFullyStatic))
static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic))
static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic))
# TODO: currently leads to a stack overflow
# static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic))
# static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveFullyStatic))
class AlsoRecursiveFullyStatic(Protocol):
parent: AlsoRecursiveFullyStatic | None
x: int
# TODO: currently leads to a stack overflow
# static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic))
```
### Regression test: narrowing with self-referential protocols
This snippet caused us to panic on an early version of the implementation for protocols.