Skip to content

Commit

Permalink
Implement match type amendment: extractors follow aliases and singlet…
Browse files Browse the repository at this point in the history
…ons (#20161)

This implements the change proposed in
scala/improvement-proposals#84.

The added pos test case presents motivating examples, the added neg test
cases demonstrate that errors are correctly reported when cycles are
present. The potential for cycle is no worse than with the existing
extraction logic as demonstrated by the existing test in
`tests/neg/mt-deskolemize.scala`.
  • Loading branch information
smarter authored May 8, 2024
2 parents 1276034 + a1930c4 commit 8f73af2
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 6 deletions.
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ object Feature:
val into = experimental("into")
val namedTuples = experimental("namedTuples")
val modularity = experimental("modularity")
val betterMatchTypeExtractors = experimental("betterMatchTypeExtractors")

def experimentalAutoEnableFeatures(using Context): List[TermName] =
defn.languageExperimentalFeatures
Expand Down Expand Up @@ -89,6 +90,8 @@ object Feature:

def scala2ExperimentalMacroEnabled(using Context) = enabled(scala2macros)

def betterMatchTypeExtractorsEnabled(using Context) = enabled(betterMatchTypeExtractors)

/** Is pureFunctions enabled for this compilation unit? */
def pureFunsEnabled(using Context) =
enabledBySetting(pureFunctions)
Expand Down
69 changes: 63 additions & 6 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import TypeOps.refineUsingParent
import collection.mutable
import util.{Stats, NoSourcePosition, EqHashMap}
import config.Config
import config.Feature.{migrateTo3, sourceVersion}
import config.Feature.{betterMatchTypeExtractorsEnabled, migrateTo3, sourceVersion}
import config.Printers.{subtyping, gadts, matchTypes, noPrinter}
import config.SourceVersion
import TypeErasure.{erasedLub, erasedGlb}
Expand Down Expand Up @@ -3518,20 +3518,77 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) {
false

case MatchTypeCasePattern.TypeMemberExtractor(typeMemberName, capture) =>
/** Try to remove references to `skolem` from a type in accordance with the spec.
*
* If `betterMatchTypeExtractorsEnabled` is enabled then references
* to `skolem` occuring are avoided by following aliases and
* singletons, otherwise no attempt made to avoid references to
* `skolem`.
*
* If any reference to `skolem` remains in the result type,
* `refersToSkolem` is set to true.
*/
class DropSkolemMap(skolem: SkolemType) extends TypeMap:
var refersToSkolem = false
def apply(tp: Type): Type =
if refersToSkolem then
return tp
tp match
case `skolem` =>
refersToSkolem = true
tp
case tp: NamedType if betterMatchTypeExtractorsEnabled =>
val pre1 = apply(tp.prefix)
if refersToSkolem then
tp match
case tp: TermRef => tp.info.widenExpr.dealias match
case info: SingletonType =>
refersToSkolem = false
apply(info)
case _ =>
tp.derivedSelect(pre1)
case tp: TypeRef => tp.info match
case info: AliasingBounds =>
refersToSkolem = false
apply(info.alias)
case _ =>
tp.derivedSelect(pre1)
else
tp.derivedSelect(pre1)
case tp: LazyRef if betterMatchTypeExtractorsEnabled =>
// By default, TypeMap maps LazyRefs lazily. We need to
// force it for `refersToSkolem` to be correctly set.
apply(tp.ref)
case _ =>
mapOver(tp)
end DropSkolemMap
/** Try to remove references to `skolem` from `u` in accordance with the spec.
*
* If any reference to `skolem` remains in the result type, return
* NoType instead.
*/
def dropSkolem(u: Type, skolem: SkolemType): Type =
val dmap = DropSkolemMap(skolem)
val res = dmap(u)
if dmap.refersToSkolem then NoType else res

val stableScrut: SingletonType = scrut match
case scrut: SingletonType => scrut
case _ => SkolemType(scrut)

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 infoRefersToSkolem = stableScrut.isInstanceOf[SkolemType] && stableScrut.occursIn(info)
val info1 = info match
case info: TypeBounds => info // Will already trigger a MatchResult.NoInstances
case _ if infoRefersToSkolem => RealTypeBounds(info, info) // Explicitly trigger a MatchResult.NoInstances
case _ => info // We have a match
val info1 = 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)
case _ =>
false
Expand Down
7 changes: 7 additions & 0 deletions library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ object language:
@compileTimeOnly("`relaxedExtensionImports` can only be used at compile time in import statements")
@deprecated("The experimental.relaxedExtensionImports language import is no longer needed since the feature is now standard", since = "3.4")
object relaxedExtensionImports

