Skip to content

Commit f4d139d

Browse files
authored
Merge pull request #12333 from dotty-staging/fix-merge-gc
Avoid leaking constraints in typer states
2 parents 69c800b + 8dc0cb5 commit f4d139d

File tree

10 files changed

+124
-62
lines changed

10 files changed

+124
-62
lines changed

compiler/src/dotty/tools/dotc/Run.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
212212
Stats.record(s"total trees at end of $phase", ast.Trees.ntrees)
213213
for (unit <- units)
214214
Stats.record(s"retained typed trees at end of $phase", unit.tpdTree.treeSize)
215+
ctx.typerState.gc()
215216
}
216217

217218
profiler.finished()

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ object Config {
3434
*/
3535
inline val checkConstraintsPropagated = false
3636

37+
/** If a constraint is over a type lambda `tl` and `tvar` is one of
38+
* the type variables associated with `tl` in the constraint, check
39+
* that the origin of `tvar` is a parameter of `tl`.
40+
*/
41+
inline val checkConsistentVars = false
42+
3743
/** Check that constraints of globally committable typer states are closed.
3844
* NOTE: When enabled, the check can cause CyclicReference errors because
3945
* it traverses all elements of a type. Such failures were observed when

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,11 @@ abstract class Constraint extends Showable {
118118
/** A new constraint with all entries coming from `tl` removed. */
119119
def remove(tl: TypeLambda)(using Context): This
120120

121-
/** A new constraint with entry `tl` renamed to a fresh type lambda */
122-
def rename(tl: TypeLambda)(using Context): This
121+
/** A new constraint with entry `from` replaced with `to`
122+
* Rerences to `from` from within other constraint bounds are updated to `to`.
123+
* Type variables are left alone.
124+
*/
125+
def subst(from: TypeLambda, to: TypeLambda)(using Context): This
123126

124127
/** Gives for each instantiated type var that does not yet have its `inst` field
125128
* set, the instance value stored in the constraint. Storing instances in constraints
@@ -150,6 +153,8 @@ abstract class Constraint extends Showable {
150153
def uninstVars: collection.Seq[TypeVar]
151154

152155
/** The weakest constraint that subsumes both this constraint and `other`.
156+
* The constraints should be _compatible_, meaning that a type lambda
157+
* occurring in both constraints is associated with the same typevars in each.
153158
*
154159
* @param otherHasErrors If true, handle incompatible constraints by
155160
* returning an approximate constraint, instead of
@@ -169,6 +174,11 @@ abstract class Constraint extends Showable {
169174
/** Check that constraint only refers to TypeParamRefs bound by itself */
170175
def checkClosed()(using Context): Unit
171176

177+
/** Check that every typevar om this constraint has as origin a type parameter
178+
* of athe type lambda that is associated with the typevar itself.
179+
*/
180+
def checkConsistentVars()(using Context): Unit
181+
172182
/** A string describing the constraint's contents without a header or trailer */
173183
def contentsToString(using Context): String
174184
}

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

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -485,49 +485,25 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
485485
throw new AssertionError(i"cannot merge $this with $other, mergeEntries($e1, $e2) failed")
486486
}
487487

488-
/** Ensure that constraint `c` does not associate different TypeVars for the
489-
* same type lambda than this constraint. Do this by renaming type lambdas
490-
* in `c` where necessary.
491-
*/
492-
def ensureNotConflicting(c: OrderingConstraint): OrderingConstraint = {
493-
def hasConflictingTypeVarsFor(tl: TypeLambda) =
494-
this.typeVarOfParam(tl.paramRefs(0)) ne c.typeVarOfParam(tl.paramRefs(0))
495-
// Note: Since TypeVars are allocated in bulk for each type lambda, we only
496-
// have to check the first one to find out if some of them are different.
497-
val conflicting = c.domainLambdas.find(tl =>
498-
this.contains(tl) && hasConflictingTypeVarsFor(tl))
499-
conflicting match {
500-
case Some(tl) => ensureNotConflicting(c.rename(tl))
501-
case None => c
502-
}
503-
}
504-
505-
val that = ensureNotConflicting(other.asInstanceOf[OrderingConstraint])
488+
val that = other.asInstanceOf[OrderingConstraint]
506489

507490
new OrderingConstraint(
508491
merge(this.boundsMap, that.boundsMap, mergeEntries),
509492
merge(this.lowerMap, that.lowerMap, mergeParams),
510493
merge(this.upperMap, that.upperMap, mergeParams))
511494
}.showing(i"constraint merge $this with $other = $result", constr)
512495

