Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[codex] Preserve specialized TypeVar bounds#3291

Open
cneuralnetwork wants to merge 1 commit intofacebook:mainfacebook/pyrefly:mainfrom
cneuralnetwork:codex/issue-3285-protocol-boundcneuralnetwork/pyrefly:codex/issue-3285-protocol-boundCopy head branch name to clipboard
Open

[codex] Preserve specialized TypeVar bounds#3291
cneuralnetwork wants to merge 1 commit intofacebook:mainfacebook/pyrefly:mainfrom
cneuralnetwork:codex/issue-3285-protocol-boundcneuralnetwork/pyrefly:codex/issue-3285-protocol-boundCopy head branch name to clipboard

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

1 participant

Morty Proxy This is a proxified and sanitized view of the page, visit original site.