Skip to content

Conversation

@Jack-GitHub12
Copy link

@Jack-GitHub12 Jack-GitHub12 commented Nov 22, 2025

Summary

For #43

This fixes a type checking bug where generic class constructors would lose their type parameters when passed through ParamSpec functions. The type checker now correctly preserves type variables (@_) instead of finalizing them to Unknown, allowing proper type inference.

The Issue

When passing a generic class constructor through a function using ParamSpec, the type parameters were incorrectly finalized to Unknown. For example:

def identity[**P, R](x: Callable[P, R]) -> Callable[P, R]:
    return x

class C[T]:
    def __init__(self, x: T) -> None:
        self.x = x

c2 = identity(C)
reveal_type(c2)  # ERROR: (x: Unknown) -> C[Unknown]

At runtime, generic class constructors should preserve their type parameters for inference. However, Pyrefly's subset checking in subset.rs wasn't instantiating fresh type variables for generic classes, causing them to be finalized to Unknown during ParamSpec inference.

The Fix

I made several changes to handle generic class constructors properly:

  1. Modified CallTarget::Class in pyrefly/lib/alt/call.rs:16 to use TargetWithTParams:
Class(TargetWithTParams<ClassTarget>),
  1. Added fresh variable instantiation for generic classes in pyrefly/lib/solver/subset.rs:34:
let fresh_class = self.type_order.instantiate_fresh_class(&class, self.type_order.solver);
  1. Added helper methods in pyrefly/lib/solver/type_order.rs:17 for accessing class type parameters and instantiating fresh class instances.

This ensures generic type parameters remain as solver variables (@_) that can be unified during inference, rather than being prematurely finalized to Unknown.

Test Plan

Updated the existing test case test_param_spec_generic_constructor in pyrefly/lib/test/paramspec.rs:

  • Removed the bug = "Generic class constructors don't work with ParamSpec" marker
  • Changed expected output from # E: revealed type: (x: Unknown) -> C[Unknown] to # E: revealed type: (x: @_) -> C[@_]
  • Added verification line x: C[int] = c2(1) to ensure type inference works

Verification:

  • Ran test locally:
cargo test test_param_spec_generic_constructor

Result: passes

  • Ran all ParamSpec tests to ensure no regressions:
cargo test paramspec

Result: passes

This commit fixes issue facebook#43 where generic class constructors don't preserve
type parameters when used with ParamSpec functions.

Changes:
1. Modified CallTarget::Class to use TargetWithTParams in call.rs,
   allowing generic type parameters to be preserved through the call
   target system
2. Updated Type::Forall handling in call.rs to also set tparams for
   Class targets
3. Improved subset checking in subset.rs to instantiate fresh type
   variables for generic class constructors, allowing the variables
   to be properly unified during ParamSpec inference instead of being
   finalized to Unknown
4. Updated call_graph.rs to handle the new CallTarget::Class structure
5. Added helper methods to type_order.rs for accessing class tparams
   and instantiating fresh class instances

The key fix is in subset.rs where we now call instantiate_fresh_class() to
create fresh solver variables for the generic class's type parameters. These
variables remain as solver variables (represented as @_) that can be unified
during callable inference, instead of being immediately finalized to Unknown.

Test result: Generic class constructors now preserve type parameters as @_
(type variables) instead of Unknown, allowing ParamSpec inference to work
correctly.
This reverts commit dedeeb2.
@meta-cla meta-cla bot added the cla signed label Nov 22, 2025
Run cargo fmt to ensure consistent code formatting across the project.
@yangdanny97 yangdanny97 self-assigned this Nov 23, 2025
@yangdanny97 yangdanny97 changed the title fix: Generic ParamSpec fix: Generic ParamSpec for constructors Nov 24, 2025
@yangdanny97
Copy link
Contributor

Thanks! This change looks good, though I don't think this completely handles #43, per sam's comment here: #43 (comment)

@meta-codesync
Copy link

meta-codesync bot commented Nov 24, 2025

@yangdanny97 has imported this pull request. If you are a Meta employee, you can view this in D87784439.

@yangdanny97
Copy link
Contributor

Hmm, one issue I found is that the type variable is not being solved by the call arguments.

In the test case you added, I see that x: C[int] = c2(1) works. We should also be able to infer C[int] from the argument to c2, wihtout the hint. However, doing a reveal_type on y after y = c2(1) gives C[Unknown]

@yangdanny97
Copy link
Contributor

I think the solution for #43 probably requires making a new Forallable type for ParamSpecValue and possibly also Concatenate

That would allow passing the type params when we solve a ParamSpec, which would enable us to properly synthesize a generic function type later.

@rchen152
Copy link
Contributor

I think the solution for #43 probably requires making a new Forallable type for ParamSpecValue and possibly also Concatenate

That would allow passing the type params when we solve a ParamSpec, which would enable us to properly synthesize a generic function type later.

I wonder if we might be able to do something similar to #1650; the fix there is specific to decorators, but we might be able to extend it.

When a generic function or constructor is passed through a ParamSpec-using
function, preserve its generic nature so each call creates fresh type
variables rather than locking types on first call.

- Add ParamSpecValue and Concatenate variants to Forallable enum
- Detect generic params in create_paramspec_value() and wrap in Forall
- Lift Forall wrapper to outer Callable in simplify_mut()
- Handle Forall<ParamSpecValue> in callable.rs to instantiate fresh vars
- Use transform_mut instead of visit_mut to recurse into ClassType's TArgs
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.

3 participants