Skip to content

[codex] Preserve specialized TypeVar bounds#3291

Open
cneuralnetwork wants to merge 1 commit intofacebook:mainfrom
cneuralnetwork:codex/issue-3285-protocol-bound
Open

[codex] Preserve specialized TypeVar bounds#3291
cneuralnetwork wants to merge 1 commit intofacebook:mainfrom
cneuralnetwork:codex/issue-3285-protocol-bound

Conversation

@cneuralnetwork
Copy link
Copy Markdown

Fixes #3285.

Summary

This fixes a false-positive bad-argument-type error when looking up a classmethod through a type[T] where T is a protocol-bounded TypeVar whose bound is itself specialized, for example T: SQLExpr[Any].

The PR changes recursive type-argument truncation so it only erases generic arguments when there is an actual recursive path back to the same class. Non-recursive specialized bounds such as P[Any] are now preserved.

Root Cause

TParams::truncate_recursive_targs was intentionally preventing fixpoint growth for recursive TypeVar bounds. The old predicate was too broad: it stripped type arguments from any ClassType whose formal type parameters had non-trivial restrictions.

That meant a bound like:

P_co = TypeVar("P_co", bound="P[Any]", covariant=True)

could be truncated from P[Any] to bare P because P's own type parameter had a bound. Later, attribute lookup on type[P_co] instantiated classmethod parameters from bare P, so the method leaked P's class TypeVar instead of using the specialized Any from the bound.

In the original issue this made:

self._expr.from_elementwise

look like:

(cls: type[SQLExprT], func: (Sequence[NativeExprT]) -> NativeExprT) -> None

instead of the expected:

(cls: type[SQLExprT], func: (Sequence[Any]) -> Any) -> None

Fix

The truncation logic now walks the restriction graph and only strips ClassType arguments when the class's embedded type parameters can reach back to the same class through their own bounds or constraints.

That keeps the recursion guard for shapes like Bar[Any] where Bar participates in its own bound cycle, while preserving ordinary specialized bounds used for protocol classmethod lookup.

Tests

Added a regression test covering a reduced protocol form of the issue:

class Namespace(Protocol[P_co, T_co]):
    @property
    def item(self) -> type[P_co]: ...

    def test(self) -> None:
        def f(x: Sequence[T_co]) -> T_co:
            return x[0]

        self.item.make(f)

Validation run locally:

cargo test test_protocol_typevar_bound_preserves_specialized_targs
cargo run -q -p pyrefly --bin pyrefly -- check /tmp/pyrefly_issue_3285.py
cargo test test_recursively_constrained_typevar
python3 test.py --no-test --no-conformance --no-jsonschema
git diff --check

I also reran a reveal_type probe for the original reproducer and confirmed the classmethod parameter is now specialized as (Sequence[Any]) -> Any.

AI Disclosure

This PR was prepared with help from Codex.

Bounded TypeVars that appear as class type parameters can carry specialized generic bounds such as P[Any]. The recursive targs truncation was dropping those arguments whenever the bound class had restricted type parameters, which made protocol classmethod lookup leak the bound class's own TypeVar instead of the specialized Any argument.

Make truncation detect an actual recursive path back to the same class before erasing class arguments. Add a protocol regression test covering facebook#3285.
@meta-cla meta-cla Bot added the cla signed label May 2, 2026
@github-actions github-actions Bot added the size/m label May 2, 2026
@cneuralnetwork cneuralnetwork marked this pull request as ready for review May 2, 2026 15:53
@yangdanny97 yangdanny97 self-assigned this May 9, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

Diff from mypy_primer, showing the effect of this PR on open source code:

