Skip to content

Commit 60af4b9

Browse files
committed
Check reverse dependencies
Can be enabled globally with Config option (set to false by default) or locally through option -Ycheck-constraint-deps. The option is currently turned on for pos, run, and patmatExhaustivity tests.
1 parent 12969d3 commit 60af4b9

File tree

6 files changed

+196
-85
lines changed

6 files changed

+196
-85
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ private sealed trait YSettings:
307307
val YforceSbtPhases: Setting[Boolean] = BooleanSetting("-Yforce-sbt-phases", "Run the phases used by sbt for incremental compilation (ExtractDependencies and ExtractAPI) even if the compiler is ran outside of sbt, for debugging.")
308308
val YdumpSbtInc: Setting[Boolean] = BooleanSetting("-Ydump-sbt-inc", "For every compiled foo.scala, output the API representation and dependencies used for sbt incremental compilation in foo.inc, implies -Yforce-sbt-phases.")
309309
val YcheckAllPatmat: Setting[Boolean] = BooleanSetting("-Ycheck-all-patmat", "Check exhaustivity and redundancy of all pattern matching (used for testing the algorithm).")
310+
val YcheckConstraintDeps: Setting[Boolean] = BooleanSetting("-Ycheck-constraint-deps", "Check dependency tracking in constraints (used for testing the algorithm).")
310311
val YretainTrees: Setting[Boolean] = BooleanSetting("-Yretain-trees", "Retain trees for top-level classes, accessible from ClassSymbol#tree")
311312
val YshowTreeIds: Setting[Boolean] = BooleanSetting("-Yshow-tree-ids", "Uniquely tag all tree nodes in debugging output.")
312313
val YfromTastyIgnoreList: Setting[List[String]] = MultiStringSetting("-Yfrom-tasty-ignore-list", "file", "List of `tasty` files in jar files that will not be loaded when using -from-tasty")