513-
def rename(tl: TypeLambda)(using Context): OrderingConstraint = {
514-
assert(contains(tl))
515-
val tl1 = ensureFresh(tl)
516-
def swapKey[T](m: ArrayValuedMap[T]) = m.remove(tl).updated(tl1, m(tl))
496+
def subst(from: TypeLambda, to: TypeLambda)(using Context): OrderingConstraint =
497+
def swapKey[T](m: ArrayValuedMap[T]) = m.remove(from).updated(to, m(from))
517498
var current = newConstraint(swapKey(boundsMap), swapKey(lowerMap), swapKey(upperMap))
518-
def subst[T <: Type](x: T): T = x.subst(tl, tl1).asInstanceOf[T]
499+
def subst[T <: Type](x: T): T = x.subst(from, to).asInstanceOf[T]
519500
current.foreachParam {(p, i) =>
520501
current = boundsLens.map(this, current, p, i, subst)
521502
current = lowerLens.map(this, current, p, i, _.map(subst))
522503
current = upperLens.map(this, current, p, i, _.map(subst))
523504
}
524-
current.foreachTypeVar { tvar =>
525-
val TypeParamRef(binder, n) = tvar.origin
526-
if (binder eq tl) tvar.setOrigin(tl1.paramRefs(n))
527-
}
528505
constr.println(i"renamed $this to $current")
529506
current.checkNonCyclic()
530-
}
531507

532508
def instType(tvar: TypeVar): Type = entry(tvar.origin) match
533509
case _: TypeBounds => NoType
@@ -547,6 +523,13 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
547523
}
548524
else tl
549525

526+
def checkConsistentVars()(using Context): Unit =
527+
for param <- domainParams do
528+
typeVarOfParam(param) match
529+
case tvar: TypeVar =>
530+
assert(tvar.origin == param, i"mismatch $tvar, $param")
531+
case _ =>
532+
550533
// ---------- Exploration --------------------------------------------------------
551534

552535
def domainLambdas: List[TypeLambda] = boundsMap.keys

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2876,12 +2876,8 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
28762876
// obviously sound, but quite restrictive. With the current formulation,
28772877
// we need to be careful that `provablyEmpty` covers all the conditions
28782878
// used to conclude disjointness in `provablyDisjoint`.
2879-
if (provablyEmpty(scrut))
2880-
NoType
2881-
else
2882-
val savedConstraint = constraint
2883-
try recur(cases)
2884-
finally constraint = savedConstraint // caseLambda additions are dropped
2879+
if (provablyEmpty(scrut)) NoType
2880+
else recur(cases)
28852881
}
28862882
}
28872883
}

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

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ object TyperState {
2222
.init(null, OrderingConstraint.empty)
2323
.setReporter(new ConsoleReporter())
2424
.setCommittable(true)
25+
26+
opaque type Snapshot = (Constraint, TypeVars, TypeVars)
27+
28+
extension (ts: TyperState)
29+
def snapshot()(using Context): Snapshot =
30+
var previouslyInstantiated: TypeVars = SimpleIdentitySet.empty
31+
for tv <- ts.ownedVars do if tv.inst.exists then previouslyInstantiated += tv
32+
(ts.constraint, ts.ownedVars, previouslyInstantiated)
33+
34+
def resetTo(state: Snapshot)(using Context): Unit =
35+
val (c, tvs, previouslyInstantiated) = state
36+
for tv <- tvs do
37+
if tv.inst.exists && !previouslyInstantiated.contains(tv) then
38+
tv.resetInst(ts)
39+
ts.ownedVars = tvs
40+
ts.constraint = c
2541
}
2642