antidote (https://github.com/Finistere/antidote)
- ERROR src/antidote/lib/interface_ext/qualifier.py:79:23-55: `QualifiedBy` is not assignable to upper bound `Predicate` of type variable `InPredicate` [bad-specialization]
- ERROR src/antidote/lib/interface_ext/qualifier.py:85:45-77: `QualifiedBy` is not assignable to upper bound `Predicate` of type variable `InPredicate` [bad-specialization]

rotki (https://github.com/rotki/rotki)
- ERROR rotkehlchen/chain/decoding/decoder.py:216:30-80: Argument `T_TxNotDecodedFilterQuery` is not assignable to parameter `filter_query` with type `T_TxNotDecodedFilterQuery` in function `rotkehlchen.db.dbtx.DBCommonTx.get_transaction_hashes_not_decoded` [bad-argument-type]

steam.py (https://github.com/Gobot1234/steam.py)
- ERROR steam/chat.py:418:38-99: `ClanMember | GroupMember | PartialMember` is not assignable to attribute `author` with type `ClanMessageAuthorT` [bad-assignment]
- ERROR steam/chat.py:418:38-99: `ClanMember | GroupMember | PartialMember` is not assignable to attribute `author` with type `GroupMessageAuthorT` [bad-assignment]

static-frame (https://github.com/static-frame/static-frame)
+ ERROR static_frame/core/frame.py:3808:16-3811:10: Returned type `InterfaceConsolidate[Self@Frame]` is not assignable to declared return type `InterfaceConsolidate[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:3808:36-3811:10: `Self@Frame` is not assignable to upper bound `Batch | Bus | Frame | FrameAssignILoc | FrameGO | FrameHE | Index | IndexHierarchy | MaskedArray[Any, Any] | Series | SeriesAssign | SeriesHE | TypeBlocks | Yarn | ndarray[Any, Any]` of type variable `TVContainer_co` [bad-specialization]
+ ERROR static_frame/core/frame.py:3826:16-37: Returned type `InterfaceValues[Self@Frame]` is not assignable to declared return type `InterfaceValues[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:3826:31-37: `Self@Frame` is not assignable to upper bound `Frame | Index | IndexHierarchy | Series` of type variable `TVContainer_co` [bad-specialization]
+ ERROR static_frame/core/frame.py:3881:16-3883:10: Returned type `InterfaceTranspose[Self@Frame]` is not assignable to declared return type `InterfaceTranspose[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:3881:34-3883:10: `Self@Frame` is not assignable to upper bound `Frame | IndexHierarchy` of type variable `TVContainer_co` [bad-specialization]
+ ERROR static_frame/core/frame.py:3893:16-3896:10: Returned type `InterfaceFillValue[Self@Frame]` is not assignable to declared return type `InterfaceFillValue[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:3893:34-3896:10: `Self@Frame` is not assignable to upper bound `Frame | Series` of type variable `TVContainer_co` [bad-specialization]
+ ERROR static_frame/core/frame.py:3934:16-3940:10: Returned type `IterNodeAxis[Self@Frame]` is not assignable to declared return type `IterNodeAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:3934:28-3940:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:3947:16-3953:10: Returned type `IterNodeAxis[Self@Frame]` is not assignable to declared return type `IterNodeAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:3947:28-3953:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:3960:16-3966:10: Returned type `IterNodeConstructorAxis[Self@Frame]` is not assignable to declared return type `IterNodeConstructorAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:3960:39-3966:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:3973:16-3979:10: Returned type `IterNodeConstructorAxis[Self@Frame]` is not assignable to declared return type `IterNodeConstructorAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:3973:39-3979:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:3986:16-3992:10: Returned type `IterNodeAxis[Self@Frame]` is not assignable to declared return type `IterNodeAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:3986:28-3992:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:3999:16-4005:10: Returned type `IterNodeAxis[Self@Frame]` is not assignable to declared return type `IterNodeAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:3999:28-4005:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4013:16-4019:10: Returned type `IterNodeGroupAxis[Self@Frame]` is not assignable to declared return type `IterNodeGroupAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4013:33-4019:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4026:16-4032:10: Returned type `IterNodeGroupAxis[Self@Frame]` is not assignable to declared return type `IterNodeGroupAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4026:33-4032:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4040:16-4046:10: Returned type `IterNodeGroupAxis[Self@Frame]` is not assignable to declared return type `IterNodeGroupAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4040:33-4046:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4053:16-4059:10: Returned type `IterNodeGroupAxis[Self@Frame]` is not assignable to declared return type `IterNodeGroupAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4053:33-4059:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4067:16-4073:10: Returned type `IterNodeDepthLevelAxis[Self@Frame]` is not assignable to declared return type `IterNodeDepthLevelAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4067:38-4073:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4080:16-4086:10: Returned type `IterNodeDepthLevelAxis[Self@Frame]` is not assignable to declared return type `IterNodeDepthLevelAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4080:38-4086:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4094:16-4100:10: Returned type `IterNodeDepthLevelAxis[Self@Frame]` is not assignable to declared return type `IterNodeDepthLevelAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4094:38-4100:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4107:16-4113:10: Returned type `IterNodeDepthLevelAxis[Self@Frame]` is not assignable to declared return type `IterNodeDepthLevelAxis[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4107:38-4113:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4121:16-4127:10: Returned type `IterNodeGroupOtherReducible[Self@Frame]` is not assignable to declared return type `IterNodeGroupOtherReducible[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4121:43-4127:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4134:16-4140:10: Returned type `IterNodeGroupOtherReducible[Self@Frame]` is not assignable to declared return type `IterNodeGroupOtherReducible[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4134:43-4140:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4148:16-4154:10: Returned type `IterNodeGroupOtherReducible[Self@Frame]` is not assignable to declared return type `IterNodeGroupOtherReducible[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4148:43-4154:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4161:16-4167:10: Returned type `IterNodeGroupOtherReducible[Self@Frame]` is not assignable to declared return type `IterNodeGroupOtherReducible[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4161:43-4167:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4180:16-4186:10: Returned type `IterNodeWindowReducible[Self@Frame]` is not assignable to declared return type `IterNodeWindowReducible[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4180:39-4186:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4198:16-4204:10: Returned type `IterNodeWindowReducible[Self@Frame]` is not assignable to declared return type `IterNodeWindowReducible[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4198:39-4204:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4216:16-4222:10: Returned type `IterNodeWindowReducible[Self@Frame]` is not assignable to declared return type `IterNodeWindowReducible[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4216:39-4222:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4234:16-4240:10: Returned type `IterNodeWindowReducible[Self@Frame]` is not assignable to declared return type `IterNodeWindowReducible[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4234:39-4240:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4246:16-4252:10: Returned type `IterNodeAxisElement[Self@Frame]` is not assignable to declared return type `IterNodeAxisElement[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4246:35-4252:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]
+ ERROR static_frame/core/frame.py:4257:16-4263:10: Returned type `IterNodeAxisElement[Self@Frame]` is not assignable to declared return type `IterNodeAxisElement[Frame]` [bad-return]
+ ERROR static_frame/core/frame.py:4257:35-4263:10: `Self@Frame` is not assignable to upper bound `Bus | Frame | Quilt | Series | Yarn` of type variable `TContainerAny` [bad-specialization]

core (https://github.com/home-assistant/core)
- ERROR homeassistant/helpers/data_entry_flow.py:83:25-47: Argument `dict[str, Any]` is not assignable to parameter `context` with type `_FlowContextT | None` in function `homeassistant.data_entry_flow.FlowManager.async_init` [bad-argument-type]

spark (https://github.com/apache/spark)
- ERROR python/pyspark/pandas/tests/indexes/test_category.py:179:35-52: `int | None` is not assignable to upper bound `BaseOffset | CategoricalDtype | ExtensionDtype | Interval | Period | bool | bytes | complex | date | datetime | dtype | float | int | list[str] | str | time | timedelta | type[bool | complex | object | str]` of type variable `S1` [bad-specialization]
+ ERROR python/pyspark/pandas/tests/indexes/test_category.py:179:35-52: `int | None` is not assignable to upper bound `BaseOffset | CategoricalDtype | ExtensionDtype | Interval | Period | bool | bytes | complex | date | datetime | dtype[generic] | float | int | list[str] | str | time | timedelta | type[bool | complex | object | str]` of type variable `S1` [bad-specialization]

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

Primer Diff Classification

❌ 1 regression(s) | ✅ 4 improvement(s) | ➖ 1 neutral | 6 project(s) total | +57, -6 errors

1 regression(s) across static-frame. error kinds: bad-specialization: Self@Frame not recognized as satisfying Frame bound, bad-return: InterfaceConsolidate[Self@Frame] vs InterfaceConsolidate[Frame]. caused by strip_recursive_class_targs(). 4 improvement(s) across antidote, rotki, steam.py, core.

Project Verdict Changes Error Kinds Root Cause
antidote ✅ Improvement -2 bad-specialization truncate_recursive_targs()
rotki ✅ Improvement -1 bad-argument-type truncate_recursive_targs()
steam.py ✅ Improvement -1 bad-assignment truncate_recursive_targs()
static-frame ❌ Regression +56 bad-specialization: Self@Frame not recognized as satisfying Frame bound strip_recursive_class_targs()
core ✅ Improvement -1 bad-argument-type strip_recursive_class_targs()
spark ➖ Neutral +1, -1 bad-specialization
Detailed analysis

❌ Regression (1)

static-frame (+56)

bad-specialization: Self@Frame not recognized as satisfying Frame bound: Self@Frame should be assignable to Frame (and thus to the TVContainer_co upper bound which includes Frame). Pyrefly fails to recognize this. 0/28 co-reported by mypy/pyright.
bad-return: InterfaceConsolidate[Self@Frame] vs InterfaceConsolidate[Frame]: Cascade from the bad-specialization issue. Since Self@Frame should be assignable to Frame, InterfaceConsolidate[Self@Frame] should be assignable to InterfaceConsolidate[Frame]. 0/28 co-reported by mypy/pyright.

Overall: The analysis is factually correct. The key claims are:

  1. Self@Frame is bounded by Frame per the typing spec - this is correct. PEP 673 specifies that Self used within a class C is equivalent to a TypeVar bounded by C.

  2. Since Self@Frame is bounded by Frame, it should satisfy any upper bound that includes Frame (like TVContainer_co which is a union including Frame) - this is correct. A type bounded by Frame means it is a subtype of Frame, and since Frame is in the union bound of TVContainer_co, Self@Frame should be assignable to that bound.

  3. The bad-return errors are cascading from the bad-specialization errors - this is correct. If InterfaceConsolidate[Self@Frame] is rejected because Self@Frame doesn't satisfy the bound, then the return type mismatch follows.

  4. 0/56 errors are co-reported by mypy or pyright - this confirms these are pyrefly-specific false positives.

  5. Looking at the source code at line 3808, the consolidate property returns InterfaceConsolidate(container=self, ...). Since self has type Self (i.e., Self@Frame), and InterfaceConsolidate is parameterized with a type variable that has Frame in its bound, Self@Frame should satisfy that bound. The declared return type is InterfaceConsolidate[TFrameAny] (which is InterfaceConsolidate[Frame]), and InterfaceConsolidate[Self@Frame] should be assignable to it given covariance (TVContainer_co is covariant as indicated by the _co suffix).

All reasoning is consistent and factually accurate.

Attribution: The change to strip_recursive_class_targs() in crates/pyrefly_types/src/types.rs now preserves type arguments for non-recursive bounds. Previously, tparams_have_restrictions() would strip TArgs from any ClassType with restricted TParams. Now tparams_can_reach_class() only strips when there's a recursive path. This means Self@Frame is now preserved in type arguments of InterfaceConsolidate, and pyrefly then fails to recognize Self@Frame as satisfying the TVContainer_co upper bound that includes Frame.

✅ Improvement (4)

antidote (-2)

These were false positives caused by over-aggressive type argument truncation. QualifiedBy satisfies Predicate[Any] and is a valid type argument for PredicateConstraint. The PR correctly narrows the truncation to only recursive cases, preserving specialized bounds like Predicate[Any].
Attribution: The change to TParams::[truncate_recursive_targs()](https://github.com/facebook/pyrefly/blob/main/crates/pyrefly_types/src/types.rs) and the replacement of tparams_have_restrictions() with tparams_can_reach_class() in crates/pyrefly_types/src/types.rs fixed this. The old tparams_have_restrictions() checked if ANY quantified in the TParams had a non-trivial restriction (bound or constraint), which was too broad — it stripped [Any] from Predicate[Any] even though Predicate doesn't recursively reference itself. The new tparams_can_reach_class() only strips TArgs when the class's type parameters can reach back to the same class through their restrictions, correctly preserving Predicate[Any].

rotki (-1)

This is a clear false positive removal. The error claimed T_TxNotDecodedFilterQuery was not assignable to T_TxNotDecodedFilterQuery — the exact same TypeVar. The root cause was pyrefly's truncate_recursive_targs being too aggressive: it stripped type arguments from ClassType bounds even when there was no recursive cycle, which corrupted TypeVar resolution. The PR narrows the truncation to only apply when there's an actual recursive path, preserving non-recursive specialized bounds like P[Any]. This fixes the false positive.
Attribution: The change to TParams::[truncate_recursive_targs()](https://github.com/facebook/pyrefly/blob/main/crates/pyrefly_types/src/types.rs) in crates/pyrefly_types/src/types.rs replaced the old tparams_have_restrictions() check (which stripped TArgs from any ClassType whose TParams had non-trivial restrictions) with the new tparams_can_reach_class() check (which only strips TArgs when there's an actual recursive path back to the same class). The old logic was too broad and was stripping specialized type arguments from non-recursive bounds, causing downstream type resolution failures. This fix preserved the specialized bounds correctly, which resolved the false positive bad-argument-type error in this project.

steam.py (-1)

The PR narrows the recursive type argument truncation to only strip args when there's an actual recursive path back to the same class. Previously, any ClassType with non-trivially restricted TParams had its args stripped, which could degrade type inference for complex generic hierarchies like steam.py's chat classes. The removed error was a false positive caused by this over-aggressive truncation.
Attribution: The change to TParams::[truncate_recursive_targs()](https://github.com/facebook/pyrefly/blob/main/crates/pyrefly_types/src/types.rs) in crates/pyrefly_types/src/types.rs replaced the broad tparams_have_restrictions() check with the more precise tparams_can_reach_class() check. This preserved specialized TypeVar bounds that don't involve recursive paths, improving type inference for the generic ChatMessage/Chat class hierarchy and removing the false bad-assignment error.

core (-1)

This is a clear improvement. The removed error was a false positive caused by pyrefly's overly broad truncation of type arguments in TypeVar bounds. The _FlowManagerT TypeVar is bounded by FlowManager[Any, Any, Any], and the async_init method's context parameter type depends on one of those Any type arguments. When pyrefly incorrectly stripped the [Any, Any, Any], the parameter type became an unresolved TypeVar instead of Any, causing the spurious bad-argument-type error. The PR's fix correctly narrows the truncation to only recursive cases, preserving non-recursive specialized bounds like this one.
Attribution: The change to TParams::[strip_recursive_class_targs()](https://github.com/facebook/pyrefly/blob/main/crates/pyrefly_types/src/types.rs) and the replacement of tparams_have_restrictions() with tparams_can_reach_class() in crates/pyrefly_types/src/types.rs is directly responsible. The old tparams_have_restrictions() checked if any TParam had a non-trivial restriction (Bound or Constraints) and stripped TArgs if so. Since FlowManager's type parameters have bounds, the old logic stripped [Any, Any, Any] from the bound FlowManager[Any, Any, Any]. The new tparams_can_reach_class() only strips TArgs when there's an actual recursive path back to the same class, which doesn't apply here since FlowManager's type parameters don't recursively reference FlowManager itself.

➖ Neutral (1)

spark (+1, -1)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

Suggested fixes

Summary: The PR correctly narrows recursive TArg truncation but exposes a pre-existing bug where Self@Frame is not recognized as satisfying a TypeVar upper bound that includes Frame.

1. In type_can_reach_class() in crates/pyrefly_types/src/types.rs, the function needs to also check through Self types (TypeVar with Self semantics). When encountering a Self@Frame type inside TArgs of InterfaceConsolidate, the function should recognize that Self@Frame's bound (Frame) can reach the target class. However, the real fix is likely not in this function but in pyrefly's bound-satisfaction/subtyping logic: when checking whether Self@Frame satisfies a TypeVar bound like TVContainer_co (which is a union including Frame), pyrefly should use Self@Frame's upper bound (Frame) to verify satisfaction. The specific check likely lives in the specialization validation code that produces bad-specialization errors. In that code path, when a type argument is a Self type (or a TypeVar with a bound), the checker should verify that the TypeVar's bound satisfies the target TypeVar's bound, not just check the TypeVar itself. This would mean: if the type argument is Self@Frame with bound=Frame, and the target bound is Union[Frame, ...], then Frame <: Union[Frame, ...] holds, so Self@Frame should be accepted.

Files: crates/pyrefly_types/src/types.rs
Confidence: medium
Affected projects: static-frame
Fixes: bad-specialization, bad-return
The 56 new errors are all pyrefly-only (0/56 co-reported by mypy/pyright). They consist of 28 bad-specialization errors (Self@Frame not satisfying Frame bound) and 28 cascading bad-return errors. The PR's truncation change is correct — it fixed 4 projects' false positives. The regression reveals a pre-existing gap in Self type bound satisfaction checking that was previously masked by the over-aggressive truncation (which stripped the Self@Frame type argument entirely). The fix should be in the subtype/bound-satisfaction checker rather than reverting the truncation improvement.

2. As a targeted workaround in tparams_can_reach_class() or strip_recursive_class_targs() in crates/pyrefly_types/src/types.rs: when checking whether to strip TArgs, also consider Self types as potentially recursive. In type_can_reach_class(), when encountering a Quantified/TypeVar that represents Self@X, treat it as if it could reach class X (since Self@X is bounded by X). This would cause InterfaceConsolidate[Self@Frame]'s TArgs to be stripped back to empty (as before the PR), avoiding the bad-specialization errors while preserving the improvements for non-Self cases. Specifically, in the match arm for non-ClassType types within type_can_reach_class(), add: if the type is a Quantified with a Bound restriction containing a ClassType that matches or can reach the target class, return true.

Files: crates/pyrefly_types/src/types.rs
Confidence: medium
Affected projects: static-frame
Fixes: bad-specialization, bad-return
This is a more surgical workaround that keeps the PR's improvements for the 4 improved projects while also handling the Self@Frame case in static-frame. Self@Frame has an implicit bound of Frame, so when Self@Frame appears in TArgs of InterfaceConsolidate, and Frame's TParams can reach InterfaceConsolidate through their restrictions, the truncation should still apply. This eliminates all 56 pyrefly-only errors in static-frame.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (1 heuristic, 5 LLM)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bad-argument-type when using protocols

2 participants