Background
PR #TBD (the Self solving fix) handles the symptom of typeshed routinely omitting explicit cls: type[Self] / self: Self annotations on method parameters by synthesizing them on the fly inside _collect_type_var_bindings. This makes calls like object.__new__(self.__class__) correctly solve Self → Anchor, and was sufficient to unblock the reported bug.
That fix is at the right architectural layer (call-site, where Pyright also solves Self), but it's still a layer above the underlying root cause. The cleaner fix is to synthesize the implicit annotations once, at FunctionType construction time, so that every consumer of FunctionType.parameters sees the correct types instead of Any.
Why the deeper fix is desirable
The current call-site synthesis is "shallow" — it only affects the binding pass that solves Self. Several other parts of the type checker still see the unfixed param_type=Any for cls/self:
validate_arg_types still sees Any for cls. It accepts the call (Any → anything is allowed) but the precision is lost.
- IDE hover and parameter display still show
cls: Any instead of cls: type[Self].
- Error messages that pretty-print parameter types still print
Any.
- Future passes that introspect parameter types (e.g., docstring generation, JSON-schema export, autocomplete) won't benefit from the implicit annotation — each one would need its own synthesis hook.
A FunctionType-construction-time fix would write the implicit annotation into the Parameter.param_type field exactly once, and all of the above would benefit automatically.
Where to do it
The synthesis should happen in the AST → FunctionType pipeline, most likely in jac/jaclang/compiler/type_system/type_evaluator.impl/construct_types.impl.jac (where Parameter objects are built from FuncSignature.params). The rule:
- Method (first param has
is_self=True) with no explicit annotation → set param_type to TypeVarType(name=\"Self\", is_self=True).
- Classmethod /
__new__ (first param has is_cls=True) with no explicit annotation → set param_type to type[Self] (a ClassType wrapping the same Self TypeVar).
- Plain function (first param is neither
is_self nor is_cls) → leave alone.
What to remove once that's done
After the construction-time synthesis lands, the following from PR #TBD becomes redundant and should be deleted:
- The
_effective_param_type nested helper in _collect_type_var_bindings (parameter_type_check.impl.jac)
- The "unannotated → synthesize Self" branches there
Keep:
- The
is_unbound_call / is_constructor_call parameter sprawl is still needed because the binding step has to mirror match_args_to_params param-stripping. (See the next bullet.)
Bonus refactor
While in the area, the param-stripping logic in match_args_to_params and _collect_type_var_bindings is duplicated. Extracting _effective_params_for_call(method, is_unbound_call, is_constructor_call) -> list[Parameter] and using it in both places would eliminate the duplication and reduce the chance of the two functions drifting out of sync.
Risk
Touching FunctionType construction is non-trivial — there are several construction sites (typeshed .pyi loading, .jac ability typing, __new__ overload resolution, dataclass synthesis). Each needs to be checked for whether the implicit synthesis applies. The full test suite (especially test_checker_pass.jac and the bootstrap tests) should be run after each touch point.
Acceptance
- All existing tests pass
- The synthesis branches in
_collect_type_var_bindings are removed
- IDE hover on
cls in def __new__(cls) -> Self shows type[Self], not Any
- A new test asserts
param.param_type is the synthesized type for an unannotated cls/self (rather than only checking the observable Self-solving behavior)
Related
- PR #TBD (the call-site fix this issue is a follow-up to)
- PEP 673 (typing.Self): https://peps.python.org/pep-0673/
- Pyright's
createSelfType / validateOverloadedFunctionArguments for the reference implementation
Background
PR #TBD (the
Selfsolving fix) handles the symptom of typeshed routinely omitting explicitcls: type[Self]/self: Selfannotations on method parameters by synthesizing them on the fly inside_collect_type_var_bindings. This makes calls likeobject.__new__(self.__class__)correctly solveSelf → Anchor, and was sufficient to unblock the reported bug.That fix is at the right architectural layer (call-site, where Pyright also solves
Self), but it's still a layer above the underlying root cause. The cleaner fix is to synthesize the implicit annotations once, at FunctionType construction time, so that every consumer ofFunctionType.parameterssees the correct types instead ofAny.Why the deeper fix is desirable
The current call-site synthesis is "shallow" — it only affects the binding pass that solves
Self. Several other parts of the type checker still see the unfixedparam_type=Anyforcls/self:validate_arg_typesstill seesAnyforcls. It accepts the call (Any → anything is allowed) but the precision is lost.cls: Anyinstead ofcls: type[Self].Any.A FunctionType-construction-time fix would write the implicit annotation into the
Parameter.param_typefield exactly once, and all of the above would benefit automatically.Where to do it
The synthesis should happen in the AST → FunctionType pipeline, most likely in
jac/jaclang/compiler/type_system/type_evaluator.impl/construct_types.impl.jac(whereParameterobjects are built fromFuncSignature.params). The rule:is_self=True) with no explicit annotation → setparam_typetoTypeVarType(name=\"Self\", is_self=True).__new__(first param hasis_cls=True) with no explicit annotation → setparam_typetotype[Self](aClassTypewrapping the sameSelfTypeVar).is_selfnoris_cls) → leave alone.What to remove once that's done
After the construction-time synthesis lands, the following from PR #TBD becomes redundant and should be deleted:
_effective_param_typenested helper in_collect_type_var_bindings(parameter_type_check.impl.jac)Keep:
is_unbound_call/is_constructor_callparameter sprawl is still needed because the binding step has to mirrormatch_args_to_paramsparam-stripping. (See the next bullet.)Bonus refactor
While in the area, the param-stripping logic in
match_args_to_paramsand_collect_type_var_bindingsis duplicated. Extracting_effective_params_for_call(method, is_unbound_call, is_constructor_call) -> list[Parameter]and using it in both places would eliminate the duplication and reduce the chance of the two functions drifting out of sync.Risk
Touching FunctionType construction is non-trivial — there are several construction sites (typeshed
.pyiloading,.jacability typing,__new__overload resolution, dataclass synthesis). Each needs to be checked for whether the implicit synthesis applies. The full test suite (especiallytest_checker_pass.jacand the bootstrap tests) should be run after each touch point.Acceptance
_collect_type_var_bindingsare removedclsindef __new__(cls) -> Selfshowstype[Self], notAnyparam.param_typeis the synthesized type for an unannotatedcls/self(rather than only checking the observable Self-solving behavior)Related
createSelfType/validateOverloadedFunctionArgumentsfor the reference implementation