Skip to content

Dedup tparam-default substitution between alt and solver layers - #4406

Open
nitishagar wants to merge 1 commit into
facebook:mainfrom
nitishagar:nitishagar/4298-dedup-tparam-default
Open

Dedup tparam-default substitution between alt and solver layers#4406
nitishagar wants to merge 1 commit into
facebook:mainfrom
nitishagar:nitishagar/4298-dedup-tparam-default

Conversation

@nitishagar

@nitishagar nitishagar commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

The logic for substituting references to earlier type parameters inside a tparam's default was duplicated in two places:

  • get_tparam_default in pyrefly/lib/alt/class/targs.rs (used during call/specialization)
  • an inlined copy in finish_class_targs in pyrefly/lib/solver/solver.rs (used during class-level finalization), flagged by a // TODO: deal with code duplication in get_tparam_default marker.

The two copies differed only in their lookup source and their fallback value for out-of-scope references — differences that are intentional, since one path runs where an out-of-scope reference is an already-reported error (returns Any(Implicit)) and the other runs during finalization with no such error (returns the param's gradual type).

This PR extracts a shared substitute_tparam_defaults helper into pyrefly_types::quantified (next to the existing as_gradual_type_helper, which already uses the same TypeVar/TypeVarTuple/ParamSpec/Quantified match idiom). The helper takes the default by value, a &dyn Fn(&Name) -> Option<Type> lookup closure, and a caller-chosen fallback Type, so each call site keeps its own lookup source and fallback semantics. Both callers are rewritten to delegate to it, and the // TODO marker is removed.

Behavior

This is a behavior-preserving refactor. Each call site passes the same lookup and fallback it used inline, so the produced types are identical for every input:

  • get_tparam_default passes name_to_idx -> checked_targs lookup with self.heap.mk_any_implicit() fallback (unchanged).
  • finish_class_targs passes seen_params -> new_targs-then-targs lookup with param.as_gradual_type() fallback (unchanged).

The two fallbacks are deliberately not collapsed — they intentionally differ (error path vs finalization path).

Test plan

  • cargo build -p pyrefly_types — clean
  • cargo build -p pyrefly — clean
  • cargo test -p pyrefly --lib7675 passed, 0 failed, 3 ignored (the full typechecker regression suite; zero behavior diff)
  • python3 test.py --no-test --no-tensor-shapes --no-conformance --no-jsonschema — clean for the touched files

Fixes #4298

@meta-cla meta-cla Bot added the cla signed label Aug 2, 2026
@github-actions github-actions Bot added the size/s label Aug 2, 2026
@meta-codesync

meta-codesync Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

This pull request has been imported. If you are a Meta employee, you can view this in D114523923. (Because this pull request was imported automatically, there will not be any future comments.)

@github-actions

This comment has been minimized.

The same TypeVar/TypeVarTuple/ParamSpec/Quantified match-and-substitute
idiom was duplicated between `get_tparam_default` (alt/class/targs.rs)
and an inline copy in `finish_class_targs` (solver/solver.rs), the latter
flagged by a `// TODO: deal with code duplication` marker.

Extract a single `substitute_tparam_defaults` helper in
`pyrefly_types::quantified` (next to the related `as_gradual_type_helper`
that already shares this idiom). The helper takes the lookup and fallback
by parameter, so each caller keeps its own lookup source and fallback —
those intentionally differ: `get_tparam_default` reads finalized
`checked_targs` and falls back to `mk_any_implicit()` (the call path
already reported the out-of-scope reference as an error), while
`finish_class_targs` reads in-progress `new_targs` and falls back to
`param.as_gradual_type()` (the class-finalization path has no such
error). Pure behavior-preserving relocation; the existing test suite is
the regression oracle.

Closes facebook#4298.
@nitishagar
nitishagar force-pushed the nitishagar/4298-dedup-tparam-default branch from 46c8136 to 6e09c5f Compare August 5, 2026 00:50
@github-actions github-actions Bot added size/s and removed size/s labels Aug 5, 2026

@rchen152 rchen152 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced this is an improvement. Helpers add conceptual overhead, and callbacks add implementation complexity, whereas the original code is simple and clear, and the duplication is small.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

@nitishagar

nitishagar commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for taking the time. A couple of thoughts on the trade-off, happy to drop this if it's not convincing:

  • The shape of this helper (default + lookup closure + fallback) is what TODO: deal with code duplication in get_tparam_default #4298 proposes, and the // TODO marker it points at has been sitting in finish_class_targs for a while. So the dedup is fulfilling a tracked request rather than being introduced speculatively.
  • My main motivation is drift risk: the two copies do the same TypeVar/TypeVarTuple/ParamSpec/Quantified match, and a future change to one (e.g. handling a new tparam form) would have to be remembered in both. The two intentionally differ only in lookup source and fallback, which are exactly the two parameterized pieces.

That said, I take the point on the &dyn Fn adding implementation weight. If the conceptual overhead isn't worth it for this size of duplication, I can just close this and #4298. Let me know which way you'd like to go.

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.

TODO: deal with code duplication in get_tparam_default

3 participants