diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 19f8cc586279..dcb81777cb6f 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -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) /** A value definition copied from `vdef` with a tpt typetree derived from it */ def derivedTermParam(vdef: ValDef)(using Context): ValDef = @@ -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( diff --git a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala index 96b4dd2632e7..9170c8edf8ad 100644 --- a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala +++ b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala @@ -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. @@ -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( diff --git a/docs/docs/reference/enums/adts.md b/docs/docs/reference/enums/adts.md index 8e05280384b4..6407d0ec5048 100644 --- a/docs/docs/reference/enums/adts.md +++ b/docs/docs/reference/enums/adts.md @@ -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. diff --git a/tests/neg-custom-args/fatal-warnings/enum-variance.check b/tests/neg-custom-args/fatal-warnings/enum-variance.check new file mode 100644 index 000000000000..22b5065f8ea5 --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/enum-variance.check @@ -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 diff --git a/tests/neg-custom-args/fatal-warnings/enum-variance.scala b/tests/neg-custom-args/fatal-warnings/enum-variance.scala new file mode 100644 index 000000000000..efe0dbbc6cdd --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/enum-variance.scala @@ -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) diff --git a/tests/neg-custom-args/typeclass-derivation2.scala b/tests/neg-custom-args/typeclass-derivation2.scala index 47c034610a76..b1a32fe65343 100644 --- a/tests/neg-custom-args/typeclass-derivation2.scala +++ b/tests/neg-custom-args/typeclass-derivation2.scala @@ -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 } diff --git a/tests/neg/enum-tparams.scala b/tests/neg/enum-tparams.scala index 95ff54c6983b..af4ba9399b35 100644 --- a/tests/neg/enum-tparams.scala +++ b/tests/neg/enum-tparams.scala @@ -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] } @@ -37,4 +37,4 @@ object Test { E.C4() // OK E.C5[List, Id]() // OK E.C5() // OK -} \ No newline at end of file +} diff --git a/tests/neg/enums.scala b/tests/neg/enums.scala index 45652373c598..bb668a1703a7 100644 --- a/tests/neg/enums.scala +++ b/tests/neg/enums.scala @@ -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 } diff --git a/tests/neg/i4470c.scala b/tests/neg/i4470c.scala index 82e4a1ffa4db..79ae8b22f5af 100644 --- a/tests/neg/i4470c.scala +++ b/tests/neg/i4470c.scala @@ -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] } } diff --git a/tests/neg/i5495.scala b/tests/neg/i5495.scala index 5da23b98e10b..db08f3119555 100644 --- a/tests/neg/i5495.scala +++ b/tests/neg/i5495.scala @@ -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 -} \ No newline at end of file +} diff --git a/tests/neg/i7459.scala b/tests/neg/i7459.scala index 88befb163c63..09b8156ca442 100644 --- a/tests/neg/i7459.scala +++ b/tests/neg/i7459.scala @@ -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 -} \ No newline at end of file +} diff --git a/tests/patmat/enum-approx.scala b/tests/patmat/enum-approx.scala index 5fbe0b75b01b..b29d5ad9d148 100644 --- a/tests/patmat/enum-approx.scala +++ b/tests/patmat/enum-approx.scala @@ -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 } @@ -28,4 +28,3 @@ object Test { case ConstNullSimple => null } } - diff --git a/tests/pos-special/fatal-warnings/i9260.scala b/tests/pos-special/fatal-warnings/i9260.scala new file mode 100644 index 000000000000..df548f393eea --- /dev/null +++ b/tests/pos-special/fatal-warnings/i9260.scala @@ -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 diff --git a/tests/pos/enum-variance.scala b/tests/pos/enum-variance.scala new file mode 100644 index 000000000000..82fa5bdf91a0 --- /dev/null +++ b/tests/pos/enum-variance.scala @@ -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 diff --git a/tests/pos/patmat.scala b/tests/pos/patmat.scala index c55308d9810a..f34c5d98336d 100644 --- a/tests/pos/patmat.scala +++ b/tests/pos/patmat.scala @@ -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._ diff --git a/tests/pos/reference/adts.scala b/tests/pos/reference/adts.scala index 8c5e1c2fd5cc..41ea6660638e 100644 --- a/tests/pos/reference/adts.scala +++ b/tests/pos/reference/adts.scala @@ -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) @@ -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 diff --git a/tests/run-custom-args/typeclass-derivation2.scala b/tests/run-custom-args/typeclass-derivation2.scala index 59b1863854e2..aec56c59219b 100644 --- a/tests/run-custom-args/typeclass-derivation2.scala +++ b/tests/run-custom-args/typeclass-derivation2.scala @@ -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 } diff --git a/tests/run-macros/i8007/Test_4.scala b/tests/run-macros/i8007/Test_4.scala index f9cc926c4819..3bce338f2aec 100644 --- a/tests/run-macros/i8007/Test_4.scala +++ b/tests/run-macros/i8007/Test_4.scala @@ -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 } @@ -41,4 +41,4 @@ enum Opt[+T] { val t7 = Sm(Person("Test", 23)) === Sm(Person("Test", 24)) println(t7) // false println -} \ No newline at end of file +} diff --git a/tests/run/enum-List2.scala b/tests/run/enum-List2.scala index 7346a6294e4f..9ceb3d278f7e 100644 --- a/tests/run/enum-List2.scala +++ b/tests/run/enum-List2.scala @@ -1,5 +1,5 @@ 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 { @@ -7,4 +7,3 @@ object Test { val xs = Cons(1, Cons(2, Cons(3, Nil))) def main(args: Array[String]) = println(xs) } - diff --git a/tests/run/enum-List2a.scala b/tests/run/enum-List2a.scala index 7346a6294e4f..9ceb3d278f7e 100644 --- a/tests/run/enum-List2a.scala +++ b/tests/run/enum-List2a.scala @@ -1,5 +1,5 @@ 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 { @@ -7,4 +7,3 @@ object Test { val xs = Cons(1, Cons(2, Cons(3, Nil))) def main(args: Array[String]) = println(xs) } - diff --git a/tests/run/enum-Option.scala b/tests/run/enum-Option.scala index 293b48a49e6c..5762b4089317 100644 --- a/tests/run/enum-Option.scala +++ b/tests/run/enum-Option.scala @@ -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 { diff --git a/tests/run/enum-Option1.scala b/tests/run/enum-Option1.scala index a3d5767244f6..407ece47a025 100644 --- a/tests/run/enum-Option1.scala +++ b/tests/run/enum-Option1.scala @@ -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 { diff --git a/tests/run/enum-values.scala b/tests/run/enum-values.scala index d8686b162e1f..cb7f5c2d533e 100644 --- a/tests/run/enum-values.scala +++ b/tests/run/enum-values.scala @@ -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[_]]: diff --git a/tests/run/generic/List.scala b/tests/run/generic/List.scala index 07ed98295ba3..b2936ab1c3a4 100644 --- a/tests/run/generic/List.scala +++ b/tests/run/generic/List.scala @@ -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] * } */ @@ -86,4 +86,4 @@ object List { case Snd(n) => n } } -} \ No newline at end of file +} diff --git a/tests/run/i9011.scala b/tests/run/i9011.scala index 93891dcc2b0a..e76f0b522409 100644 --- a/tests/run/i9011.scala +++ b/tests/run/i9011.scala @@ -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._ @@ -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)) -} \ No newline at end of file +} diff --git a/tests/run/typeclass-derivation-doc-example.scala b/tests/run/typeclass-derivation-doc-example.scala index 0d2195ae474d..c70b1bac5c50 100644 --- a/tests/run/typeclass-derivation-doc-example.scala +++ b/tests/run/typeclass-derivation-doc-example.scala @@ -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 } diff --git a/tests/run/typeclass-derivation1.scala b/tests/run/typeclass-derivation1.scala index acc18048a75f..a6f8096c8ecb 100644 --- a/tests/run/typeclass-derivation1.scala +++ b/tests/run/typeclass-derivation1.scala @@ -11,7 +11,7 @@ object Deriving { } enum Lst[+T] { - case Cons(hd: T, tl: Lst[T]) + case Cons[T](hd: T, tl: Lst[T]) extends Lst[T] case Nil } @@ -95,4 +95,4 @@ object Test extends App { assert(!eq2.equals(xss, yss)) assert(!eq2.equals(yss, xss)) assert(eq2.equals(yss, yss)) -} \ No newline at end of file +} diff --git a/tests/run/typeclass-derivation2a.scala b/tests/run/typeclass-derivation2a.scala index 687c9c436d7c..4b9447b5591a 100644 --- a/tests/run/typeclass-derivation2a.scala +++ b/tests/run/typeclass-derivation2a.scala @@ -115,7 +115,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 } diff --git a/tests/run/typeclass-derivation3.scala b/tests/run/typeclass-derivation3.scala index 3a879908cb69..d2e56735445e 100644 --- a/tests/run/typeclass-derivation3.scala +++ b/tests/run/typeclass-derivation3.scala @@ -6,7 +6,7 @@ object datatypes { // 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 } object Lst {} diff --git a/tests/semanticdb/metac.expect b/tests/semanticdb/metac.expect index 6bbfad8b94d9..41a2ac13737b 100644 --- a/tests/semanticdb/metac.expect +++ b/tests/semanticdb/metac.expect @@ -701,7 +701,7 @@ _empty_/Enums.Maybe.$fromOrdinal(). => method $fromOrdinal _empty_/Enums.Maybe.$fromOrdinal().(_$ordinal) => param _$ordinal _empty_/Enums.Maybe.$values. => val method $values _empty_/Enums.Maybe.Just# => final case enum class Just -_empty_/Enums.Maybe.Just#[A] => typeparam A +_empty_/Enums.Maybe.Just#[A] => covariant typeparam A _empty_/Enums.Maybe.Just#_1(). => method _1 _empty_/Enums.Maybe.Just#``(). => primary ctor _empty_/Enums.Maybe.Just#``().(value) => val param value