Skip to content

Commit d871df6

Browse files
committed
Fix constraint merging in FunProto
After the previous commit a few tests started failing due to TyperStates attempting to instantiate a type variable with an owningState pointing to a different TyperState. The issue is that after `mergeConstraintWith`, multiple TyperState can own the same type variable. This is fine as long as only one of them is committable since the other ones won't attempt to instantiate any type variable, but that's not necessarily the case in FunProto#typedArgs where we merge the constraints of the FunProto context into the passed context. This commit fixes this by instantiating any type variable in the constraints of the FunProto which do not exist in the passed TyperState before calling `mergeConstraintWith`. This ensures that merging can be done without changing the ownership of any type variable, thus keeping both TyperState safely committable.
1 parent d387e8f commit d871df6

File tree

5 files changed

+73
-20
lines changed

5 files changed

+73
-20
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: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ class TyperState() {
8282
/** The set of uninstantiated type variables which have this state as their owning state.
8383
*
8484
* Invariant:
85-
* `tstate.ownedVars.contains(tvar)` iff `tvar.owningState.get eq tstate`
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")
@@ -161,8 +163,7 @@ class TyperState() {
161163
constraint.typeVarOfParam(tl.paramRefs(0)) ne other.typeVarOfParam(tl.paramRefs(0))
162164
// Note: Since TypeVars are allocated in bulk for each type lambda, we only
163165
// 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+
val conflicting = constraint.domainLambdas.find(constraint.hasConflictingTypeVarsFor(_, other))
166167
for tl <- conflicting do
167168
val tl1 = constraint.ensureFresh(tl)
168169
for case (tvar: TypeVar, pref1) <- tl.paramRefs.map(constraint.typeVarOfParam).lazyZip(tl1.paramRefs) do
@@ -172,6 +173,12 @@ class TyperState() {
172173
ts.constraint = ts.constraint.subst(tl, tl1)
173174
ts = ts.previous
174175

176+
/** Integrate the constraints from `that` into this TyperState.
177+
*
178+
* @pre If `that` is committable, it must not contain any type variable which
179+
* does not exist in `this` (in other words, all its type variables must
180+
* be owned by a common parent of `this` and `that`).
181+
*/
175182
def mergeConstraintWith(that: TyperState)(using Context): Unit =
176183
that.ensureNotConflicting(constraint)
177184
constraint = constraint & (that.constraint, otherHasErrors = that.reporter.errorsReported)
@@ -180,7 +187,15 @@ class TyperState() {
180187
for tl <- constraint.domainLambdas do
181188
if constraint.isRemovable(tl) then constraint = constraint.remove(tl)
182189

190+
/** Take ownership of `tvar`.
191+
*
192+
* @pre `tvar` is not owned by a committable TyperState. This ensures
193+
* each tvar can only be instantiated by one TyperState.
194+
*/
183195
private def includeVar(tvar: TypeVar)(using Context): Unit =
196+
val oldState = tvar.owningState.get
197+
assert(oldState == null || !oldState.isCommittable,
198+
i"$this attempted to take ownership of $tvar which is already owned by committable $oldState")
184199
tvar.owningState = new WeakReference(this)
185200
ownedVars += tvar
186201

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)