Skip to content

Commit

Permalink
Capture the kse3 issue in test cases
Browse files Browse the repository at this point in the history
The underlying type of an opaque type is only visible to anything within
the scope that contains that opaque type.  So, for instance, a Worm
opaque type in a Worms object, anything within the Worms object sees the
underlying type.  So Worms.Worm is an opaque abstract type with bounds,
while Worms.this.Worm is an opaque type with an underlying type.  But
you can also reference Worms.Worm while being inside of the Worms
object.

The change I made to opaque types allowed for member selection to see
the underlying type when in a scope that can see that, which makes it
consistent with how TypeComparer allows those two types to be seen as
equivalent, when in the right scope.

In kse3, it seems, the fact that this wasn't done was utilised by using
an "external" reference to the opaque type, which avoided the underlying
type's method being selected, and the extension method being selected
instead.

While my change broke kse3, I believe the change is good, as it brings
consistency.  And it is possible to fix the kse3 code, by using the
"universal function call syntax" (to borrow from Rust nomenclature) for
calling the extension method.  Alternatively, the extension methods can
be defined where they really don't see the underlying type, and then the
companion object can be made to include the extension methods (to keep
them in implicit scope).
  • Loading branch information
dwijnand committed Jul 24, 2024
1 parent 0e36424 commit 4fc8564
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 0 deletions.
7 changes: 7 additions & 0 deletions tests/neg/i21239.check
Original file line number Diff line number Diff line change
@@ -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`
7 changes: 7 additions & 0 deletions tests/neg/i21239.orig.check
Original file line number Diff line number Diff line change
@@ -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`
33 changes: 33 additions & 0 deletions tests/neg/i21239.orig.scala
Original file line number Diff line number Diff line change
@@ -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]
14 changes: 14 additions & 0 deletions tests/neg/i21239.scala
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions tests/pos/i21239.alt.scala
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions tests/pos/i21239.orig.scala
Original file line number Diff line number Diff line change
@@ -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]
18 changes: 18 additions & 0 deletions tests/pos/i21239.scala
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 4fc8564

Please sign in to comment.