Skip to content

Commit c47a2d3

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 c47a2d3

File tree

5 files changed

+73
-24
lines changed

5 files changed

+73
-24
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 & 7 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")
@@ -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.find(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,6 +169,12 @@ 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)
@@ -180,7 +183,15 @@ class TyperState() {
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

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)