Skip to content

Commit

Permalink
Fix #19607: Allow to instantiate *wildcard* type captures to TypeBoun…
Browse files Browse the repository at this point in the history
…ds. (#19627)

When matching in a match type, if we encounter a `TypeBounds` scrutinee
and we have a wildcard capture on the right, we used to pick the `hi`
bound "because anything between between `lo` and `hi` would work".

It turns out that *nothing* between `lo` and `hi` works when the type
constructor is invariant. Instead, we must be keep the type bounds, and
instantiate the wildcard capture to a wildcard type argument.

This is fine because a wildcard capture can never be referred to in the
body of the case. However, previously this could never happen in
successful cases, and we therefore used the presence of a `TypeBounds`
in the `instances` as the canonical signal for "fail as not specific".
We now use a separate `noInstances` list to be that signal.

This change departs from the letter of the spec but not from its spirit.
As evidenced by the wording, the spec always *intended* for "the pick"
to one that would always succeed. We wrongly assumed `hi` was always
working.

---

Companion PR to fix the spec/SIP:
scala/improvement-proposals#77
  • Loading branch information
odersky authored Feb 15, 2024
2 parents fe77e3f + 784497d commit fc593df
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 18 deletions.
41 changes: 23 additions & 18 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3388,29 +3388,38 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) {
// Actual matching logic

val instances = Array.fill[Type](spec.captureCount)(NoType)
val noInstances = mutable.ListBuffer.empty[(TypeName, TypeBounds)]

def rec(pattern: MatchTypeCasePattern, scrut: Type, variance: Int, scrutIsWidenedAbstract: Boolean): Boolean =
pattern match
case MatchTypeCasePattern.Capture(num, isWildcard) =>
case MatchTypeCasePattern.Capture(num, /* isWildcard = */ true) =>
// instantiate the wildcard in a way that the subtype test always succeeds
instances(num) = variance match
case 1 => scrut.hiBound // actually important if we are not in a class type constructor
case -1 => scrut.loBound
case 0 => scrut
!instances(num).isError

case MatchTypeCasePattern.Capture(num, /* isWildcard = */ false) =>
def failNotSpecific(bounds: TypeBounds): TypeBounds =
noInstances += spec.origMatchCase.paramNames(num) -> bounds
bounds

instances(num) = scrut match
case scrut: TypeBounds =>
if isWildcard then
// anything will do, as long as it conforms to the bounds for the subsequent `scrut <:< instantiatedPat` test
scrut.hi
else if scrutIsWidenedAbstract then
// always keep the TypeBounds so that we can report the correct NoInstances
scrut
if scrutIsWidenedAbstract then
failNotSpecific(scrut)
else
variance match
case 1 => scrut.hi
case -1 => scrut.lo
case 0 => scrut
case 0 => failNotSpecific(scrut)
case _ =>
if !isWildcard && scrutIsWidenedAbstract && variance != 0 then
// force a TypeBounds to report the correct NoInstances
if scrutIsWidenedAbstract && variance != 0 then
// fail as not specific
// the Nothing and Any bounds are used so that they are not displayed; not for themselves in particular
if variance > 0 then TypeBounds(defn.NothingType, scrut)
else TypeBounds(scrut, defn.AnyType)
if variance > 0 then failNotSpecific(TypeBounds(defn.NothingType, scrut))
else failNotSpecific(TypeBounds(scrut, defn.AnyType))
else
scrut
!instances(num).isError
Expand Down Expand Up @@ -3486,12 +3495,8 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) {
MatchResult.Stuck

if rec(spec.pattern, scrut, variance = 1, scrutIsWidenedAbstract = false) then
if instances.exists(_.isInstanceOf[TypeBounds]) then
MatchResult.NoInstance {
constrainedCaseLambda.paramNames.zip(instances).collect {
case (name, bounds: TypeBounds) => (name, bounds)
}
}
if noInstances.nonEmpty then
MatchResult.NoInstance(noInstances.toList)
else
val defn.MatchCase(instantiatedPat, reduced) =
instantiateParamsSpec(instances, constrainedCaseLambda)(constrainedCaseLambda.resultType): @unchecked
Expand Down
12 changes: 12 additions & 0 deletions tests/pos/i19607.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
trait Foo
trait Bar[T]

type MatchType[T] = T match
case Bar[?] => Nothing
case _ => T

object Test:
def foo(b: Bar[? >: Foo]): Unit =
summon[MatchType[b.type] =:= Nothing]
summon[MatchType[Bar[? >: Foo]] =:= Nothing]
end Test

0 comments on commit fc593df

Please sign in to comment.