Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion compiler/src/dotty/tools/dotc/inlines/Inliner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +966 to +971

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks pretty ad-hoc for this specific case, if the problem is selectionType creates different skolem, can't we make those methods idempotent? Why Inliner.typedSelect special? I guess all other selectionType call-sites also have the same foot-gun.

val reselectedType = tree.typeOpt match
case tpe: NamedType
if tpe.denotationIsCurrent

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this condition is required?

&& skolemizesPrefix(tpe.prefix, tree.name)
&& tpe.prefix == qual1.tpe.widenIfUnstable => tpe

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe PR description says that when we encounter the same selection path, we should keep the existing denotation that already contains the skolem.

However, this condition seems weaker than that, it only checks whether the prefix type is the same after widening. When make different unstable prefixes end up as the same type, this condition can't distinguish between them, and I'm worried it might end up wrongly reusing a denotation (contains skolems) meant for a different qualifier. Why is this condition sufficient?

case _ => selectionType(tree, qual1)

def isConcreteImplementationOf(reselected: Symbol, overridden: Symbol)(using Context): Boolean =
reselected.isTerm
Expand Down
11 changes: 10 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
25 changes: 25 additions & 0 deletions tests/neg/i26681b.scala
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions tests/run/i26681.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ok
ok
23 changes: 23 additions & 0 deletions tests/run/i26681.scala
Original file line number Diff line number Diff line change
@@ -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))
Loading