Skip to content

Commit

Permalink
Fix #21402: Always allow type member extraction for stable scrutinees…
Browse files Browse the repository at this point in the history
… in match types. (#21700)

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 authored Oct 8, 2024
2 parents 6c7619a + c529ac2 commit 2023c5d
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 2023c5d

Please sign in to comment.