diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 64fa2b8abf12..1125e09539b6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -767,7 +767,22 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val qual1 = qual.cast(liftedTp) val tree1 = cpy.Select(tree0)(qual1, selName) val rawType1 = selectionType(tree1, qual1) - tryType(tree1, qual1, rawType1) + val adapted = tryType(tree1, qual1, rawType1) + if !adapted.isEmpty && sourceVersion == `3.6-migration` then + val adaptedOld = tryExt(tree, qual) + if !adaptedOld.isEmpty then + val symOld = adaptedOld.symbol + val underlying = liftedTp match + case tp: TypeProxy => i" ${tp.translucentSuperType}" + case _ => "" + report.migrationWarning( + em"""Previously this selected the extension ${symOld}${symOld.showExtendedLocation} + |Now it selects $selName on the opaque type's underlying type$underlying + | + |You can change this back by selecting $adaptedOld + |Or by defining the extension method outside of the opaque type's scope. + |""", tree0) + adapted else EmptyTree // Otherwise, try to expand a named tuple selection diff --git a/tests/neg/i21239.check b/tests/neg/i21239.check new file mode 100644 index 000000000000..5b6f2f8bcef5 --- /dev/null +++ b/tests/neg/i21239.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/i21239.scala:14:18 ------------------------------------------------------------ +14 | def get2: V = get // error + | ^^^ + | Found: AnyRef + | Required: V + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i21239.orig.check b/tests/neg/i21239.orig.check new file mode 100644 index 000000000000..26895bd50ed3 --- /dev/null +++ b/tests/neg/i21239.orig.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/i21239.orig.scala:32:8 -------------------------------------------------------- +32 | get // error + | ^^^ + | Found: AnyRef + | Required: V + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i21239.orig.scala b/tests/neg/i21239.orig.scala new file mode 100644 index 000000000000..3fb39d93446b --- /dev/null +++ b/tests/neg/i21239.orig.scala @@ -0,0 +1,33 @@ +// 1 +// A re-minimisated reproduction of the original issue in kse3 +// The one in the issue removes the usage of the package +// in the second extension bundle, which is crucial to +// why my change broke this code +package kse.flow + +import java.util.concurrent.atomic.AtomicReference + +opaque type Worm[V] = AtomicReference[AnyRef] +object Worm: + val notSetSentinel: AnyRef = new AnyRef {} + + extension [V](worm: Worm[V]) + inline def wormAsAtomic: AtomicReference[AnyRef] = worm + + extension [V](worm: kse.flow.Worm[V]) + + inline def setIfEmpty(v: => V): Boolean = + var old = worm.wormAsAtomic.get() + if old eq Worm.notSetSentinel then + worm.wormAsAtomic.compareAndSet(old, v.asInstanceOf[AnyRef]) + else false + + inline def get: V = worm.wormAsAtomic.get() match + case x if x eq Worm.notSetSentinel => throw new java.lang.IllegalStateException("Retrieved value before being set") + case x => x.asInstanceOf[V] + + inline def getOrSet(v: => V): V = worm.wormAsAtomic.get() match + case x if x eq Worm.notSetSentinel => + setIfEmpty(v) + get // error + case x => x.asInstanceOf[V] diff --git a/tests/neg/i21239.scala b/tests/neg/i21239.scala new file mode 100644 index 000000000000..4eb4d5808857 --- /dev/null +++ b/tests/neg/i21239.scala @@ -0,0 +1,14 @@ +// 2 +// A more minimised reproduction +package lib + +import java.util.concurrent.atomic.AtomicReference + +opaque type Worm[V] = AtomicReference[AnyRef] +object Worm: + extension [V](worm: Worm[V]) + inline def wormAsAtomic: AtomicReference[AnyRef] = worm + + extension [V](worm: lib.Worm[V]) + def get: V = worm.wormAsAtomic.get().asInstanceOf[V] + def get2: V = get // error diff --git a/tests/pos/i21239.alt.scala b/tests/pos/i21239.alt.scala new file mode 100644 index 000000000000..13a1647115f7 --- /dev/null +++ b/tests/pos/i21239.alt.scala @@ -0,0 +1,28 @@ +// 4 +// An alternative way to fix it, +// defining the extension method externally, +// in a scope that doesn't see through +// the opaque type definition. +// The setup here also makes sure those extension +// are on the opaque type's companion object +// (via class extension), meaning that they continue +// to be in implicit scope (as enforced by the usage test) +import java.util.concurrent.atomic.AtomicReference + +package lib: + object Worms: + opaque type Worm[V] = AtomicReference[AnyRef] + object Worm extends WormOps: + extension [V](worm: Worm[V]) + inline def wormAsAtomic: AtomicReference[AnyRef] = worm + + import Worms.Worm + trait WormOps: + extension [V](worm: Worm[V]) + def get: V = worm.wormAsAtomic.get().asInstanceOf[V] + def get2: V = get + +package test: + import lib.Worms.Worm + object Test: + def usage(worm: Worm[String]): String = worm.get2 diff --git a/tests/pos/i21239.orig.scala b/tests/pos/i21239.orig.scala new file mode 100644 index 000000000000..56666bab4b4d --- /dev/null +++ b/tests/pos/i21239.orig.scala @@ -0,0 +1,34 @@ +// 5 +// Finally, an alternative way to fix the original issue, +// by reimplementing `getOrSet` to not even need +// our `get` extension. +import java.util.concurrent.atomic.AtomicReference + +opaque type Worm[V] = AtomicReference[AnyRef] +object Worm: + val notSetSentinel: AnyRef = new AnyRef {} + + extension [V](worm: Worm[V]) + inline def wormAsAtomic: AtomicReference[AnyRef] = worm // deprecate? + + inline def setIfEmpty(v: => V): Boolean = + val x = worm.get() + if x eq notSetSentinel then + val value = v + worm.set(value.asInstanceOf[AnyRef]) + true + else false + + inline def get: V = + val x = worm.get() + if x eq notSetSentinel then + throw IllegalStateException("Retrieved value before being set") + else x.asInstanceOf[V] + + inline def getOrSet(v: => V): V = + val x = worm.get() + if x eq notSetSentinel then + val value = v + worm.set(value.asInstanceOf[AnyRef]) + value + else x.asInstanceOf[V] diff --git a/tests/pos/i21239.scala b/tests/pos/i21239.scala new file mode 100644 index 000000000000..950f90c233d8 --- /dev/null +++ b/tests/pos/i21239.scala @@ -0,0 +1,18 @@ +// 3 +// One way to fix the issue, using the +// "universal function call syntax" +// (to borrow from what Rust calls the syntax to +// disambiguate which trait's method is intended.) +import java.util.concurrent.atomic.AtomicReference + +package lib: + opaque type Worm[V] = AtomicReference[AnyRef] + object Worm: + extension [V](worm: Worm[V]) + def get: V = worm.get().asInstanceOf[V] + def get2: V = Worm.get(worm) + +package test: + import lib.Worm + object Test: + def usage(worm: Worm[String]): String = worm.get2 diff --git a/tests/warn/i21239.Frac.check b/tests/warn/i21239.Frac.check new file mode 100644 index 000000000000..3c2868479f42 --- /dev/null +++ b/tests/warn/i21239.Frac.check @@ -0,0 +1,8 @@ +-- Migration Warning: tests/warn/i21239.Frac.scala:14:8 ---------------------------------------------------------------- +14 | f + Frac.wrap(((-g.numerator).toLong << 32) | (g.unwrap & 0xFFFFFFFFL)) // warn + | ^^^ + | Previously this selected the extension method + in object Frac + | Now it selects + on the opaque type's underlying type Long + | + | You can change this back by selecting kse.maths.Frac.+(f) + | Or by defining the extension method outside of the opaque type's scope. diff --git a/tests/warn/i21239.Frac.scala b/tests/warn/i21239.Frac.scala new file mode 100644 index 000000000000..b09dbfd6ecad --- /dev/null +++ b/tests/warn/i21239.Frac.scala @@ -0,0 +1,15 @@ +package kse.maths + +import scala.language.`3.6-migration` + +opaque type Frac = Long +object Frac { + inline def wrap(f: Long): kse.maths.Frac = f + extension (f: Frac) + inline def unwrap: Long = f + inline def numerator: Int = ((f: Long) >>> 32).toInt + extension (f: kse.maths.Frac) + def +(g: Frac): kse.maths.Frac = f // eliding domain-specific addition logic + def -(g: Frac): kse.maths.Frac = + f + Frac.wrap(((-g.numerator).toLong << 32) | (g.unwrap & 0xFFFFFFFFL)) // warn +}