diff --git a/compiler/src/dotty/tools/dotc/config/Config.scala b/compiler/src/dotty/tools/dotc/config/Config.scala index b97529296c19..6bad889890d4 100644 --- a/compiler/src/dotty/tools/dotc/config/Config.scala +++ b/compiler/src/dotty/tools/dotc/config/Config.scala @@ -15,13 +15,6 @@ object Config { */ final val checkConstraintsNonCyclic = false - /** Make sure none of the bounds of a parameter in an OrderingConstraint - * contains this parameter at its toplevel (i.e. as an operand of a - * combination of &'s and |'s.). The check is performed each time a new bound - * is added to the constraint. - */ - final val checkConstraintsSeparated = false - /** Check that each constraint resulting from a subtype test * is satisfiable. */ diff --git a/compiler/src/dotty/tools/dotc/core/Constraint.scala b/compiler/src/dotty/tools/dotc/core/Constraint.scala index b40b806c85bb..d10841530e9c 100644 --- a/compiler/src/dotty/tools/dotc/core/Constraint.scala +++ b/compiler/src/dotty/tools/dotc/core/Constraint.scala @@ -151,7 +151,7 @@ abstract class Constraint extends Showable { def & (other: Constraint, otherHasErrors: Boolean)(implicit ctx: Context): Constraint /** Check that no constrained parameter contains itself as a bound */ - def checkNonCyclic()(implicit ctx: Context): Unit + def checkNonCyclic()(implicit ctx: Context): this.type /** Check that constraint only refers to TypeParamRefs bound by itself */ def checkClosed()(implicit ctx: Context): Unit diff --git a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala index 0f61fd2e25fe..1cb30f2f71eb 100644 --- a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala +++ b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala @@ -8,7 +8,7 @@ import Symbols._ import Decorators._ import Flags._ import config.Config -import config.Printers.{constr, typr} +import config.Printers.typr import dotty.tools.dotc.reporting.trace /** Methods for adding constraints and solving them. @@ -24,8 +24,7 @@ import dotty.tools.dotc.reporting.trace */ trait ConstraintHandling[AbstractContext] { - def constr_println(msg: => String): Unit = constr.println(msg) - def typr_println(msg: => String): Unit = typr.println(msg) + def constr: config.Printers.Printer = config.Printers.constr implicit def ctx(implicit ac: AbstractContext): Context @@ -86,83 +85,86 @@ trait ConstraintHandling[AbstractContext] { def fullBounds(param: TypeParamRef)(implicit actx: AbstractContext): TypeBounds = nonParamBounds(param).derivedTypeBounds(fullLowerBound(param), fullUpperBound(param)) - protected def addOneBound(param: TypeParamRef, bound: Type, isUpper: Boolean)(implicit actx: AbstractContext): Boolean = - !constraint.contains(param) || { - def occursIn(bound: Type): Boolean = { - val b = bound.dealias - (b eq param) || { - b match { - case b: AndType => occursIn(b.tp1) || occursIn(b.tp2) - case b: OrType => occursIn(b.tp1) || occursIn(b.tp2) - case b: TypeVar => occursIn(b.origin) - case b: TermRef => occursIn(b.underlying) - case _ => false - } - } - } - if (Config.checkConstraintsSeparated) - assert(!occursIn(bound), s"$param occurs in $bound") - + protected def addOneBound(param: TypeParamRef, bound: Type, isUpper: Boolean)(using AbstractContext): Boolean = + if !constraint.contains(param) then true + else val oldBounds @ TypeBounds(lo, hi) = constraint.nonParamBounds(param) - val equalBounds = isUpper && (lo eq bound) || !isUpper && (bound eq hi) - if (equalBounds && - !bound.existsPart(bp => bp.isInstanceOf[WildcardType] || (bp eq param))) { + val equalBounds = (if isUpper then lo else hi) eq bound + if equalBounds + && !bound.existsPart(bp => bp.isInstanceOf[WildcardType] || (bp eq param)) + then // The narrowed bounds are equal and do not contain wildcards, // so we can remove `param` from the constraint. // (Handling wildcards requires choosing a bound, but we don't know which // bound to choose here, this is handled in `ConstraintHandling#approximation`) constraint = constraint.replace(param, bound) true - } - else { + else // Narrow one of the bounds of type parameter `param` // If `isUpper` is true, ensure that `param <: `bound`, otherwise ensure // that `param >: bound`. - val narrowedBounds = { + val narrowedBounds = val saved = homogenizeArgs homogenizeArgs = Config.alignArgsInAnd try - if (isUpper) oldBounds.derivedTypeBounds(lo, hi & bound) + if isUpper then oldBounds.derivedTypeBounds(lo, hi & bound) else oldBounds.derivedTypeBounds(lo | bound, hi) finally homogenizeArgs = saved - } val c1 = constraint.updateEntry(param, narrowedBounds) - (c1 eq constraint) || { + (c1 eq constraint) + || { constraint = c1 val TypeBounds(lo, hi) = constraint.entry(param) isSubType(lo, hi) } - } - } - - private def location(implicit ctx: Context) = "" // i"in ${ctx.typerState.stateChainStr}" // use for debugging - - protected def addUpperBound(param: TypeParamRef, bound: Type)(implicit actx: AbstractContext): Boolean = { - def description = i"constraint $param <: $bound to\n$constraint" - if (bound.isRef(defn.NothingClass) && ctx.typerState.isGlobalCommittable) { - def msg = s"!!! instantiated to Nothing: $param, constraint = ${constraint.show}" - if (Config.failOnInstantiationToNothing) assert(false, msg) + end addOneBound + + protected def addBoundTransitively(param: TypeParamRef, rawBound: Type, isUpper: Boolean)(implicit actx: AbstractContext): Boolean = + + /** Adjust the bound `tp` in the following ways: + * + * 1. Toplevel occurrences of TypeRefs that are instantiated in the current + * constraint are also dereferenced. + * 2. Toplevel occurrences of ExprTypes lead to a `NoType` return, which + * causes the addOneBound operation to fail. + * + * An occurrence is toplevel if it is the bound itself, or a term in some + * combination of `&` or `|` types. + */ + def adjust(tp: Type): Type = tp match + case tp: AndOrType => + val p1 = adjust(tp.tp1) + val p2 = adjust(tp.tp2) + if p1.exists && p2.exists then tp.derivedAndOrType(p1, p2) else NoType + case tp: TypeVar if constraint.contains(tp.origin) => + adjust(tp.underlying) + case tp: ExprType => + // ExprTypes are not value types, so type parameters should not + // be instantiated to ExprTypes. A scenario where such an attempted + // instantiation can happen is if we unify (=> T) => () with A => () + // where A is a TypeParamRef. See the comment on EtaExpansion.etaExpand + // why types such as (=> T) => () can be constructed and i7969.scala + // as a test where this happens. + // Note that scalac by contrast allows such instantiations. But letting + // type variables be ExprTypes has its own problems (e.g. you can't write + // the resulting types down) and is largely unknown terrain. + NoType + case _ => + tp + + def description = i"constraint $param ${if isUpper then "<:" else ":>"} $rawBound to\n$constraint" + constr.println(i"adding $description$location") + if isUpper && rawBound.isRef(defn.NothingClass) && ctx.typerState.isGlobalCommittable then + def msg = i"!!! instantiated to Nothing: $param, constraint = $constraint" + if Config.failOnInstantiationToNothing + then assert(false, msg) else ctx.log(msg) - } - constr_println(i"adding $description$location") - val lower = constraint.lower(param) - val res = - addOneBound(param, bound, isUpper = true) && - lower.forall(addOneBound(_, bound, isUpper = true)) - constr_println(i"added $description = $res$location") - res - } - - protected def addLowerBound(param: TypeParamRef, bound: Type)(implicit actx: AbstractContext): Boolean = { - def description = i"constraint $param >: $bound to\n$constraint" - constr_println(i"adding $description") - val upper = constraint.upper(param) - val res = - addOneBound(param, bound, isUpper = false) && - upper.forall(addOneBound(_, bound, isUpper = false)) - constr_println(i"added $description = $res$location") - res - } + def others = if isUpper then constraint.lower(param) else constraint.upper(param) + val bound = adjust(rawBound) + bound.exists + && addOneBound(param, bound, isUpper) && others.forall(addOneBound(_, bound, isUpper)) + .reporting(i"added $description = $result$location", constr) + end addBoundTransitively protected def addLess(p1: TypeParamRef, p2: TypeParamRef)(implicit actx: AbstractContext): Boolean = { def description = i"ordering $p1 <: $p2 to\n$constraint" @@ -173,20 +175,22 @@ trait ConstraintHandling[AbstractContext] { val up2 = p2 :: constraint.exclusiveUpper(p2, p1) val lo1 = constraint.nonParamBounds(p1).lo val hi2 = constraint.nonParamBounds(p2).hi - constr_println(i"adding $description down1 = $down1, up2 = $up2$location") + constr.println(i"adding $description down1 = $down1, up2 = $up2$location") constraint = constraint.addLess(p1, p2) down1.forall(addOneBound(_, hi2, isUpper = true)) && up2.forall(addOneBound(_, lo1, isUpper = false)) } - constr_println(i"added $description = $res$location") + constr.println(i"added $description = $res$location") res } + def location(implicit ctx: Context) = "" // i"in ${ctx.typerState.stateChainStr}" // use for debugging + /** Make p2 = p1, transfer all bounds of p2 to p1 * @pre less(p1)(p2) */ private def unify(p1: TypeParamRef, p2: TypeParamRef)(implicit actx: AbstractContext): Boolean = { - constr_println(s"unifying $p1 $p2") + constr.println(s"unifying $p1 $p2") assert(constraint.isLess(p1, p2)) val down = constraint.exclusiveLower(p2, p1) val up = constraint.exclusiveUpper(p1, p2) @@ -199,7 +203,6 @@ trait ConstraintHandling[AbstractContext] { up.forall(addOneBound(_, lo, isUpper = false)) } - protected def isSubType(tp1: Type, tp2: Type, whenFrozen: Boolean)(implicit actx: AbstractContext): Boolean = if (whenFrozen) isSubTypeWhenFrozen(tp1, tp2) @@ -283,7 +286,7 @@ trait ConstraintHandling[AbstractContext] { case _: TypeBounds => val bound = if (fromBelow) fullLowerBound(param) else fullUpperBound(param) val inst = avoidParam(bound) - typr_println(s"approx ${param.show}, from below = $fromBelow, bound = ${bound.show}, inst = ${inst.show}") + typr.println(s"approx ${param.show}, from below = $fromBelow, bound = ${bound.show}, inst = ${inst.show}") inst case inst => assert(inst.exists, i"param = $param\nconstraint = $constraint") @@ -389,7 +392,7 @@ trait ConstraintHandling[AbstractContext] { val upper = constraint.upper(param) if lower.nonEmpty && !bounds.lo.isRef(defn.NothingClass) || upper.nonEmpty && !bounds.hi.isAny - then constr_println(i"INIT*** $tl") + then constr.println(i"INIT*** $tl") lower.forall(addOneBound(_, bounds.hi, isUpper = true)) && upper.forall(addOneBound(_, bounds.lo, isUpper = false)) case _ => @@ -418,154 +421,65 @@ trait ConstraintHandling[AbstractContext] { * not be AndTypes and lower bounds may not be OrTypes. This is assured by the * way isSubType is organized. */ - protected def addConstraint(param: TypeParamRef, bound: Type, fromBelow: Boolean)(implicit actx: AbstractContext): Boolean = { - def description = i"constr $param ${if (fromBelow) ">:" else "<:"} $bound:\n$constraint" - //checkPropagated(s"adding $description")(true) // DEBUG in case following fails - checkPropagated(s"added $description") { - addConstraintInvocations += 1 - - /** When comparing lambdas we might get constraints such as - * `A <: X0` or `A = List[X0]` where `A` is a constrained parameter - * and `X0` is a lambda parameter. The constraint for `A` is not allowed - * to refer to such a lambda parameter because the lambda parameter is - * not visible where `A` is defined. Consequently, we need to - * approximate the bound so that the lambda parameter does not appear in it. - * If `tp` is an upper bound, we need to approximate with something smaller, - * otherwise something larger. - * Test case in pos/i94-nada.scala. This test crashes with an illegal instance - * error in Test2 when the rest of the SI-2712 fix is applied but `pruneLambdaParams` is - * missing. - */ - def pruneLambdaParams(tp: Type) = - if (comparedTypeLambdas.nonEmpty) { - val approx = new ApproximatingTypeMap { - if (!fromBelow) variance = -1 - def apply(t: Type): Type = t match { - case t @ TypeParamRef(tl: TypeLambda, n) if comparedTypeLambdas contains tl => - val bounds = tl.paramInfos(n) - range(bounds.lo, bounds.hi) - case _ => - mapOver(t) - } + protected def addConstraint(param: TypeParamRef, bound: Type, fromBelow: Boolean)(implicit actx: AbstractContext): Boolean = + + /** When comparing lambdas we might get constraints such as + * `A <: X0` or `A = List[X0]` where `A` is a constrained parameter + * and `X0` is a lambda parameter. The constraint for `A` is not allowed + * to refer to such a lambda parameter because the lambda parameter is + * not visible where `A` is defined. Consequently, we need to + * approximate the bound so that the lambda parameter does not appear in it. + * If `tp` is an upper bound, we need to approximate with something smaller, + * otherwise something larger. + * Test case in pos/i94-nada.scala. This test crashes with an illegal instance + * error in Test2 when the rest of the SI-2712 fix is applied but `pruneLambdaParams` is + * missing. + */ + def avoidLambdaParams(tp: Type) = + if comparedTypeLambdas.nonEmpty then + val approx = new ApproximatingTypeMap { + if (!fromBelow) variance = -1 + def apply(t: Type): Type = t match { + case t @ TypeParamRef(tl: TypeLambda, n) if comparedTypeLambdas contains tl => + val bounds = tl.paramInfos(n) + range(bounds.lo, bounds.hi) + case _ => + mapOver(t) } - approx(tp) - } - else tp - - def addParamBound(bound: TypeParamRef) = - constraint.entry(param) match { - case _: TypeBounds => - if (fromBelow) addLess(bound, param) else addLess(param, bound) - case tp => - if (fromBelow) isSubType(bound, tp) else isSubType(tp, bound) } - - /** Drop all constrained parameters that occur at the toplevel in `bound` and - * handle them by `addLess` calls. - * The preconditions make sure that such parameters occur only - * in one of two ways: - * - * 1. - * - * P <: Ts1 | ... | Tsm (m > 0) - * Tsi = T1 & ... Tn (n >= 0) - * Some of the Ti are constrained parameters - * - * 2. - * - * Ts1 & ... & Tsm <: P (m > 0) - * Tsi = T1 | ... | Tn (n >= 0) - * Some of the Ti are constrained parameters - * - * In each case we cannot leave the parameter in place, - * because that would risk making a parameter later a subtype or supertype - * of a bound where the parameter occurs again at toplevel, which leads to cycles - * in the subtyping test. So we intentionally narrow the constraint by - * recording an isLess relationship instead (even though this is not implied - * by the bound). - * - * Normally, narrowing a constraint is better than widening it, because - * narrowing leads to incompleteness (which we face anyway, see for - * instance `TypeComparer#either`) but widening leads to unsoundness, - * but note the special handling in `ConstrainResult` mode below. - * - * A test case that demonstrates the problem is i864.scala. - * Turn Config.checkConstraintsSeparated on to get an accurate diagnostic - * of the cycle when it is created. - * - * @return The pruned type if all `addLess` calls succeed, `NoType` otherwise. - */ - def prune(bound: Type): Type = bound match { - case bound: AndType => - val p1 = prune(bound.tp1) - val p2 = prune(bound.tp2) - if (p1.exists && p2.exists) bound.derivedAndType(p1, p2) - else NoType - case bound: OrType => - val p1 = prune(bound.tp1) - val p2 = prune(bound.tp2) - if (p1.exists && p2.exists) bound.derivedOrType(p1, p2) - else NoType - case bound: TypeVar if constraint contains bound.origin => - prune(bound.underlying) - case bound: TypeParamRef => - constraint.entry(bound) match { - case NoType => pruneLambdaParams(bound) - case _: TypeBounds => - if (!addParamBound(bound)) NoType - else if (fromBelow) defn.NothingType - else defn.AnyType - case inst => - prune(inst) - } - case bound: ExprType => - // ExprTypes are not value types, so type parameters should not - // be instantiated to ExprTypes. A scenario where such an attempted - // instantiation can happen is if we unify (=> T) => () with A => () - // where A is a TypeParamRef. See the comment on EtaExpansion.etaExpand - // why types such as (=> T) => () can be constructed and i7969.scala - // as a test where this happens. - // Note that scalac by contrast allows such instantiations. But letting - // type variables be ExprTypes has its own problems (e.g. you can't write - // the resulting types down) and is largely unknown terrain. - NoType - case _ => - pruneLambdaParams(bound) + approx(tp) + else tp + + def addParamBound(bound: TypeParamRef) = + constraint.entry(param) match { + case _: TypeBounds => + if (fromBelow) addLess(bound, param) else addLess(param, bound) + case tp => + if (fromBelow) isSubType(bound, tp) else isSubType(tp, bound) } - def kindCompatible(tp1: Type, tp2: Type): Boolean = - val tparams1 = tp1.typeParams - val tparams2 = tp2.typeParams - tparams1.corresponds(tparams2)((p1, p2) => kindCompatible(p1.paramInfo, p2.paramInfo)) - && (tparams1.isEmpty || kindCompatible(tp1.hkResult, tp2.hkResult)) - || tp1.hasAnyKind - || tp2.hasAnyKind + def kindCompatible(tp1: Type, tp2: Type): Boolean = + val tparams1 = tp1.typeParams + val tparams2 = tp2.typeParams + tparams1.corresponds(tparams2)((p1, p2) => kindCompatible(p1.paramInfo, p2.paramInfo)) + && (tparams1.isEmpty || kindCompatible(tp1.hkResult, tp2.hkResult)) + || tp1.hasAnyKind + || tp2.hasAnyKind + + def description = i"constr $param ${if (fromBelow) ">:" else "<:"} $bound:\n$constraint" - try bound match { + //checkPropagated(s"adding $description")(true) // DEBUG in case following fails + checkPropagated(s"added $description") { + addConstraintInvocations += 1 + try bound match case bound: TypeParamRef if constraint contains bound => addParamBound(bound) case _ => - val savedConstraint = constraint - val pbound = prune(bound) - val constraintsNarrowed = constraint ne savedConstraint - - val res = - pbound.exists - && kindCompatible(param, pbound) - && (if fromBelow then addLowerBound(param, pbound) else addUpperBound(param, pbound)) - // If we're in `ConstrainResult` mode, we don't want to commit to a - // set of constraints that would later prevent us from typechecking - // arguments, so if `pruneParams` had to narrow the constraints, we - // simply do not record any new constraint. - // Unlike in `TypeComparer#either`, the same reasoning does not apply - // to GADT mode because this code is never run on GADT constraints. - if ctx.mode.is(Mode.ConstrainResult) && constraintsNarrowed then - constraint = savedConstraint - res - } + val pbound = avoidLambdaParams(bound) + kindCompatible(param, pbound) && addBoundTransitively(param, pbound, !fromBelow) finally addConstraintInvocations -= 1 } - } + end addConstraint /** Check that constraint is fully propagated. See comment in Config.checkConstraintsPropagated */ def checkPropagated(msg: => String)(result: Boolean)(implicit actx: AbstractContext): Boolean = { diff --git a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala index 2faf721a9da8..929750b99ff8 100644 --- a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala @@ -155,8 +155,7 @@ final class ProperGadtConstraint private( else if (isUpper) addLess(symTvar.origin, boundTvar.origin) else addLess(boundTvar.origin, symTvar.origin) case bound => - if (isUpper) addUpperBound(symTvar.origin, bound) - else addLowerBound(symTvar.origin, bound) + addBoundTransitively(symTvar.origin, bound, isUpper) } ).reporting({ val descr = if (isUpper) "upper" else "lower" @@ -271,7 +270,7 @@ final class ProperGadtConstraint private( // ---- Debug ------------------------------------------------------------ - override def constr_println(msg: => String): Unit = gadtsConstr.println(msg) + override def constr = gadtsConstr override def toText(printer: Printer): Texts.Text = constraint.toText(printer) diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index 54527637231a..2b286f05338a 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -26,7 +26,6 @@ object OrderingConstraint { /** A new constraint with given maps */ private def newConstraint(boundsMap: ParamBounds, lowerMap: ParamOrdering, upperMap: ParamOrdering)(implicit ctx: Context) : OrderingConstraint = { val result = new OrderingConstraint(boundsMap, lowerMap, upperMap) - if (Config.checkConstraintsNonCyclic) result.checkNonCyclic() ctx.run.recordConstraintSize(result, result.boundsMap.size) result } @@ -207,20 +206,6 @@ class OrderingConstraint(private val boundsMap: ParamBounds, // ---------- Adding TypeLambdas -------------------------------------------------- - /** The list of parameters P such that, for a fresh type parameter Q: - * - * Q <: tp implies Q <: P and isUpper = true, or - * tp <: Q implies P <: Q and isUpper = false - */ - def dependentParams(tp: Type, isUpper: Boolean): List[TypeParamRef] = tp match { - case param: TypeParamRef if contains(param) => - param :: (if (isUpper) upper(param) else lower(param)) - case tp: AndType => dependentParams(tp.tp1, isUpper) | (dependentParams(tp.tp2, isUpper)) - case tp: OrType => dependentParams(tp.tp1, isUpper).intersect(dependentParams(tp.tp2, isUpper)) - case _ => - Nil - } - /** The bound type `tp` without constrained parameters which are clearly * dependent. A parameter in an upper bound is clearly dependent if it appears * in a hole of a context H given by: @@ -271,12 +256,14 @@ class OrderingConstraint(private val boundsMap: ParamBounds, /** The bound type `tp` without clearly dependent parameters. * A top or bottom type if type consists only of dependent parameters. + * TODO: try to do without normalization? It would mean it is more efficient + * to pull out full bounds from a constraint. * @param isUpper If true, `bound` is an upper bound, else a lower bound. */ private def normalizedType(tp: Type, paramBuf: mutable.ListBuffer[TypeParamRef], isUpper: Boolean)(implicit ctx: Context): Type = stripParams(tp, paramBuf, isUpper) - .orElse(if (isUpper) defn.AnyType else defn.NothingType) + .orElse(if (isUpper) defn.AnyKindType else defn.NothingType) def add(poly: TypeLambda, tvars: List[TypeVar])(implicit ctx: Context): This = { assert(!contains(poly)) @@ -307,12 +294,57 @@ class OrderingConstraint(private val boundsMap: ParamBounds, hiBuf.clear() i += 1 } - if (Config.checkConstraintsNonCyclic) checkNonCyclic() - current + current.checkNonCyclic() } // ---------- Updates ------------------------------------------------------------ + /** If `inst` is a TypeBounds, make sure it does not contain toplevel + * references to `param`. Toplevel means: the term itself or a factor in some + * combination of `&` or `|` types. + * Any such references are replace by `Nothing` in the lower bound and `Any` + * in the upper bound. + * References can be direct or indirect through instantiations of other + * parameters in the constraint. + */ + private def ensureNonCyclic(param: TypeParamRef, inst: Type)(using Context): Type = + + def recur(tp: Type, fromBelow: Boolean): Type = tp match + case tp: AndOrType => + val r1 = recur(tp.tp1, fromBelow) + val r2 = recur(tp.tp2, fromBelow) + if (r1 eq tp.tp1) && (r2 eq tp.tp2) then tp + else if tp.isAnd then r1 & r2 + else r1 | r2 + case tp: TypeParamRef => + if tp eq param then + if fromBelow then defn.NothingType else defn.AnyType + else entry(tp) match + case NoType => tp + case TypeBounds(lo, hi) => if lo eq hi then recur(lo, fromBelow) else tp + case inst => recur(inst, fromBelow) + case tp: TypeVar => + val underlying1 = recur(tp.underlying, fromBelow) + if underlying1 ne tp.underlying then underlying1 else tp + case tp: AnnotatedType => + val parent1 = recur(tp.parent, fromBelow) + if parent1 ne tp.parent then tp.derivedAnnotatedType(parent1, tp.annot) else tp + case _ => + val tp1 = tp.dealiasKeepAnnots + if tp1 ne tp then + val tp2 = recur(tp1, fromBelow) + if tp2 ne tp1 then tp2 else tp + else tp + + inst match + case bounds: TypeBounds => + bounds.derivedTypeBounds( + recur(bounds.lo, fromBelow = true), + recur(bounds.hi, fromBelow = false)) + case _ => + inst + end ensureNonCyclic + /** Add the fact `param1 <: param2` to the constraint `current` and propagate * `<:<` relationships between parameters ("edges") but not bounds. */ @@ -328,115 +360,72 @@ class OrderingConstraint(private val boundsMap: ParamBounds, current2 } - def addLess(param1: TypeParamRef, param2: TypeParamRef)(implicit ctx: Context): This = - order(this, param1, param2) + /** The list of parameters P such that, for a fresh type parameter Q: + * + * Q <: tp implies Q <: P and isUpper = true, or + * tp <: Q implies P <: Q and isUpper = false + */ + private def dependentParams(tp: Type, isUpper: Boolean): List[TypeParamRef] = tp match + case param: TypeParamRef if contains(param) => + param :: (if (isUpper) upper(param) else lower(param)) + case tp: AndType if isUpper => + dependentParams(tp.tp1, isUpper) | (dependentParams(tp.tp2, isUpper)) + case tp: OrType if !isUpper => + dependentParams(tp.tp1, isUpper).intersect(dependentParams(tp.tp2, isUpper)) + case _ => + Nil - def updateEntry(current: This, param: TypeParamRef, tp: Type)(implicit ctx: Context): This = { + private def updateEntry(current: This, param: TypeParamRef, tp: Type)(implicit ctx: Context): This = { var current1 = boundsLens.update(this, current, param, tp) tp match { case TypeBounds(lo, hi) => - for (p <- dependentParams(lo, isUpper = false)) + for p <- dependentParams(lo, isUpper = false) do current1 = order(current1, p, param) - for (p <- dependentParams(hi, isUpper = true)) + for p <- dependentParams(hi, isUpper = true) do current1 = order(current1, param, p) case _ => } current1 } + /** The public version of `updateEntry`. Guarantees that there are no cycles */ def updateEntry(param: TypeParamRef, tp: Type)(implicit ctx: Context): This = - updateEntry(this, param, tp) + updateEntry(this, param, ensureNonCyclic(param, tp)).checkNonCyclic() - def unify(p1: TypeParamRef, p2: TypeParamRef)(implicit ctx: Context): This = { + def addLess(param1: TypeParamRef, param2: TypeParamRef)(implicit ctx: Context): This = + order(this, param1, param2).checkNonCyclic() + + def unify(p1: TypeParamRef, p2: TypeParamRef)(implicit ctx: Context): This = val p1Bounds = (nonParamBounds(p1) & nonParamBounds(p2)).substParam(p2, p1) updateEntry(p1, p1Bounds).replace(p2, p1) - } -// ---------- Removals ------------------------------------------------------------ +// ---------- Replacements and Removals ------------------------------------- /** A new constraint which is derived from this constraint by removing * the type parameter `param` from the domain and replacing all top-level occurrences - * of the parameter elsewhere in the constraint by type `tp`, or a conservative - * approximation of it if that is needed to avoid cycles. - * Occurrences nested inside a refinement or prefix are not affected. - * - * The reason we need to substitute top-level occurrences of the parameter - * is to deal with situations like the following. Say we have in the constraint - * - * P <: Q & String - * Q - * - * and we replace Q with P. Then substitution gives - * - * P <: P & String - * - * this would be a cyclic constraint is therefore changed by `normalize` and - * `recombine` below to - * - * P <: String - * - * approximating the RHS occurrence of P with Any. Without the substitution we - * would not find out where we need to approximate. Occurrences of parameters - * that are not top-level are not affected. + * of the parameter elsewhere in the constraint by type `tp`. */ - def replace(param: TypeParamRef, tp: Type)(implicit ctx: Context): OrderingConstraint = { + def replace(param: TypeParamRef, tp: Type)(implicit ctx: Context): OrderingConstraint = val replacement = tp.dealiasKeepAnnots.stripTypeVar - if (param == replacement) this - else { + if param == replacement then this.checkNonCyclic() + else assert(replacement.isValueTypeOrLambda) - val poly = param.binder - val idx = param.paramNum - - def removeParam(ps: List[TypeParamRef]) = - ps.filterNot(p => p.binder.eq(poly) && p.paramNum == idx) - - def replaceParam(tp: Type, atPoly: TypeLambda, atIdx: Int): Type = tp match { - case bounds @ TypeBounds(lo, hi) => - - def recombineAnd(and: AndType, op: (Type, Boolean) => Type, isUpper: Boolean): Type = { - val tp1 = op(and.tp1, isUpper) - val tp2 = op(and.tp2, isUpper) - if (tp1.eq(and.tp1) && tp2.eq(and.tp2)) and - else tp1 & tp2 - } - - def recombineOr(or: OrType, op: (Type, Boolean) => Type, isUpper: Boolean): Type = { - val tp1 = op(or.tp1, isUpper) - val tp2 = op(or.tp2, isUpper) - if (tp1.eq(or.tp1) && tp2.eq(or.tp2)) or - else tp1 | tp2 - } - - def normalize(tp: Type, isUpper: Boolean): Type = tp match { - case p: TypeParamRef if p.binder == atPoly && p.paramNum == atIdx => - if (isUpper) defn.AnyType else defn.NothingType - case tp: AndType if isUpper => recombineAnd(tp, normalize, isUpper) - case tp: OrType if !isUpper => recombineOr (tp, normalize, isUpper) - case _ => tp - } + var current = + if isRemovable(param.binder) then remove(param.binder) + else updateEntry(this, param, replacement) - def replaceIn(tp: Type, isUpper: Boolean): Type = tp match { - case `param` => normalize(replacement, isUpper) - case tp: AndType if isUpper => recombineAnd(tp, replaceIn, isUpper) - case tp: OrType if !isUpper => recombineOr (tp, replaceIn, isUpper) - case _ => tp.substParam(param, replacement) - } + def removeParam(ps: List[TypeParamRef]) = ps.filter(param ne _) - bounds.derivedTypeBounds(replaceIn(lo, isUpper = false), replaceIn(hi, isUpper = true)) - case _ => - tp.substParam(param, replacement) - } + def replaceParam(tp: Type, atPoly: TypeLambda, atIdx: Int): Type = + current.ensureNonCyclic(atPoly.paramRefs(atIdx), tp.substParam(param, replacement)) - var current = - if (isRemovable(poly)) remove(poly) else updateEntry(param, replacement) - current.foreachParam {(p, i) => + current.foreachParam { (p, i) => current = boundsLens.map(this, current, p, i, replaceParam(_, p, i)) current = lowerLens.map(this, current, p, i, removeParam) current = upperLens.map(this, current, p, i, removeParam) } - current - } - } + current.checkNonCyclic() + end replace def remove(pt: TypeLambda)(implicit ctx: Context): This = { def removeFromOrdering(po: ParamOrdering) = { @@ -447,6 +436,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds, po.remove(pt).mapValuesNow(removeFromBoundss) } newConstraint(boundsMap.remove(pt), removeFromOrdering(lowerMap), removeFromOrdering(upperMap)) + .checkNonCyclic() } def isRemovable(pt: TypeLambda): Boolean = { @@ -460,36 +450,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds, allRemovable(paramCount(entries) - 1) } -// ---------- Exploration -------------------------------------------------------- - - def domainLambdas: List[TypeLambda] = boundsMap.keys - - def domainParams: List[TypeParamRef] = - for { - (poly, entries) <- boundsMap.toList - n <- 0 until paramCount(entries) - if entries(n).exists - } - yield poly.paramRefs(n) - - def forallParams(p: TypeParamRef => Boolean): Boolean = - boundsMap.forallBinding { (poly, entries) => - !0.until(paramCount(entries)).exists(i => isBounds(entries(i)) && !p(poly.paramRefs(i))) - } - - def foreachParam(p: (TypeLambda, Int) => Unit): Unit = - boundsMap.foreachBinding { (poly, entries) => - 0.until(poly.paramNames.length).foreach(p(poly, _)) - } - - def foreachTypeVar(op: TypeVar => Unit): Unit = - boundsMap.foreachBinding { (poly, entries) => - for (i <- 0 until paramCount(entries)) - typeVar(entries, i) match { - case tv: TypeVar if !tv.inst.exists => op(tv) - case _ => - } - } +// ----------- Joins ----------------------------------------------------- def & (other: Constraint, otherHasErrors: Boolean)(implicit ctx: Context): OrderingConstraint = { @@ -565,8 +526,8 @@ class OrderingConstraint(private val boundsMap: ParamBounds, val TypeParamRef(binder, n) = tvar.origin if (binder eq tl) tvar.setOrigin(tl1.paramRefs(n)) } - constr.println(i"renamd $this to $current") - current + constr.println(i"renamed $this to $current") + current.checkNonCyclic() } def ensureFresh(tl: TypeLambda)(implicit ctx: Context): TypeLambda = @@ -582,18 +543,36 @@ class OrderingConstraint(private val boundsMap: ParamBounds, } else tl - override def checkClosed()(implicit ctx: Context): Unit = { - def isFreeTypeParamRef(tp: Type) = tp match { - case TypeParamRef(binder: TypeLambda, _) => !contains(binder) - case _ => false +// ---------- Exploration -------------------------------------------------------- + + def domainLambdas: List[TypeLambda] = boundsMap.keys + + def domainParams: List[TypeParamRef] = + for { + (poly, entries) <- boundsMap.toList + n <- 0 until paramCount(entries) + if entries(n).exists + } + yield poly.paramRefs(n) + + def forallParams(p: TypeParamRef => Boolean): Boolean = + boundsMap.forallBinding { (poly, entries) => + !0.until(paramCount(entries)).exists(i => isBounds(entries(i)) && !p(poly.paramRefs(i))) + } + + def foreachParam(p: (TypeLambda, Int) => Unit): Unit = + boundsMap.foreachBinding { (poly, entries) => + 0.until(poly.paramNames.length).foreach(p(poly, _)) + } + + def foreachTypeVar(op: TypeVar => Unit): Unit = + boundsMap.foreachBinding { (poly, entries) => + for (i <- 0 until paramCount(entries)) + typeVar(entries, i) match { + case tv: TypeVar if !tv.inst.exists => op(tv) + case _ => + } } - def checkClosedType(tp: Type, where: String) = - if (tp != null) - assert(!tp.existsPart(isFreeTypeParamRef), i"unclosed constraint: $this refers to $tp in $where") - boundsMap.foreachBinding((_, tps) => tps.foreach(checkClosedType(_, "bounds"))) - lowerMap.foreachBinding((_, paramss) => paramss.foreach(_.foreach(checkClosedType(_, "lower")))) - upperMap.foreachBinding((_, paramss) => paramss.foreach(_.foreach(checkClosedType(_, "upper")))) - } private var myUninstVars: mutable.ArrayBuffer[TypeVar] = _ @@ -612,13 +591,51 @@ class OrderingConstraint(private val boundsMap: ParamBounds, myUninstVars } -// ---------- Cyclic checking ------------------------------------------- +// ---------- Checking ----------------------------------------------- - def checkNonCyclic()(implicit ctx: Context): Unit = - domainParams.foreach(checkNonCyclic) + def checkNonCyclic()(implicit ctx: Context): this.type = + if Config.checkConstraintsNonCyclic then domainParams.foreach(checkNonCyclic) + this private def checkNonCyclic(param: TypeParamRef)(implicit ctx: Context): Unit = - assert(!isLess(param, param), i"cyclic constraint involving $param in $this") + assert(!isLess(param, param), i"cyclic ordering involving $param in $this, upper = ${upper(param)}") + + def recur(tp: Type)(using Context): Unit = tp match + case tp: AndOrType => + recur(tp.tp1) + recur(tp.tp2) + case tp: TypeParamRef => + assert(tp ne param, i"cyclic bound for $param: ${entry(param)} in $this") + entry(tp) match + case NoType => + case TypeBounds(lo, hi) => if lo eq hi then recur(lo) + case inst => recur(inst) + case tp: TypeVar => + recur(tp.underlying) + case TypeBounds(lo, hi) => + recur(lo) + recur(hi) + case _ => + val tp1 = tp.dealias + if tp1 ne tp then recur(tp1) + + recur(entry(param)) + end checkNonCyclic + + override def checkClosed()(using Context): Unit = + + def isFreeTypeParamRef(tp: Type) = tp match + case TypeParamRef(binder: TypeLambda, _) => !contains(binder) + case _ => false + + def checkClosedType(tp: Type, where: String) = + if tp != null then + assert(!tp.existsPart(isFreeTypeParamRef), i"unclosed constraint: $this refers to $tp in $where") + + boundsMap.foreachBinding((_, tps) => tps.foreach(checkClosedType(_, "bounds"))) + lowerMap.foreachBinding((_, paramss) => paramss.foreach(_.foreach(checkClosedType(_, "lower")))) + upperMap.foreachBinding((_, paramss) => paramss.foreach(_.foreach(checkClosedType(_, "upper")))) + end checkClosed // ---------- Invalidation ------------------------------------------- @@ -631,6 +648,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds, // ---------- toText ----------------------------------------------------- override def toText(printer: Printer): Text = { + //Printer.debugPrintUnique = true def entryText(tp: Type) = tp match { case tp: TypeBounds => tp.toText(printer) @@ -663,6 +681,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds, Text(ups.map(_.toText(printer)), ", ") Text(deps, "\n") } + //Printer.debugPrintUnique = false Text.lines(List(header, uninstVarsText, constrainedText, boundsText, orderingText, ")")) } diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index a565e593923d..100891a50df0 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -26,6 +26,8 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def maxToTextRecursions: Int = 100 + protected def showUniqueIds = ctx.settings.uniqid.value || Printer.debugPrintUnique + protected final def limiter: MessageLimiter = ctx.property(MessageLimiter).get protected def controlled(op: => Text): Text = limiter.controlled(op) @@ -248,14 +250,14 @@ class PlainPrinter(_ctx: Context) extends Printer { /** If -uniqid is set, the hashcode of the lambda type, after a # */ protected def lambdaHash(pt: LambdaType): Text = - if (ctx.settings.uniqid.value) + if (showUniqueIds) try "#" + pt.hashCode catch { case ex: NullPointerException => "" } else "" /** If -uniqid is set, the unique id of symbol, after a # */ protected def idString(sym: Symbol): String = - if (ctx.settings.uniqid.value || Printer.debugPrintUnique) "#" + sym.id else "" + if (showUniqueIds || Printer.debugPrintUnique) "#" + sym.id else "" def nameString(sym: Symbol): String = simpleNameString(sym) + idString(sym) // + "<" + (if (sym.exists) sym.owner else "") + ">" diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 98d231de643c..50be7e0e4558 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -213,7 +213,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case tp: RefinedType if defn.isFunctionType(tp) && !printDebug => toTextDependentFunction(tp.refinedInfo.asInstanceOf[MethodType]) case tp: TypeRef => - if (tp.symbol.isAnonymousClass && !ctx.settings.uniqid.value) + if (tp.symbol.isAnonymousClass && !showUniqueIds) toText(tp.info) else if (tp.symbol.is(Param)) tp.prefix match { @@ -729,7 +729,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { protected def optAscription[T >: Untyped](tpt: Tree[T]): Text = optText(tpt)(": " ~ _) private def idText(tree: untpd.Tree): Text = - if ((ctx.settings.uniqid.value || Printer.debugPrintUnique) && tree.hasType && tree.symbol.exists) s"#${tree.symbol.id}" else "" + if showUniqueIds && tree.hasType && tree.symbol.exists then s"#${tree.symbol.id}" else "" private def useSymbol(tree: untpd.Tree) = tree.hasType && tree.symbol.exists && ctx.settings.YprintSyms.value diff --git a/tests/neg/i6565.scala b/tests/pos/i6565.scala similarity index 84% rename from tests/neg/i6565.scala rename to tests/pos/i6565.scala index d5fab12842d3..5e199001ff4e 100644 --- a/tests/neg/i6565.scala +++ b/tests/pos/i6565.scala @@ -12,6 +12,6 @@ lazy val ok: Lifted[String] = { // ok despite map returning a union point("a").map(_ => if true then "foo" else error) // ok } -lazy val bad: Lifted[String] = { // found Lifted[Object] - point("a").flatMap(_ => point("b").map(_ => if true then "foo" else error)) // error +lazy val nowAlsoOK: Lifted[String] = { + point("a").flatMap(_ => point("b").map(_ => if true then "foo" else error)) }