2743
class TyperState() {
@@ -44,6 +60,8 @@ class TyperState() {
4460
def constraint_=(c: Constraint)(using Context): Unit = {
4561
if (Config.debugCheckConstraintsClosed && isGlobalCommittable) c.checkClosed()
4662
myConstraint = c
63+
if Config.checkConsistentVars && !ctx.reporter.errorsReported then
64+
c.checkConsistentVars()
4765
}
4866

4967
private var previousConstraint: Constraint = _
@@ -61,7 +79,11 @@ class TyperState() {
6179

6280
private var isCommitted: Boolean = _
6381

64-
/** The set of uninstantiated type variables which have this state as their owning state */
82+
/** The set of uninstantiated type variables which have this state as their owning state
83+
* NOTE: It could be that a variable in `ownedVars` is already instantiated. This is because
84+
* the link between ownedVars and variable instantiation in TypeVar#setInst is made up
85+
* from a weak reference and weak references can have spurious nulls.
86+
*/
6587
private var myOwnedVars: TypeVars = _
6688
def ownedVars: TypeVars = myOwnedVars
6789
def ownedVars_=(vs: TypeVars): Unit = myOwnedVars = vs
@@ -120,19 +142,50 @@ class TyperState() {
120142
if constraint ne targetState.constraint then
121143
Stats.record("typerState.commit.new constraint")
122144
constr.println(i"committing $this to $targetState, fromConstr = $constraint, toConstr = ${targetState.constraint}")
123-
if targetState.constraint eq previousConstraint then targetState.constraint = constraint
124-
else targetState.mergeConstraintWith(this)
125-
if !ownedVars.isEmpty then
126-
for tvar <- ownedVars do
127-
tvar.owningState = new WeakReference(targetState)
128-
targetState.ownedVars ++= ownedVars
145+
if targetState.constraint eq previousConstraint then
146+
targetState.constraint = constraint
147+
if !ownedVars.isEmpty then ownedVars.foreach(targetState.includeVar)
148+
else
149+
targetState.mergeConstraintWith(this)
129150
targetState.gc()
130151
reporter.flush()
131152
isCommitted = true
132153
}
133154

155+
/** Ensure that this constraint does not associate different TypeVars for the
156+
* same type lambda than the `other` constraint. Do this by renaming type lambdas
157+
* in this constraint and its predecessors where necessary.
158+
*/
159+
def ensureNotConflicting(other: Constraint)(using Context): Unit =
160+
def hasConflictingTypeVarsFor(tl: TypeLambda) =
161+
constraint.typeVarOfParam(tl.paramRefs(0)) ne other.typeVarOfParam(tl.paramRefs(0))
162+
// Note: Since TypeVars are allocated in bulk for each type lambda, we only
163+
// have to check the first one to find out if some of them are different.
164+
val conflicting = constraint.domainLambdas.find(tl =>
165+
other.contains(tl) && hasConflictingTypeVarsFor(tl))
166+
for tl <- conflicting do
167+
val tl1 = constraint.ensureFresh(tl)
168+
for case (tvar: TypeVar, pref1) <- tl.paramRefs.map(constraint.typeVarOfParam).lazyZip(tl1.paramRefs) do
169+
tvar.setOrigin(pref1)
170+
var ts = this
171+
while ts.constraint.domainLambdas.contains(tl) do
172+
ts.constraint = ts.constraint.subst(tl, tl1)
173+
ts = ts.previous
174+
134175
def mergeConstraintWith(that: TyperState)(using Context): Unit =
176+
that.ensureNotConflicting(constraint)
135177
constraint = constraint & (that.constraint, otherHasErrors = that.reporter.errorsReported)
178+
for tvar <- constraint.uninstVars do
179+
if !isOwnedAnywhere(this, tvar) then ownedVars += tvar
180+
for tl <- constraint.domainLambdas do
181+
if constraint.isRemovable(tl) then constraint = constraint.remove(tl)
182+
183+
private def includeVar(tvar: TypeVar)(using Context): Unit =
184+
tvar.owningState = new WeakReference(this)
185+
ownedVars += tvar
186+
187+
private def isOwnedAnywhere(ts: TyperState, tvar: TypeVar): Boolean =
188+
ts.ownedVars.contains(tvar) || ts.previous != null && isOwnedAnywhere(ts.previous, tvar)
136189

137190
/** Make type variable instances permanent by assigning to `inst` field if
138191
* type variable instantiation cannot be retracted anymore. Then, remove
@@ -143,14 +196,14 @@ class TyperState() {
143196
Stats.record("typerState.gc")
144197
val toCollect = new mutable.ListBuffer[TypeLambda]
145198
for tvar <- ownedVars do
146-
if !tvar.inst.exists then
199+
if !tvar.inst.exists then // See comment of `ownedVars` for why this test is necessary
147200
val inst = constraint.instType(tvar)
148201
if inst.exists then
149-
tvar.inst = inst
150-
val lam = tvar.origin.binder
151-
if constraint.isRemovable(lam) then toCollect += lam
152-
for poly <- toCollect do
153-
constraint = constraint.remove(poly)
202+
tvar.setInst(inst)
203+
val tl = tvar.origin.binder
204+
if constraint.isRemovable(tl) then toCollect += tl
205+
for tl <- toCollect do
206+
constraint = constraint.remove(tl)
154207

155208
override def toString: String = {
156209
def ids(state: TyperState): List[String] =

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4396,13 +4396,17 @@ object Types {
43964396
private var myInst: Type = NoType
43974397

43984398
private[core] def inst: Type = myInst
4399-
private[core] def inst_=(tp: Type): Unit = {
4399+
private[core] def setInst(tp: Type): Unit =
44004400
myInst = tp
4401-
if (tp.exists && (owningState ne null)) {
4402-
owningState.get.ownedVars -= this
4403-
owningState = null // no longer needed; null out to avoid a memory leak
4404-
}
4405-
}
4401+
if tp.exists && owningState != null then
4402+
val owningState1 = owningState.get
4403+
if owningState1 != null then
4404+
owningState1.ownedVars -= this
4405+
owningState = null // no longer needed; null out to avoid a memory leak
4406+
4407+
private[core] def resetInst(ts: TyperState): Unit =
4408+
myInst = NoType
4409+
owningState = new WeakReference(ts)
44064410

44074411
/** The state owning the variable. This is at first `creatorState`, but it can
44084412
* be changed to an enclosing state on a commit.
@@ -4454,7 +4458,7 @@ object Types {
44544458
assert(tp ne this, s"self instantiation of ${tp.show}, constraint = ${ctx.typerState.constraint.show}")
44554459
typr.println(s"instantiating ${this.show} with ${tp.show}")
44564460
if ((ctx.typerState eq owningState.get) && !TypeComparer.subtypeCheckInProgress)
4457-
inst = tp
4461+
setInst(tp)
44584462
ctx.typerState.constraint = ctx.typerState.constraint.replace(origin, tp)
44594463
tp
44604464
}
@@ -4593,10 +4597,16 @@ object Types {
45934597
myReduced =
45944598
trace(i"reduce match type $this $hashCode", matchTypes, show = true) {
45954599
def matchCases(cmp: TrackingTypeComparer): Type =
4600+
val saved = ctx.typerState.snapshot()
45964601
try cmp.matchCases(scrutinee.normalized, cases)
45974602
catch case ex: Throwable =>
45984603
handleRecursive("reduce type ", i"$scrutinee match ...", ex)
4599-
finally updateReductionContext(cmp.footprint)
4604+
finally
4605+
updateReductionContext(cmp.footprint)
4606+
ctx.typerState.resetTo(saved)
4607+
// this drops caseLambdas in constraint and undoes any typevar
4608+
// instantiations during matchtype reduction
4609+
46004610
TypeComparer.tracked(matchCases)
46014611
}
46024612
myReduced

compiler/src/dotty/tools/dotc/transform/TreeChecker.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ class TreeChecker extends Phase with SymTransformer {
128128
report.echo(s"checking ${ctx.compilationUnit} after phase ${fusedPhase}")(using ctx)
129129

130130
inContext(ctx) {
131+
assert(ctx.typerState.constraint.domainLambdas.isEmpty,
132+
i"non-empty constraint at end of $fusedPhase: ${ctx.typerState.constraint}, ownedVars = ${ctx.typerState.ownedVars.toList}%, %")
131133
assertSelectWrapsNew(ctx.compilationUnit.tpdTree)
132134
}
133135

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,7 +3247,7 @@ class Typer extends Namer
32473247
replaceSingletons(tp)
32483248
}
32493249
wtp.paramInfos.foreach(instantiate)
3250-
val constr = ctx.typerState.constraint
3250+
val saved = ctx.typerState.snapshot()
32513251

32523252
def dummyArg(tp: Type) = untpd.Ident(nme.???).withTypeUnchecked(tp)
32533253

@@ -3347,8 +3347,9 @@ class Typer extends Namer
33473347
if (propFail.exists) {
33483348
// If there are several arguments, some arguments might already
33493349
// have influenced the context, binding variables, but later ones
3350-
// might fail. In that case the constraint needs to be reset.
3351-
ctx.typerState.constraint = constr
3350+
// might fail. In that case the constraint and instantiated variables
3351+
// need to be reset.
3352+
ctx.typerState.resetTo(saved)
33523353

33533354
// If method has default params, fall back to regular application
33543355
// where all inferred implicits are passed as named args.

tests/neg/i9568.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
| .
88
| I found:
99
|
10-
| Test.blaMonad[Nothing, S](Test.blaMonad[F, S])
10+
| Test.blaMonad[F, S](Test.blaMonad[F, S])
1111
|
12-
| But method blaMonad in object Test does not match type => Monad[Nothing].
12+
| But method blaMonad in object Test does not match type => Monad[F].

0 commit comments

Comments
 (0)