Issue 1718: Instantiate/Pushdown Unification Plan
Context
Issue #1718 calls out two duplication points:
TypedExpr.instantiateTovsType.instantiate.pushDownCovariantin bothInfer.instDataConandTypedExpr.instantiateTo.
The objective is the smallest refactor that reduces duplication while preserving:
- existing typechecking behavior,
- instantiation evidence,
- soundness (no widen-only shortcuts where evidence exists).
This document is an implementation and testing plan only (no src/main changes in this PR).
Current Behavior Snapshot
Instantiation paths today
Type.instantiate:- structural matcher with explicit left vars, right existentials, and right-side env scope.
- returns
Type.Instantiation(frees, subs, toFrees, toSubs). - used in inference codepaths that can validate kind-correct substitutions.
-
TypedExpr.instantiateTo: - local solver for
Genericexpression specialization toRho. - contains local pushdown retry logic and expression-level wrapping/fallback.
- falls back to annotation when it cannot solve.
Pattern pushdown today
Infer.instDataConhas localpushDownCovariantto handle constructor-pattern sigma withforall.TypedExprhas a similar localpushDownCovariantused byinstantiateTo.
These are conceptually the same transform but implemented twice with slightly different shape.
Coverage Baseline (Before This PR)
From sbt ';clean;set ThisBuild / coverageEnabled := true;coreJVM/test;cli/test;coreJVM/coverageReport;cli/coverageReport' on February 19, 2026:
coreJVM: statement84.28%, branch81.50%.cli: statement48.29%, branch39.53%.
Relevant uncovered branches in Infer:
typeCheckPatternlist-patternforall Listfast-path (Infer.scala:2661,Infer.scala:2663).instDataConlocalpushDownCovariantbranches that re-wrap recursiveForAllresults (Infer.scala:2875,Infer.scala:2877,Infer.scala:2890,Infer.scala:2891).
TypedExpr.instantiateTo and Type.instantiate are already at 100% method coverage, but call-site behavior still benefits from regression tests.
Tests Added In This PR
Added to RankNInferTest:
list patterns can consume forall-list scrutinees:- targets
typeCheckPatternforall Listspecialization fast-path. -
pattern instantiation pushes forall through mixed-variance constructor args: - targets
instDataCon.pushDownCovariantrecursiveForAllwrapping branches. -
polymorphic match result can be instantiated at higher-order call sites: - regression on polymorphic match results specialized at use sites (instantiate/coerce call graph behavior).
Refactor Plan (Issue 1718)
Phase 1: Unify covariant pushdown
- Extract a shared type-level helper in
Type(or closely related rankn module): - input: quantifier-bearing type plus a variance/kind lookup for applied heads.
- output: transformed type with
forallpushed through covariant positions conservatively. -
Replace both current call sites with thin wrappers:
Infer.instDataConwrapper only supplies constructor arg variance/kind context.TypedExpr.instantiateTowrapper supplies resolver from available kinds map.-
Keep behavior conservative:
- if variance info is missing, return input unchanged.
- do not invent substitutions or quantifier elimination not already allowed.
Phase 2: Delegate TypedExpr.instantiateTo to Type.instantiate
- Rework
TypedExpr.instantiateToas orchestration only: - pre-step: split quantifiers from
gen.quantType. - solver step: call
Type.instantiate. - post-step: substitute expression types using returned
subs. - quantifier rewrapping/pushGeneric/fallback remains local to
TypedExpr. -
Preserve evidence:
- use
Instantiation.subsas the primary witness for specialization. - preserve unsolved frees by rebuilding
Genericwrappers when possible. - only use annotation fallback when solver cannot provide a coherent witness.
-
Keep existing fallback semantics:
- if solve fails, keep
ann(gen, instTpe)behavior. - avoid replacing evidence-driven coercions with widen-only coercions.
Phase 3: Clean call graph and reduce duplication
- Remove duplicated local solver code once all tests pass.
- Remove duplicated local pushdown implementation.
- Keep helper APIs small:
- one shared pushdown helper,
- one shared instantiate solver (
Type.instantiate) consumed by both inference and typed-expression coercion.
Testing Plan For Refactor
Required gates
- existing
coreJVM/testandcli/testmust remain green. - targeted coverage for issue-relevant methods must be 100% statement/branch:
Type.instantiate,TypedExpr.instantiateTo,- shared pushdown helper (new),
Infer.typeCheckPatternlistforall Listbranch,- constructor-pattern pushdown branches previously uncovered.
Additional regression matrix
- pattern checks with:
- mixed variance constructors,
forallscrutinee types,- list patterns under
forall. -
specialization/coercion with:
- polymorphic match branches,
- higher-order call sites expecting monomorphic function types,
- existential-heavy argument/application paths where apply fallback currently exists.
-
soundness checks:
- keep existing ill-typed programs ill-typed (especially unsound variance/existential cases),
- ensure no new widening-only acceptance appears where evidence should be required.
Minimal-Change Sequencing
- land shared pushdown helper + call-site rewires first (no instantiate unification yet).
- land
instantiateTodelegation toType.instantiatesecond. - remove dead duplicated code last.
- after each step: run focused test subset + full
coreJVM/test.
This order minimizes blast radius while keeping bisectable behavioral changes.