diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 7e0867f087eb..73a26d6c7c85 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2572,7 +2572,20 @@ object Types extends TypeUtils { } private def memberDenot(prefix: Type, name: Name, allowPrivate: Boolean)(using Context): Denotation = - if (allowPrivate) prefix.member(name) else prefix.nonPrivateMember(name) + // We need a valid prefix for `asSeenFrom`, as in `memberBasedOnFlags`. + val pre0 = prefix match + case prefix: ClassInfo => prefix.appliedRef + case prefix => prefix.widenIfUnstable + // When the prefix of a term selection is not a legal prefix, the typer computes the + // member as seen from a skolem of the prefix, so that a member type that mentions the + // prefix stays precise (see `TypeAssigner.maybeSkolemizePrefix`). Recomputing the + // denotation here -- after unpickling, most importantly -- has to skolemize the same + // way, or the reference silently acquires a more approximate info than the one it was + // pickled with, and the tree no longer survives a pickling round-trip. + val pre = + if name.isTermName && !TypeOps.isLegalPrefix(pre0) then QualSkolemType(pre0) + else pre0 + prefix.findMember(name, pre, excluded = if allowPrivate then EmptyFlags else Private) private def argDenot(param: TypeSymbol)(using Context): Denotation = { val cls = param.owner diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 998cb0b945e4..8b8132e24923 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -963,7 +963,18 @@ class Inliner(val call: tpd.Tree)(using Context): // For instance in tests/pos/i22070 when we type `Featureful[?]#toFeatures`, // `selectionType` will skolemize the prefix, find the denotation, // and then set that denotation for the `TermRef(Featureful[?], symbol toFeatures)`. - val reselectedType = selectionType(tree, qual1) + // But skip it when it cannot select anything else than what we already have: for an + // unstable prefix `selectionType` creates a *fresh* skolem on every call (see + // `maybeSkolemizePrefix`), and since the `NamedType` it returns is shared, computing + // it again silently changes the meaning of the types that were derived from the + // previous denotation. That is how the same selection ended up carrying two different + // skolems in i26681. + val reselectedType = tree.typeOpt match + case tpe: NamedType + if tpe.denotationIsCurrent + && skolemizesPrefix(tpe.prefix, tree.name) + && tpe.prefix == qual1.tpe.widenIfUnstable => tpe + case _ => selectionType(tree, qual1) def isConcreteImplementationOf(reselected: Symbol, overridden: Symbol)(using Context): Boolean = reselected.isTerm diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index d8f7dd32c9bd..1fe8ae1a0a77 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -120,11 +120,20 @@ trait TypeAssigner { * @see QualSkolemType, TypeOps#asSeenFrom */ def maybeSkolemizePrefix(qualType: Type, name: Name)(using Context): Type = - if (name.isTermName && !TypeOps.isLegalPrefix(qualType)) + if skolemizesPrefix(qualType, name) then QualSkolemType(qualType) else qualType + /** Does selecting `name` on `qualType` skolemize the prefix? + * + * Callers that may compute a selection type more than once need this: each call to + * `maybeSkolemizePrefix` makes a *fresh* skolem, so for such a selection recomputing + * the type is not a no-op even when it finds the very same member. + */ + def skolemizesPrefix(qualType: Type, name: Name)(using Context): Boolean = + name.isTermName && !TypeOps.isLegalPrefix(qualType) + /** The type of the selection `tree`, where `qual1` is the typed qualifier part. */ def selectionType(tree: untpd.RefTree, qual1: Tree)(using Context): Type = val qualType = diff --git a/tests/neg/i26681b.scala b/tests/neg/i26681b.scala new file mode 100644 index 000000000000..845cb82410e2 --- /dev/null +++ b/tests/neg/i26681b.scala @@ -0,0 +1,25 @@ +// https://github.com/scala/scala3/issues/26681 +// +// Selecting on an unstable prefix skolemizes that prefix, and each selection must +// get its *own* skolem: two evaluations of `unstable` return two different `Owner`s, +// so their `w` are unrelated and `Box[w.type]` must not unify across occurrences. +// +// Memoizing the `QualSkolemType` per prefix type would make `maybeSkolemizePrefix` +// idempotent -- which is tempting, since a fresh skolem per call is what made the +// same selection carry two different skolems in i26681 -- but it also makes every +// case below compile. This test pins the property so that stays visible. +class Box[T](val x: T) + +class Owner: + val w: Int = 8 + val b: Box[w.type] = new Box(w) + def get: Box[w.type] = new Box(w) + def set(bb: Box[w.type]): Unit = () + +def unstable: Owner = new Owner + +def same[T](a: Box[T], b: Box[T]): Unit = () + +def t1 = same(unstable.b, unstable.b) // error +def t2 = same(unstable.get, unstable.get) // error +def t3 = unstable.set(unstable.get) // error diff --git a/tests/run/i26681.check b/tests/run/i26681.check new file mode 100644 index 000000000000..79ebd0860f49 --- /dev/null +++ b/tests/run/i26681.check @@ -0,0 +1,2 @@ +ok +ok diff --git a/tests/run/i26681.scala b/tests/run/i26681.scala new file mode 100644 index 000000000000..f409aef89978 --- /dev/null +++ b/tests/run/i26681.scala @@ -0,0 +1,23 @@ +// https://github.com/scala/scala3/issues/26681 +class Box[T] + +class Owner: + val w: Int = 8 + val b: Box[w.type] = new Box[w.type] + +def use[T](b: Box[T]): String = "ok" + +def unstable: Owner = new Owner + +transparent inline def f(inline rhs: Any): Any = + inline rhs match + case r: Box[t] => use[t](r) + +@main def Test = + // `unstable.b` has an unstable prefix, so its type is `Owner#b`, whose info is + // computed by skolemizing the prefix. Retyping the selection while inlining used + // to create a second skolem and overwrite the denotation of `Owner#b`, so the type + // `t` inferred when reducing the inline match no longer matched the scrutinee. + println(f(unstable.b)) + val o = unstable + println(f(o.b))