Skip to content

Commit b546ece

Browse files
authored
Merge pull request #12703 from dotty-staging/fix-12677
Don't allow wildcard types in constraints
2 parents ce6a47b + 25e311f commit b546ece

File tree

7 files changed

+85
-84
lines changed

7 files changed

+85
-84
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ object Config {
3434
*/
3535
inline val checkConstraintsPropagated = false
3636

37+
/** Check that constraint bounds do not contain wildcard types */
38+
inline val checkNoWildcardsInConstraint = false
39+
3740
/** If a constraint is over a type lambda `tl` and `tvar` is one of
3841
* the type variables associated with `tl` in the constraint, check
3942
* that the origin of `tvar` is a parameter of `tl`.

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

+18-80
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Flags._
1010
import config.Config
1111
import config.Printers.typr
1212
import reporting.trace
13+
import typer.ProtoTypes.newTypeVar
1314
import StdNames.tpnme
1415

1516
/** Methods for adding constraints and solving them.
@@ -78,22 +79,29 @@ trait ConstraintHandling {
7879
def fullBounds(param: TypeParamRef)(using Context): TypeBounds =
7980
nonParamBounds(param).derivedTypeBounds(fullLowerBound(param), fullUpperBound(param))
8081

81-
protected def addOneBound(param: TypeParamRef, bound: Type, isUpper: Boolean)(using Context): Boolean =
82+
/** If true, eliminate wildcards in bounds by avoidance, otherwise replace
83+
* them by fresh variables.
84+
*/
85+
protected def approximateWildcards: Boolean = true
86+
87+
protected def addOneBound(param: TypeParamRef, rawBound: Type, isUpper: Boolean)(using Context): Boolean =
8288
if !constraint.contains(param) then true
83-
else if !isUpper && param.occursIn(bound) then
89+
else if !isUpper && param.occursIn(rawBound) then
8490
// We don't allow recursive lower bounds when defining a type,
8591
// so we shouldn't allow them as constraints either.
8692
false
8793
else
94+
val dropWildcards = new AvoidWildcardsMap:
95+
if !isUpper then variance = -1
96+
override def mapWild(t: WildcardType) =
97+
if approximateWildcards then super.mapWild(t)
98+
else newTypeVar(apply(t.effectiveBounds).toBounds)
99+
val bound = dropWildcards(rawBound)
88100
val oldBounds @ TypeBounds(lo, hi) = constraint.nonParamBounds(param)
89101
val equalBounds = (if isUpper then lo else hi) eq bound
90-
if equalBounds
91-
&& !bound.existsPart(bp => bp.isInstanceOf[WildcardType] || (bp eq param))
92-
then
93-
// The narrowed bounds are equal and do not contain wildcards,
102+
if equalBounds && !bound.existsPart(_ eq param, stopAtStatic = true) then
103+
// The narrowed bounds are equal and not recursive,
94104
// so we can remove `param` from the constraint.
95-
// (Handling wildcards requires choosing a bound, but we don't know which
96-
// bound to choose here, this is handled in `ConstraintHandling#approximation`)
97105
constraint = constraint.replace(param, bound)
98106
true
99107
else
@@ -245,81 +253,11 @@ trait ConstraintHandling {
245253
* @pre `param` is in the constraint's domain.
246254
*/
247255
final def approximation(param: TypeParamRef, fromBelow: Boolean)(using Context): Type =
248-
249-
/** Substitute wildcards with fresh TypeParamRefs, to be compared with
250-
* other bound, so that they can be instantiated.
251-
*/
252-
object substWildcards extends TypeMap:
253-
override def stopAtStatic = true
254-
255-
var trackedPolis: List[PolyType] = Nil
256-
def apply(tp: Type) = tp match
257-
case tp: WildcardType =>
258-
val poly = PolyType(tpnme.EMPTY :: Nil)(pt => tp.bounds :: Nil, pt => defn.AnyType)
259-
trackedPolis = poly :: trackedPolis
260-
poly.paramRefs.head
261-
case _ =>
262-
mapOver(tp)
263-
end substWildcards
264-
265-
/** Replace TypeParamRefs substituted for wildcards by `substWildCards`
266-
* and any remaining wildcards by a safe approximation
267-
*/
268-
val replaceWildcards = new TypeMap:
269-
override def stopAtStatic = true
270-
271-
/** Try to instantiate a wildcard or TypeParamRef representing a wildcard
272-
* to a type that is known to conform to it.
273-
* This means:
274-
* If fromBelow is true, we minimize the type overall
275-
* Hence, if variance < 0, pick the maximal safe type: bounds.lo
276-
* (i.e. the whole bounds range is over the type).
277-
* If variance > 0, pick the minimal safe type: bounds.hi
278-
* (i.e. the whole bounds range is under the type).
279-
* If variance == 0, pick bounds.lo anyway (this is arbitrary but in line with
280-
* the principle that we pick the smaller type when in doubt).
281-
* If fromBelow is false, we maximize the type overall and reverse the bounds
282-
* If variance != 0. For variance == 0, we still minimize.
283-
* In summary we pick the bound given by this table:
284-
*
285-
* variance | -1 0 1
286-
* ------------------------
287-
* from below | lo lo hi
288-
* from above | hi lo lo
289-
*/
290-
def pickOneBound(bounds: TypeBounds) =
291-
if variance == 0 || fromBelow == (variance < 0) then bounds.lo
292-
else bounds.hi
293-
294-
def apply(tp: Type) = mapOver {
295-
tp match
296-
case tp: WildcardType =>
297-
pickOneBound(tp.bounds)
298-
case tp: TypeParamRef if substWildcards.trackedPolis.contains(tp.binder) =>
299-
pickOneBound(fullBounds(tp))
300-
case _ => tp
301-
}
302-
end replaceWildcards
303-
304256
constraint.entry(param) match
305257
case entry: TypeBounds =>
306258
val useLowerBound = fromBelow || param.occursIn(entry.hi)
307-
val rawBound = if useLowerBound then fullLowerBound(param) else fullUpperBound(param)
308-
val bound = substWildcards(rawBound)
309-
val inst =
310-
if bound eq rawBound then bound
311-
else
312-
// Get rid of wildcards by mapping them to fresh TypeParamRefs
313-
// with constraints derived from comparing both bounds, and then
314-
// instantiating. See pos/i10161.scala for a test where this matters.
315-
val saved = constraint
316-
try
317-
for poly <- substWildcards.trackedPolis do addToConstraint(poly, Nil)
318-
if useLowerBound then bound <:< fullUpperBound(param)
319-
else fullLowerBound(param) <:< bound
320-
replaceWildcards(bound)
321-
finally constraint = saved
322-
typr.println(s"approx ${param.show}, from below = $fromBelow, bound = ${bound.show}, inst = ${inst.show}")
259+
val inst = if useLowerBound then fullLowerBound(param) else fullUpperBound(param)
260+
typr.println(s"approx ${param.show}, from below = $fromBelow, inst = ${inst.show}")
323261
inst
324262
case inst =>
325263
assert(inst.exists, i"param = $param\nconstraint = $constraint")

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,11 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
280280
var current = this
281281
val todos = new mutable.ListBuffer[(OrderingConstraint, TypeParamRef) => OrderingConstraint]
282282
var i = 0
283+
val dropWildcards = AvoidWildcardsMap()
283284
while (i < poly.paramNames.length) {
284285
val param = poly.paramRefs(i)
285-
val stripped = stripParams(nonParamBounds(param), todos, isUpper = true)
286+
val bounds = dropWildcards(nonParamBounds(param))
287+
val stripped = stripParams(bounds, todos, isUpper = true)
286288
current = updateEntry(current, param, stripped)
287289
while todos.nonEmpty do
288290
current = todos.head(current, param)
@@ -376,6 +378,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
376378
Nil
377379

378380
private def updateEntry(current: This, param: TypeParamRef, tp: Type)(using Context): This = {
381+
if Config.checkNoWildcardsInConstraint then assert(!tp.containsWildcardTypes)
379382
var current1 = boundsLens.update(this, current, param, tp)
380383
tp match {
381384
case TypeBounds(lo, hi) =>

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

+10
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
139139
try topLevelSubType(tp1, tp2)
140140
finally useNecessaryEither = saved
141141

142+
/** Use avoidance to get rid of wildcards in constraint bounds if
143+
* we are doing a necessary comparison, or the mode is TypeVarsMissContext.
144+
* The idea is that under either of these conditions we are not interested
145+
* in creating a fresh type variable to replace the wildcard. I verified
146+
* that several tests break if one or the other part of the disjunction is dropped.
147+
* (for instance, i12677.scala demands `useNecessaryEither` in the condition)
148+
*/
149+
override protected def approximateWildcards: Boolean =
150+
useNecessaryEither || ctx.mode.is(Mode.TypevarsMissContext)
151+
142152
def testSubType(tp1: Type, tp2: Type): CompareResult =
143153
GADTused = false
144154
if !topLevelSubType(tp1, tp2) then CompareResult.Fail

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

+15-1
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ object Types {
439439

440440
/** Does this type contain wildcard types? */
441441
final def containsWildcardTypes(using Context) =
442-
existsPart(_.isInstanceOf[WildcardType], stopAtStatic = true)
442+
existsPart(_.isInstanceOf[WildcardType], stopAtStatic = true, forceLazy = false)
443443

444444
// ----- Higher-order combinators -----------------------------------
445445

@@ -5053,6 +5053,11 @@ object Types {
50535053

50545054
/** Wildcard type, possibly with bounds */
50555055
abstract case class WildcardType(optBounds: Type) extends CachedGroundType with TermType {
5056+
5057+
def effectiveBounds(using Context): TypeBounds = optBounds match
5058+
case bounds: TypeBounds => bounds
5059+
case _ => TypeBounds.empty
5060+
50565061
def derivedWildcardType(optBounds: Type)(using Context): WildcardType =
50575062
if (optBounds eq this.optBounds) this
50585063
else if (!optBounds.exists) WildcardType
@@ -5696,6 +5701,15 @@ object Types {
56965701
lo.toText(printer) ~ ".." ~ hi.toText(printer)
56975702
}
56985703

5704+
/** Approximate wildcards by their bounds */
5705+
class AvoidWildcardsMap(using Context) extends ApproximatingTypeMap:
5706+
protected def mapWild(t: WildcardType) =
5707+
val bounds = t.effectiveBounds
5708+
range(atVariance(-variance)(apply(bounds.lo)), apply(bounds.hi))
5709+
def apply(t: Type): Type = t match
5710+
case t: WildcardType => mapWild(t)
5711+
case _ => mapOver(t)
5712+
56995713
// ----- TypeAccumulators ----------------------------------------------------
57005714

57015715
abstract class TypeAccumulator[T](implicit protected val accCtx: Context)

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -707,8 +707,10 @@ object ProtoTypes {
707707
if wildcardOnly
708708
|| ctx.mode.is(Mode.TypevarsMissContext)
709709
|| !ref.underlying.widenExpr.isValueTypeOrWildcard
710-
then WildcardType
711-
else newDepTypeVar(ref)
710+
then
711+
WildcardType(ref.underlying.substParams(mt, mt.paramRefs.map(_ => WildcardType)).toBounds)
712+
else
713+
newDepTypeVar(ref)
712714
mt.resultType.substParams(mt, mt.paramRefs.map(replacement))
713715
else mt.resultType
714716

tests/pos/i12677.scala

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
class F[A]
2+
object F {
3+
def apply[A](a: => A) = new F[A]
4+
}
5+
6+
trait TC[A] { type Out }
7+
object TC {
8+
implicit def tc[A]: TC[A] { type Out = String } = ???
9+
}
10+
11+
// ====================================================================================
12+
object Bug {
13+
final class CustomHook[A] {
14+
def blah(implicit tc: TC[A]): CustomHook[tc.Out] = ???
15+
}
16+
17+
def i: CustomHook[Int] = ???
18+
val f = F(i.blah)
19+
f: F[CustomHook[String]] // error
20+
}
21+
22+
// ====================================================================================
23+
object Workaround {
24+
final class CustomHook[A] {
25+
def blah[B](implicit tc: TC[A] { type Out = B }): CustomHook[B] = ??? // raise type
26+
}
27+
28+
def i: CustomHook[Int] = ???
29+
val f = F(i.blah)
30+
f: F[CustomHook[String]] // works
31+
}

0 commit comments

Comments
 (0)