Skip to content

Simplify polyfunction logic #18200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 60 additions & 55 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import TypeOps.refineUsingParent
import collection.mutable
import util.{Stats, NoSourcePosition, EqHashMap}
import config.Config
import config.Feature.migrateTo3
import config.Feature.{ccEnabled, migrateTo3}
import config.Printers.{subtyping, gadts, matchTypes, noPrinter}
import TypeErasure.{erasedLub, erasedGlb}
import TypeApplications._
Expand Down Expand Up @@ -641,42 +641,15 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
def compareRefined: Boolean =
val tp1w = tp1.widen

// FIXME: this shortcut is currently necessary to correctly compare function types in CC phase.
// Going to `hasMatchingMember` breaks tests (e.g. neg-custom-args/captures/lazyref.scala). See #18200.
if ctx.phase == Phases.checkCapturesPhase then

// A relaxed version of subtyping for dependent functions where method types
// are treated as contravariant.
// TODO: Merge with isSubInfo in hasMatchingMember. Currently, we can't since
// the isSubinfo of hasMatchingMember has problems dealing with PolyTypes
// (---> orphan params during pickling)
def isSubInfo(info1: Type, info2: Type): Boolean = (info1, info2) match
case (info1: PolyType, info2: PolyType) =>
info1.paramNames.hasSameLengthAs(info2.paramNames)
&& isSubInfo(info1.resultType, info2.resultType.subst(info2, info1))
case (info1: MethodType, info2: MethodType) =>
matchingMethodParams(info1, info2, precise = false)
&& isSubInfo(info1.resultType, info2.resultType.subst(info2, info1))
case (info1 @ CapturingType(parent1, refs1), info2: Type) =>
subCaptures(refs1, info2.captureSet, frozenConstraint).isOK && sameBoxed(info1, info2, refs1)
&& isSubInfo(parent1, info2)
case (info1: Type, CapturingType(parent2, refs2)) =>
val refs1 = info1.captureSet
(refs1.isAlwaysEmpty || subCaptures(refs1, refs2, frozenConstraint).isOK) && sameBoxed(info1, info2, refs1)
&& isSubInfo(info1, parent2)
case _ =>
isSubType(info1, info2)

def compareInfo(info1: Type, info2: Type): Boolean =
isSubInfo(info1, info2, NoType)
if defn.isFunctionType(tp2) then
if tp2.derivesFrom(defn.PolyFunctionClass) then
// TODO should we handle ErasedFunction is this same way?
tp1.member(nme.apply).info match
case info1: PolyType =>
return isSubInfo(info1, tp2.refinedInfo)
case _ =>
else
tp1w.widenDealias match
case tp1: RefinedType =>
return isSubInfo(tp1.refinedInfo, tp2.refinedInfo)
case _ =>
tp1w.widenDealias match
case tp1: RefinedType => return compareInfo(tp1.refinedInfo, tp2.refinedInfo)
case _ =>

val skipped2 = skipMatching(tp1w, tp2)
if (skipped2 eq tp2) || !Config.fastPathForRefinedSubtype then
Expand Down Expand Up @@ -1954,6 +1927,45 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
case _ =>
refines.map(RefinedType(tp, _, _): Type).reduce(AndType(_, _))

// A relaxed version of isSubType, which compares method types
// under the standard arrow rule which is contravariant in the parameter types,
// but under the condition that signatures might have to match (see sigsOK)
// This relaxed version is needed to correctly compare dependent function types.
// See pos/i12211.scala.
def isSubInfo(info1: Type, info2: Type, symInfo1: Type, sigsOK: ((Type, Type) => Boolean) | Null = null, fallbackFn: ((Type, Type) => Boolean) | Null = null): Boolean = trace(i"isSubInfo $info1 <:< $info2"):
def fallback = if fallbackFn != null then fallbackFn(info1, info2) else isSubType(info1, info2)

