Skip to content

Type Checker: Synthesize implicit Self annotations at FunctionType construction time #5520

@marsninja

Description

@marsninja

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions