fix: Generic ParamSpec for constructors
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:
- Modified
CallTarget::Classin pyrefly/lib/alt/call.rs:16 to useTargetWithTParams:
Class(TargetWithTParams<ClassTarget>),
- 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);
- 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
Thanks! This change looks good, though I don't think this completely handles #43, per sam's comment here: https://github.com/facebook/pyrefly/issues/43#issuecomment-3226102136
@yangdanny97 has imported this pull request. If you are a Meta employee, you can view this in D87784439.
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]
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 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 https://github.com/facebook/pyrefly/pull/1650; the fix there is specific to decorators, but we might be able to extend it.