Skip to content

Commit 6daa7b5

Browse files
authored
Merge pull request #9709 from dotty-staging/topic/enum-unification
fix #9260 - Enums copy variance of parent
2 parents f143077 + d4769b9 commit 6daa7b5

30 files changed

+156
-43
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,9 @@ object desugar {
136136
)
137137

138138
/** A derived type definition watching `sym` */
139-
def derivedTypeParam(sym: TypeSymbol)(using Context): TypeDef =
140-
TypeDef(sym.name, DerivedFromParamTree().watching(sym)).withFlags(TypeParam)
139+
def derivedTypeParamWithVariance(sym: TypeSymbol)(using Context): TypeDef =
140+
val variance = VarianceFlags & sym.flags
141+
TypeDef(sym.name, DerivedFromParamTree().watching(sym)).withFlags(TypeParam | Synthetic | variance)
141142

142143
/** A value definition copied from `vdef` with a tpt typetree derived from it */
143144
def derivedTermParam(vdef: ValDef)(using Context): ValDef =
@@ -407,7 +408,7 @@ object desugar {
407408

408409
val originalTparams = constr1.tparams
409410
val originalVparamss = constr1.vparamss
410-
lazy val derivedEnumParams = enumClass.typeParams.map(derivedTypeParam)
411+
lazy val derivedEnumParams = enumClass.typeParams.map(derivedTypeParamWithVariance)
411412
val impliedTparams =
412413
if (isEnumCase) {
413414
val tparamReferenced = typeParamIsReferenced(

compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import util.SrcPos
1212
import config.Printers.variances
1313
import config.Feature.migrateTo3
1414
import reporting.trace
15+
import printing.Formatting.hl
1516

1617
/** Provides `check` method to check that all top-level definitions
1718
* in tree are variance correct. Does not recurse inside methods.
@@ -166,7 +167,16 @@ class VarianceChecker(using Context) {
166167
private object Traverser extends TreeTraverser {
167168
def checkVariance(sym: Symbol, pos: SrcPos) = Validator.validateDefinition(sym) match {
168169
case Some(VarianceError(tvar, required)) =>
169-
def msg = i"${varianceLabel(tvar.flags)} $tvar occurs in ${varianceLabel(required)} position in type ${sym.info} of $sym"
170+
def msg =
171+
val enumAddendum =
172+
val towner = tvar.owner
173+
if towner.isAllOf(EnumCase) && towner.isClass && tvar.is(Synthetic) then
174+
val example =
175+
"See an example at http://dotty.epfl.ch/docs/reference/enums/adts.html#parameter-variance-of-enums"
176+
i"\n${hl("enum case")} ${towner.name} requires explicit declaration of $tvar to resolve this issue.\n$example"
177+
else
178+
""
179+
i"${varianceLabel(tvar.flags)} $tvar occurs in ${varianceLabel(required)} position in type ${sym.info} of $sym$enumAddendum"
170180
if (migrateTo3 &&
171181
(sym.owner.isConstructor || sym.ownersIterator.exists(_.isAllOf(ProtectedLocal))))
172182
report.migrationWarning(

docs/docs/reference/enums/adts.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,56 @@ enum Color(val rgb: Int) {
9494
}
9595
```
9696

97+
### Parameter Variance of Enums
98+
99+
By default, parameterized cases of enums with type parameters will copy the type parameters of their parent, along
100+
with any variance notations. As usual, it is important to use type parameters carefully when they are variant, as shown
101+
below:
102+
103+
The following `View` enum has a contravariant type parameter `T` and a single case `Refl`, representing a function
104+
mapping a type `T` to itself:
105+
```scala
106+
enum View[-T]:
107+
case Refl(f: T => T)
108+
```
109+
The definition of `Refl` is incorrect, as it uses contravariant type `T` in the covariant result position of a
110+
function type, leading to the following error:
111+
```scala
112+
-- Error: View.scala:2:12 --------
113+
2 | case Refl(f: T => T)
114+
| ^^^^^^^^^
115+
|contravariant type T occurs in covariant position in type T => T of value f
116+
|enum case Refl requires explicit declaration of type T to resolve this issue.
117+
```
118+
Because `Refl` does not declare explicit parameters, it looks to the compiler like the following:
119+
```scala
120+
enum View[-T]:
121+
case Refl[/*synthetic*/-T1](f: T1 => T1) extends View[T1]
122+
```
123+
124+
The compiler has inferred for `Refl` the contravariant type parameter `T1`, following `T` in `View`.
125+
We can now clearly see that `Refl` needs to declare its own non-variant type parameter to correctly type `f`,
126+
and can remedy the error by the following change to `Refl`:
127+
128+
```diff
129+
enum View[-T]:
130+
- case Refl(f: T => T)
131+
+ case Refl[R](f: R => R) extends View[R]
132+
```
133+
Above, type `R` is chosen as the parameter for `Refl` to highlight that it has a different meaning to
134+
type `T` in `View`, but any name will do.
135+
136+
After some further changes, a more complete implementation of `View` can be given as follows and be used
137+
as the function type `T => U`:
138+
139+
```scala
140+
enum View[-T, +U] extends (T => U):
141+
case Refl[R](f: R => R) extends View[R, R]
142+
143+
final def apply(t: T): U = this match
144+
case refl: Refl[r] => refl.f(t)
145+
```
146+
97147
### Syntax of Enums
98148

99149
Changes to the syntax fall in two categories: enum definitions and cases inside enums.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- Error: tests/neg-custom-args/fatal-warnings/enum-variance.scala:2:12 ------------------------------------------------
2+
2 | case Refl(f: T => T) // error: enum case Refl requires explicit declaration of type T
3+
| ^^^^^^^^^
4+
| contravariant type T occurs in covariant position in type T => T of value f
5+
| enum case Refl requires explicit declaration of type T to resolve this issue.
6+
| See an example at http://dotty.epfl.ch/docs/reference/enums/adts.html#parameter-variance-of-enums
7+
-- Error: tests/neg-custom-args/fatal-warnings/enum-variance.scala:5:16 ------------------------------------------------
8+
5 | case Refl[-T](f: T => T) extends ExplicitView[T] // error: contravariant type T occurs in covariant position
9+
| ^^^^^^^^^
10+
| contravariant type T occurs in covariant position in type T => T of value f
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
enum View[-T]:
2+
case Refl(f: T => T) // error: enum case Refl requires explicit declaration of type T
3+
4+
enum ExplicitView[-T]: // desugared version of View
5+
case Refl[-T](f: T => T) extends ExplicitView[T] // error: contravariant type T occurs in covariant position
6+
7+
enum InvariantView[-T, +U] extends (T => U):
8+
case Refl[T](f: T => T) extends InvariantView[T, T]
9+
10+
final def apply(t: T): U = this match
11+
case refl: Refl[t] => refl.f(t)

tests/neg-custom-args/typeclass-derivation2.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ object TypeLevel {
122122

123123
// An algebraic datatype
124124
enum Lst[+T] {
125-
case Cons(hd: T, tl: Lst[T])
125+
case Cons[T](hd: T, tl: Lst[T]) extends Lst[T]
126126
case Nil
127127
}
128128

tests/neg/enum-tparams.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
object Test {
22

33
enum Opt[+T] {
4-
case S(x: T) extends Opt[T]
4+
case S[T](x: T) extends Opt[T]
55
case I(x: Int) extends Opt[Int]
6-
case V() extends Opt[`T`]
7-
case P(x: List[T]) extends Opt[String]
6+
case V[T]() extends Opt[`T`]
7+
case P[T](x: List[T]) extends Opt[String]
88
case N extends Opt[Nothing]
99
}
1010

@@ -37,4 +37,4 @@ object Test {
3737
E.C4() // OK
3838
E.C5[List, Id]() // OK
3939
E.C5() // OK
40-
}
40+
}

tests/neg/enums.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ enum Captured[T] {
3636
}
3737

3838
enum Option[+T] derives Eql {
39-
case Some(x: T)
39+
case Some[T](x: T) extends Option[T]
4040
case None
4141
}
4242

tests/neg/i4470c.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
object DuplicatedEnum {
22
enum Maybe[+T] { // error
3-
case Some(x: T)
3+
case Some[T](x: T) extends Maybe[T]
44
}
55

66
enum Maybe[+T] { // error
7-
case Some(x: T)
7+
case Some[T](x: T) extends Maybe[T]
88
}
99
}

tests/neg/i5495.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
lazy enum LazyList[+A] { // error: sealed abstract types cannot be lazy enum
2-
case :: (head: A, tail: LazyList[A])
2+
case ::[A] (head: A, tail: LazyList[A]) extends LazyList[A]
33
case Nil
4-
}
4+
}

tests/neg/i7459.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,6 @@ object Eq {
5858

5959

6060
enum Opt[+T] derives Eq {
61-
case Sm(t: T)
61+
case Sm[T](t: T) extends Opt[T]
6262
case Nn
63-
}
63+
}

tests/patmat/enum-approx.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ enum Fun[-T, +U >: Null] {
88

99
case Identity[T, U >: Null](g: T => U) extends Fun[T, U]
1010
case ConstNull
11-
case ConstNullClass(x: T)
11+
case ConstNullClass[T](x: T) extends Fun[T, Null]
1212
case ConstNullSimple
1313
}
1414

@@ -28,4 +28,3 @@ object Test {
2828
case ConstNullSimple => null
2929
}
3030
}
31-
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package asts
2+
3+
enum Ast[-T >: Null]:
4+
case DefDef()
5+
6+
trait AstImpl[T >: Null]:
7+
type Ast = asts.Ast[T]
8+
type DefDef = Ast.DefDef[T]
9+
end AstImpl
10+
11+
object untpd extends AstImpl[Null]:
12+
13+
def DefDef(ast: Ast): DefDef = ast match
14+
case ast: DefDef => ast
15+
16+
end untpd

tests/pos/enum-variance.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class Animal
2+
class Dog extends Animal
3+
4+
enum Opt[+T]:
5+
case Sm(t: T)
6+
case None
7+
8+
val smDog: Opt.Sm[Dog] = new Opt.Sm(Dog())
9+
val smAnimal: Opt.Sm[Animal] = smDog
10+
11+
enum Show[-T]:
12+
case Refl(op: T => String)
13+
14+
def show(t: T): String = this match
15+
case Refl(op) => op(t)
16+
17+
val reflAnimal: Show.Refl[Animal] = new Show.Refl(_.toString)
18+
val reflDog: Show.Refl[Dog] = reflAnimal

tests/pos/patmat.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ object Test {
3535
}
3636

3737
enum Option[+T] {
38-
case Some(value: T)
38+
case Some[T](value: T) extends Option[T]
3939
case None
4040
}
4141
import Option._

tests/pos/reference/adts.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ package adts
22
object t1:
33

44
enum Option[+T]:
5-
case Some(x: T)
5+
case Some[T](x: T) extends Option[T]
66
case None
77

88
object t2:
99

1010
enum Option[+T]:
11-
case Some(x: T) extends Option[T]
12-
case None extends Option[Nothing]
11+
case Some[T](x: T) extends Option[T]
12+
case None extends Option[Nothing]
1313

1414
enum Color(val rgb: Int):
1515
case Red extends Color(0xFF0000)
@@ -20,7 +20,7 @@ enum Color(val rgb: Int):
2020
object t3:
2121

2222
enum Option[+T]:
23-
case Some(x: T) extends Option[T]
23+
case Some[T](x: T) extends Option[T]
2424
case None
2525

2626
def isDefined: Boolean = this match

tests/run-custom-args/typeclass-derivation2.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ object TypeLevel {
124124

125125
// An algebraic datatype
126126
enum Lst[+T] { // derives Eq, Pickler, Show
127-
case Cons(hd: T, tl: Lst[T])
127+
case Cons[T](hd: T, tl: Lst[T]) extends Lst[T]
128128
case Nil
129129
}
130130

tests/run-macros/i8007/Test_4.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Macro3.eqGen
66
case class Person(name: String, age: Int)
77

88
enum Opt[+T] {
9-
case Sm(t: T)
9+
case Sm[T](t: T) extends Opt[T]
1010
case Nn
1111
}
1212

@@ -41,4 +41,4 @@ enum Opt[+T] {
4141
val t7 = Sm(Person("Test", 23)) === Sm(Person("Test", 24))
4242
println(t7) // false
4343
println
44-
}
44+
}

tests/run/enum-List2.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
enum List[+T] {
2-
case Cons(x: T, xs: List[T])
2+
case Cons[T](x: T, xs: List[T]) extends List[T]
33
case Nil
44
}
55
object Test {
66
import List._
77
val xs = Cons(1, Cons(2, Cons(3, Nil)))
88
def main(args: Array[String]) = println(xs)
99
}
10-

tests/run/enum-List2a.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
enum List[+T] {
2-
case Cons(x: T, xs: List[T])
2+
case Cons[T](x: T, xs: List[T]) extends List[T]
33
case Nil
44
}
55
object Test {
66
import List._
77
val xs = Cons(1, Cons(2, Cons(3, Nil)))
88
def main(args: Array[String]) = println(xs)
99
}
10-

tests/run/enum-Option.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
enum Option[+T] extends Serializable {
2-
case Some(x: T)
2+
case Some[T](x: T) extends Option[T]
33
case None
44

55
def isDefined: Boolean = this match {

tests/run/enum-Option1.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
enum Option1[+T] extends Serializable {
2-
case Some1(x: T)
2+
case Some1[T](x: T) extends Option1[T]
33
case None1
44

55
def isDefined: Boolean = this match {

tests/run/enum-values.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ enum Expr[-T >: Null]:
1111
case AnyTree
1212

1313
enum ListLike[+T]:
14-
case Cons(head: T, tail: ListLike[T])
14+
case Cons[T](head: T, tail: ListLike[T]) extends ListLike[T]
1515
case EmptyListLike
1616

1717
enum TypeCtorsK[F[_]]:

tests/run/generic/List.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ object List0 {
4747
}
4848

4949
/** enum List[+T] {
50-
* case Cons(x: T, xs: List[T])
50+
* case Cons[T](x: T, xs: List[T]) extends List[T]
5151
* case Nil extends List[Nothing]
5252
* }
5353
*/
@@ -86,4 +86,4 @@ object List {
8686
case Snd(n) => n
8787
}
8888
}
89-
}
89+
}

tests/run/i9011.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
enum Opt[+T] derives Eq:
2-
case Sm(t: T)
2+
case Sm[T](t: T) extends Opt[T]
33
case Nn
44

55
import scala.deriving._
@@ -54,4 +54,4 @@ object Test extends App {
5454
val eqoi = summon[Eq[Opt[Int]]]
5555
assert(eqoi.eqv(Sm(23), Sm(23)))
5656
assert(eqoi.eqv(Nn, Nn))
57-
}
57+
}

tests/run/typeclass-derivation-doc-example.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ object Eq {
4646
}
4747

4848
enum Opt[+T] derives Eq {
49-
case Sm(t: T)
49+
case Sm[T](t: T) extends Opt[T]
5050
case Nn
5151
}
5252

0 commit comments

Comments
 (0)