Skip to content

fix #9260 - Enums copy variance of parent #9709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,9 @@ object desugar {
)

/** A derived type definition watching `sym` */
def derivedTypeParam(sym: TypeSymbol)(using Context): TypeDef =
TypeDef(sym.name, DerivedFromParamTree().watching(sym)).withFlags(TypeParam)
def derivedTypeParamWithVariance(sym: TypeSymbol)(using Context): TypeDef =
val variance = VarianceFlags & sym.flags
TypeDef(sym.name, DerivedFromParamTree().watching(sym)).withFlags(TypeParam | Synthetic | variance)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, we should probably set Synthetic on the other derivedTypeParam and on derivedTermParam assuming that doesn't break things.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah so maybe it's not worth it, what does it change the position to?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from line 0-15:16 to line 0-11:14

Copy link
Member Author

@bishabosha bishabosha Sep 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this worth investigating in this PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not


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

val originalTparams = constr1.tparams
val originalVparamss = constr1.vparamss
lazy val derivedEnumParams = enumClass.typeParams.map(derivedTypeParam)
lazy val derivedEnumParams = enumClass.typeParams.map(derivedTypeParamWithVariance)
val impliedTparams =
if (isEnumCase) {
val tparamReferenced = typeParamIsReferenced(
Expand Down
12 changes: 11 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import util.SrcPos
import config.Printers.variances
import config.Feature.migrateTo3
import reporting.trace
import printing.Formatting.hl

/** Provides `check` method to check that all top-level definitions
* in tree are variance correct. Does not recurse inside methods.
Expand Down Expand Up @@ -166,7 +167,16 @@ class VarianceChecker(using Context) {
private object Traverser extends TreeTraverser {
def checkVariance(sym: Symbol, pos: SrcPos) = Validator.validateDefinition(sym) match {
case Some(VarianceError(tvar, required)) =>
def msg = i"${varianceLabel(tvar.flags)} $tvar occurs in ${varianceLabel(required)} position in type ${sym.info} of $sym"
def msg =
val enumAddendum =
val towner = tvar.owner
if towner.isAllOf(EnumCase) && towner.isClass && tvar.is(Synthetic) then
val example =
"See an example at http://dotty.epfl.ch/docs/reference/enums/adts.html#parameter-variance-of-enums"
i"\n${hl("enum case")} ${towner.name} requires explicit declaration of $tvar to resolve this issue.\n$example"
else
""
i"${varianceLabel(tvar.flags)} $tvar occurs in ${varianceLabel(required)} position in type ${sym.info} of $sym$enumAddendum"
if (migrateTo3 &&
(sym.owner.isConstructor || sym.ownersIterator.exists(_.isAllOf(ProtectedLocal))))
report.migrationWarning(
Expand Down
50 changes: 50 additions & 0 deletions docs/docs/reference/enums/adts.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,56 @@ enum Color(val rgb: Int) {
}
```

### Parameter Variance of Enums

By default, parameterized cases of enums with type parameters will copy the type parameters of their parent, along
with any variance notations. As usual, it is important to use type parameters carefully when they are variant, as shown
below:

The following `View` enum has a contravariant type parameter `T` and a single case `Refl`, representing a function
mapping a type `T` to itself:
```scala
enum View[-T]:
case Refl(f: T => T)
```
The definition of `Refl` is incorrect, as it uses contravariant type `T` in the covariant result position of a
function type, leading to the following error:
```scala
-- Error: View.scala:2:12 --------
2 | case Refl(f: T => T)
| ^^^^^^^^^
|contravariant type T occurs in covariant position in type T => T of value f
|enum case Refl requires explicit declaration of type T to resolve this issue.
```
Because `Refl` does not declare explicit parameters, it looks to the compiler like the following:
```scala
enum View[-T]:
case Refl[/*synthetic*/-T1](f: T1 => T1) extends View[T1]
```

The compiler has inferred for `Refl` the contravariant type parameter `T1`, following `T` in `View`.
We can now clearly see that `Refl` needs to declare its own non-variant type parameter to correctly type `f`,
and can remedy the error by the following change to `Refl`:

```diff
enum View[-T]:
- case Refl(f: T => T)
+ case Refl[R](f: R => R) extends View[R]
```
Above, type `R` is chosen as the parameter for `Refl` to highlight that it has a different meaning to
type `T` in `View`, but any name will do.

After some further changes, a more complete implementation of `View` can be given as follows and be used
as the function type `T => U`:

```scala
enum View[-T, +U] extends (T => U):
case Refl[R](f: R => R) extends View[R, R]

final def apply(t: T): U = this match
case refl: Refl[r] => refl.f(t)
```

### Syntax of Enums

Changes to the syntax fall in two categories: enum definitions and cases inside enums.
Expand Down
10 changes: 10 additions & 0 deletions tests/neg-custom-args/fatal-warnings/enum-variance.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Error: tests/neg-custom-args/fatal-warnings/enum-variance.scala:2:12 ------------------------------------------------
2 | case Refl(f: T => T) // error: enum case Refl requires explicit declaration of type T
| ^^^^^^^^^
| contravariant type T occurs in covariant position in type T => T of value f
| enum case Refl requires explicit declaration of type T to resolve this issue.
| See an example at http://dotty.epfl.ch/docs/reference/enums/adts.html#parameter-variance-of-enums
-- Error: tests/neg-custom-args/fatal-warnings/enum-variance.scala:5:16 ------------------------------------------------
5 | case Refl[-T](f: T => T) extends ExplicitView[T] // error: contravariant type T occurs in covariant position
| ^^^^^^^^^
| contravariant type T occurs in covariant position in type T => T of value f
11 changes: 11 additions & 0 deletions tests/neg-custom-args/fatal-warnings/enum-variance.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
enum View[-T]:
case Refl(f: T => T) // error: enum case Refl requires explicit declaration of type T

enum ExplicitView[-T]: // desugared version of View
case Refl[-T](f: T => T) extends ExplicitView[T] // error: contravariant type T occurs in covariant position

enum InvariantView[-T, +U] extends (T => U):
case Refl[T](f: T => T) extends InvariantView[T, T]

final def apply(t: T): U = this match
case refl: Refl[t] => refl.f(t)
2 changes: 1 addition & 1 deletion tests/neg-custom-args/typeclass-derivation2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ object TypeLevel {

// An algebraic datatype
enum Lst[+T] {
case Cons(hd: T, tl: Lst[T])
case Cons[T](hd: T, tl: Lst[T]) extends Lst[T]
case Nil
}

Expand Down
8 changes: 4 additions & 4 deletions tests/neg/enum-tparams.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
object Test {

enum Opt[+T] {
case S(x: T) extends Opt[T]
case S[T](x: T) extends Opt[T]
case I(x: Int) extends Opt[Int]
case V() extends Opt[`T`]
case P(x: List[T]) extends Opt[String]
case V[T]() extends Opt[`T`]
case P[T](x: List[T]) extends Opt[String]
case N extends Opt[Nothing]
}

Expand Down Expand Up @@ -37,4 +37,4 @@ object Test {
E.C4() // OK
E.C5[List, Id]() // OK
E.C5() // OK
}
}
2 changes: 1 addition & 1 deletion tests/neg/enums.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ enum Captured[T] {
}

enum Option[+T] derives Eql {
case Some(x: T)
case Some[T](x: T) extends Option[T]
case None
}

Expand Down
4 changes: 2 additions & 2 deletions tests/neg/i4470c.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
object DuplicatedEnum {
enum Maybe[+T] { // error
case Some(x: T)
case Some[T](x: T) extends Maybe[T]
}

enum Maybe[+T] { // error
case Some(x: T)
case Some[T](x: T) extends Maybe[T]
}
}
4 changes: 2 additions & 2 deletions tests/neg/i5495.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
lazy enum LazyList[+A] { // error: sealed abstract types cannot be lazy enum
case :: (head: A, tail: LazyList[A])
case ::[A] (head: A, tail: LazyList[A]) extends LazyList[A]
case Nil
}
}
4 changes: 2 additions & 2 deletions tests/neg/i7459.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ object Eq {


enum Opt[+T] derives Eq {
case Sm(t: T)
case Sm[T](t: T) extends Opt[T]
case Nn
}
}
3 changes: 1 addition & 2 deletions tests/patmat/enum-approx.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ enum Fun[-T, +U >: Null] {

case Identity[T, U >: Null](g: T => U) extends Fun[T, U]
case ConstNull
case ConstNullClass(x: T)
case ConstNullClass[T](x: T) extends Fun[T, Null]
case ConstNullSimple
}

Expand All @@ -28,4 +28,3 @@ object Test {
case ConstNullSimple => null
}
}

16 changes: 16 additions & 0 deletions tests/pos-special/fatal-warnings/i9260.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package asts

enum Ast[-T >: Null]:
case DefDef()

trait AstImpl[T >: Null]:
type Ast = asts.Ast[T]
type DefDef = Ast.DefDef[T]
end AstImpl

object untpd extends AstImpl[Null]:

def DefDef(ast: Ast): DefDef = ast match
case ast: DefDef => ast

end untpd
18 changes: 18 additions & 0 deletions tests/pos/enum-variance.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Animal
class Dog extends Animal

enum Opt[+T]:
case Sm(t: T)
case None

val smDog: Opt.Sm[Dog] = new Opt.Sm(Dog())
val smAnimal: Opt.Sm[Animal] = smDog

enum Show[-T]:
case Refl(op: T => String)

def show(t: T): String = this match
case Refl(op) => op(t)

val reflAnimal: Show.Refl[Animal] = new Show.Refl(_.toString)
val reflDog: Show.Refl[Dog] = reflAnimal
2 changes: 1 addition & 1 deletion tests/pos/patmat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ object Test {
}

enum Option[+T] {
case Some(value: T)
case Some[T](value: T) extends Option[T]
case None
}
import Option._
Expand Down
8 changes: 4 additions & 4 deletions tests/pos/reference/adts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package adts
object t1:

enum Option[+T]:
case Some(x: T)
case Some[T](x: T) extends Option[T]
case None

object t2:

enum Option[+T]:
case Some(x: T) extends Option[T]
case None extends Option[Nothing]
case Some[T](x: T) extends Option[T]
case None extends Option[Nothing]

enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
Expand All @@ -20,7 +20,7 @@ enum Color(val rgb: Int):
object t3:

enum Option[+T]:
case Some(x: T) extends Option[T]
case Some[T](x: T) extends Option[T]
case None

def isDefined: Boolean = this match
Expand Down
2 changes: 1 addition & 1 deletion tests/run-custom-args/typeclass-derivation2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ object TypeLevel {

// An algebraic datatype
enum Lst[+T] { // derives Eq, Pickler, Show
case Cons(hd: T, tl: Lst[T])
case Cons[T](hd: T, tl: Lst[T]) extends Lst[T]
case Nil
}

Expand Down
4 changes: 2 additions & 2 deletions tests/run-macros/i8007/Test_4.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Macro3.eqGen
case class Person(name: String, age: Int)

enum Opt[+T] {
case Sm(t: T)
case Sm[T](t: T) extends Opt[T]
case Nn
}

Expand Down Expand Up @@ -41,4 +41,4 @@ enum Opt[+T] {
val t7 = Sm(Person("Test", 23)) === Sm(Person("Test", 24))
println(t7) // false
println
}
}
3 changes: 1 addition & 2 deletions tests/run/enum-List2.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
enum List[+T] {
case Cons(x: T, xs: List[T])
case Cons[T](x: T, xs: List[T]) extends List[T]
case Nil
}
object Test {
import List._
val xs = Cons(1, Cons(2, Cons(3, Nil)))
def main(args: Array[String]) = println(xs)
}

3 changes: 1 addition & 2 deletions tests/run/enum-List2a.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
enum List[+T] {
case Cons(x: T, xs: List[T])
case Cons[T](x: T, xs: List[T]) extends List[T]
case Nil
}
object Test {
import List._
val xs = Cons(1, Cons(2, Cons(3, Nil)))
def main(args: Array[String]) = println(xs)
}

2 changes: 1 addition & 1 deletion tests/run/enum-Option.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
enum Option[+T] extends Serializable {
case Some(x: T)
case Some[T](x: T) extends Option[T]
case None

def isDefined: Boolean = this match {
Expand Down
2 changes: 1 addition & 1 deletion tests/run/enum-Option1.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
enum Option1[+T] extends Serializable {
case Some1(x: T)
case Some1[T](x: T) extends Option1[T]
case None1

def isDefined: Boolean = this match {
Expand Down
2 changes: 1 addition & 1 deletion tests/run/enum-values.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ enum Expr[-T >: Null]:
case AnyTree

enum ListLike[+T]:
case Cons(head: T, tail: ListLike[T])
case Cons[T](head: T, tail: ListLike[T]) extends ListLike[T]
case EmptyListLike

enum TypeCtorsK[F[_]]:
Expand Down
4 changes: 2 additions & 2 deletions tests/run/generic/List.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ object List0 {
}

/** enum List[+T] {
* case Cons(x: T, xs: List[T])
* case Cons[T](x: T, xs: List[T]) extends List[T]
* case Nil extends List[Nothing]
* }
*/
Expand Down Expand Up @@ -86,4 +86,4 @@ object List {
case Snd(n) => n
}
}
}
}
4 changes: 2 additions & 2 deletions tests/run/i9011.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
enum Opt[+T] derives Eq:
case Sm(t: T)
case Sm[T](t: T) extends Opt[T]
case Nn

import scala.deriving._
Expand Down Expand Up @@ -54,4 +54,4 @@ object Test extends App {
val eqoi = summon[Eq[Opt[Int]]]
assert(eqoi.eqv(Sm(23), Sm(23)))
assert(eqoi.eqv(Nn, Nn))
}
}
2 changes: 1 addition & 1 deletion tests/run/typeclass-derivation-doc-example.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ object Eq {
}

enum Opt[+T] derives Eq {
case Sm(t: T)
case Sm[T](t: T) extends Opt[T]
case Nn
}

Expand Down
Loading