From c529ac28bd03c9deacb1daff0279a90e3dc95b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 4 Oct 2024 10:00:40 +0200 Subject: [PATCH] Fix #21402: Always allow type member extraction for stable scrutinees 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. --- .../dotty/tools/dotc/core/TypeComparer.scala | 40 +++++++++++++----- tests/pos/i21402.scala | 41 +++++++++++++++++++ .../match-type-extract-path-dependent.scala | 27 ++++++++++++ 3 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 tests/pos/i21402.scala create mode 100644 tests/pos/match-type-extract-path-dependent.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8fc6307c426c..145a038dd856 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -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 diff --git a/tests/pos/i21402.scala b/tests/pos/i21402.scala new file mode 100644 index 000000000000..4ddf201ef8b4 --- /dev/null +++ b/tests/pos/i21402.scala @@ -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 + } +} diff --git a/tests/pos/match-type-extract-path-dependent.scala b/tests/pos/match-type-extract-path-dependent.scala new file mode 100644 index 000000000000..68c902ef0b8c --- /dev/null +++ b/tests/pos/match-type-extract-path-dependent.scala @@ -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