def tryCapturing = info1.widenDealias match
case CapturingType(parent1, _) =>
val refs1 = info1.captureSet
subCaptures(refs1, info2.captureSet, frozenConstraint).isOK && sameBoxed(info1, info2, refs1)
&& isSubInfo(parent1, info2, symInfo1, sigsOK, fallbackFn)
case _ => info2.widenDealias match
case CapturingType(parent2, _) =>
val refs1 = info1.captureSet
val refs2 = info2.captureSet
(refs1.isAlwaysEmpty || subCaptures(refs1, refs2, frozenConstraint).isOK) && sameBoxed(info1, info2, refs1)
&& isSubInfo(info1, parent2, symInfo1, sigsOK, fallbackFn)
case _ => fallback

info2 match
case info2: PolyType if ccEnabled => info1 match // See TODO about `ccEnabled` in `sigsOK`, this should also go under `relaxedSubtyping`.
case info1: PolyType =>
comparingTypeLambdas(info1, info2):
info1.paramNames.hasSameLengthAs(info2.paramNames)
&& isSubInfo(info1.resultType, info2.resultType.subst(info2, info1), symInfo1.resultType, sigsOK, fallbackFn)
// Signature checks are never necessary because polymorphic
// refinements are only allowed for the `apply` method of a
// PolyFunction.
case _ => tryCapturing
case info2: MethodType => info1 match
case info1: MethodType =>
matchingMethodParams(info1, info2, precise = false)
&& isSubInfo(info1.resultType, info2.resultType.subst(info2, info1), symInfo1.resultType, sigsOK, fallbackFn)
&& (if sigsOK != null then sigsOK(symInfo1, info2) else true)
case _ => tryCapturing
case _ => tryCapturing

/** Can comparing this type on the left lead to an either? This is the case if
* the type is and AndType or contains embedded occurrences of AndTypes
*/
Expand Down Expand Up @@ -2008,30 +2020,23 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
// conceivably dispatch without knowing precise parameter signatures. One can signal
// this by inheriting from the `scala.reflect.SignatureCanBeImprecise` marker trait,
// in which case the signature test is elided.
//
// Additionally, for refined function types we do not need to check
// signatures since they're erased in a special way, so we skip the
// signature check but only under `ccEnabled` for now.
// TODO: add a new relaxedSubtyping feature for this (and make a SIP)
// since this is useful in general, but is a language change.
def sigsOK(symInfo: Type, info2: Type) =
tp2.underlyingClassRef(refinementOK = true).member(name).exists
(ccEnabled && defn.isFunctionType(tp2))
|| tp2.underlyingClassRef(refinementOK = true).member(name).exists
|| tp2.derivesFrom(defn.WithoutPreciseParameterTypesClass)
|| symInfo.isInstanceOf[MethodType]
&& symInfo.signature.consistentParams(info2.signature)

def tp1IsSingleton: Boolean = tp1.isInstanceOf[SingletonType]

// A relaxed version of isSubType, which compares method types
// under the standard arrow rule which is contravariant in the parameter types,
// but under the condition that signatures might have to match (see sigsOK)
// This relaxed version is needed to correctly compare dependent function types.
// See pos/i12211.scala.
def isSubInfo(info1: Type, info2: Type, symInfo: Type): Boolean =
info2 match
case info2: MethodType =>
info1 match
case info1: MethodType =>
val symInfo1 = symInfo.stripPoly
matchingMethodParams(info1, info2, precise = false)
&& isSubInfo(info1.resultType, info2.resultType.subst(info2, info1), symInfo1.resultType)
&& sigsOK(symInfo1, info2)
case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) }
case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) }
def fallback = (info1: Type, info2: Type) =>
val tp1IsSingleton: Boolean = tp1.isInstanceOf[SingletonType]
inFrozenGadtIf(tp1IsSingleton):
isSubType(info1, info2)

def qualifies(m: SingleDenotation): Boolean =
val info2 = tp2.refinedInfo
Expand All @@ -2046,7 +2051,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
// OK{ { def x(): T } <: { def x: T} // if x is Java defined
ExprType(info1.resType)
case info1 => info1
isSubInfo(info1, info2, m.symbol.info.orElse(info1))
isSubInfo(info1, info2, m.symbol.info.orElse(info1), sigsOK, fallback)
|| matchAbstractTypeMember(m.info)
|| (tp1.isStable && m.symbol.isStableMember && isSubType(TermRef(tp1, m.symbol), tp2.refinedInfo))

Expand Down