Skip to content

Commit

Permalink
Fix scala#21402: Always allow type member extraction for stable scrut…
Browse files Browse the repository at this point in the history
…inees in match types.

Previously, through the various code paths, we basically allowed
type member extraction for stable scrutinees if the type member
was an alias or a class member. In the alias case, we took the
alias, whereas in the class case, we recreated a selection on the
stable scrutinee. We did not allow that on abstract type members.

We now uniformly do it for all kinds of type members. If the
scrutinee is a (non-skolem) stable type, we do not even look at
the info of the type member. We directly create a selection to it,
which corresponds to what we did before for class members.

We only try to dealias type members if the scrutinee type is not
a stable type.
  • Loading branch information
sjrd committed Oct 8, 2024
1 parent a672e05 commit c529ac2
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 11 deletions.
40 changes: 29 additions & 11 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3681,19 +3681,37 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) {

stableScrut.member(typeMemberName) match
case denot: SingleDenotation if denot.exists =>
val info = denot.info match
case alias: AliasingBounds => alias.alias // Extract the alias
case ClassInfo(prefix, cls, _, _, _) => prefix.select(cls) // Re-select the class from the prefix
case info => info // Notably, RealTypeBounds, which will eventually give a MatchResult.NoInstances
val info1 = stableScrut match
val info = stableScrut match
case skolem: SkolemType =>
dropSkolem(info, skolem).orElse:
info match
case info: TypeBounds => info // Will already trigger a MatchResult.NoInstances
case _ => RealTypeBounds(info, info) // Explicitly trigger a MatchResult.NoInstances
case _ => info
rec(capture, info1, variance = 0, scrutIsWidenedAbstract)
/* If it is a skolem type, we cannot have class selections nor
* abstract type selections. If it is an alias, we try to remove
* any reference to the skolem from the right-hand-side. If that
* succeeds, we take the result, otherwise we fail as not-specific.
*/

def adaptToTriggerNotSpecific(info: Type): Type = info match
case info: TypeBounds => info
case _ => RealTypeBounds(info, info)

denot.info match
case denotInfo: AliasingBounds =>
val alias = denotInfo.alias
dropSkolem(alias, skolem).orElse(adaptToTriggerNotSpecific(alias))
case ClassInfo(prefix, cls, _, _, _) =>
// for clean error messages
adaptToTriggerNotSpecific(prefix.select(cls))
case denotInfo =>
adaptToTriggerNotSpecific(denotInfo)

case _ =>
// The scrutinee type is truly stable. We select the type member directly on it.
stableScrut.select(typeMemberName)
end info

rec(capture, info, variance = 0, scrutIsWidenedAbstract)

case _ =>
// The type member was not found; no match
false
end rec

Expand Down
41 changes: 41 additions & 0 deletions tests/pos/i21402.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
abstract class AbstractServiceKey:
type Protocol

abstract class ServiceKey[T] extends AbstractServiceKey:
type Protocol = T

type Aux[P] = AbstractServiceKey { type Protocol = P }
type Service[K <: Aux[?]] = K match
case Aux[t] => ActorRef[t]
type Subscriber[K <: Aux[?]] = K match
case Aux[t] => ActorRef[ReceptionistMessages.Listing[t]]

trait ActorRef[-T]

object ReceptionistMessages:
final case class Listing[T](key: ServiceKey[T])

class TypedMultiMap[T <: AnyRef, K[_ <: T]]:
def get(key: T): Set[K[key.type]] = ???
transparent inline def getInlined(key: T): Set[K[key.type]] = ???
inline def inserted(key: T, value: K[key.type]): TypedMultiMap[T, K] = ???

object LocalReceptionist {
final case class State(
services: TypedMultiMap[AbstractServiceKey, Service],
subscriptions: TypedMultiMap[AbstractServiceKey, Subscriber]
):
def testInsert(key: AbstractServiceKey)(serviceInstance: ActorRef[key.Protocol]): State = {
val fails = services.inserted(key, serviceInstance) // error
???
}

def testGet[T](key: AbstractServiceKey): Unit = {
val newState: State = ???
val fails: Set[ActorRef[key.Protocol]] = newState.services.get(key) // error
val works: Set[ActorRef[key.Protocol]] = newState.services.getInlined(key) // workaround

val fails2: Set[ActorRef[ReceptionistMessages.Listing[key.Protocol]]] = newState.subscriptions.get(key) // error
val works2: Set[ActorRef[ReceptionistMessages.Listing[key.Protocol]]] = newState.subscriptions.getInlined(key) // workaround
}
}
27 changes: 27 additions & 0 deletions tests/pos/match-type-extract-path-dependent.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Test that match types can extract path-dependent abstract types out of singleton types

trait Base:
type Value

def getValue(): Value
def setValue(v: Value): Unit
end Base

object Extractor:
type Helper[X] = Base { type Value = X }

type Extract[B <: Base] = B match
case Helper[x] => x
end Extractor

object Test:
import Extractor.Extract

/* As is, this is a bit silly, since we could use `b.Value` instead. However,
* in larger examples with more indirections, it is not always possible to
* directly use the path-dependent version. See i21402 for a real-world use
* case.
*/
def foo(b: Base): Extract[b.type] = b.getValue()
def bar(b: Base, v: Extract[b.type]): Unit = b.setValue(v)
end Test

0 comments on commit c529ac2

Please sign in to comment.