Skip to content

Commit 8f73af2

Browse files
authored
Implement match type amendment: extractors follow aliases and singletons (#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`.
2 parents 1276034 + a1930c4 commit 8f73af2

File tree

5 files changed

+190
-6
lines changed

5 files changed

+190
-6
lines changed

compiler/src/dotty/tools/dotc/config/Feature.scala

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ object Feature:
3535
val into = experimental("into")
3636
val namedTuples = experimental("namedTuples")
3737
val modularity = experimental("modularity")
38+
val betterMatchTypeExtractors = experimental("betterMatchTypeExtractors")
3839

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

9091
def scala2ExperimentalMacroEnabled(using Context) = enabled(scala2macros)
9192

93+
def betterMatchTypeExtractorsEnabled(using Context) = enabled(betterMatchTypeExtractors)
94+
9295
/** Is pureFunctions enabled for this compilation unit? */
9396
def pureFunsEnabled(using Context) =
9497
enabledBySetting(pureFunctions)

compiler/src/dotty/tools/dotc/core/TypeComparer.scala

+63-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import TypeOps.refineUsingParent
1010
import collection.mutable
1111
import util.{Stats, NoSourcePosition, EqHashMap}
1212
import config.Config
13-
import config.Feature.{migrateTo3, sourceVersion}
13+
import config.Feature.{betterMatchTypeExtractorsEnabled, migrateTo3, sourceVersion}
1414
import config.Printers.{subtyping, gadts, matchTypes, noPrinter}
1515
import config.SourceVersion
1616
import TypeErasure.{erasedLub, erasedGlb}
@@ -3518,20 +3518,77 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) {
35183518
false
35193519

35203520
case MatchTypeCasePattern.TypeMemberExtractor(typeMemberName, capture) =>
3521+
/** Try to remove references to `skolem` from a type in accordance with the spec.
3522+
*
3523+
* If `betterMatchTypeExtractorsEnabled` is enabled then references
3524+
* to `skolem` occuring are avoided by following aliases and
3525+
* singletons, otherwise no attempt made to avoid references to
3526+
* `skolem`.
3527+
*
3528+
* If any reference to `skolem` remains in the result type,
3529+
* `refersToSkolem` is set to true.
3530+
*/
3531+
class DropSkolemMap(skolem: SkolemType) extends TypeMap:
3532+
var refersToSkolem = false
3533+
def apply(tp: Type): Type =
3534+
if refersToSkolem then
3535+
return tp
3536+
tp match
3537+
case `skolem` =>
3538+
refersToSkolem = true
3539+
tp
3540+
case tp: NamedType if betterMatchTypeExtractorsEnabled =>
3541+
val pre1 = apply(tp.prefix)
3542+
if refersToSkolem then
3543+
tp match
3544+
case tp: TermRef => tp.info.widenExpr.dealias match
3545+
case info: SingletonType =>
3546+
refersToSkolem = false
3547+
apply(info)
3548+
case _ =>
3549+
tp.derivedSelect(pre1)
3550+
case tp: TypeRef => tp.info match
3551+
case info: AliasingBounds =>
3552+
refersToSkolem = false
3553+
apply(info.alias)
3554+
case _ =>
3555+
tp.derivedSelect(pre1)
3556+
else
3557+
tp.derivedSelect(pre1)
3558+
case tp: LazyRef if betterMatchTypeExtractorsEnabled =>
3559+
// By default, TypeMap maps LazyRefs lazily. We need to
3560+
// force it for `refersToSkolem` to be correctly set.
3561+
apply(tp.ref)
3562+
case _ =>
3563+
mapOver(tp)
3564+
end DropSkolemMap
3565+
/** Try to remove references to `skolem` from `u` in accordance with the spec.
3566+
*
3567+
* If any reference to `skolem` remains in the result type, return
3568+
* NoType instead.
3569+
*/
3570+
def dropSkolem(u: Type, skolem: SkolemType): Type =
3571+
val dmap = DropSkolemMap(skolem)
3572+
val res = dmap(u)
3573+
if dmap.refersToSkolem then NoType else res
3574+
35213575
val stableScrut: SingletonType = scrut match
35223576
case scrut: SingletonType => scrut
35233577
case _ => SkolemType(scrut)
3578+
35243579
stableScrut.member(typeMemberName) match
35253580
case denot: SingleDenotation if denot.exists =>
35263581
val info = denot.info match
35273582
case alias: AliasingBounds => alias.alias // Extract the alias
35283583
case ClassInfo(prefix, cls, _, _, _) => prefix.select(cls) // Re-select the class from the prefix
35293584
case info => info // Notably, RealTypeBounds, which will eventually give a MatchResult.NoInstances
3530-
val infoRefersToSkolem = stableScrut.isInstanceOf[SkolemType] && stableScrut.occursIn(info)
3531-
val info1 = info match
3532-
case info: TypeBounds => info // Will already trigger a MatchResult.NoInstances
3533-
case _ if infoRefersToSkolem => RealTypeBounds(info, info) // Explicitly trigger a MatchResult.NoInstances
3534-
case _ => info // We have a match
3585+
val info1 = stableScrut match
3586+
case skolem: SkolemType =>
3587+
dropSkolem(info, skolem).orElse:
3588+
info match
3589+
case info: TypeBounds => info // Will already trigger a MatchResult.NoInstances
3590+
case _ => RealTypeBounds(info, info) // Explicitly trigger a MatchResult.NoInstances
3591+
case _ => info
35353592
rec(capture, info1, variance = 0, scrutIsWidenedAbstract)
35363593
case _ =>
35373594
false

library/src/scala/runtime/stdLibPatches/language.scala

+7
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ object language:
117117
@compileTimeOnly("`relaxedExtensionImports` can only be used at compile time in import statements")
118118
@deprecated("The experimental.relaxedExtensionImports language import is no longer needed since the feature is now standard", since = "3.4")
119119
object relaxedExtensionImports
120+
121+
/** Enhance match type extractors to follow aliases and singletons.
122+
*
123+
* @see [[https://github.com/scala/improvement-proposals/pull/84]]
124+
*/
125+
@compileTimeOnly("`betterMatchTypeExtractors` can only be used at compile time in import statements")
126+
object betterMatchTypeExtractors
120127
end experimental
121128

122129
/** The deprecated object contains features that are no longer officially suypported in Scala.

tests/neg/mt-deskolemize-2.scala

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//> using options -language:experimental.betterMatchTypeExtractors
2+
3+
trait Expr:
4+
type Value
5+
object Expr:
6+
type Of[V] = Expr { type Value = V }
7+
type ExtractValue[F <: Expr] = F match
8+
case Expr.Of[v] => v
9+
import Expr.ExtractValue
10+
11+
class SimpleLoop1 extends Expr:
12+
type Value = ExtractValue[SimpleLoop2]
13+
14+
class SimpleLoop2 extends Expr:
15+
type Value = ExtractValue[SimpleLoop1]
16+
17+
object Test1:
18+
val x: ExtractValue[SimpleLoop1] = 1 // error
19+
20+
trait Description:
21+
type Elem <: Tuple
22+
23+
class PrimBroken extends Expr:
24+
type Value = Alias
25+
type Alias = Value // error
26+
27+
class Prim extends Expr:
28+
type Value = BigInt
29+
30+
class VecExpr[E <: Expr] extends Expr:
31+
type Value = Vector[ExtractValue[E]]
32+
33+
trait ProdExpr extends Expr:
34+
val description: Description
35+
type Value = Tuple.Map[description.Elem, [X] =>> ExtractValue[X & Expr]]
36+
37+
38+
class MyExpr1 extends ProdExpr:
39+
final val description = new Description:
40+
type Elem = (VecExpr[Prim], MyExpr2)
41+
42+
class MyExpr2 extends ProdExpr:
43+
final val description = new Description:
44+
type Elem = (VecExpr[VecExpr[MyExpr1]], Prim)
45+
46+
trait Constable[E <: Expr]:
47+
def lit(v: ExtractValue[E]): E
48+
object Constable:
49+
given [E <: Expr]: Constable[E] = ???
50+
51+
object Test2:
52+
def fromLiteral[E <: Expr : Constable](v: ExtractValue[E]): E =
53+
summon[Constable[E]].lit(v)
54+
val x0: ExtractValue[Prim] = "" // error
55+
val x1: ExtractValue[PrimBroken] = 1 // error
56+
57+
val foo: MyExpr2 = new MyExpr2
58+
val v: foo.Value = (Vector(Vector()), 1) // error: Recursion limit exceeded
59+
val c: MyExpr2 = fromLiteral:
60+
(Vector(Vector()), 1) // error: Recursion limit exceeded

tests/pos/mt-deskolemize.scala

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//> using options -language:experimental.betterMatchTypeExtractors
2+
3+
trait Expr:
4+
type Value
5+
6+
object Expr:
7+
type Of[V] = Expr { type Value = V }
8+
type ExtractValue[F <: Expr] = F match
9+
case Expr.Of[v] => v
10+
import Expr.ExtractValue
11+
12+
class Prim extends Expr:
13+
type Value = Alias
14+
type Alias = BigInt
15+
16+
class VecExpr[E <: Expr] extends Expr:
17+
type Value = Vector[ExtractValue[E]]
18+
19+
trait Description:
20+
type Elem <: Tuple
21+
22+
trait ProdExpr extends Expr:
23+
val description: Description
24+
type Value = Tuple.Map[description.Elem, [X] =>> ExtractValue[X & Expr]]
25+
26+
class MyExpr1 extends ProdExpr:
27+
final val description = new Description:
28+
type Elem = (VecExpr[Prim], Prim)
29+
30+
class MyExpr2 extends ProdExpr:
31+
final val description = new Description:
32+
type Elem = (VecExpr[VecExpr[MyExpr1]], Prim)
33+
34+
trait ProdExprAlt[T <: Tuple] extends Expr:
35+
type Value = Tuple.Map[T, [X] =>> ExtractValue[X & Expr]]
36+
37+
class MyExpr3 extends ProdExprAlt[(Prim, VecExpr[Prim], Prim)]
38+
39+
trait Constable[E <: Expr]:
40+
def lit(v: ExtractValue[E]): E
41+
object Constable:
42+
given [E <: Expr]: Constable[E] = ???
43+
44+
object Test:
45+
def fromLiteral[E <: Expr : Constable](v: ExtractValue[E]): E =
46+
summon[Constable[E]].lit(v)
47+
val a: Prim = fromLiteral(1)
48+
val b: VecExpr[Prim] = fromLiteral(Vector(1))
49+
val c: MyExpr1 = fromLiteral((Vector(1), 1))
50+
val d: MyExpr2 = fromLiteral(Vector(Vector((Vector(1), 1))), 2)
51+
val e: MyExpr3 = fromLiteral((1, Vector(1), 1))
52+
val f: ProdExprAlt[(MyExpr1, VecExpr[MyExpr3])] = fromLiteral:
53+
(
54+
(Vector(1), 1),
55+
Vector((1, Vector(1), 1), (2, Vector(1), 2))
56+
)
57+
val g: Expr { type Alias = Int; type Value = Alias } = fromLiteral(1)

0 commit comments

Comments
 (0)