/** Enhance match type extractors to follow aliases and singletons.
*
* @see [[https://github.com/scala/improvement-proposals/pull/84]]
*/
@compileTimeOnly("`betterMatchTypeExtractors` can only be used at compile time in import statements")
object betterMatchTypeExtractors
end experimental

/** The deprecated object contains features that are no longer officially suypported in Scala.
Expand Down
60 changes: 60 additions & 0 deletions tests/neg/mt-deskolemize-2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//> using options -language:experimental.betterMatchTypeExtractors

trait Expr:
type Value
object Expr:
type Of[V] = Expr { type Value = V }
type ExtractValue[F <: Expr] = F match
case Expr.Of[v] => v
import Expr.ExtractValue

class SimpleLoop1 extends Expr:
type Value = ExtractValue[SimpleLoop2]

class SimpleLoop2 extends Expr:
type Value = ExtractValue[SimpleLoop1]

object Test1:
val x: ExtractValue[SimpleLoop1] = 1 // error

trait Description:
type Elem <: Tuple

class PrimBroken extends Expr:
type Value = Alias
type Alias = Value // error

class Prim extends Expr:
type Value = BigInt

class VecExpr[E <: Expr] extends Expr:
type Value = Vector[ExtractValue[E]]

trait ProdExpr extends Expr:
val description: Description
type Value = Tuple.Map[description.Elem, [X] =>> ExtractValue[X & Expr]]


class MyExpr1 extends ProdExpr:
final val description = new Description:
type Elem = (VecExpr[Prim], MyExpr2)

class MyExpr2 extends ProdExpr:
final val description = new Description:
type Elem = (VecExpr[VecExpr[MyExpr1]], Prim)

trait Constable[E <: Expr]:
def lit(v: ExtractValue[E]): E
object Constable:
given [E <: Expr]: Constable[E] = ???

object Test2:
def fromLiteral[E <: Expr : Constable](v: ExtractValue[E]): E =
summon[Constable[E]].lit(v)
val x0: ExtractValue[Prim] = "" // error
val x1: ExtractValue[PrimBroken] = 1 // error

val foo: MyExpr2 = new MyExpr2
val v: foo.Value = (Vector(Vector()), 1) // error: Recursion limit exceeded
val c: MyExpr2 = fromLiteral:
(Vector(Vector()), 1) // error: Recursion limit exceeded
57 changes: 57 additions & 0 deletions tests/pos/mt-deskolemize.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//> using options -language:experimental.betterMatchTypeExtractors

trait Expr:
type Value

object Expr:
type Of[V] = Expr { type Value = V }
type ExtractValue[F <: Expr] = F match
case Expr.Of[v] => v
import Expr.ExtractValue

class Prim extends Expr:
type Value = Alias
type Alias = BigInt

class VecExpr[E <: Expr] extends Expr:
type Value = Vector[ExtractValue[E]]

trait Description:
type Elem <: Tuple

trait ProdExpr extends Expr:
val description: Description
type Value = Tuple.Map[description.Elem, [X] =>> ExtractValue[X & Expr]]

class MyExpr1 extends ProdExpr:
final val description = new Description:
type Elem = (VecExpr[Prim], Prim)

class MyExpr2 extends ProdExpr:
final val description = new Description:
type Elem = (VecExpr[VecExpr[MyExpr1]], Prim)

trait ProdExprAlt[T <: Tuple] extends Expr:
type Value = Tuple.Map[T, [X] =>> ExtractValue[X & Expr]]

class MyExpr3 extends ProdExprAlt[(Prim, VecExpr[Prim], Prim)]

trait Constable[E <: Expr]:
def lit(v: ExtractValue[E]): E
object Constable:
given [E <: Expr]: Constable[E] = ???

object Test:
def fromLiteral[E <: Expr : Constable](v: ExtractValue[E]): E =
summon[Constable[E]].lit(v)
val a: Prim = fromLiteral(1)
val b: VecExpr[Prim] = fromLiteral(Vector(1))
val c: MyExpr1 = fromLiteral((Vector(1), 1))
val d: MyExpr2 = fromLiteral(Vector(Vector((Vector(1), 1))), 2)
val e: MyExpr3 = fromLiteral((1, Vector(1), 1))
val f: ProdExprAlt[(MyExpr1, VecExpr[MyExpr3])] = fromLiteral:
(
(Vector(1), 1),
Vector((1, Vector(1), 1), (2, Vector(1), 2))
)
val g: Expr { type Alias = Int; type Value = Alias } = fromLiteral(1)

0 comments on commit 8f73af2

Please sign in to comment.