compiler/src/dotty/tools/dotc/core/Constraint.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ abstract class Constraint extends Showable {
166166
*/
167167
def hasConflictingTypeVarsFor(tl: TypeLambda, that: Constraint): Boolean
168168

169+
/** Does `param` occur at the toplevel in `tp` ?
170+
* Toplevel means: the type itself or a factor in some
171+
* combination of `&` or `|` types.
172+
*/
173+
def occursAtToplevel(param: TypeParamRef, tp: Type)(using Context): Boolean
174+
169175
/** A map that associates type parameters of this constraint with all other type
170176
* parameters that refer to them in their bounds covariantly, such that, if the
171177
* type parameter is instantiated to a larger type, the constraint would be narrowed.
@@ -191,12 +197,6 @@ abstract class Constraint extends Showable {
191197
/** Check that no constrained parameter contains itself as a bound */
192198
def checkWellFormed()(using Context): this.type
193199

194-
/** Does `param` occur at the toplevel in `tp` ?
195-
* Toplevel means: the type itself or a factor in some
196-
* combination of `&` or `|` types.
197-
*/
198-
def occursAtToplevel(param: TypeParamRef, tp: Type)(using Context): Boolean
199-
200200
/** Check that constraint only refers to TypeParamRefs bound by itself */
201201
def checkClosed()(using Context): Unit
202202

compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala

Lines changed: 175 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import cc.{CapturingType, derivedCapturingType}
1818

1919
object OrderingConstraint {
2020

21+
@sharable private var id = 0
22+
private def nextId =
23+
id += 1
24+
id
25+
2126
type ArrayValuedMap[T] = SimpleIdentityMap[TypeLambda, Array[T]]
2227

2328
/** The type of `OrderingConstraint#boundsMap` */
@@ -140,6 +145,10 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
140145

141146
type This = OrderingConstraint
142147

148+
var id = nextId
149+
//if id == 118 then
150+
// new Error(s"at $id").printStackTrace()
151+
143152
/** A new constraint with given maps and given set of hard typevars */
144153
def newConstraint( // !!! Dotty problem: Making newConstraint `private` causes -Ytest-pickler failure.
145154
boundsMap: ParamBounds = this.boundsMap,
@@ -245,8 +254,40 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
245254
//.showing(i"outer depends on $tv with ${tvdeps.toList}%, % = $result")
246255
if co then test(coDeps, upperLens) else test(contraDeps, lowerLens)
247256

257+
/** Modify traversals in two respects:
258+
* - when encountering an application C[Ts], where C is a type variable or parameter
259+
* that has an instantiation in this constraint, assume the type parameters of
260+
* the instantiation instead of the type parameters of C when traversing the
261+
* arguments Ts. That can make a difference for the variance in which an argument
262+
* is traversed. Example constraint:
263+
*
264+
* constrainded types: C[X], A
265+
* A >: C[B]
266+
* C := Option
267+
*
268+
* Here, B is traversed with variance +1 instead of 0. Test case: pos/t3152.scala
269+
*
270+
* - When typing a prefx, don't avoid negative variances. This matters only for the
271+
* corner case where a parameter is instantiated to Nothing (see comment in
272+
* TypeAccumulator#applyToPrefix). When determining instantiation directions
273+
* (which is what dependency variances are for), it can be ignored.
274+
*/
275+
private trait ConstraintAwareTraversal[T] extends TypeAccumulator[T]:
276+
override def tyconTypeParams(tp: AppliedType)(using Context): List[ParamInfo] =
277+
def tparams(tycon: Type): List[ParamInfo] = tycon match
278+
case tycon: TypeVar if !tycon.isInstantiated => tparams(tycon.origin)
279+
case tycon: TypeParamRef =>
280+
entry(tycon) match
281+
case _: TypeBounds => tp.tyconTypeParams
282+
case tycon1 if tycon1.typeParams.nonEmpty => tycon1.typeParams
283+
case _ => tp.tyconTypeParams
284+
case _ => tp.tyconTypeParams
285+
tparams(tp.tycon)
286+
override def applyToPrefix(x: T, tp: NamedType): T =
287+
this(x, tp.prefix)
288+
248289
private class Adjuster(srcParam: TypeParamRef)(using Context)
249-
extends TypeTraverser, ConstraintAwareTraversal:
290+
extends TypeTraverser, ConstraintAwareTraversal[Unit]:
250291

251292
var add: Boolean = compiletime.uninitialized
252293
val seen = util.HashSet[LazyRef]()
@@ -257,9 +298,12 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
257298

258299
def traverse(t: Type) = t match
259300
case param: TypeParamRef =>
260-
if contains(param) then
261-
if variance >= 0 then coDeps = update(coDeps, param)
262-
if variance <= 0 then contraDeps = update(contraDeps, param)
301+
entry(param) match
302+
case _: TypeBounds =>
303+
if variance >= 0 then coDeps = update(coDeps, param)
304+
if variance <= 0 then contraDeps = update(contraDeps, param)
305+
case tp =>
306+
traverse(tp)
263307
case tp: LazyRef =>
264308
if !seen.contains(tp) then
265309
seen += tp
@@ -287,39 +331,40 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
287331
* and/or prefix of `bound`, just add the new parts of `bound`.
288332
* @param isLower `bound` and `prevBound` are lower bounds
289333
*/
290-
def adjustDelta(bound: Type, prevBound: Type, isLower: Boolean): Boolean =
291-
if bound eq prevBound then true
334+
def adjustDelta(bound: Type, prevBound: Type, isLower: Boolean, baseCase: => Boolean): Boolean =
335+
if bound eq prevBound then
336+
baseCase
292337
else bound match
293338
case bound: AndOrType =>
294-
adjustDelta(bound.tp1, prevBound, isLower) && {
339+
adjustDelta(bound.tp1, prevBound, isLower, baseCase) && {
295340
adjustReferenced(bound.tp2, isLower, add = true)
296341
true
297342
}
298343
case _ => false
299344

300-
/** Adjust dependencies to account for the delta of previous bound `prevBound`
301-
* and new bound `bound`.
302-
* @param isLower `bound` and `prevBound` are lower bounds
345+
/** Adjust dependencies to account for the delta of previous bounds `prevBounds`
346+
* and new bounds `bounds`.
347+
* @param add true if the bounds are added, false if they are removed
303348
*/
304-
def adjustBounds(bound: Type, prevBound: Type, isLower: Boolean) =
305-
if !adjustDelta(bound, prevBound, isLower) then
306-
adjustReferenced(prevBound, isLower, add = false)
307-
adjustReferenced(bound, isLower, add = true)
349+
def adjustBounds(bounds: TypeBounds, add: Boolean) =
350+
adjustReferenced(bounds.lo, isLower = true, add)
351+
adjustReferenced(bounds.hi, isLower = false, add)
308352

309353
entry match
310-
case TypeBounds(lo, hi) =>
354+
case entry @ TypeBounds(lo, hi) =>
311355
prevEntry match
312-
case TypeBounds(plo, phi) =>
313-
adjustBounds(lo, plo, isLower = true)
314-
adjustBounds(hi, phi, isLower = false)
356+
case prevEntry @ TypeBounds(plo, phi) =>
357+
if !adjustDelta(lo, plo, isLower = true,
358+
adjustDelta(hi, phi, isLower = false, true))
359+
then
360+
adjustBounds(prevEntry, add = false)
361+
adjustBounds(entry, add = true)
315362
case _ =>
316-
adjustReferenced(lo, isLower = true, add = true)
317-
adjustReferenced(hi, isLower = false, add = true)
363+
adjustBounds(entry, add = true)
318364
case _ =>
319365
prevEntry match
320-
case TypeBounds(plo, phi) =>
321-
adjustReferenced(plo, isLower = true, add = false)
322-
adjustReferenced(phi, isLower = false, add = false)
366+
case prevEntry: TypeBounds =>
367+
adjustBounds(prevEntry, add = false)
323368
case _ =>
324369
dropDeps(srcParam)
325370
this
@@ -408,6 +453,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
408453
}
409454

410455
def add(poly: TypeLambda, tvars: List[TypeVar])(using Context): This = {
456+
checkWellFormed() // TODO: drop
411457
assert(!contains(poly))
412458
val nparams = poly.paramNames.length
413459
val entries1 = new Array[Type](nparams * 2)
@@ -563,7 +609,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
563609
boundsLens.update(this, current, param, newEntry).adjustDeps(newEntry, oldEntry, param)
564610

565611
private def updateEntry(current: This, param: TypeParamRef, newEntry: Type)(using Context): This = {
566-
//println(i"update $param to $tp in $current")
612+
//println(i"update $param to $newEntry in $current")
567613
if Config.checkNoWildcardsInConstraint then assert(!newEntry.containsWildcardTypes)
568614
var current1 = updateEntryNoOrdering(current, param, newEntry, current.entry(param))
569615
newEntry match {
@@ -596,12 +642,12 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
596642
else
597643
assert(replacement.isValueTypeOrLambda)
598644

599-
val droppedTypeVar = typeVarOfParam(param)
645+
val replacedTypeVar = typeVarOfParam(param)
646+
//println(i"replace $param, $replacedTypeVar with $replacement in $this")
600647

601-
//println(i"replace $param, $droppedTypeVar with $replacement in $this")
602-
val dropTypeVar = new TypeMap:
648+
def mapReplacedTypeVarTo(to: Type) = new TypeMap:
603649
override def apply(t: Type): Type =
604-
if t.exists && (t eq droppedTypeVar) then param else mapOver(t)
650+
if (t eq replacedTypeVar) && t.exists then to else mapOver(t)
605651

606652
var current = this
607653

@@ -616,7 +662,29 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
616662
if other != param then
617663
val oldEntry = current.entry(other)
618664
val newEntry = current.ensureNonCyclic(other, oldEntry.substParam(param, replacement))
619-
current = updateEntryNoOrdering(current, other, newEntry, dropTypeVar(oldEntry))
665+
current = boundsLens.update(this, current, other, newEntry)
666+
var oldDepEntry = oldEntry
667+
var newDepEntry = newEntry
668+
replacedTypeVar match
669+
case tvar: TypeVar =>
670+
if tvar.isInstantiated
671+
then
672+
// replace is called from TypeVar's instantiateWith,
673+
// forget about instantiation for old dependencies
674+
oldDepEntry = mapReplacedTypeVarTo(param)(oldDepEntry)
675+
else
676+
// replace is called from unify,
677+
// assume parameter has been replaced for new dependencies
678+
// (the actual replacement is done below)
679+
newDepEntry = mapReplacedTypeVarTo(replacement)(newDepEntry)
680+
case _ =>
681+
if oldDepEntry ne newDepEntry then
682+
if current eq this then
683+
// We can end up here if oldEntry eq newEntry, so posssibly no new constraint
684+
// was created, but oldDepEntry ne newDepEntry. In that case we must make
685+
// sure we have a new constraint before updating dependencies.
686+
current = newConstraint()
687+
current.adjustDeps(newDepEntry, oldDepEntry, other)
620688
}
621689

622690
current =
@@ -703,6 +771,26 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
703771
assert(tvar.origin == param, i"mismatch $tvar, $param")
704772
case _ =>
705773

774+
def occursAtToplevel(param: TypeParamRef, inst: Type)(using Context): Boolean =
775+
def occurs(tp: Type)(using Context): Boolean = tp match
776+
case tp: AndOrType =>
777+
occurs(tp.tp1) || occurs(tp.tp2)
778+
case tp: TypeParamRef =>
779+
(tp eq param) || entry(tp).match
780+
case NoType => false
781+
case TypeBounds(lo, hi) => (lo eq hi) && occurs(lo)
782+
case inst => occurs(inst)
783+
case tp: TypeVar =>
784+
occurs(tp.underlying)
785+
case TypeBounds(lo, hi) =>
786+
occurs(lo) || occurs(hi)
787+
case _ =>
788+
val tp1 = tp.dealias
789+
(tp1 ne tp) && occurs(tp1)
790+
791+
occurs(inst)
792+
end occursAtToplevel
793+
706794
// ---------- Exploration --------------------------------------------------------
707795

708796
def domainLambdas: List[TypeLambda] = boundsMap.keys
@@ -755,7 +843,60 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
755843

756844
// ---------- Checking -----------------------------------------------
757845

846+
/** Depending on Config settngs, check that there are no cycles and that
847+
* reverse depenecies are correct.
848+
*/
758849
def checkWellFormed()(using Context): this.type =
850+
851+
/** Check that each dependency A -> B in coDeps and contraDeps corresponds to
852+
* a reference to A at the right variance in the entry of B.
853+
*/
854+
def checkBackward(deps: ReverseDeps, depsName: String, v: Int)(using Context): Unit =
855+
deps.foreachBinding { (param, params) =>
856+
for srcParam <- params do
857+
assert(contains(srcParam) && occursAtVariance(param, v, in = entry(srcParam)),
858+
i"wrong $depsName backwards reference $param -> $srcParam in $thisConstraint")
859+
}
860+
861+
/** A type traverser that checks that all references bound in the constraint
862+
* are accounted for in coDeps and/or contraDeps.
863+
*/
864+
def checkForward(srcParam: TypeParamRef)(using Context) =
865+
new TypeTraverser with ConstraintAwareTraversal[Unit]:
866+
val seen = util.HashSet[LazyRef]()
867+
def traverse(t: Type): Unit = t match
868+
case param: TypeParamRef if param ne srcParam =>
869+
def check(deps: ReverseDeps, directDeps: List[TypeParamRef], depsName: String) =
870+
assert(deps.at(param).contains(srcParam) || directDeps.contains(srcParam),
871+
i"missing $depsName backwards reference $param -> $srcParam in $thisConstraint")
872+
entry(param) match
873+
case _: TypeBounds =>
874+
if variance >= 0 then check(contraDeps, upper(param), "contra")
875+
if variance <= 0 then check(coDeps, lower(param), "co")
876+
case tp =>
877+
traverse(tp)
878+
case tp: LazyRef =>
879+
if !seen.contains(tp) then
880+
seen += tp
881+
traverse(tp.ref)
882+
case _ => traverseChildren(t)
883+
884+
/** Does `param` occur at variance `v` or else at variance 0 in entry `in`? */
885+
def occursAtVariance(param: TypeParamRef, v: Int, in: Type)(using Context): Boolean =
886+
val test = new TypeAccumulator[Boolean] with ConstraintAwareTraversal[Boolean]:
887+
def apply(x: Boolean, t: Type): Boolean =
888+
if x then true
889+
else t match
890+
case t: TypeParamRef =>
891+
entry(t) match
892+
case _: TypeBounds =>
893+
t == param && (variance == 0 || variance == v)
894+
case e =>
895+
apply(x, e)
896+
case _ =>
897+
foldOver(x, t)
898+
test(false, in)
899+
759900
if Config.checkConstraintsNonCyclic then
760901
domainParams.foreach { param =>
761902
val inst = entry(param)
@@ -764,38 +905,14 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
764905
assert(!occursAtToplevel(param, inst),
765906
s"cyclic bound for $param: ${inst.show} in ${this.show}")
766907
}
767-
if Config.checkConstraintDeps then
768-
def checkDeps(deps: ReverseDeps) = ()/*
769-
deps.foreachBinding { (tv, tvs) =>
770-
for tv1 <- tvs do
771-
assert(!tv1.instanceOpt.exists, i"$this")
772-
}*/
773-
checkDeps(coDeps)
774-
checkDeps(contraDeps)
908+
if Config.checkConstraintDeps || ctx.settings.YcheckConstraintDeps.value then
909+
checkBackward(coDeps, "co", -1)
910+
checkBackward(contraDeps, "contra", +1)
911+
domainParams.foreach(p => if contains(p) then checkForward(p).traverse(entry(p)))
912+
775913
this
776914
end checkWellFormed
777915

778-
def occursAtToplevel(param: TypeParamRef, inst: Type)(using Context): Boolean =
779-
780-
def occurs(tp: Type)(using Context): Boolean = tp match
781-
case tp: AndOrType =>
782-
occurs(tp.tp1) || occurs(tp.tp2)
783-
case tp: TypeParamRef =>
784-
(tp eq param) || entry(tp).match
785-
case NoType => false
786-
case TypeBounds(lo, hi) => (lo eq hi) && occurs(lo)
787-
case inst => occurs(inst)
788-
case tp: TypeVar =>
789-
occurs(tp.underlying)
790-
case TypeBounds(lo, hi) =>
791-
occurs(lo) || occurs(hi)
792-
case _ =>
793-
val tp1 = tp.dealias
794-
(tp1 ne tp) && occurs(tp1)
795-
796-
occurs(inst)
797-
end occursAtToplevel
798-
799916
override def checkClosed()(using Context): Unit =
800917

801918
def isFreeTypeParamRef(tp: Type) = tp match

0 commit comments

Comments
 (0)