Skip to content

Commit 7599095

Browse files
authored
Merge pull request #12519 from dotty-staging/reown-tvars
Properly re-own type variables when merging constraints
2 parents 74ea1af + acc0e9d commit 7599095

File tree

5 files changed

+84
-34
lines changed

5 files changed

+84
-34
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ abstract class Constraint extends Showable {
162162
*/
163163
def & (other: Constraint, otherHasErrors: Boolean)(using Context): Constraint
164164

165+
/** Whether `tl` is present in both `this` and `that` but is associated with
166+
* different TypeVars there, meaning that the constraints cannot be merged.
167+
*/
168+
def hasConflictingTypeVarsFor(tl: TypeLambda, that: Constraint): Boolean
169+
165170
/** Check that no constrained parameter contains itself as a bound */
166171
def checkNonCyclic()(using Context): this.type
167172

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,12 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
493493
merge(this.upperMap, that.upperMap, mergeParams))
494494
}.showing(i"constraint merge $this with $other = $result", constr)
495495

496+
def hasConflictingTypeVarsFor(tl: TypeLambda, that: Constraint): Boolean =
497+
contains(tl) && that.contains(tl) &&
498+
// Since TypeVars are allocated in bulk for each type lambda, we only have
499+
// to check the first one to find out if some of them are different.
500+
(this.typeVarOfParam(tl.paramRefs(0)) ne that.typeVarOfParam(tl.paramRefs(0)))
501+
496502
def subst(from: TypeLambda, to: TypeLambda)(using Context): OrderingConstraint =
497503
def swapKey[T](m: ArrayValuedMap[T]) = m.remove(from).updated(to, m(from))
498504
var current = newConstraint(swapKey(boundsMap), swapKey(lowerMap), swapKey(upperMap))

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

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,11 @@ class TyperState() {
7979

8080
private var isCommitted: Boolean = _
8181

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.
82+
/** The set of uninstantiated type variables which have this state as their owning state.
83+
*
84+
* Invariant:
85+
* if `tstate.isCommittable` then
86+
* `tstate.ownedVars.contains(tvar)` iff `tvar.owningState.get eq tstate`
8687
*/
8788
private var myOwnedVars: TypeVars = _
8889
def ownedVars: TypeVars = myOwnedVars
@@ -138,6 +139,7 @@ class TyperState() {
138139
def commit()(using Context): Unit = {
139140
Stats.record("typerState.commit")
140141
assert(isCommittable)
142+
setCommittable(false)
141143
val targetState = ctx.typerState
142144
if constraint ne targetState.constraint then
143145
Stats.record("typerState.commit.new constraint")
@@ -157,12 +159,7 @@ class TyperState() {
157159
* in this constraint and its predecessors where necessary.
158160
*/
159161
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))
162+
val conflicting = constraint.domainLambdas.filter(constraint.hasConflictingTypeVarsFor(_, other))
166163
for tl <- conflicting do
167164
val tl1 = constraint.ensureFresh(tl)
168165
for case (tvar: TypeVar, pref1) <- tl.paramRefs.map(constraint.typeVarOfParam).lazyZip(tl1.paramRefs) do
@@ -172,15 +169,29 @@ class TyperState() {
172169
ts.constraint = ts.constraint.subst(tl, tl1)
173170
ts = ts.previous
174171

172+
/** Integrate the constraints from `that` into this TyperState.
173+
*
174+
* @pre If `that` is committable, it must not contain any type variable which
175+
* does not exist in `this` (in other words, all its type variables must
176+
* be owned by a common parent of `this` and `that`).
177+
*/
175178
def mergeConstraintWith(that: TyperState)(using Context): Unit =
176179
that.ensureNotConflicting(constraint)
177180
constraint = constraint & (that.constraint, otherHasErrors = that.reporter.errorsReported)
178181
for tvar <- constraint.uninstVars do
179-
if !isOwnedAnywhere(this, tvar) then ownedVars += tvar
182+
if !isOwnedAnywhere(this, tvar) then includeVar(tvar)
180183
for tl <- constraint.domainLambdas do
181184
if constraint.isRemovable(tl) then constraint = constraint.remove(tl)
182185

186+
/** Take ownership of `tvar`.
187+
*
188+
* @pre `tvar` is not owned by a committable TyperState. This ensures
189+
* each tvar can only be instantiated by one TyperState.
190+
*/
183191
private def includeVar(tvar: TypeVar)(using Context): Unit =
192+
val oldState = tvar.owningState.get
193+
assert(oldState == null || !oldState.isCommittable,
194+
i"$this attempted to take ownership of $tvar which is already owned by committable $oldState")
184195
tvar.owningState = new WeakReference(this)
185196
ownedVars += tvar
186197

@@ -196,12 +207,13 @@ class TyperState() {
196207
Stats.record("typerState.gc")
197208
val toCollect = new mutable.ListBuffer[TypeLambda]
198209
for tvar <- ownedVars do
199-
if !tvar.inst.exists then // See comment of `ownedVars` for why this test is necessary
200-
val inst = constraint.instType(tvar)
201-
if inst.exists then
202-
tvar.setInst(inst)
203-
val tl = tvar.origin.binder
204-
if constraint.isRemovable(tl) then toCollect += tl
210+
assert(tvar.owningState.get eq this, s"Inconsistent state in $this: it owns $tvar whose owningState is ${tvar.owningState.get}")
211+
assert(!tvar.inst.exists, s"Inconsistent state in $this: it owns $tvar which is already instantiated")
212+
val inst = constraint.instType(tvar)
213+
if inst.exists then
214+
tvar.setInst(inst)
215+
val tl = tvar.origin.binder
216+
if constraint.isRemovable(tl) then toCollect += tl
205217
for tl <- toCollect do
206218
constraint = constraint.remove(tl)
207219

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -979,8 +979,10 @@ trait Implicits:
979979
val result =
980980
result0 match {
981981
case result: SearchSuccess =>
982-
result.tstate.commit()
983-
ctx.gadt.restore(result.gstate)
982+
if result.tstate ne ctx.typerState then
983+
result.tstate.commit()
984+
if result.gstate ne ctx.gadt then
985+
ctx.gadt.restore(result.gstate)
984986
if hasSkolem(false, result.tree) then
985987
report.error(SkolemInInferred(result.tree, pt, argument), ctx.source.atSpan(span))
986988
implicits.println(i"success: $result")

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

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -385,21 +385,46 @@ object ProtoTypes {
385385
* before it is typed. The second Int parameter is the parameter index.
386386
*/
387387
def typedArgs(norm: (untpd.Tree, Int) => untpd.Tree = sameTree)(using Context): List[Tree] =
388-
if (state.typedArgs.size == args.length) state.typedArgs
389-
else {
390-
val prevConstraint = protoCtx.typerState.constraint
391-
392-
try
393-
inContext(protoCtx) {
394-
val args1 = args.mapWithIndexConserve((arg, idx) =>
395-
cacheTypedArg(arg, arg => typer.typed(norm(arg, idx)), force = false))
396-
if !args1.exists(arg => isUndefined(arg.tpe)) then state.typedArgs = args1
397-
args1
398-
}
399-
finally
400-
if (protoCtx.typerState.constraint ne prevConstraint)
401-
ctx.typerState.mergeConstraintWith(protoCtx.typerState)
402-
}
388+
if state.typedArgs.size == args.length then state.typedArgs
389+
else
390+
val passedTyperState = ctx.typerState
391+
inContext(protoCtx) {
392+
val protoTyperState = protoCtx.typerState
393+
val oldConstraint = protoTyperState.constraint
394+
val args1 = args.mapWithIndexConserve((arg, idx) =>
395+
cacheTypedArg(arg, arg => typer.typed(norm(arg, idx)), force = false))
396+
val newConstraint = protoTyperState.constraint
397+
398+
if !args1.exists(arg => isUndefined(arg.tpe)) then state.typedArgs = args1
399+
400+
// We only need to propagate constraints if we typed the arguments in a different
401+
// TyperState and if that created additional constraints.
402+
if (passedTyperState ne protoTyperState) && (oldConstraint ne newConstraint) then
403+
// To respect the pre-condition of `mergeConstraintWith` and keep
404+
// `protoTyperState` committable we must ensure that it does not
405+
// contain any type variable which don't already exist in the passed
406+
// TyperState. This is achieved by instantiating any such type
407+
// variable.
408+
if protoTyperState.isCommittable then
409+
val passedConstraint = passedTyperState.constraint
410+
val newLambdas = newConstraint.domainLambdas.filter(tl =>
411+
!passedConstraint.contains(tl) || passedConstraint.hasConflictingTypeVarsFor(tl, newConstraint))
412+
val newTvars = newLambdas.flatMap(_.paramRefs).map(newConstraint.typeVarOfParam)
413+
414+
args1.foreach(arg => Inferencing.instantiateSelected(arg.tpe, newTvars))
415+
416+
// `instantiateSelected` can leave some type variables uninstantiated,
417+
// so we maximize them in a second pass.
418+
newTvars.foreach {
419+
case tvar: TypeVar if !tvar.isInstantiated =>
420+
tvar.instantiate(fromBelow = false)
421+
case _ =>
422+
}
423+
424+
passedTyperState.mergeConstraintWith(protoTyperState)
425+
end if
426+
args1
427+
}
403428

404429
/** Type single argument and remember the unadapted result in `myTypedArg`.
405430
* used to avoid repeated typings of trees when backtracking.

0 commit comments

Comments
 (0)