diff --git a/compiler/src/dotty/tools/dotc/config/Config.scala b/compiler/src/dotty/tools/dotc/config/Config.scala index 0ea5089ed13c..67545233136d 100644 --- a/compiler/src/dotty/tools/dotc/config/Config.scala +++ b/compiler/src/dotty/tools/dotc/config/Config.scala @@ -227,9 +227,12 @@ object Config { */ inline val reuseSymDenotations = true - /** If true, check levels of type variables and create fresh ones as needed. - * This is necessary for soundness (see 3ab18a9), but also causes several - * regressions that should be fixed before turning this on. + /** If `checkLevelsOnConstraints` is true, check levels of type variables + * and create fresh ones as needed when bounds are first entered intot he constraint. + * If `checkLevelsOnInstantiation` is true, allow level-incorrect constraints but + * fix levels on type variable instantiation. */ - inline val checkLevels = false + inline val checkLevelsOnConstraints = false + inline val checkLevelsOnInstantiation = true + } diff --git a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala index 7b96062dda95..747500465c0a 100644 --- a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala +++ b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala @@ -12,6 +12,7 @@ import config.Printers.typr import typer.ProtoTypes.{newTypeVar, representedParamRef} import UnificationDirection.* import NameKinds.AvoidNameKind +import util.SimpleIdentitySet /** Methods for adding constraints and solving them. * @@ -74,7 +75,43 @@ trait ConstraintHandling { protected def necessaryConstraintsOnly(using Context): Boolean = ctx.mode.is(Mode.GadtConstraintInference) || myNecessaryConstraintsOnly - protected var trustBounds = true + /** If `trustBounds = false` we perform comparisons in a pessimistic way as follows: + * Given an abstract type `A >: L <: H`, a subtype comparison of any type + * with `A` will compare against both `L` and `H`. E.g. + * + * T <:< A if T <:< L and T <:< H + * A <:< T if L <:< T and H <:< T + * + * This restricted form makes sure we don't "forget" types when forming + * unions and intersections with abstract types that have bad bounds. E.g. + * the following example from neg/i8900.scala that @smarter came up with: + * We have a type variable X with constraints + * + * X >: 1, X >: x.M + * + * where `x` is a locally nested variable and `x.M` has bad bounds + * + * x.M >: Int | String <: Int & String + * + * If we trust bounds, then the lower bound of `X` is `x.M` since `x.M >: 1`. + * Then even if we correct levels on instantiation to eliminate the local `x`, + * it is alreay too late, we'd get `Int & String` as instance, which does not + * satisfy the original constraint `X >: 1`. + * + * But if `trustBounds` is false, we do not conclude the `x.M >: 1` since + * we compare both bounds and the upper bound `Int & String` is not a supertype + * of `1`. So the lower bound is `1 | x.M` and when we level-avoid that we + * get `1 | Int & String`, which simplifies to `Int`. + */ + private var myTrustBounds = true + + inline def withUntrustedBounds(op: => Type): Type = + val saved = myTrustBounds + myTrustBounds = false + try op finally myTrustBounds = saved + + def trustBounds: Boolean = + !Config.checkLevelsOnInstantiation || myTrustBounds def checkReset() = assert(addConstraintInvocations == 0) @@ -97,7 +134,7 @@ trait ConstraintHandling { level <= maxLevel || ctx.isAfterTyper || !ctx.typerState.isCommittable // Leaks in these cases shouldn't break soundness || level == Int.MaxValue // See `nestingLevel` above. - || !Config.checkLevels + || !Config.checkLevelsOnConstraints /** If `param` is nested deeper than `maxLevel`, try to instantiate it to a * fresh type variable of level `maxLevel` and return the new variable. @@ -262,16 +299,14 @@ trait ConstraintHandling { // If `isUpper` is true, ensure that `param <: `bound`, otherwise ensure // that `param >: bound`. val narrowedBounds = - val savedHomogenizeArgs = homogenizeArgs - val savedTrustBounds = trustBounds + val saved = homogenizeArgs homogenizeArgs = Config.alignArgsInAnd try - trustBounds = false - if isUpper then oldBounds.derivedTypeBounds(lo, hi & bound) - else oldBounds.derivedTypeBounds(lo | bound, hi) + withUntrustedBounds( + if isUpper then oldBounds.derivedTypeBounds(lo, hi & bound) + else oldBounds.derivedTypeBounds(lo | bound, hi)) finally - homogenizeArgs = savedHomogenizeArgs - trustBounds = savedTrustBounds + homogenizeArgs = saved //println(i"narrow bounds for $param from $oldBounds to $narrowedBounds") val c1 = constraint.updateEntry(param, narrowedBounds) (c1 eq constraint) @@ -431,24 +466,98 @@ trait ConstraintHandling { } } + /** Fix instance type `tp` by avoidance so that it does not contain references + * to types at level > `maxLevel`. + * @param tp the type to be fixed + * @param fromBelow whether type was obtained from lower bound + * @param maxLevel the maximum level of references allowed + * @param param the parameter that was instantiated + */ + private def fixLevels(tp: Type, fromBelow: Boolean, maxLevel: Int, param: TypeParamRef)(using Context) = + + def needsFix(tp: NamedType) = + (tp.prefix eq NoPrefix) && tp.symbol.nestingLevel > maxLevel + + /** An accumulator that determines whether levels need to be fixed + * and computes on the side sets of nested type variables that need + * to be instantiated. + */ + class NeedsLeveling extends TypeAccumulator[Boolean]: + if !fromBelow then variance = -1 + + /** Nested type variables that should be instiated to theor lower (respoctively + * upper) bounds. + */ + var nestedVarsLo, nestedVarsHi: SimpleIdentitySet[TypeVar] = SimpleIdentitySet.empty + + def apply(need: Boolean, tp: Type) = + need || tp.match + case tp: NamedType => + needsFix(tp) + || !stopBecauseStaticOrLocal(tp) && apply(need, tp.prefix) + case tp: TypeVar => + val inst = tp.instanceOpt + if inst.exists then apply(need, inst) + else if tp.nestingLevel > maxLevel then + if variance > 0 then nestedVarsLo += tp + else if variance < 0 then nestedVarsHi += tp + else + // For invariant type variables, we use a different strategy. + // Rather than instantiating to a bound and then propagating in an + // AvoidMap, change the nesting level of an invariant type + // variable to `maxLevel`. This means that the type variable will be + // instantiated later to a less nested type. If there are other references + // to the same type variable that do not come from the type undergoing + // `fixLevels`, this could lead to coarser types. But it has the potential + // to give a better approximation for the current type, since it avoids forming + // a Range in invariant position, which can lead to very coarse types further out. + constr.println(i"widening nesting level of type variable $tp from ${tp.nestingLevel} to $maxLevel") + ctx.typerState.setNestingLevel(tp, maxLevel) + true + else false + case _ => + foldOver(need, tp) + end NeedsLeveling + + class LevelAvoidMap extends TypeOps.AvoidMap: + if !fromBelow then variance = -1 + def toAvoid(tp: NamedType) = needsFix(tp) + + if !Config.checkLevelsOnInstantiation || ctx.isAfterTyper then tp + else + val needsLeveling = NeedsLeveling() + if needsLeveling(false, tp) then + typr.println(i"instance $tp for $param needs leveling to $maxLevel, nested = ${needsLeveling.nestedVarsLo.toList} | ${needsLeveling.nestedVarsHi.toList}") + needsLeveling.nestedVarsLo.foreach(_.instantiate(fromBelow = true)) + needsLeveling.nestedVarsHi.foreach(_.instantiate(fromBelow = false)) + LevelAvoidMap()(tp) + else tp + end fixLevels + /** Solve constraint set for given type parameter `param`. * If `fromBelow` is true the parameter is approximated by its lower bound, * otherwise it is approximated by its upper bound, unless the upper bound * contains a reference to the parameter itself (such occurrences can arise * for F-bounded types, `addOneBound` ensures that they never occur in the * lower bound). + * The solved type is not allowed to contain references to types nested deeper + * than `maxLevel`. * Wildcard types in bounds are approximated by their upper or lower bounds. * The constraint is left unchanged. * @return the instantiating type * @pre `param` is in the constraint's domain. */ - final def approximation(param: TypeParamRef, fromBelow: Boolean)(using Context): Type = + final def approximation(param: TypeParamRef, fromBelow: Boolean, maxLevel: Int)(using Context): Type = constraint.entry(param) match case entry: TypeBounds => val useLowerBound = fromBelow || param.occursIn(entry.hi) - val inst = if useLowerBound then fullLowerBound(param) else fullUpperBound(param) - typr.println(s"approx ${param.show}, from below = $fromBelow, inst = ${inst.show}") - inst + val rawInst = withUntrustedBounds( + if useLowerBound then fullLowerBound(param) else fullUpperBound(param)) + val levelInst = fixLevels(rawInst, fromBelow, maxLevel, param) + if levelInst ne rawInst then + typr.println(i"level avoid for $maxLevel: $rawInst --> $levelInst") + typr.println(i"approx $param, from below = $fromBelow, inst = $levelInst") + levelInst case inst => assert(inst.exists, i"param = $param\nconstraint = $constraint") inst @@ -560,9 +669,11 @@ trait ConstraintHandling { * lower bounds; otherwise it is the glb of its upper bounds. However, * a lower bound instantiation can be a singleton type only if the upper bound * is also a singleton type. + * The instance type is not allowed to contain references to types nested deeper + * than `maxLevel`. */ - def instanceType(param: TypeParamRef, fromBelow: Boolean)(using Context): Type = { - val approx = approximation(param, fromBelow).simplified + def instanceType(param: TypeParamRef, fromBelow: Boolean, maxLevel: Int)(using Context): Type = { + val approx = approximation(param, fromBelow, maxLevel).simplified if fromBelow then val widened = widenInferred(approx, param) // Widening can add extra constraints, in particular the widened type might @@ -572,7 +683,7 @@ trait ConstraintHandling { // (we do not check for non-toplevel occurences: those should never occur // since `addOneBound` disallows recursive lower bounds). if constraint.occursAtToplevel(param, widened) then - instanceType(param, fromBelow) + instanceType(param, fromBelow, maxLevel) else widened else diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 3dfafe6837d0..919598c41d6e 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -165,7 +165,7 @@ object Contexts { protected def scope_=(scope: Scope): Unit = _scope = scope final def scope: Scope = _scope - /** The current type comparer */ + /** The current typerstate */ private var _typerState: TyperState = _ protected def typerState_=(typerState: TyperState): Unit = _typerState = typerState final def typerState: TyperState = _typerState diff --git a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala index ae87dd662d56..a8b5eee4902d 100644 --- a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala @@ -47,7 +47,7 @@ sealed abstract class GadtConstraint extends Showable { def isNarrowing: Boolean /** See [[ConstraintHandling.approximation]] */ - def approximation(sym: Symbol, fromBelow: Boolean)(using Context): Type + def approximation(sym: Symbol, fromBelow: Boolean, maxLevel: Int = Int.MaxValue)(using Context): Type def symbols: List[Symbol] @@ -205,9 +205,9 @@ final class ProperGadtConstraint private( def isNarrowing: Boolean = wasConstrained - override def approximation(sym: Symbol, fromBelow: Boolean)(using Context): Type = { + override def approximation(sym: Symbol, fromBelow: Boolean, maxLevel: Int)(using Context): Type = { val res = - approximation(tvarOrError(sym).origin, fromBelow = fromBelow) match + approximation(tvarOrError(sym).origin, fromBelow, maxLevel) match case tpr: TypeParamRef => // Here we do externalization when the returned type is a TypeParamRef, // b/c ConstraintHandling.approximation may return internal types when @@ -317,7 +317,7 @@ final class ProperGadtConstraint private( override def addToConstraint(params: List[Symbol])(using Context): Boolean = unsupported("EmptyGadtConstraint.addToConstraint") override def addBound(sym: Symbol, bound: Type, isUpper: Boolean)(using Context): Boolean = unsupported("EmptyGadtConstraint.addBound") - override def approximation(sym: Symbol, fromBelow: Boolean)(using Context): Type = unsupported("EmptyGadtConstraint.approximation") + override def approximation(sym: Symbol, fromBelow: Boolean, maxLevel: Int)(using Context): Type = unsupported("EmptyGadtConstraint.approximation") override def symbols: List[Symbol] = Nil diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index c07fa91ac1a0..f53426022e44 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2869,11 +2869,11 @@ object TypeComparer { def subtypeCheckInProgress(using Context): Boolean = comparing(_.subtypeCheckInProgress) - def instanceType(param: TypeParamRef, fromBelow: Boolean)(using Context): Type = - comparing(_.instanceType(param, fromBelow)) + def instanceType(param: TypeParamRef, fromBelow: Boolean, maxLevel: Int = Int.MaxValue)(using Context): Type = + comparing(_.instanceType(param, fromBelow, maxLevel)) - def approximation(param: TypeParamRef, fromBelow: Boolean)(using Context): Type = - comparing(_.approximation(param, fromBelow)) + def approximation(param: TypeParamRef, fromBelow: Boolean, maxLevel: Int = Int.MaxValue)(using Context): Type = + comparing(_.approximation(param, fromBelow, maxLevel)) def bounds(param: TypeParamRef)(using Context): TypeBounds = comparing(_.bounds(param)) @@ -2953,7 +2953,7 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) { case param @ TypeParamRef(b, n) if b eq caseLambda => insts(n) = if canApprox then - approximation(param, fromBelow = variance >= 0).simplified + approximation(param, fromBelow = variance >= 0, Int.MaxValue).simplified else constraint.entry(param) match case entry: TypeBounds => val lo = fullLowerBound(param) diff --git a/compiler/src/dotty/tools/dotc/core/TyperState.scala b/compiler/src/dotty/tools/dotc/core/TyperState.scala index 34f9a0139142..81b60c608e28 100644 --- a/compiler/src/dotty/tools/dotc/core/TyperState.scala +++ b/compiler/src/dotty/tools/dotc/core/TyperState.scala @@ -10,7 +10,7 @@ import config.Config import config.Printers.constr import collection.mutable import java.lang.ref.WeakReference -import util.Stats +import util.{Stats, SimpleIdentityMap} import Decorators._ import scala.annotation.internal.sharable @@ -23,24 +23,26 @@ object TyperState { .setReporter(new ConsoleReporter()) .setCommittable(true) - opaque type Snapshot = (Constraint, TypeVars, TypeVars) + type LevelMap = SimpleIdentityMap[TypeVar, Integer] + + opaque type Snapshot = (Constraint, TypeVars, LevelMap) extension (ts: TyperState) def snapshot()(using Context): Snapshot = - var previouslyInstantiated: TypeVars = SimpleIdentitySet.empty - for tv <- ts.ownedVars do if tv.inst.exists then previouslyInstantiated += tv - (ts.constraint, ts.ownedVars, previouslyInstantiated) + (ts.constraint, ts.ownedVars, ts.upLevels) def resetTo(state: Snapshot)(using Context): Unit = - val (c, tvs, previouslyInstantiated) = state - for tv <- tvs do - if tv.inst.exists && !previouslyInstantiated.contains(tv) then + val (constraint, ownedVars, upLevels) = state + for tv <- ownedVars do + if !ts.ownedVars.contains(tv) then // tv has been instantiated tv.resetInst(ts) - ts.ownedVars = tvs - ts.constraint = c + ts.constraint = constraint + ts.ownedVars = ownedVars + ts.upLevels = upLevels } class TyperState() { + import TyperState.LevelMap private var myId: Int = _ def id: Int = myId @@ -89,6 +91,8 @@ class TyperState() { def ownedVars: TypeVars = myOwnedVars def ownedVars_=(vs: TypeVars): Unit = myOwnedVars = vs + private var upLevels: LevelMap = _ + /** Initializes all fields except reporter, isCommittable, which need to be * set separately. */ @@ -99,6 +103,7 @@ class TyperState() { this.myConstraint = constraint this.previousConstraint = constraint this.myOwnedVars = SimpleIdentitySet.empty + this.upLevels = SimpleIdentityMap.empty this.isCommitted = false this @@ -106,13 +111,27 @@ class TyperState() { def fresh(reporter: Reporter = StoreReporter(this.reporter, fromTyperState = true), committable: Boolean = this.isCommittable): TyperState = util.Stats.record("TyperState.fresh") - TyperState().init(this, this.constraint) + val ts = TyperState().init(this, this.constraint) .setReporter(reporter) .setCommittable(committable) + ts.upLevels = upLevels + ts /** The uninstantiated variables */ def uninstVars: collection.Seq[TypeVar] = constraint.uninstVars + /** The nestingLevel of `tv` in this typer state */ + def nestingLevel(tv: TypeVar): Int = + val own = upLevels(tv) + if own == null then tv.initNestingLevel else own.intValue() + + /** Set the nestingLevel of `tv` in this typer state + * @pre this level must be smaller than `tv.initNestingLevel` + */ + def setNestingLevel(tv: TypeVar, level: Int) = + assert(level < tv.initNestingLevel) + upLevels = upLevels.updated(tv, level) + /** The closest ancestor of this typer state (including possibly this typer state itself) * which is not yet committed, or which does not have a parent. */ @@ -164,6 +183,12 @@ class TyperState() { if !ownedVars.isEmpty then ownedVars.foreach(targetState.includeVar) else targetState.mergeConstraintWith(this) + + upLevels.foreachBinding { (tv, level) => + if level < targetState.nestingLevel(tv) then + targetState.setNestingLevel(tv, level) + } + targetState.gc() isCommitted = true ownedVars = SimpleIdentitySet.empty diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index fd355abf982a..65352aaae219 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4497,19 +4497,11 @@ object Types { * is different from the variable's creation state (meaning unrolls are possible) * in the current typer state. * - * @param origin The parameter that's tracked by the type variable. - * @param creatorState The typer state in which the variable was created. - * @param nestingLevel Symbols with a nestingLevel strictly greater than this - * will not appear in the instantiation of this type variable. - * This is enforced in `ConstraintHandling` by: - * - Maintaining the invariant that the `nonParamBounds` - * of a type variable never refer to a type with a - * greater `nestingLevel` (see `legalBound` for the reason - * why this cannot be delayed until instantiation). - * - On instantiation, replacing any param in the param bound - * with a level greater than nestingLevel (see `fullLowerBound`). + * @param origin the parameter that's tracked by the type variable. + * @param creatorState the typer state in which the variable was created. + * @param initNestingLevel the initial nesting level of the type variable. (c.f. nestingLevel) */ - final class TypeVar private(initOrigin: TypeParamRef, creatorState: TyperState | Null, val nestingLevel: Int) extends CachedProxyType with ValueType { + final class TypeVar private(initOrigin: TypeParamRef, creatorState: TyperState | Null, val initNestingLevel: Int) extends CachedProxyType with ValueType { private var currentOrigin = initOrigin def origin: TypeParamRef = currentOrigin @@ -4532,6 +4524,7 @@ object Types { owningState = null // no longer needed; null out to avoid a memory leak private[core] def resetInst(ts: TyperState): Unit = + assert(myInst.exists) myInst = NoType owningState = new WeakReference(ts) @@ -4541,6 +4534,26 @@ object Types { private[core] var owningState: WeakReference[TyperState] | Null = if (creatorState == null) null else new WeakReference(creatorState) + /** The nesting level of this type variable in the current typer state. This is usually + * the same as `initNestingLevel`, but can be decremented by calling `TyperState#setNestingLevel`. + * Symbols with a nestingLevel strictly greater than this level will not appear in the + * instantiation of this type variable. This is enforced in `ConstraintHandling`, + * dependig on the Config flags setting `checkLevelsOnConstraints` and `checkLevelsOnInstantiation`. + * + * Under `checkLevelsOnConstraints` we maintain the invariant that + * the `nonParamBounds` of a type variable never refer to a type with a + * greater `nestingLevel` (see `legalBound` for the reason why this + * cannot be delayed until instantiation). Then, on instantiation, + * we replace any param in the param bound with a level greater than + * nestingLevel (see `fullLowerBound`). + * + * Under `checkLevelsOnInstantiation`, we avoid incorrect levels only + * when a type variable is instantiated, see `ConstraintHandling$fixLevels`. + * Under this mode, the `nestingLevel` of a type variable can be made + * smaller when fixing the levels for some other type variable instance. + */ + def nestingLevel(using Context): Int = ctx.typerState.nestingLevel(this) + /** The instance type of this variable, or NoType if the variable is currently * uninstantiated */ @@ -4574,7 +4587,7 @@ object Types { * is also a singleton type. */ def instantiate(fromBelow: Boolean)(using Context): Type = - val tp = TypeComparer.instanceType(origin, fromBelow) + val tp = TypeComparer.instanceType(origin, fromBelow, nestingLevel) if myInst.exists then // The line above might have triggered instantiation of the current type variable myInst else diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 8d80d69fdfa9..475a258e8330 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -206,7 +206,7 @@ class TreePickler(pickler: TastyPickler) { } else if (tpe.prefix == NoPrefix) { writeByte(if (tpe.isType) TYPEREFdirect else TERMREFdirect) - if Config.checkLevels && !symRefs.contains(sym) && !sym.isPatternBound && !sym.hasAnnotation(defn.QuotedRuntimePatterns_patternTypeAnnot) then + if Config.checkLevelsOnConstraints && !symRefs.contains(sym) && !sym.isPatternBound && !sym.hasAnnotation(defn.QuotedRuntimePatterns_patternTypeAnnot) then report.error(i"pickling reference to as yet undefined $tpe with symbol ${sym}", sym.srcPos) pickleSymRef(sym) } diff --git a/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala b/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala index b5e2e7d475ad..debf51872d5a 100644 --- a/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala +++ b/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala @@ -302,7 +302,8 @@ class InlineReducer(inliner: Inliner)(using Context): def addTypeBindings(typeBinds: TypeBindsMap)(using Context): Unit = typeBinds.foreachBinding { case (sym, shouldBeMinimized) => - newTypeBinding(sym, ctx.gadt.approximation(sym, fromBelow = shouldBeMinimized)) + newTypeBinding(sym, + ctx.gadt.approximation(sym, fromBelow = shouldBeMinimized, maxLevel = Int.MaxValue)) } def registerAsGadtSyms(typeBinds: TypeBindsMap)(using Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index 88f06122310e..ca977a6799f8 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -376,7 +376,7 @@ object TypeTestsCasts { private[transform] def foundClasses(tp: Type)(using Context): List[Symbol] = def go(tp: Type, acc: List[Type])(using Context): List[Type] = tp.dealias match case OrType(tp1, tp2) => go(tp2, go(tp1, acc)) - case AndType(tp1, tp2) => (for t1 <- go(tp1, Nil); t2 <- go(tp2, Nil); yield AndType(t1, t2)) ::: acc + case AndType(tp1, tp2) => (for t1 <- go(tp1, Nil); t2 <- go(tp2, Nil) yield AndType(t1, t2)) ::: acc case _ => tp :: acc go(tp, Nil).map(effectiveClass) } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index f650304c3f0e..c662d6f00045 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -2216,7 +2216,13 @@ trait Applications extends Compatibility { * have added constraints to type parameters which are no longer * implied after harmonization. No essential constraints are lost by this because * the result of harmonization will be compared again with the expected type. - * Test cases where this matters are in pos/harmomize.scala. + * Test cases where this matters are in neg/harmomize.scala and run/weak-conformance.scala. + * + * Note: this assumes that the internal typing of the arguments using `op` does + * not leave any constraints, so the only info that is reset is the relationship + * between the argument's types and the expected type. I am not sure this will + * always be the case. If that property does not hold, we risk forgetting constraints + * which could lead to unsoundness. */ def harmonic[T](harmonize: List[T] => List[T], pt: Type)(op: => List[T])(using Context): List[T] = if (!isFullyDefined(pt, ForceDegree.none)) { diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 40f9906038f6..e6426cc54cd5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1683,7 +1683,11 @@ class Namer { typer: Typer => // are better ways to achieve this. It would be good if we could get rid of this code. // It seems at least partially redundant with the nesting level checking on TypeVar // instantiation. - if !Config.checkLevels then + // It turns out if we fix levels on instantiation we still need this code. + // Examples that fail otherwise are pos/scalaz-redux.scala and pos/java-futures.scala. + // So fixing levels at instantiation avoids the soundness problem but apparently leads + // to type inference problems since it comes too late. + if !Config.checkLevelsOnConstraints then val hygienicType = TypeOps.avoid(rhsType, termParamss.flatten) if (!hygienicType.isValueType || !(hygienicType <:< tpt.tpe)) report.error(i"return type ${tpt.tpe} of lambda cannot be made hygienic;\n" + diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 8adbb38df743..71292f4590b1 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -85,6 +85,3 @@ i4176-gadt.scala i13974a.scala java-inherited-type1 - -# avoidance bug -i15174.scala \ No newline at end of file diff --git a/tests/pending/run/i8861.scala b/tests/neg/i8861.scala similarity index 94% rename from tests/pending/run/i8861.scala rename to tests/neg/i8861.scala index e1e802a5c72b..7599848efe94 100644 --- a/tests/pending/run/i8861.scala +++ b/tests/neg/i8861.scala @@ -1,3 +1,4 @@ +// Compiles under 14026 object Test { sealed trait Container { s => type A @@ -22,7 +23,7 @@ object Test { // now infers `c.visit[(Int & M | String & M)]` def minimalFail[M](c: Container { type A = M }): M = c.visit( int = vi => vi.i : vi.A, - str = vs => vs.t : vs.A + str = vs => vs.t : vs.A // error ) def main(args: Array[String]): Unit = { diff --git a/tests/pos/i8900-promote.scala b/tests/neg/i8900-promote.scala similarity index 79% rename from tests/pos/i8900-promote.scala rename to tests/neg/i8900-promote.scala index 7d3a2ff96bed..8be0029214b4 100644 --- a/tests/pos/i8900-promote.scala +++ b/tests/neg/i8900-promote.scala @@ -1,3 +1,4 @@ +// Compiles under #14026 class Inv[A <: Singleton](x: A) object Inv { def empty[A <: Singleton]: Inv[A] = new Inv(???) @@ -12,7 +13,8 @@ object Test { def inv(cond: Boolean) = // used to leak: Inv[x.type] if (cond) val x: Int = 1 - new Inv(x) + new Inv(x) // error else - Inv.empty + Inv.empty // error + } diff --git a/tests/pending/neg/i8900.scala b/tests/neg/i8900.scala similarity index 100% rename from tests/pending/neg/i8900.scala rename to tests/neg/i8900.scala diff --git a/tests/pos/i15595.scala b/tests/pos/i15595.scala new file mode 100644 index 000000000000..b5d6cf402ed3 --- /dev/null +++ b/tests/pos/i15595.scala @@ -0,0 +1,14 @@ +trait MatchResult[+T] + +@main def Test() = { + def convert[T <: Seq[_], U <: MatchResult[_]](fn: T => U)(implicit x: Seq[_] = Seq.empty): U = ??? + def resultOf[T](v: T): MatchResult[T] = ??? + + convert { _ => + type R = String + resultOf[R](???) + // this would not lead to crash: + // val x = resultOf[R](???) + // x + } +} \ No newline at end of file diff --git a/tests/pos/java-futures.scala b/tests/pos/java-futures.scala new file mode 100644 index 000000000000..f6c841675c92 --- /dev/null +++ b/tests/pos/java-futures.scala @@ -0,0 +1,26 @@ +import java.util.concurrent.{TimeUnit, TimeoutException, Future, Executors => JExecutors} + +class TestSource +trait LoggedRunnable extends Runnable + + + +object Test: + + val filteredSources: List[TestSource] = ??? + + def encapsulatedCompilation(testSource: TestSource): LoggedRunnable = ??? + + def testSuite(): this.type = + val pool = JExecutors.newWorkStealingPool(Runtime.getRuntime.availableProcessors()) + val eventualResults = for target <- filteredSources yield + pool.submit(encapsulatedCompilation(target)) + + for fut <- eventualResults do + try fut.get() + catch case ex: Exception => + System.err.println(ex.getMessage) + ex.printStackTrace() + + this + diff --git a/tests/pos/scalaz-redux.scala b/tests/pos/scalaz-redux.scala new file mode 100644 index 000000000000..49a442966487 --- /dev/null +++ b/tests/pos/scalaz-redux.scala @@ -0,0 +1,20 @@ + +sealed abstract class LazyEither[A, B] { + + def fold[X](left: (=> A) => X, right: (=> B) => X): X = + this match { + case LazyLeft(a) => left(a()) + case LazyRight(b) => right(b()) + } +} + +object LazyEither { + + final case class LeftProjection[A, B](e: LazyEither[A, B]) extends AnyVal { + + def getOrElse[AA >: A](default: => AA): AA = + e.fold(z => z, _ => default) + } +} +private case class LazyLeft[A, B](a: () => A) extends LazyEither[A, B] +private case class LazyRight[A, B](b: () => B) extends LazyEither[A, B]