From 364818075ea4004402f2549c096e5e4a20dd6f34 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 15 Feb 2018 16:19:54 +0100 Subject: [PATCH 01/17] Simplify Enums I had a lingering unease that the rules for enums were too complex, and the syntax too cumbersome. I now think I have found a way to simplify things considerably. The key was to take away one capabilitiy: that cases can have bodies which can define members. Arguably, if we choose an ADT decompositon of a problem it's good style to write all methods using pattern matching instead of overriding individual cases. So this removes an unnecessary choice. What's more, once we have eliminated case bodies we also have eliminated scope confusion. All that remains are the case parameters and extends clause. Extends clauses of cases can be handled like supercalls in constructors: I.e. the enclosing enum class is not visible for them. This means we can treat enums unequivocally as classes. They can have methods and other statements just like other classes can. Cases in enums are seen as a form of constructors. We do not need a distinction between enum class and enum object anymore. Enums can have companion objects just like normal classes can, of course. This also means that type parameters of enums scope naturally over cases, just like they scope over secondary constructors. We do not need to repeat them in cases anymore, which is a huge relief. This first commit changes the syntax and docs. It still needs to be implemented. --- docs/docs/internals/syntax.md | 33 +++--- docs/docs/reference/enums/adts.md | 59 +++++----- docs/docs/reference/enums/desugarEnums.md | 133 +++++++++++----------- docs/docs/reference/enums/enums.md | 35 ++---- tests/patmat/enum-Tree.scala | 2 +- tests/run/enum-List1.scala | 7 +- tests/run/enum-List2.scala | 7 +- tests/run/enum-List2a.scala | 5 +- tests/run/enum-List3.scala | 2 +- tests/run/enum-Tree.scala | 2 +- tests/run/enum-approx.scala | 20 ++-- tests/run/planets.scala | 5 +- 12 files changed, 142 insertions(+), 168 deletions(-) diff --git a/docs/docs/internals/syntax.md b/docs/docs/internals/syntax.md index 40825d221589..8818fb5b8386 100644 --- a/docs/docs/internals/syntax.md +++ b/docs/docs/internals/syntax.md @@ -290,16 +290,6 @@ AccessQualifier ::= ‘[’ (id | ‘this’) ‘]’ Annotation ::= ‘@’ SimpleType {ParArgumentExprs} Apply(tpe, args) -TemplateBody ::= [nl] ‘{’ [SelfType] TemplateStat {semi TemplateStat} ‘}’ (self, stats) -TemplateStat ::= Import - | {Annotation [nl]} {Modifier} Def - | {Annotation [nl]} {Modifier} Dcl - | EnumCaseStat - | Expr1 - | -SelfType ::= id [‘:’ InfixType] ‘=>’ ValDef(_, name, tpt, _) - | ‘this’ ‘:’ InfixType ‘=>’ - Import ::= ‘import’ ImportExpr {‘,’ ImportExpr} ImportExpr ::= StableId ‘.’ (id | ‘_’ | ImportSelectors) Import(expr, sels) ImportSelectors ::= ‘{’ {ImportSelector ‘,’} (ImportSelector | ‘_’) ‘}’ @@ -335,19 +325,14 @@ DefDef ::= DefSig [‘:’ Type] ‘=’ Expr | ‘this’ DefParamClause DefParamClauses DefDef(_, , Nil, vparamss, EmptyTree, expr | Block) (‘=’ ConstrExpr | [nl] ConstrBlock) -TmplDef ::= ([‘case’ | `enum'] ‘class’ | trait’) ClassDef +TmplDef ::= ([‘case’] ‘class’ | trait’) ClassDef | [‘case’] ‘object’ ObjectDef | `enum' EnumDef ClassDef ::= id ClassConstr TemplateOpt ClassDef(mods, name, tparams, templ) ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsParamClauses with DefDef(_, , Nil, vparamss, EmptyTree, EmptyTree) as first stat ConstrMods ::= {Annotation} [AccessModifier] ObjectDef ::= id TemplateOpt ModuleDef(mods, name, template) // no constructor -EnumDef ::= id ClassConstr [`extends' [ConstrApps]] EnumDef(mods, name, tparams, template) - [nl] ‘{’ EnumCaseStat {semi EnumCaseStat} ‘}’ -EnumCaseStat ::= {Annotation [nl]} {Modifier} EnumCase -EnumCase ::= `case' (EnumClassDef | ObjectDef | ids) -EnumClassDef ::= id [ClsTpeParamClause | ClsParamClause] ClassDef(mods, name, tparams, templ) - ClsParamClauses TemplateOpt +EnumDef ::= id ClassConstr [`extends' [ConstrApps]] EnumBody EnumDef(mods, name, tparams, template) TemplateOpt ::= [‘extends’ Template | [nl] TemplateBody] Template ::= ConstrApps [TemplateBody] | TemplateBody Template(constr, parents, self, stats) ConstrApps ::= ConstrApp {‘with’ ConstrApp} @@ -357,6 +342,20 @@ ConstrExpr ::= SelfInvocation SelfInvocation ::= ‘this’ ArgumentExprs {ArgumentExprs} ConstrBlock ::= ‘{’ SelfInvocation {semi BlockStat} ‘}’ +TemplateBody ::= [nl] ‘{’ [SelfType] TemplateStat {semi TemplateStat} ‘}’ (self, stats) +TemplateStat ::= Import + | {Annotation [nl]} {Modifier} Def + | {Annotation [nl]} {Modifier} Dcl + | Expr1 + | +SelfType ::= id [‘:’ InfixType] ‘=>’ ValDef(_, name, tpt, _) + | ‘this’ ‘:’ InfixType ‘=>’ + +EnumBody ::= [nl] ‘{’ [SelfType] EnumStat {semi EnumStat} ‘}’ +EnumStat ::= TemplateStat + | {Annotation [nl]} {Modifier} EnumCase +EnumCase ::= `case' (id [ClsTpeParamClause] {ClsParamClause} | ids) + TopStatSeq ::= TopStat {semi TopStat} TopStat ::= {Annotation [nl]} {Modifier} TmplDef | Import diff --git a/docs/docs/reference/enums/adts.md b/docs/docs/reference/enums/adts.md index 92851057fa5d..b0226c297e50 100644 --- a/docs/docs/reference/enums/adts.md +++ b/docs/docs/reference/enums/adts.md @@ -10,28 +10,28 @@ how an `Option` type can be represented as an ADT: ```scala enum Option[+T] { - case Some[+T](x: T) + case Some(x: T) case None } ``` -This example introduces `Option` enum class with a covariant type -parameter `T`, together with two cases, `Some` and `None`. `Some` is -parameterized with a type parameter `T` and a value parameter `x`. It -is a shorthand for writing a case class that extends `Option`. Since -`None` is not parameterized it is treated as a normal enum value. +This example introduces an `Option` enum with a covariant type +parameter `T` consisting of two cases, `Some` and `None`. `Some` is +parameterized with a value parameter `x`. It is a shorthand for writing a +case class that extends `Option`. Since `None` is not parameterized, it +is treated as a normal enum value. The `extends` clauses that were omitted in the example above can also be given explicitly: ```scala enum Option[+T] { - case Some[+T](x: T) extends Option[T] - case None extends Option[Nothing] + case Some(x: T) extends Option[T] + case None extends Option[Nothing] } ``` -Note that the parent type of `None` is inferred as +Note that the parent type of the `None` value is inferred as `Option[Nothing]`. Generally, all covariant type parameters of the enum class are minimized in a compiler-generated extends clause whereas all contravariant type parameters are maximized. If `Option` was non-variant, @@ -59,24 +59,22 @@ scala> new Option.Some(2) val res3: t2.Option.Some[Int] = Some(2) ``` -As all other enums, ADTs can have methods on both the enum class and -its companion object. For instance, here is `Option` again, with an -`isDefined` method and an `Option(...)` constructor. +As all other enums, ADTs can define methods. For instance, here is `Option` again, with an +`isDefined` method and an `Option(...)` constructor in its companion object. ```scala -enum class Option[+T] { - def isDefined: Boolean +enum Option[+T] { + case Some(x: T) extends Option[T] + case None + + def isDefined: Boolean = this match { + case None => false + case some => true + } } object Option { def apply[T >: Null](x: T): Option[T] = if (x == null) None else Some(x) - - case Some[+T](x: T) { - def isDefined = true - } - case None { - def isDefined = false - } } ``` @@ -98,23 +96,20 @@ enum Color(val rgb: Int) { ### Syntax of Enums -Changes to the syntax fall in two categories: enum classes and cases inside enums. +Changes to the syntax fall in two categories: enum definitions and cases inside enums. The changes are specified below as deltas with respect to the Scala syntax given [here](http://dotty.epfl.ch/docs/internals/syntax.html) - 1. Enum definitions and enum classes are defined as follows: + 1. Enum definitions are defined as follows: - TmplDef ::= `enum' `class’ ClassDef - | `enum' EnumDef - EnumDef ::= id ClassConstr [`extends' [ConstrApps]] - [nl] `{’ EnumCaseStat {semi EnumCaseStat} `}’ + TmplDef ::= `enum' EnumDef + EnumDef ::= id ClassConstr [`extends' [ConstrApps]] EnumBody + EnumBody ::= [nl] ‘{’ [SelfType] EnumStat {semi EnumStat} ‘}’ + EnumStat ::= TemplateStat + | {Annotation [nl]} {Modifier} EnumCase 2. Cases of enums are defined as follows: - EnumCaseStat ::= {Annotation [nl]} {Modifier} EnumCase - EnumCase ::= `case' (EnumClassDef | ObjectDef | ids) - EnumClassDef ::= id [ClsTpeParamClause | ClsParamClause] - ClsParamClauses TemplateOpt - TemplateStat ::= ... | EnumCaseStat + EnumCase ::= `case' (id [ClsTpeParamClause] {ClsParamClause} | ids) ### Reference diff --git a/docs/docs/reference/enums/desugarEnums.md b/docs/docs/reference/enums/desugarEnums.md index 1d3c1c448313..eb40cb06c47b 100644 --- a/docs/docs/reference/enums/desugarEnums.md +++ b/docs/docs/reference/enums/desugarEnums.md @@ -3,7 +3,7 @@ layout: doc-page title: "Translation of Enums and ADTs" --- -The compiler expands enum classes and cases to code that only uses +The compiler expands enums and their cases to code that only uses Scala's other language features. As such, enums in Scala are convenient _syntactic sugar_, but they are not essential to understand Scala's core. @@ -11,103 +11,101 @@ Scala's core. We now explain the expansion of enums in detail. First, some terminology and notational conventions: - - We use `E` as a name of an enum class, and `C` as a name of an enum case that appears in the companion object of `E`. - - We use `<...>` for syntactic constructs that in some circumstances might be empty. For instance `` represents either the body of a case between `{...}` or nothing at all. + - We use `E` as a name of an enum, and `C` as a name of a case that appears in `E`. + - We use `<...>` for syntactic constructs that in some circumstances might be empty. For instance, + `` represents one or more a parameter lists `(...)` or nothing at all. - Enum cases fall into three categories: - _Class cases_ are those cases that are parameterized, either with a type parameter section `[...]` or with one or more (possibly empty) parameter sections `(...)`. - - _Simple cases_ are cases of a non-generic enum class that have neither parameters nor an extends clause or body. That is, they consist of a name only. + - _Simple cases_ are cases of a non-generic enum that have neither parameters nor an extends clause or body. That is, they consist of a name only. - _Value cases_ are all cases that do not have a parameter section but that do have a (possibly generated) extends clause and/or a body. Simple cases and value cases are collectively called _singleton cases_. The desugaring rules imply that class cases are mapped to case classes, and singleton cases are mapped to `val` definitions. -There are eight desugaring rules. Rules (1) and (2) desugar enums and -enum classes. Rules (3) and (4) define extends clauses for cases that -are missing them. Rules (5 - 7) define how such expanded cases map -into case classes, case objects or vals. Finally, rule (8) expands -comma separated simple cases into a sequence of cases. +There are eight desugaring rules. Rule (1) desugar enum definitions. Rules +(2) and (3) desugar simple cases. Rules (4) to (6) define extends clauses for cases that +are missing them. Rules (7) and (8) define how such cases with extends clauses +map into case classes or vals. 1. An `enum` definition - enum E ... { } + enum E ... { } - expands to an enum class and a companion object + expands to a `sealed` `abstract` class that extends the `scala.Enum` trait and + an associated companion object that contains the defined cases, expanded according + to rules (2 - 8). - enum class E ... - object E { } + sealed abstract class E ... extends with scala.Enum { } + object E { } -2. An enum class definition +2. A simple case consisting of a comma-separated list of enum names - enum class E ... extends ... + case C_1, ..., C_n - expands to a `sealed` `abstract` class that extends the `scala.Enum` trait: + expands to - sealed abstract class E ... extends with scala.Enum ... + case C_1; ...; case C_n -3. If `E` is an enum class without type parameters, then a case in its companion object without an extends clause + Any modifiers or annotations on the original case extend to all expanded + cases. - case C +3. A simple case - expands to + case C - case C extends E + of an enum `E` that does not take type parameters expands to -4. If `E` is an enum class with type parameters `Ts`, then a case in its - companion object without an extends clause + val C = $new(n, "C") - case C + Here, `$new` is a private method that creates an instance of of `E` (see + below). - expands according to two alternatives, depending whether `C` has type - parameters or not. If `C` has type parameters, they must have the same - names and appear in the same order as the enum type parameters `Ts` - (variances may be different, however). In this case +4. If `E` is an enum with type parameters - case C [Ts] + V1 T1 > L1 <: U1 , ... , Vn Tn >: Ln <: Un (n > 0) - expands to + where each of the variances `Vi` is either `'+'` or `'-'`, then a simple case - case C[Ts] extends E[Ts] + case C - For the case where `C` does not have type parameters, assume `E`'s type - parameters are + expands to - V1 T1 > L1 <: U1 , ... , Vn Tn >: Ln <: Un (n > 0) + case C extends E[B1, ..., Bn] - where each of the variances `Vi` is either `'+'` or `'-'`. Then the case - expands to + where `Bi` is `Li` if `Vi = '+'` and `Ui` if `Vi = '-'`. This result is then further + rewritten with rule (7). Simple cases of enums with non-variant type + parameters are not permitted. - case C extends E[B1, ..., Bn] +5. A class case without an extends clause - where `Bi` is `Li` if `Vi = '+'` and `Ui` if `Vi = '-'`. It is an error if - `Bi` refers to some other type parameter `Tj (j = 0,..,n-1)`. It is also - an error if `E` has type parameters that are non-variant. + case C -5. A class case + of an enum `E` that does not take type parameters expands to - case C ... + case C extends E - expands analogous to a final case class: + This result is then further rewritten with rule (8). - final case class C ... +6. If `E` is an enum with type parameters `Ts`, a class case with neither type parameters nor + an extends clause - However, unlike for a regular case class, the return type of the associated - `apply` method is a fully parameterized type instance of the enum class `E` - itself instead of `C`. Also the enum case defines an `enumTag` method of - the form + case C - def enumTag = n + expands to - where `n` is the ordinal number of the case in the companion object, - starting from 0. + case C[Ts] extends E[Ts] -6. A value case + This result is then further rewritten with rule (8). For class cases that have type parameters + themselves, an extends clause needs to be given explicitly. - case C extends +7. A value case - expands to a value definition + case C extends + + expands to a value definition in `E`'s companion object: val C = new { ; def enumTag = n; $values.register(this) } @@ -116,27 +114,24 @@ comma separated simple cases into a sequence of cases. as one of the `enumValues` of the enumeration (see below). `$values` is a compiler-defined private value in the companion object. -7. A simple case - - case C +8. A class case - of an enum class `E` that does not take type parameters expands to - - val C = $new(n, "C") + case C extends - Here, `$new` is a private method that creates an instance of of `E` (see - below). + expands analogous to a final case class in `E`'s companion object: -8. A simple case consisting of a comma-separated list of enum names + final case class C extends - case C_1, ..., C_n + However, unlike for a regular case class, the return type of the associated + `apply` method is a fully parameterized type instance of the enum class `E` + itself instead of `C`. Also the enum case defines an `enumTag` method of + the form - expands to + def enumTag = n - case C_1; ...; case C_n + where `n` is the ordinal number of the case in the companion object, + starting from 0. - Any modifiers or annotations on the original case extend to all expanded - cases. ### Equality @@ -149,7 +144,7 @@ be compared only to other values of the same enum type. Furtermore, generic ### Translation of Enumerations -Non-generic enum classes `E` that define one or more singleton cases +Non-generic enums `E` that define one or more singleton cases are called _enumerations_. Companion objects of enumerations define the following additional members. diff --git a/docs/docs/reference/enums/enums.md b/docs/docs/reference/enums/enums.md index 48e9646542ef..420ac849c597 100644 --- a/docs/docs/reference/enums/enums.md +++ b/docs/docs/reference/enums/enums.md @@ -13,22 +13,11 @@ enum Color { This defines a new `sealed` class, `Color`, with three values, `Color.Red`, `Color.Green`, `Color.Blue`. The color values are members of `Color`s -companion object. The `Color` definition above is equivalent to the -following more explicit definition of an _enum class_ and a companion -object: - -```scala -enum class Color -object Color { - case Red - case Green - case Blue -} -``` +companion object. ### Parameterized enums -Enum classes can be parameterized. +Enums can be parameterized. ```scala enum Color(val rgb: Int) { @@ -53,7 +42,7 @@ scala> red.enumTag val res0: Int = 0 ``` -The companion object of an enum class also defines three utility methods. +The companion object of an enum also defines three utility methods. The `enumValue` and `enumValueNamed` methods obtain an enum value by its tag or its name. The `enumValues` method returns all enum values defined in an enumeration in an `Iterable`. @@ -69,20 +58,14 @@ val res3: collection.Iterable[Color] = MapLike(Red, Green, Blue) ### User-defined members of enums -It is possible to add your own definitions to an enum class or its -companion object. To make clear what goes where you need to use the -longer syntax which defines an enum class alongside its companion -object explicitly. In the following example, we define some methods in -class `Planet` and a `main` method in its companion object. +It is possible to add your own definitions to an enum. Example: ```scala -enum class Planet(mass: Double, radius: Double) { +enum Planet(mass: Double, radius: Double) { private final val G = 6.67300E-11 def surfaceGravity = G * mass / (radius * radius) def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity -} -object Planet { case MERCURY extends Planet(3.303e+23, 2.4397e6) case VENUS extends Planet(4.869e+24, 6.0518e6) case EARTH extends Planet(5.976e+24, 6.37814e6) @@ -91,7 +74,13 @@ object Planet { case SATURN extends Planet(5.688e+26, 6.0268e7) case URANUS extends Planet(8.686e+25, 2.5559e7) case NEPTUNE extends Planet(1.024e+26, 2.4746e7) +} +``` + +It is also possible to define an explicit companion object for an enum: +```scala +object Planet { def main(args: Array[String]) = { val earthWeight = args(0).toDouble val mass = earthWeight/EARTH.surfaceGravity @@ -103,7 +92,7 @@ object Planet { ### Implementation -Enum classes are represented as `sealed` classes that extend the `scala.Enum` trait. +Enums are represented as `sealed` classes that extend the `scala.Enum` trait. This trait defines a single method, `enumTag`: ```scala diff --git a/tests/patmat/enum-Tree.scala b/tests/patmat/enum-Tree.scala index 68e4fc012545..ef5bd7a5714e 100644 --- a/tests/patmat/enum-Tree.scala +++ b/tests/patmat/enum-Tree.scala @@ -5,7 +5,7 @@ enum Tree[T] { case Succ(n: Tree[Int]) extends Tree[Int] case Pred(n: Tree[Int]) extends Tree[Int] case IsZero(n: Tree[Int]) extends Tree[Boolean] - case If[T](cond: Tree[Boolean], thenp: Tree[T], elsep: Tree[T]) + case If(cond: Tree[Boolean], thenp: Tree[T], elsep: Tree[T]) } object Test { diff --git a/tests/run/enum-List1.scala b/tests/run/enum-List1.scala index 03a81bd991bd..f3e672e53571 100644 --- a/tests/run/enum-List1.scala +++ b/tests/run/enum-List1.scala @@ -1,7 +1,6 @@ -enum class List[T] -object List { - case Cons[T](x: T, xs: List[T]) - case Nil[T]() +enum List[T] { + case Cons(x: T, xs: List[T]) + case Nil() } object Test { import List._ diff --git a/tests/run/enum-List2.scala b/tests/run/enum-List2.scala index 6a1d8d894b78..7346a6294e4f 100644 --- a/tests/run/enum-List2.scala +++ b/tests/run/enum-List2.scala @@ -1,7 +1,6 @@ -enum class List[+T] -object List { - case Cons[+T](x: T, xs: List[T]) - case Nil extends List[Nothing] +enum List[+T] { + case Cons(x: T, xs: List[T]) + case Nil } object Test { import List._ diff --git a/tests/run/enum-List2a.scala b/tests/run/enum-List2a.scala index d4f47c2da4ef..7346a6294e4f 100644 --- a/tests/run/enum-List2a.scala +++ b/tests/run/enum-List2a.scala @@ -1,6 +1,5 @@ -enum class List[+T] -object List { - case Cons[+T](x: T, xs: List[T]) +enum List[+T] { + case Cons(x: T, xs: List[T]) case Nil } object Test { diff --git a/tests/run/enum-List3.scala b/tests/run/enum-List3.scala index 8275e8bc3264..5ce268f8a7bf 100644 --- a/tests/run/enum-List3.scala +++ b/tests/run/enum-List3.scala @@ -1,6 +1,6 @@ enum List[+T] { case Cons[T](x: T, xs: List[T]) - case Nil + case Nil extends List[Nothing] } object Test { import List._ diff --git a/tests/run/enum-Tree.scala b/tests/run/enum-Tree.scala index 68e4fc012545..ef5bd7a5714e 100644 --- a/tests/run/enum-Tree.scala +++ b/tests/run/enum-Tree.scala @@ -5,7 +5,7 @@ enum Tree[T] { case Succ(n: Tree[Int]) extends Tree[Int] case Pred(n: Tree[Int]) extends Tree[Int] case IsZero(n: Tree[Int]) extends Tree[Boolean] - case If[T](cond: Tree[Boolean], thenp: Tree[T], elsep: Tree[T]) + case If(cond: Tree[Boolean], thenp: Tree[T], elsep: Tree[T]) } object Test { diff --git a/tests/run/enum-approx.scala b/tests/run/enum-approx.scala index 7811b3909bf0..f18af97df40a 100644 --- a/tests/run/enum-approx.scala +++ b/tests/run/enum-approx.scala @@ -1,14 +1,14 @@ -enum class Fun[-T, +U >: Null] { - def f: T => U = null -} -object Fun { - case Identity[T, U >: Null](override val f: T => U) extends Fun[T, U] - case ConstNull { - override def f = x => null - } - case ConstNullClass() { - override def f = x => null +enum Fun[-T, +U >: Null] { + def f: T => U = this match { + case Identity(g) => g + case ConstNull => (_ => null) + case ConstNullClass() => (_ => null) + case ConstNullSimple => null } + + case Identity[T, U >: Null](g: T => U) extends Fun[T, U] + case ConstNull + case ConstNullClass() case ConstNullSimple } diff --git a/tests/run/planets.scala b/tests/run/planets.scala index bcbfd7eeb3cc..22c731428f70 100644 --- a/tests/run/planets.scala +++ b/tests/run/planets.scala @@ -1,9 +1,8 @@ -enum class Planet(mass: Double, radius: Double) { +enum Planet(mass: Double, radius: Double) { private final val G = 6.67300E-11 def surfaceGravity = G * mass / (radius * radius) def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity -} -object Planet { + case MERCURY extends Planet(3.303e+23, 2.4397e6) case VENUS extends Planet(4.869e+24, 6.0518e6) case EARTH extends Planet(5.976e+24, 6.37814e6) From 6a36ccc4903a1584c59342d4252af02c60092c28 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 15 Feb 2018 16:28:47 +0100 Subject: [PATCH 02/17] Fixes to indentation --- docs/docs/reference/enums/desugarEnums.md | 10 +++++----- docs/docs/reference/enums/enums.md | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/docs/reference/enums/desugarEnums.md b/docs/docs/reference/enums/desugarEnums.md index eb40cb06c47b..f7bb2c21aa2b 100644 --- a/docs/docs/reference/enums/desugarEnums.md +++ b/docs/docs/reference/enums/desugarEnums.md @@ -38,8 +38,8 @@ map into case classes or vals. an associated companion object that contains the defined cases, expanded according to rules (2 - 8). - sealed abstract class E ... extends with scala.Enum { } - object E { } + sealed abstract class E ... extends with scala.Enum { } + object E { } 2. A simple case consisting of a comma-separated list of enum names @@ -79,13 +79,13 @@ map into case classes or vals. rewritten with rule (7). Simple cases of enums with non-variant type parameters are not permitted. -5. A class case without an extends clause +5. A class case without an extends clause - case C + case C of an enum `E` that does not take type parameters expands to - case C extends E + case C extends E This result is then further rewritten with rule (8). diff --git a/docs/docs/reference/enums/enums.md b/docs/docs/reference/enums/enums.md index 420ac849c597..3868961e3a1f 100644 --- a/docs/docs/reference/enums/enums.md +++ b/docs/docs/reference/enums/enums.md @@ -129,4 +129,5 @@ val Red: Color = $new(0, "Red") ### Reference -For more info, see [Issue #1970](https://github.com/lampepfl/dotty/issues/1970). +For more info, see [Issue #1970](https://github.com/lampepfl/dotty/issues/1970) and +[PR #4003](https://github.com/lampepfl/dotty/pull/4003). From aa24efe42794fa037858d44f27a23821c1791325 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 15 Feb 2018 18:59:54 +0100 Subject: [PATCH 03/17] Change parser to new enum syntax --- .../dotty/tools/dotc/parsing/Parsers.scala | 117 ++++++++---------- docs/docs/internals/syntax.md | 2 +- docs/docs/reference/enums/adts.md | 2 +- tests/patmat/enum-Tree.scala | 2 +- tests/run/enum-Option.scala | 17 ++- 5 files changed, 64 insertions(+), 76 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 1870777ef588..c1b4a1c8f069 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -308,7 +308,6 @@ object Parsers { * class constructor */ private[this] var inClassConstrAnnots = false - private def fromWithinClassConstr[T](body: => T): T = { val saved = inClassConstrAnnots inClassConstrAnnots = true @@ -316,6 +315,14 @@ object Parsers { finally inClassConstrAnnots = saved } + private[this] var inEnum = false + private def withinEnum[T](isEnum: Boolean)(body: => T): T = { + val saved = inEnum + inEnum = isEnum + try body + finally inEnum = saved + } + def migrationWarningOrError(msg: String, offset: Int = in.offset) = if (in.isScala2Mode) ctx.migrationWarning(msg, source atPos Position(offset)) @@ -1975,7 +1982,7 @@ object Parsers { * | var ValDcl * | def DefDcl * | type {nl} TypeDcl - * EnumCase ::= `case' (EnumClassDef | ObjectDef) + * EnumCase ::= `case' (id ClassConstr [`extends' ConstrApps]] | ids) */ def defOrDcl(start: Int, mods: Modifiers): Tree = in.token match { case VAL => @@ -1990,7 +1997,7 @@ object Parsers { defDefOrDcl(start, posMods(start, mods)) case TYPE => typeDefOrDcl(start, posMods(start, mods)) - case CASE => + case CASE if inEnum => enumCase(start, mods) case _ => tmplDef(start, mods) @@ -2135,7 +2142,7 @@ object Parsers { } } - /** TmplDef ::= ([`case' | `enum]'] ‘class’ | trait’) ClassDef + /** TmplDef ::= ([`case'] ‘class’ | trait’) ClassDef * | [`case'] `object' ObjectDef * | `enum' EnumDef */ @@ -2152,9 +2159,7 @@ object Parsers { case CASEOBJECT => objectDef(start, posMods(start, mods | Case | Module)) case ENUM => - val enumMod = atPos(in.skipToken()) { Mod.Enum() } - if (in.token == CLASS) tmplDef(start, addMod(mods, enumMod)) - else enumDef(start, mods, enumMod) + enumDef(start, mods, atPos(in.skipToken()) { Mod.Enum() }) case _ => syntaxErrorOrIncomplete(ExpectedStartOfTopLevelDefinition()) EmptyTree @@ -2198,55 +2203,18 @@ object Parsers { ModuleDef(name, template).withMods(mods).setComment(in.getDocComment(start)) } - /** id ClassConstr [`extends' [ConstrApps]] - * [nl] ‘{’ EnumCaseStats ‘}’ + /** EnumDef ::= id ClassConstr [`extends' [ConstrApps]] EnumBody */ - def enumDef(start: Offset, mods: Modifiers, enumMod: Mod): Thicket = { - val point = nameStart + def enumDef(start: Offset, mods: Modifiers, enumMod: Mod): TypeDef = atPos(start, nameStart) { val modName = ident() val clsName = modName.toTypeName val constr = classConstr(clsName) - val parents = - if (in.token == EXTENDS) { - in.nextToken(); - newLineOptWhenFollowedBy(LBRACE) - if (in.token == LBRACE) Nil else tokenSeparated(WITH, constrApp) - } - else Nil - val clsDef = atPos(start, point) { - TypeDef(clsName, Template(constr, parents, EmptyValDef, Nil)) - .withMods(addMod(mods, enumMod)).setComment(in.getDocComment(start)) - } - newLineOptWhenFollowedBy(LBRACE) - val modDef = atPos(in.offset) { - val body = inBraces(enumCaseStats()) - ModuleDef(modName, Template(emptyConstructor, Nil, EmptyValDef, body)) - .withMods(mods) - } - Thicket(clsDef :: modDef :: Nil) - } - - /** EnumCaseStats = EnumCaseStat {semi EnumCaseStat} */ - def enumCaseStats(): List[DefTree] = { - val cases = new ListBuffer[DefTree] += enumCaseStat() - var exitOnError = false - while (!isStatSeqEnd && !exitOnError) { - acceptStatSep() - if (isCaseIntro) - cases += enumCaseStat() - else if (!isStatSep) { - exitOnError = mustStartStat - syntaxErrorOrIncomplete("illegal start of case") - } - } - cases.toList + val impl = templateOpt(constr, isEnum = true) + TypeDef(clsName, impl).withMods(addMod(mods, enumMod)).setComment(in.getDocComment(start)) } - /** EnumCaseStat = {Annotation [nl]} {Modifier} EnumCase */ - def enumCaseStat(): DefTree = - enumCase(in.offset, defAnnotsMods(modifierTokens)) - - /** EnumCase = `case' (EnumClassDef | ObjectDef) */ + /** EnumCase = `case' (id ClassConstr [`extends' ConstrApps] | ids) + */ def enumCase(start: Offset, mods: Modifiers): DefTree = { val mods1 = mods.withAddedMod(atPos(in.offset)(Mod.EnumCase())) | Case accept(CASE) @@ -2257,18 +2225,36 @@ object Parsers { atPos(start, nameStart) { val id = termIdent() - if (in.token == LBRACKET || in.token == LPAREN) - classDefRest(start, mods1, id.name.toTypeName) - else if (in.token == COMMA) { + if (in.token == COMMA) { in.nextToken() val ids = commaSeparated(() => termIdent()) PatDef(mods1, id :: ids, TypeTree(), EmptyTree) } - else - objectDefRest(start, mods1, id.name.asTermName) + else { + val caseDef = + if (in.token == LBRACKET || in.token == LPAREN || in.token == AT || isModifier) { + val clsName = id.name.toTypeName + val constr = classConstr(clsName, isCaseClass = true) + TypeDef(clsName, caseTemplate(constr)) + } + else + ModuleDef(id.name.toTermName, caseTemplate(emptyConstructor)) + caseDef.withMods(mods).setComment(in.getDocComment(start)) + } } } + /** [`extends' ConstrApps] */ + def caseTemplate(constr: DefDef): Template = { + val parents = + if (in.token == EXTENDS) { + in.nextToken() + tokenSeparated(WITH, constrApp) + } + else Nil + Template(constr, parents, EmptyValDef, Nil) + } + /* -------- TEMPLATES ------------------------------------------- */ /** ConstrApp ::= SimpleType {ParArgumentExprs} @@ -2285,32 +2271,34 @@ object Parsers { * @return a pair consisting of the template, and a boolean which indicates * whether the template misses a body (i.e. no {...} part). */ - def template(constr: DefDef): (Template, Boolean) = { + def template(constr: DefDef, isEnum: Boolean = false): (Template, Boolean) = { newLineOptWhenFollowedBy(LBRACE) - if (in.token == LBRACE) (templateBodyOpt(constr, Nil), false) + if (in.token == LBRACE) (templateBodyOpt(constr, Nil, isEnum), false) else { val parents = tokenSeparated(WITH, constrApp) newLineOptWhenFollowedBy(LBRACE) + if (isEnum && in.token != LBRACE) + syntaxErrorOrIncomplete(ExpectedTokenButFound(LBRACE, in.token)) val missingBody = in.token != LBRACE - (templateBodyOpt(constr, parents), missingBody) + (templateBodyOpt(constr, parents, isEnum), missingBody) } } /** TemplateOpt = [`extends' Template | TemplateBody] */ - def templateOpt(constr: DefDef): Template = - if (in.token == EXTENDS) { in.nextToken(); template(constr)._1 } + def templateOpt(constr: DefDef, isEnum: Boolean = false): Template = + if (in.token == EXTENDS) { in.nextToken(); template(constr, isEnum)._1 } else { newLineOptWhenFollowedBy(LBRACE) - if (in.token == LBRACE) template(constr)._1 + if (in.token == LBRACE) template(constr, isEnum)._1 else Template(constr, Nil, EmptyValDef, Nil) } /** TemplateBody ::= [nl] `{' TemplateStatSeq `}' */ - def templateBodyOpt(constr: DefDef, parents: List[Tree]) = { + def templateBodyOpt(constr: DefDef, parents: List[Tree], isEnum: Boolean) = { val (self, stats) = - if (in.token == LBRACE) templateBody() else (EmptyValDef, Nil) + if (in.token == LBRACE) withinEnum(isEnum)(templateBody()) else (EmptyValDef, Nil) Template(constr, parents, self, stats) } @@ -2378,9 +2366,10 @@ object Parsers { * TemplateStat ::= Import * | Annotations Modifiers Def * | Annotations Modifiers Dcl - * | EnumCaseStat * | Expr1 * | + * EnumStat ::= TemplateStat + * | Annotations Modifiers EnumCase */ def templateStatSeq(): (ValDef, List[Tree]) = checkNoEscapingPlaceholders { var self: ValDef = EmptyValDef diff --git a/docs/docs/internals/syntax.md b/docs/docs/internals/syntax.md index 8818fb5b8386..f5084ce6d3dd 100644 --- a/docs/docs/internals/syntax.md +++ b/docs/docs/internals/syntax.md @@ -354,7 +354,7 @@ SelfType ::= id [‘:’ InfixType] ‘=>’ EnumBody ::= [nl] ‘{’ [SelfType] EnumStat {semi EnumStat} ‘}’ EnumStat ::= TemplateStat | {Annotation [nl]} {Modifier} EnumCase -EnumCase ::= `case' (id [ClsTpeParamClause] {ClsParamClause} | ids) +EnumCase ::= `case' (id ClassConstr [`extends' ConstrApps]] | ids) TopStatSeq ::= TopStat {semi TopStat} TopStat ::= {Annotation [nl]} {Modifier} TmplDef diff --git a/docs/docs/reference/enums/adts.md b/docs/docs/reference/enums/adts.md index b0226c297e50..1d5c67d4f731 100644 --- a/docs/docs/reference/enums/adts.md +++ b/docs/docs/reference/enums/adts.md @@ -109,7 +109,7 @@ The changes are specified below as deltas with respect to the Scala syntax given 2. Cases of enums are defined as follows: - EnumCase ::= `case' (id [ClsTpeParamClause] {ClsParamClause} | ids) + EnumCase ::= `case' (id ClassConstr [`extends' ConstrApps]] | ids) ### Reference diff --git a/tests/patmat/enum-Tree.scala b/tests/patmat/enum-Tree.scala index ef5bd7a5714e..4225b802fb9b 100644 --- a/tests/patmat/enum-Tree.scala +++ b/tests/patmat/enum-Tree.scala @@ -5,7 +5,7 @@ enum Tree[T] { case Succ(n: Tree[Int]) extends Tree[Int] case Pred(n: Tree[Int]) extends Tree[Int] case IsZero(n: Tree[Int]) extends Tree[Boolean] - case If(cond: Tree[Boolean], thenp: Tree[T], elsep: Tree[T]) + case If(cond: Tree[Boolean], thenp: Tree[T], elsep: Tree[T]) extends Tree[T] } object Test { diff --git a/tests/run/enum-Option.scala b/tests/run/enum-Option.scala index 364c35fa0b6d..45cd9835bd02 100644 --- a/tests/run/enum-Option.scala +++ b/tests/run/enum-Option.scala @@ -1,15 +1,14 @@ -enum class Option[+T >: Null] extends Serializable { - def isDefined: Boolean +enum Option[+T >: Null] extends Serializable { + case Some(x: T) + case None + + def isDefined: Boolean = this match { + case None => false + case some => true + } } object Option { def apply[T >: Null](x: T): Option[T] = if (x == null) None else Some(x) - - case Some[+T >: Null](x: T) { - def isDefined = true - } - case None { - def isDefined = false - } } object Test { From c8623db2c1489eb8d893ac15cdcb1ff4727b640b Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 16 Feb 2018 15:06:47 +0100 Subject: [PATCH 04/17] Fix generated modifiers for enum cases --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index c1b4a1c8f069..3dd2d1e3f688 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2239,7 +2239,7 @@ object Parsers { } else ModuleDef(id.name.toTermName, caseTemplate(emptyConstructor)) - caseDef.withMods(mods).setComment(in.getDocComment(start)) + caseDef.withMods(mods1).setComment(in.getDocComment(start)) } } } From 4ecf1ad1c262b1cf9987e0594cab7c3d240787d2 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 16 Feb 2018 16:05:18 +0100 Subject: [PATCH 05/17] Provide for watching term references We have the DerivedTypeTree abstraction to generate a TypeTree during desugaring that watches a symbol that does not yet exist. We now need to generalize this so that we can also create term references (i.e. term idents) that watch a symbol. The necessity comes from enums an enum like enum Color { case C1; ...; case Cn; ... } needs to be expanded to sealed abstract class Color { import Color.{C1, ..., Cn} ... } object Color { case class C1; ...; case class Cn } Otherwise the `...` in class `Color` could not refer to the cases. The problem is that we cannot simply write an untyped ident `Color` in the import clause, because a different `Color` might be defined or inherited in the `enum`, so we would get a wrong binding. We need to refer to the `Color` companion object as a symbol, but this one does not yet exist at the point where we expand. Hence the need for a new mechanism. An added complexity comes from the fact that these references go to the ValDef part of a synthesized companion objects. But these objects might be merged with user-defined ones later. We have to make sure that References attachments are correctly passed along in such merges. --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 12 ++++++++---- compiler/src/dotty/tools/dotc/ast/untpd.scala | 4 ++-- compiler/src/dotty/tools/dotc/typer/Namer.scala | 12 +++++++++++- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index a55434c54bd0..a18ad9b4063a 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -33,11 +33,15 @@ object desugar { // ----- DerivedTypeTrees ----------------------------------- class SetterParamTree extends DerivedTypeTree { - def derivedType(sym: Symbol)(implicit ctx: Context) = sym.info.resultType + def derivedTree(sym: Symbol)(implicit ctx: Context) = tpd.TypeTree(sym.info.resultType) } class TypeRefTree extends DerivedTypeTree { - def derivedType(sym: Symbol)(implicit ctx: Context) = sym.typeRef + def derivedTree(sym: Symbol)(implicit ctx: Context) = tpd.TypeTree(sym.typeRef) + } + + class TermRefTree extends DerivedTypeTree { + def derivedTree(sym: Symbol)(implicit ctx: Context) = tpd.ref(sym) } /** A type tree that computes its type from an existing parameter. @@ -73,7 +77,7 @@ object desugar { * * parameter name == reference name ++ suffix */ - def derivedType(sym: Symbol)(implicit ctx: Context) = { + def derivedTree(sym: Symbol)(implicit ctx: Context) = { val relocate = new TypeMap { val originalOwner = sym.owner def apply(tp: Type) = tp match { @@ -91,7 +95,7 @@ object desugar { mapOver(tp) } } - relocate(sym.info) + tpd.TypeTree(relocate(sym.info)) } } diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 22fd83688eaf..74f3f6438fd8 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -228,8 +228,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { */ def ensureCompletions(implicit ctx: Context): Unit = () - /** The method that computes the type of this tree */ - def derivedType(originalSym: Symbol)(implicit ctx: Context): Type + /** The method that computes the tree with the derived type */ + def derivedTree(originalSym: Symbol)(implicit ctx: Context): tpd.Tree } /** Property key containing TypeTrees whose type is computed diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 16554609744b..af7bde9c2852 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -526,6 +526,13 @@ class Namer { typer: Typer => mdef.putAttachment(ExpandedTree, Thicket(trees.filter(_ != tree))) } + /** Transfer all references to `from` to `to` */ + def transferReferences(from: ValDef, to: ValDef): Unit = { + val fromRefs = from.removeAttachment(References).getOrElse(Nil) + val toRefs = to.removeAttachment(References).getOrElse(Nil) + to.putAttachment(References, fromRefs ++ toRefs) + } + /** Merge the module class `modCls` in the expanded tree of `mdef` with the given stats */ def mergeModuleClass(mdef: Tree, modCls: TypeDef, stats: List[Tree]): TypeDef = { var res: TypeDef = null @@ -580,9 +587,12 @@ class Namer { typer: Typer => case vdef @ ValDef(name, _, _) if valid(vdef) => moduleValDef.get(name) match { case Some((stat1, vdef1)) => - if (vdef.mods.is(Synthetic) && !vdef1.mods.is(Synthetic)) + if (vdef.mods.is(Synthetic) && !vdef1.mods.is(Synthetic)) { + transferReferences(vdef, vdef1) removeInExpanded(stat, vdef) + } else if (!vdef.mods.is(Synthetic) && vdef1.mods.is(Synthetic)) { + transferReferences(vdef1, vdef) removeInExpanded(stat1, vdef1) moduleValDef(name) = (stat, vdef) } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index fc56b72eef12..7a8e62e5c3a6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1152,7 +1152,7 @@ class Typer extends Namer tree.ensureCompletions tree.getAttachment(untpd.OriginalSymbol) match { case Some(origSym) => - TypeTree(tree.derivedType(origSym)).withPos(tree.pos) + tree.derivedTree(origSym).withPos(tree.pos) // btw, no need to remove the attachment. The typed // tree is different from the untyped one, so the // untyped tree is no longer accessed after all From e1470f488b4345efe9f395fcea074ad292d5b3c2 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 16 Feb 2018 16:10:13 +0100 Subject: [PATCH 06/17] Implement new enum scheme Implement as is described in the docs. Update docs and tests where necessary. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 106 +++++++++++------- .../dotty/tools/dotc/ast/DesugarEnums.scala | 44 ++++---- .../dotc/reporting/diagnostic/messages.scala | 10 -- .../dotc/reporting/ErrorMessagesTests.scala | 13 --- docs/docs/reference/enums/desugarEnums.md | 8 +- tests/{run => neg}/enum-List3.scala | 2 +- tests/neg/enums.scala | 10 +- tests/patmat/enum-Tree.scala | 2 +- tests/pos/i3460.scala | 6 +- tests/pos/objXfun.scala | 3 +- tests/pos/patmat.scala | 3 +- tests/pos/reference/adts.scala | 23 ++-- tests/pos/reference/enums.scala | 22 +--- tests/run/enum-List3.check | 1 - tests/run/enum-Option.scala | 3 +- tests/run/enum-Option1.scala | 21 ++++ 16 files changed, 140 insertions(+), 137 deletions(-) rename tests/{run => neg}/enum-List3.scala (74%) delete mode 100644 tests/run/enum-List3.check create mode 100644 tests/run/enum-Option1.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index a18ad9b4063a..c38180d96e00 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -305,14 +305,19 @@ object desugar { val isCaseObject = mods.is(Case) && mods.is(Module) val isImplicit = mods.is(Implicit) val isEnum = mods.hasMod[Mod.Enum] && !mods.is(Module) - val isEnumCase = isLegalEnumCase(cdef) + val isEnumCase = mods.hasMod[Mod.EnumCase] val isValueClass = parents.nonEmpty && isAnyVal(parents.head) - // This is not watertight, but `extends AnyVal` will be replaced by `inline` later. - + // This is not watertight, but `extends AnyVal` will be replaced by `inline` later. val originalTparams = constr1.tparams val originalVparamss = constr1.vparamss - val constrTparams = originalTparams.map(toDefParam) + lazy val derivedEnumParams = enumClass.typeParams.map(derivedTypeParam) + val impliedTparams = + if (isEnumCase && originalTparams.isEmpty) + derivedEnumParams.map(tdef => tdef.withFlags(tdef.mods.flags | PrivateLocal)) + else + originalTparams + val constrTparams = impliedTparams.map(toDefParam) val constrVparamss = if (originalVparamss.isEmpty) { // ensure parameter list is non-empty if (isCaseClass) ctx.error(CaseClassMissingParamList(cdef), cdef.namePos) @@ -321,18 +326,34 @@ object desugar { else originalVparamss.nestedMap(toDefParam) val constr = cpy.DefDef(constr1)(tparams = constrTparams, vparamss = constrVparamss) - // Add constructor type parameters and evidence implicit parameters - // to auxiliary constructors - val normalizedBody = impl.body map { - case ddef: DefDef if ddef.name.isConstructorName => - decompose( - defDef( - addEvidenceParams( - cpy.DefDef(ddef)(tparams = constrTparams), - evidenceParams(constr1).map(toDefParam)))) - case stat => - stat + val (normalizedBody, enumCases, enumCompanionRef) = { + // Add constructor type parameters and evidence implicit parameters + // to auxiliary constructors; set defaultGetters as a side effect. + def expandConstructor(tree: Tree) = tree match { + case ddef: DefDef if ddef.name.isConstructorName => + decompose( + defDef( + addEvidenceParams( + cpy.DefDef(ddef)(tparams = constrTparams), + evidenceParams(constr1).map(toDefParam)))) + case stat => + stat + } + // The Identifiers defined by a case + def caseIds(tree: Tree) = tree match { + case tree: MemberDef => Ident(tree.name.toTermName) :: Nil + case PatDef(_, ids, _, _) => ids + } + val stats = impl.body.map(expandConstructor) + if (isEnum) { + val (enumCases, enumStats) = stats.partition(DesugarEnums.isEnumCase) + val enumCompanionRef = new TermRefTree() + val enumImport = Import(enumCompanionRef, enumCases.flatMap(caseIds)) + (enumImport :: enumStats, enumCases, enumCompanionRef) + } + else (stats, Nil, EmptyTree) } + def anyRef = ref(defn.AnyRefAlias.typeRef) val derivedTparams = constrTparams.map(derivedTypeParam(_)) @@ -365,20 +386,16 @@ object desugar { val classTypeRef = appliedRef(classTycon) // a reference to `enumClass`, with type parameters coming from the case constructor - lazy val enumClassTypeRef = enumClass.primaryConstructor.info match { - case info: PolyType => - if (constrTparams.isEmpty) - interpolatedEnumParent(cdef.pos.startPos) - else if ((constrTparams.corresponds(info.paramNames))((param, name) => param.name == name)) - appliedRef(enumClassRef) - else { - ctx.error(i"explicit extends clause needed because type parameters of case and enum class differ" - , cdef.pos.startPos) - appliedTypeTree(enumClassRef, constrTparams map (_ => anyRef)) - } - case _ => + lazy val enumClassTypeRef = + if (enumClass.typeParams.isEmpty) enumClassRef - } + else if (originalTparams.isEmpty) + appliedRef(enumClassRef) + else { + ctx.error(i"explicit extends clause needed because both enum case and enum class have type parameters" + , cdef.pos.startPos) + appliedTypeTree(enumClassRef, constrTparams map (_ => anyRef)) + } // new C[Ts](paramss) lazy val creatorExpr = New(classTypeRef, constrVparamss nestedMap refOfDef) @@ -432,6 +449,7 @@ object desugar { } // Case classes and case objects get Product parents + // Enum cases get an inferred parent if no parents are given var parents1 = parents if (isEnumCase && parents.isEmpty) parents1 = enumClassTypeRef :: Nil @@ -477,7 +495,7 @@ object desugar { .withMods(companionMods | Synthetic)) .withPos(cdef.pos).toList - val companionMeths = defaultGetters ::: eqInstances + val companionMembers = defaultGetters ::: eqInstances ::: enumCases // The companion object definitions, if a companion is needed, Nil otherwise. // companion definitions include: @@ -490,18 +508,17 @@ object desugar { // For all other classes, the parent is AnyRef. val companions = if (isCaseClass) { - // The return type of the `apply` method + // The return type of the `apply` method, and an (empty or singleton) list + // of widening coercions val (applyResultTpt, widenDefs) = if (!isEnumCase) (TypeTree(), Nil) else if (parents.isEmpty || enumClass.typeParams.isEmpty) (enumClassTypeRef, Nil) - else { - val tparams = enumClass.typeParams.map(derivedTypeParam) - enumApplyResult(cdef, parents, tparams, appliedRef(enumClassRef, tparams)) - } + else + enumApplyResult(cdef, parents, derivedEnumParams, appliedRef(enumClassRef, derivedEnumParams)) - val parent = + val companionParent = if (constrTparams.nonEmpty || constrVparamss.length > 1 || mods.is(Abstract) || @@ -523,10 +540,10 @@ object desugar { DefDef(nme.unapply, derivedTparams, (unapplyParam :: Nil) :: Nil, TypeTree(), unapplyRHS) .withMods(synthetic) } - companionDefs(parent, applyMeths ::: unapplyMeth :: companionMeths) + companionDefs(companionParent, applyMeths ::: unapplyMeth :: companionMembers) } - else if (companionMeths.nonEmpty) - companionDefs(anyRef, companionMeths) + else if (companionMembers.nonEmpty) + companionDefs(anyRef, companionMembers) else if (isValueClass) { constr0.vparamss match { case (_ :: Nil) :: _ => companionDefs(anyRef, Nil) @@ -535,6 +552,13 @@ object desugar { } else Nil + enumCompanionRef match { + case ref: TermRefTree => // have the enum import watch the companion object + val (modVal: ValDef) :: _ = companions + ref.watching(modVal) + case _ => + } + // For an implicit class C[Ts](p11: T11, ..., p1N: T1N) ... (pM1: TM1, .., pMN: TMN), the method // synthetic implicit C[Ts](p11: T11, ..., p1N: T1N) ... (pM1: TM1, ..., pMN: TMN): C[Ts] = // new C[Ts](p11, ..., p1N) ... (pM1, ..., pMN) = @@ -567,7 +591,7 @@ object desugar { } val cdef1 = addEnumFlags { - val originalTparamsIt = originalTparams.toIterator + val originalTparamsIt = impliedTparams.toIterator val originalVparamsIt = originalVparamss.toIterator.flatten val tparamAccessors = derivedTparams.map(_.withMods(originalTparamsIt.next().mods)) val caseAccessor = if (isCaseClass) CaseAccessor else EmptyFlags @@ -607,7 +631,7 @@ object desugar { val moduleName = checkNotReservedName(mdef).asTermName val impl = mdef.impl val mods = mdef.mods - lazy val isEnumCase = isLegalEnumCase(mdef) + lazy val isEnumCase = mods.hasMod[Mod.EnumCase] if (mods is Package) PackageDef(Ident(moduleName), cpy.ModuleDef(mdef)(nme.PACKAGE, impl).withMods(mods &~ Package) :: Nil) else if (isEnumCase) @@ -654,7 +678,7 @@ object desugar { */ def patDef(pdef: PatDef)(implicit ctx: Context): Tree = flatTree { val PatDef(mods, pats, tpt, rhs) = pdef - if (mods.hasMod[Mod.EnumCase] && enumCaseIsLegal(pdef)) + if (mods.hasMod[Mod.EnumCase]) pats map { case id: Ident => expandSimpleEnumCase(id.name.asTermName, mods, diff --git a/compiler/src/dotty/tools/dotc/ast/DesugarEnums.scala b/compiler/src/dotty/tools/dotc/ast/DesugarEnums.scala index 4d1dda5b8930..8ae41cf95367 100644 --- a/compiler/src/dotty/tools/dotc/ast/DesugarEnums.scala +++ b/compiler/src/dotty/tools/dotc/ast/DesugarEnums.scala @@ -6,7 +6,6 @@ import core._ import util.Positions._, Types._, Contexts._, Constants._, Names._, NameOps._, Flags._ import SymDenotations._, Symbols._, StdNames._, Annotations._, Trees._ import Decorators._ -import reporting.diagnostic.messages.EnumCaseDefinitionInNonEnumOwner import collection.mutable.ListBuffer import util.Property import typer.ErrorReporting._ @@ -23,20 +22,21 @@ object DesugarEnums { /** Attachment containing the number of enum cases and the smallest kind that was seen so far. */ val EnumCaseCount = new Property.Key[(Int, CaseKind.Value)] - /** the enumeration class that is a companion of the current object */ - def enumClass(implicit ctx: Context) = ctx.owner.linkedClass - - /** Is this an enum case that's situated in a companion object of an enum class? */ - def isLegalEnumCase(tree: MemberDef)(implicit ctx: Context): Boolean = - tree.mods.hasMod[Mod.EnumCase] && enumCaseIsLegal(tree) + /** The enumeration class that belongs to an enum case. This works no matter + * whether the case is still in the enum class or it has been transferred to the + * companion object. + */ + def enumClass(implicit ctx: Context): Symbol = { + val cls = ctx.owner + if (cls.is(Module)) cls.linkedClass else cls + } - /** Is enum case `tree` situated in a companion object of an enum class? */ - def enumCaseIsLegal(tree: Tree)(implicit ctx: Context): Boolean = ( - ctx.owner.is(ModuleClass) && enumClass.derivesFrom(defn.EnumClass) - || { ctx.error(EnumCaseDefinitionInNonEnumOwner(ctx.owner), tree.pos) - false - } - ) + /** Is `tree` an (untyped) enum case? */ + def isEnumCase(tree: Tree)(implicit ctx: Context): Boolean = tree match { + case tree: MemberDef => tree.mods.hasMod[Mod.EnumCase] + case PatDef(mods, _, _, _) => mods.hasMod[Mod.EnumCase] + case _ => false + } /** A reference to the enum class `E`, possibly followed by type arguments. * Each covariant type parameter is approximated by its lower bound. @@ -69,7 +69,7 @@ object DesugarEnums { /** Add implied flags to an enum class or an enum case */ def addEnumFlags(cdef: TypeDef)(implicit ctx: Context) = if (cdef.mods.hasMod[Mod.Enum]) cdef.withFlags(cdef.mods.flags | Abstract | Sealed) - else if (isLegalEnumCase(cdef)) cdef.withFlags(cdef.mods.flags | Final) + else if (isEnumCase(cdef)) cdef.withFlags(cdef.mods.flags | Final) else cdef private def valuesDot(name: String) = Select(Ident(nme.DOLLAR_VALUES), name.toTermName) @@ -193,24 +193,20 @@ object DesugarEnums { } /** Expand a module definition representing a parameterless enum case */ - def expandEnumModule(name: TermName, impl: Template, mods: Modifiers, pos: Position)(implicit ctx: Context): Tree = + def expandEnumModule(name: TermName, impl: Template, mods: Modifiers, pos: Position)(implicit ctx: Context): Tree = { + assert(impl.body.isEmpty) if (impl.parents.isEmpty) - if (impl.body.isEmpty) - expandSimpleEnumCase(name, mods, pos) - else { - val parent = interpolatedEnumParent(pos) - expandEnumModule(name, cpy.Template(impl)(parents = parent :: Nil), mods, pos) - } + expandSimpleEnumCase(name, mods, pos) else { def toStringMeth = DefDef(nme.toString_, Nil, Nil, TypeTree(defn.StringType), Literal(Constant(name.toString))) .withFlags(Override) val (tagMeth, scaffolding) = enumTagMeth(CaseKind.Object) - val impl1 = cpy.Template(impl)(body = - impl.body ++ List(tagMeth, toStringMeth) ++ registerCall) + val impl1 = cpy.Template(impl)(body = List(tagMeth, toStringMeth) ++ registerCall) val vdef = ValDef(name, TypeTree(), New(impl1)).withMods(mods | Final) flatTree(scaffolding ::: vdef :: Nil).withPos(pos) } + } /** Expand a simple enum case */ def expandSimpleEnumCase(name: TermName, mods: Modifiers, pos: Position)(implicit ctx: Context): Tree = diff --git a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala index 3c7720b65aa9..2c5425d99ec2 100644 --- a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala @@ -1783,16 +1783,6 @@ object messages { hl"""A class marked with the ${"final"} keyword cannot be extended""" } - case class EnumCaseDefinitionInNonEnumOwner(owner: Symbol)(implicit ctx: Context) - extends Message(EnumCaseDefinitionInNonEnumOwnerID) { - val kind = "Syntax" - val msg = em"case not allowed here, since owner ${owner} is not an ${"enum"} object" - val explanation = - hl"""${"enum"} cases are only allowed within the companion ${"object"} of an ${"enum class"}. - |If you want to create an ${"enum"} case, make sure the corresponding ${"enum class"} exists - |and has the ${"enum"} keyword.""" - } - case class ExpectedTypeBoundOrEquals(found: Token)(implicit ctx: Context) extends Message(ExpectedTypeBoundOrEqualsID) { val kind = "Syntax" diff --git a/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala b/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala index 9d950953f550..6decf37b67ab 100644 --- a/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala +++ b/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala @@ -986,19 +986,6 @@ class ErrorMessagesTests extends ErrorMessagesTest { assertEquals(parent.show, "class A") } - @Test def enumCaseDefinitionInNonEnumOwner = - checkMessagesAfter("frontend") { - """object Qux { - | case Foo - |} - """.stripMargin - }.expect { (ictx, messages) => - implicit val ctx: Context = ictx - assertMessageCount(1, messages) - val EnumCaseDefinitionInNonEnumOwner(owner) :: Nil = messages - assertEquals("object Qux", owner.show) - } - @Test def tailrecNotApplicableNeitherPrivateNorFinal = checkMessagesAfter("tailrec") { """ diff --git a/docs/docs/reference/enums/desugarEnums.md b/docs/docs/reference/enums/desugarEnums.md index f7bb2c21aa2b..a218529e1b94 100644 --- a/docs/docs/reference/enums/desugarEnums.md +++ b/docs/docs/reference/enums/desugarEnums.md @@ -36,9 +36,13 @@ map into case classes or vals. expands to a `sealed` `abstract` class that extends the `scala.Enum` trait and an associated companion object that contains the defined cases, expanded according - to rules (2 - 8). + to rules (2 - 8). The enum trait starts with a compiler-generated import that imports + the names `` of all cases so that they can be used without prefix in the trait. - sealed abstract class E ... extends with scala.Enum { } + sealed abstract class E ... extends with scala.Enum { + import E.{ } + + } object E { } 2. A simple case consisting of a comma-separated list of enum names diff --git a/tests/run/enum-List3.scala b/tests/neg/enum-List3.scala similarity index 74% rename from tests/run/enum-List3.scala rename to tests/neg/enum-List3.scala index 5ce268f8a7bf..3229cdd82dec 100644 --- a/tests/run/enum-List3.scala +++ b/tests/neg/enum-List3.scala @@ -1,5 +1,5 @@ enum List[+T] { - case Cons[T](x: T, xs: List[T]) + case Cons[T](x: T, xs: List[T]) // error: missing extends case Nil extends List[Nothing] } object Test { diff --git a/tests/neg/enums.scala b/tests/neg/enums.scala index a7c641d11d12..964a8106fdd4 100644 --- a/tests/neg/enums.scala +++ b/tests/neg/enums.scala @@ -1,12 +1,8 @@ package enums enum List[+T] { - case Cons[T](x: T, xs: List[T]) // ok - case Snoc[U](xs: List[U], x: U) // error: different type parameters -} - -enum class X { - case Y // error: case not allowed here + case Cons[T](x: T, xs: List[T]) // error: missing extends + case Snoc[U](xs: List[U], x: U) // error: missing extends } enum E1[T] { @@ -22,7 +18,7 @@ enum E3[-T <: Ordered[T]] { } enum Option[+T] { - case Some[T](x: T) + case Some(x: T) case None } diff --git a/tests/patmat/enum-Tree.scala b/tests/patmat/enum-Tree.scala index 4225b802fb9b..25993f459875 100644 --- a/tests/patmat/enum-Tree.scala +++ b/tests/patmat/enum-Tree.scala @@ -5,7 +5,7 @@ enum Tree[T] { case Succ(n: Tree[Int]) extends Tree[Int] case Pred(n: Tree[Int]) extends Tree[Int] case IsZero(n: Tree[Int]) extends Tree[Boolean] - case If(cond: Tree[Boolean], thenp: Tree[T], elsep: Tree[T]) extends Tree[T] + case If[X](cond: Tree[Boolean], thenp: Tree[X], elsep: Tree[X]) extends Tree[X] } object Test { diff --git a/tests/pos/i3460.scala b/tests/pos/i3460.scala index 5ba4e6021cdf..6f5c39155719 100644 --- a/tests/pos/i3460.scala +++ b/tests/pos/i3460.scala @@ -1,7 +1,7 @@ -enum class Ducks - -object Ducks { +enum Ducks { case Dewey +} +object Ducks { def wooohoo: Int = 1 } diff --git a/tests/pos/objXfun.scala b/tests/pos/objXfun.scala index c05a08d9ba41..8b1dfbc84390 100644 --- a/tests/pos/objXfun.scala +++ b/tests/pos/objXfun.scala @@ -2,7 +2,6 @@ object Foo extends (Int => Int) { // OK def apply(x: Int) = x } -enum class E(x: Int) // used to generate Int => new E(x) as the parent of object E --> crash -object E { +enum E(x: Int) { // used to generate Int => new E(x) as the parent of object E --> crash case C(x: Int) extends E(x) } diff --git a/tests/pos/patmat.scala b/tests/pos/patmat.scala index 87b5e2ef372b..94d4580c5267 100644 --- a/tests/pos/patmat.scala +++ b/tests/pos/patmat.scala @@ -1,5 +1,4 @@ object Test { - val xs = List(1, 2, 3) xs match { @@ -36,7 +35,7 @@ object Test { } enum Option[+T] { - case Some[+T](value: T) + case Some(value: T) case None } import Option._ diff --git a/tests/pos/reference/adts.scala b/tests/pos/reference/adts.scala index 6fc670ada71b..716211f8b10f 100644 --- a/tests/pos/reference/adts.scala +++ b/tests/pos/reference/adts.scala @@ -2,7 +2,7 @@ package adts object t1 { enum Option[+T] { - case Some[T](x: T) + case Some(x: T) case None } @@ -11,8 +11,8 @@ enum Option[+T] { object t2 { enum Option[+T] { - case Some[T](x: T) extends Option[T] - case None extends Option[Nothing] + case Some(x: T) extends Option[T] + case None extends Option[Nothing] } @@ -27,19 +27,18 @@ enum Color(val rgb: Int) { object t3 { -enum class Option[+T] { - def isDefined: Boolean +enum Option[+T] { + case Some(x: T) extends Option[T] + case None + + def isDefined: Boolean = this match { + case None => false + case some => true + } } object Option { def apply[T >: Null](x: T): Option[T] = if (x == null) None else Some(x) - - case Some[+T](x: T) { - def isDefined = true - } - case None { - def isDefined = false - } } } diff --git a/tests/pos/reference/enums.scala b/tests/pos/reference/enums.scala index 1db1b055d9b0..71179c7aed61 100644 --- a/tests/pos/reference/enums.scala +++ b/tests/pos/reference/enums.scala @@ -10,43 +10,29 @@ enum Color { object t2 { -enum class Color -object Color { +enum Color { case Red case Green case Blue } - } object t3 { -enum class Color(val rgb: Int) -object Color { - case Red extends Color(0xFF0000) - case Green extends Color(0x00FF00) - case Blue extends Color(0x0000FF) -} - -} - -object t4 { - enum Color(val rgb: Int) { case Red extends Color(0xFF0000) case Green extends Color(0x00FF00) case Blue extends Color(0x0000FF) } + } -enum class Planet(mass: Double, radius: Double) { +enum Planet(mass: Double, radius: Double) { private final val G = 6.67300E-11 def surfaceGravity = G * mass / (radius * radius) def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity -} -object Planet { case MERCURY extends Planet(3.303e+23, 2.4397e6) case VENUS extends Planet(4.869e+24, 6.0518e6) case EARTH extends Planet(5.976e+24, 6.37814e6) @@ -55,7 +41,9 @@ object Planet { case SATURN extends Planet(5.688e+26, 6.0268e7) case URANUS extends Planet(8.686e+25, 2.5559e7) case NEPTUNE extends Planet(1.024e+26, 2.4746e7) +} +object Planet { def main(args: Array[String]) = { val earthWeight = args(0).toDouble val mass = earthWeight/EARTH.surfaceGravity diff --git a/tests/run/enum-List3.check b/tests/run/enum-List3.check deleted file mode 100644 index 1d4812de1f64..000000000000 --- a/tests/run/enum-List3.check +++ /dev/null @@ -1 +0,0 @@ -Cons(1,Cons(2,Cons(3,Nil))) diff --git a/tests/run/enum-Option.scala b/tests/run/enum-Option.scala index 45cd9835bd02..293b48a49e6c 100644 --- a/tests/run/enum-Option.scala +++ b/tests/run/enum-Option.scala @@ -1,4 +1,4 @@ -enum Option[+T >: Null] extends Serializable { +enum Option[+T] extends Serializable { case Some(x: T) case None @@ -12,6 +12,7 @@ object Option { } object Test { + import Option._ def main(args: Array[String]) = { assert(Some(None).isDefined) Option("22") match { case Option.Some(x) => assert(x == "22") } diff --git a/tests/run/enum-Option1.scala b/tests/run/enum-Option1.scala new file mode 100644 index 000000000000..a3d5767244f6 --- /dev/null +++ b/tests/run/enum-Option1.scala @@ -0,0 +1,21 @@ +enum Option1[+T] extends Serializable { + case Some1(x: T) + case None1 + + def isDefined: Boolean = this match { + case None1 => false + case some => true + } +} +object Option1 { + def apply[T >: Null](x: T): Option1[T] = if (x == null) None1 else Some1(x) +} + +object Test { + import Option1._ + def main(args: Array[String]) = { + assert(Some1(None1).isDefined) + Option1("22") match { case Option1.Some1(x) => assert(x == "22") } + assert(Some1(None1) != None1) + } +} From 6b13112a61119268ec69aa32e6244a950929582b Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 16 Feb 2018 16:35:54 +0100 Subject: [PATCH 07/17] Fix another test --- tests/patmat/planets.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/patmat/planets.scala b/tests/patmat/planets.scala index bcbfd7eeb3cc..22c731428f70 100644 --- a/tests/patmat/planets.scala +++ b/tests/patmat/planets.scala @@ -1,9 +1,8 @@ -enum class Planet(mass: Double, radius: Double) { +enum Planet(mass: Double, radius: Double) { private final val G = 6.67300E-11 def surfaceGravity = G * mass / (radius * radius) def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity -} -object Planet { + case MERCURY extends Planet(3.303e+23, 2.4397e6) case VENUS extends Planet(4.869e+24, 6.0518e6) case EARTH extends Planet(5.976e+24, 6.37814e6) From fa0a25f16c099121e670cc2a6661b29d6208cc23 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 17 Feb 2018 15:11:11 +0100 Subject: [PATCH 08/17] Allow case classes that only have type parameters A case class like case class C[T] is OK, it cannot be mistaken for an object. --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index c38180d96e00..0ad3c4a84375 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -320,7 +320,8 @@ object desugar { val constrTparams = impliedTparams.map(toDefParam) val constrVparamss = if (originalVparamss.isEmpty) { // ensure parameter list is non-empty - if (isCaseClass) ctx.error(CaseClassMissingParamList(cdef), cdef.namePos) + if (isCaseClass && originalTparams.isEmpty) + ctx.error(CaseClassMissingParamList(cdef), cdef.namePos) ListOfNil } else originalVparamss.nestedMap(toDefParam) From 0ee328187310b55ec088f21da66a163b005cc4b7 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 17 Feb 2018 18:17:06 +0100 Subject: [PATCH 09/17] Implement access checking for enum cases Previously, enum cases moved from the enum to its companion object could "accidentally" refer to definitions defined in the object, but inaccessible from the enum. We now check that no such accesses occur. --- .../dotty/tools/dotc/ast/DesugarEnums.scala | 4 +- .../src/dotty/tools/dotc/typer/Checking.scala | 94 ++++++++++++++++++- .../src/dotty/tools/dotc/typer/ReTyper.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 13 ++- tests/neg/enums.scala | 7 ++ tests/neg/enumsAccess.scala | 86 +++++++++++++++++ tests/pos/enum-List-control.scala | 10 +- tests/run/enum-Color.scala | 1 + tests/run/generic/Enum.scala | 25 +++++ 9 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 tests/neg/enumsAccess.scala create mode 100644 tests/run/generic/Enum.scala diff --git a/compiler/src/dotty/tools/dotc/ast/DesugarEnums.scala b/compiler/src/dotty/tools/dotc/ast/DesugarEnums.scala index 8ae41cf95367..e6e48f75ede5 100644 --- a/compiler/src/dotty/tools/dotc/ast/DesugarEnums.scala +++ b/compiler/src/dotty/tools/dotc/ast/DesugarEnums.scala @@ -68,8 +68,8 @@ object DesugarEnums { /** Add implied flags to an enum class or an enum case */ def addEnumFlags(cdef: TypeDef)(implicit ctx: Context) = - if (cdef.mods.hasMod[Mod.Enum]) cdef.withFlags(cdef.mods.flags | Abstract | Sealed) - else if (isEnumCase(cdef)) cdef.withFlags(cdef.mods.flags | Final) + if (cdef.mods.hasMod[Mod.Enum]) cdef.withMods(cdef.mods.withFlags(cdef.mods.flags | Abstract | Sealed)) + else if (isEnumCase(cdef)) cdef.withMods(cdef.mods.withFlags(cdef.mods.flags | Final)) else cdef private def valuesDot(name: String) = Select(Ident(nme.DOLLAR_VALUES), name.toTermName) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 4dd960e4fae3..cf20379e301a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -28,6 +28,7 @@ import Decorators._ import Uniques._ import ErrorReporting.{err, errorType} import config.Printers.typr +import NameKinds.DefaultGetterName import collection.mutable import SymDenotations.NoCompleter @@ -741,9 +742,100 @@ trait Checking { tp.foreachPart(check, stopAtStatic = true) tp } + + /** Check that all non-synthetic references of the form `` or + * `this.` in `tree` that refer to a member of `badOwner` are + * `allowed`. + */ + def checkRefsLegal(tree: tpd.Tree, badOwner: Symbol, allowed: (Name, Symbol) => Boolean, where: String)(implicit ctx: Context): Unit = { + tree.foreachSubTree { tree => + tree match { + case Ident(_) | Select(This(_), _) if tree.pos.isSourceDerived => + val sym = tree.symbol + if (sym.maybeOwner == badOwner && !allowed(tree.asInstanceOf[RefTree].name, sym)) + ctx.error(i"illegal reference to $sym from $where: $tree // ${tree.toString}", tree.pos) + case _ => + } + } + } + + /** Check that all case classes that extend `scala.Enum` are `enum` cases */ + def checkEnum(cdef: untpd.TypeDef, cls: Symbol)(implicit ctx: Context): Unit = { + import untpd.modsDeco + def isEnumAnonCls = + cls.isAnonymousClass && + cls.owner.isTerm && + (cls.owner.flagsUNSAFE.is(Case) || cls.owner.name == nme.DOLLAR_NEW) + if (!cdef.mods.hasMod[untpd.Mod.EnumCase] && !isEnumAnonCls) + ctx.error(em"normal case $cls in ${cls.owner} cannot extend an enum", cdef.pos) + } + + /** Check that all references coming from enum cases in an enum companion object + * are legal. + * @param cdef the enum companion object class + * @param enumCtx the context immediately enclosing the corresponding enum + */ + private def checkEnumCaseRefsLegal(cdef: TypeDef, enumCtx: Context)(implicit ctx: Context): Unit = { + def check(tree: Tree) = { + // allow access to `sym` if a typedIdent just outside the enclosing enum + // would have produced the same symbol without errors + def allowAccess(name: Name, sym: Symbol): Boolean = { + val testCtx = enumCtx.fresh.setNewTyperState() + val ref = ctx.typer.typedIdent(untpd.Ident(name), WildcardType)(testCtx) + ref.symbol == sym && !testCtx.reporter.hasErrors + } + checkRefsLegal(tree, cdef.symbol, allowAccess, "enum case") + } + cdef.rhs match { + case impl: Template => + for (stat <- impl.body) + if (stat.symbol.is(Case)) + stat match { + case TypeDef(_, Template(DefDef(_, tparams, vparamss, _, _), parents, _, _)) => + tparams.foreach(check) + vparamss.foreach(_.foreach(check)) + parents.foreach(check) + case vdef: ValDef => + vdef.rhs match { + case Block((clsDef @ TypeDef(_, impl: Template)) :: Nil, _) + if clsDef.symbol.isAnonymousClass => + impl.parents.foreach(check) + case _ => + } + case _ => + } + else if (stat.symbol.is(Module) && stat.symbol.linkedClass.is(Case)) + stat match { + case TypeDef(_, impl: Template) => + for ((defaultGetter @ + DefDef(DefaultGetterName(nme.CONSTRUCTOR, _), _, _, _, _)) <- impl.body) + check(defaultGetter.rhs) + case _ => + } + case _ => + } + } + + /** Check all enum cases in all enum companions in `stats` for legal accesses. + * @param enumContexts a map from`enum` symbols to the contexts enclosing their definitions + */ + def checkEnumCompanions(stats: List[Tree], enumContexts: collection.Map[Symbol, Context])(implicit ctx: Context): List[Tree] = { + for (stat @ TypeDef(_, _) <- stats) + if (stat.symbol.is(Module)) + for (enumContext <- enumContexts.get(stat.symbol.linkedClass)) + checkEnumCaseRefsLegal(stat, enumContext) + stats + } +} + +trait ReChecking extends Checking { + import tpd._ + override def checkEnum(cdef: untpd.TypeDef, cls: Symbol)(implicit ctx: Context): Unit = () + override def checkRefsLegal(tree: tpd.Tree, badOwner: Symbol, allowed: (Name, Symbol) => Boolean, where: String)(implicit ctx: Context): Unit = () + override def checkEnumCompanions(stats: List[Tree], enumContexts: collection.Map[Symbol, Context])(implicit ctx: Context): List[Tree] = stats } -trait NoChecking extends Checking { +trait NoChecking extends ReChecking { import tpd._ override def checkNonCyclic(sym: Symbol, info: TypeBounds, reportErrors: Boolean)(implicit ctx: Context): Type = info override def checkValue(tree: Tree, proto: Type)(implicit ctx: Context): tree.type = tree diff --git a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala index be4218b72bf8..8de89f25aab6 100644 --- a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala +++ b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala @@ -22,7 +22,7 @@ import config.Printers.typr * * Otherwise, everything is as in Typer. */ -class ReTyper extends Typer { +class ReTyper extends Typer with ReChecking { import tpd._ private def assertTyped(tree: untpd.Tree)(implicit ctx: Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 7a8e62e5c3a6..d9daebe2cbe1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1406,7 +1406,7 @@ class Typer extends Namer if (sym.isInlineMethod) Inliner.registerInlineInfo(sym, _ => rhs1) assignType(cpy.DefDef(ddef)(name, tparams1, vparamss1, tpt1, rhs1), sym) - //todo: make sure dependent method types do not depend on implicits or by-name params + //todo: make sure dependent method types do not depend on implicits or by-name params } def typedTypeDef(tdef: untpd.TypeDef, sym: Symbol)(implicit ctx: Context): Tree = track("typedTypeDef") { @@ -1506,6 +1506,7 @@ class Typer extends Namer checkVariance(impl1) if (!cls.is(AbstractOrTrait) && !ctx.isAfterTyper) checkRealizableBounds(cls, cdef.namePos) + if (cls.is(Case) && cls.derivesFrom(defn.EnumClass)) checkEnum(cdef, cls) val cdef1 = assignType(cpy.TypeDef(cdef)(name, impl1), cls) if (ctx.phase.isTyper && cdef1.tpe.derivesFrom(defn.DynamicClass) && !ctx.dynamicsEnabled) { val isRequired = parents1.exists(_.tpe.isRef(defn.DynamicClass)) @@ -1813,6 +1814,8 @@ class Typer extends Namer def typedStats(stats: List[untpd.Tree], exprOwner: Symbol)(implicit ctx: Context): List[tpd.Tree] = { val buf = new mutable.ListBuffer[Tree] + val enumContexts = new mutable.HashMap[Symbol, Context] + // A map from `enum` symbols to the contexts enclosing their definitions @tailrec def traverse(stats: List[untpd.Tree])(implicit ctx: Context): List[Tree] = stats match { case (imp: untpd.Import) :: rest => val imp1 = typed(imp) @@ -1827,6 +1830,12 @@ class Typer extends Namer case mdef1: DefDef if Inliner.hasBodyToInline(mdef1.symbol) => buf ++= inlineExpansion(mdef1) case mdef1 => + import untpd.modsDeco + mdef match { + case mdef: untpd.TypeDef if mdef.mods.hasMod[untpd.Mod.Enum] => + enumContexts(mdef1.symbol) = ctx + case _ => + } buf += mdef1 } traverse(rest) @@ -1846,7 +1855,7 @@ class Typer extends Namer val exprOwnerOpt = if (exprOwner == ctx.owner) None else Some(exprOwner) ctx.withProperty(ExprOwner, exprOwnerOpt) } - traverse(stats)(localCtx) + checkEnumCompanions(traverse(stats)(localCtx), enumContexts) } /** Given an inline method `mdef`, the method rewritten so that its body diff --git a/tests/neg/enums.scala b/tests/neg/enums.scala index 964a8106fdd4..1ff70ed9abcf 100644 --- a/tests/neg/enums.scala +++ b/tests/neg/enums.scala @@ -17,6 +17,13 @@ enum E3[-T <: Ordered[T]] { case C // error: cannot determine type argument } +enum E4 { + case C +} + +case class C4() extends E4 // error: cannot extend enum +case object O4 extends E4 // error: cannot extend enum + enum Option[+T] { case Some(x: T) case None diff --git a/tests/neg/enumsAccess.scala b/tests/neg/enumsAccess.scala new file mode 100644 index 000000000000..0bd4427863c5 --- /dev/null +++ b/tests/neg/enumsAccess.scala @@ -0,0 +1,86 @@ +package enums + +object test1 { + + enum E4 { + case C1(x: INT) // error: illegal reference + case C2(x: Int = defaultX) // error: illegal reference + case C3[T <: INT] // error: illegal reference + } + + object E4 { + type INT = Integer + val defaultX = 2 + } +} + +object test2 { + import E5._ + object E5 { + type INT = Integer + val defaultX = 2 + } + + enum E5 { + case C1(x: INT) // ok + case C2(x: Int = defaultX) // ok + case C3[T <: INT] // ok + } +} + +object test3 { + object E5 { + type INT = Integer + val defaultX = 2 + } + + import E5._ + + enum E5 { + case C1(x: INT) // ok + case C2(x: Int = defaultX)// ok + case C3[T <: INT] // ok + } +} + +object test4 { + + enum E5 { + case C1(x: INT) // error: illegal reference + case C2(x: Int = defaultX) // error: illegal reference + case C3[T <: INT] // error: illegal reference + } + + import E5._ + + object E5 { + type INT = Integer + val defaultX = 2 + } +} + +object test5 { + enum E5[T](x: T) { + case C3() extends E5[INT](defaultX)// error: illegal reference // error: illegal reference + case C4 extends E5[INT](defaultX) // error: illegal reference // error: illegal reference + case C5 extends E5[E5[_]](E5.this) // error: type mismatch + } + + object E5 { + type INT = Integer + val defaultX = 2 + } +} + +object test6 { + import E5._ + enum E5[T](x: T) { + case C3() extends E5[INT](defaultX) // ok + case C4 extends E5[INT](defaultX) // ok + } + + object E5 { + type INT = Integer + val defaultX = 2 + } +} diff --git a/tests/pos/enum-List-control.scala b/tests/pos/enum-List-control.scala index d9df176d16f6..b269df43b242 100644 --- a/tests/pos/enum-List-control.scala +++ b/tests/pos/enum-List-control.scala @@ -1,11 +1,17 @@ abstract sealed class List[T] extends Enum object List { - final case class Cons[T](x: T, xs: List[T]) extends List[T] { + final class Cons[T](x: T, xs: List[T]) extends List[T] { def enumTag = 0 } - final case class Nil[T]() extends List[T] { + object Cons { + def apply[T](x: T, xs: List[T]): List[T] = new Cons(x, xs) + } + final class Nil[T]() extends List[T] { def enumTag = 1 } + object Nil { + def apply[T](): List[T] = new Nil() + } } object Test { import List._ diff --git a/tests/run/enum-Color.scala b/tests/run/enum-Color.scala index f4f6aaef84d5..31b850329407 100644 --- a/tests/run/enum-Color.scala +++ b/tests/run/enum-Color.scala @@ -1,5 +1,6 @@ enum Color { case Red, Green, Blue + class Color // Just to throw a spanner in the works } object Test { diff --git a/tests/run/generic/Enum.scala b/tests/run/generic/Enum.scala new file mode 100644 index 000000000000..ce64ac054933 --- /dev/null +++ b/tests/run/generic/Enum.scala @@ -0,0 +1,25 @@ +package generic + +trait Enum { + def enumTag: Int +} + +object runtime { + class EnumValues[E <: Enum] { + private[this] var myMap: Map[Int, E] = Map() + private[this] var fromNameCache: Map[String, E] = null + + def register(v: E) = { + require(!myMap.contains(v.enumTag)) + myMap = myMap.updated(v.enumTag, v) + fromNameCache = null + } + + def fromInt: Map[Int, E] = myMap + def fromName: Map[String, E] = { + if (fromNameCache == null) fromNameCache = myMap.values.map(v => v.toString -> v).toMap + fromNameCache + } + def values: Iterable[E] = myMap.values + } +} \ No newline at end of file From d2cea4fc3a591b0ee43b4802f04faa2dba44af04 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 17 Feb 2018 19:10:01 +0100 Subject: [PATCH 10/17] Make `this` prefixes of types always elidable Without that change, a reference to an enclosing type gets always converted to a TypeTree. This undermines access checking for constructors, which is implemented in the next commit. --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index a0eb74be1422..e177d719dd6d 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -309,6 +309,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { case NoPrefix => true case pre: ThisType => + tp.isType || pre.cls.isStaticOwner || tp.symbol.isParamOrAccessor && !pre.cls.is(Trait) && ctx.owner.enclosingClass == pre.cls // was ctx.owner.enclosingClass.derivesFrom(pre.cls) which was not tight enough From 16b1421343b3a1cf964b02b47f72a8f3aa2b4337 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 17 Feb 2018 19:12:47 +0100 Subject: [PATCH 11/17] Access check secondary constructor parameters We handle a supercall of a secondary constructor specially, preventing accesses to members of the enclosing class. We need to do the same for parameters of such constructors. We now use two different mechanisms for that: superCallContexts for the super call and checkRefsLegal for the parameters. We should experiment at some point with doing the supercall checks also with chekRefsLegal. --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index d9daebe2cbe1..09dfe59bd56d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1405,6 +1405,10 @@ class Typer extends Namer // Overwrite inline body to make sure it is not evaluated twice if (sym.isInlineMethod) Inliner.registerInlineInfo(sym, _ => rhs1) + if (sym.isConstructor && !sym.isPrimaryConstructor) + for (param <- tparams1 ::: vparamss1.flatten) + checkRefsLegal(param, sym.owner, (name, sym) => sym.is(TypeParam), "secondary constructor") + assignType(cpy.DefDef(ddef)(name, tparams1, vparamss1, tpt1, rhs1), sym) //todo: make sure dependent method types do not depend on implicits or by-name params } From 48b7656d28f9a65981cfbf817953b6e6d1302ba6 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 17 Feb 2018 19:29:34 +0100 Subject: [PATCH 12/17] Update reference with currently implemented rules --- docs/docs/reference/enums/desugarEnums.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/docs/reference/enums/desugarEnums.md b/docs/docs/reference/enums/desugarEnums.md index a218529e1b94..d62dee49a685 100644 --- a/docs/docs/reference/enums/desugarEnums.md +++ b/docs/docs/reference/enums/desugarEnums.md @@ -136,7 +136,6 @@ map into case classes or vals. where `n` is the ordinal number of the case in the companion object, starting from 0. - ### Equality An `enum` type contains a `scala.Eq` instance that restricts values of the `enum` type to @@ -171,3 +170,20 @@ Companion objects that contain at least one simple case define in addition: def toString = name $values.register(this) // register enum value so that `valueOf` and `values` can return it. } + +### Scopes for Enum Cases + +A case in an `enum` is treated similarly to a secondary constructor. It cannot access +with simple identifiers value parameters or instance members of the enclosing `enum`. + +Even though translated enum cases are located in the enum's companion object, they +cannot access with simple identifiers any members defined locally in the enclosing +object either. The compiler is free to typecheck enum cases in the scope of the +enclosing companion object but it must then flag any illegal accesses to the object's +members. + +### Other Rules + +A normal case class which is not produced from an enum case is not allowed to extend +`scala.Enum`. This ensures that the only cases of an anum are the ones that are +explictly declared in it. \ No newline at end of file From 8aa24ace9204f66d40648690aec09619410cd128 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 18 Feb 2018 12:22:04 +0100 Subject: [PATCH 13/17] Also check unselected `this` references in enum cases. --- .../src/dotty/tools/dotc/typer/Checking.scala | 25 +++++++++++++------ docs/docs/reference/enums/desugarEnums.md | 13 ++++------ tests/neg/enumsAccess.scala | 10 ++++++++ 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index cf20379e301a..ee0c46182c20 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -745,18 +745,27 @@ trait Checking { /** Check that all non-synthetic references of the form `` or * `this.` in `tree` that refer to a member of `badOwner` are - * `allowed`. + * `allowed`. Also check that there are no other explicit `this` references + * to `badOwner`. */ def checkRefsLegal(tree: tpd.Tree, badOwner: Symbol, allowed: (Name, Symbol) => Boolean, where: String)(implicit ctx: Context): Unit = { - tree.foreachSubTree { tree => - tree match { - case Ident(_) | Select(This(_), _) if tree.pos.isSourceDerived => - val sym = tree.symbol - if (sym.maybeOwner == badOwner && !allowed(tree.asInstanceOf[RefTree].name, sym)) - ctx.error(i"illegal reference to $sym from $where: $tree // ${tree.toString}", tree.pos) - case _ => + val checker = new TreeTraverser { + def traverse(t: Tree)(implicit ctx: Context) = { + def check(owner: Symbol, checkedSym: Symbol) = + if (t.pos.isSourceDerived && owner == badOwner) + t match { + case t: RefTree if allowed(t.name, checkedSym) => + case _ => ctx.error(i"illegal reference to $checkedSym from $where", t.pos) + } + val sym = t.symbol + t match { + case Ident(_) | Select(This(_), _) => check(sym.maybeOwner, sym) + case This(_) => check(sym, sym) + case _ => traverseChildren(t) + } } } + checker.traverse(tree) } /** Check that all case classes that extend `scala.Enum` are `enum` cases */ diff --git a/docs/docs/reference/enums/desugarEnums.md b/docs/docs/reference/enums/desugarEnums.md index d62dee49a685..d1880349130e 100644 --- a/docs/docs/reference/enums/desugarEnums.md +++ b/docs/docs/reference/enums/desugarEnums.md @@ -173,14 +173,11 @@ Companion objects that contain at least one simple case define in addition: ### Scopes for Enum Cases -A case in an `enum` is treated similarly to a secondary constructor. It cannot access -with simple identifiers value parameters or instance members of the enclosing `enum`. - -Even though translated enum cases are located in the enum's companion object, they -cannot access with simple identifiers any members defined locally in the enclosing -object either. The compiler is free to typecheck enum cases in the scope of the -enclosing companion object but it must then flag any illegal accesses to the object's -members. +A case in an `enum` is treated similarly to a secondary constructor. It can access neither the enclosing `enum` using `this`, nor its value parameters or instance members using simple +identifiers. + +Even though translated enum cases are located in the enum's companion object, referencing +this object or its members via `this` or a simple identifier is also illegal. The compiler typechecks enum cases in the scope of the enclosing companion object but flags any such illegal accesses as errors. ### Other Rules diff --git a/tests/neg/enumsAccess.scala b/tests/neg/enumsAccess.scala index 0bd4427863c5..c35beba60e11 100644 --- a/tests/neg/enumsAccess.scala +++ b/tests/neg/enumsAccess.scala @@ -84,3 +84,13 @@ object test6 { val defaultX = 2 } } + +object test7 { + + trait Arg + + enum E(x: Arg) { + case C extends E(this) // error: illegal reference to `this` + } + object E extends Arg +} From ea23a1948847063dc4420e94982dea3ea4fb63de Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 18 Feb 2018 14:57:09 +0100 Subject: [PATCH 14/17] Small fixes in docs --- docs/docs/reference/enums/desugarEnums.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/docs/reference/enums/desugarEnums.md b/docs/docs/reference/enums/desugarEnums.md index d1880349130e..ac53adef5911 100644 --- a/docs/docs/reference/enums/desugarEnums.md +++ b/docs/docs/reference/enums/desugarEnums.md @@ -93,8 +93,7 @@ map into case classes or vals. This result is then further rewritten with rule (8). -6. If `E` is an enum with type parameters `Ts`, a class case with neither type parameters nor - an extends clause +6. If `E` is an enum with type parameters `Ts`, a class case with neither type parameters nor an extends clause case C @@ -102,8 +101,7 @@ map into case classes or vals. case C[Ts] extends E[Ts] - This result is then further rewritten with rule (8). For class cases that have type parameters - themselves, an extends clause needs to be given explicitly. + This result is then further rewritten with rule (8). For class cases that have type parameters themselves, an extends clause needs to be given explicitly. 7. A value case @@ -159,7 +157,7 @@ the following additional members. - A method `enumValues` which returns an `Iterable[E]` of all singleton case values in `E`, in the order of their definitions. -Companion objects that contain at least one simple case define in addition: +Companion objects of enumerations that contain at least one simple case define in addition: - A private method `$new` which defines a new simple case value with given ordinal number and name. This method can be thought as being defined as @@ -182,5 +180,5 @@ this object or its members via `this` or a simple identifier is also illegal. Th ### Other Rules A normal case class which is not produced from an enum case is not allowed to extend -`scala.Enum`. This ensures that the only cases of an anum are the ones that are +`scala.Enum`. This ensures that the only cases of an enum are the ones that are explictly declared in it. \ No newline at end of file From b4258e20e46b869e3843e3808bb8211dd3b5d172 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 19 Feb 2018 16:40:58 +0100 Subject: [PATCH 15/17] Add test case --- tests/neg/constrParams.scala | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/neg/constrParams.scala diff --git a/tests/neg/constrParams.scala b/tests/neg/constrParams.scala new file mode 100644 index 000000000000..38805a2e39e7 --- /dev/null +++ b/tests/neg/constrParams.scala @@ -0,0 +1,8 @@ + + +class C { + type INT = Int + class I + def this(x: INT) = this() // error: illegal access + def this(x: I) = this() // error: illegal access +} From e8ea6e90b49511799c4826cbf24e7cf613fb9597 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 20 Feb 2018 17:40:39 +0100 Subject: [PATCH 16/17] Allow cross case references when access checking cases --- .../src/dotty/tools/dotc/typer/Checking.scala | 77 +++++++++++-------- tests/run/enums.scala | 8 ++ 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index ee0c46182c20..7a2dec251a33 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -785,42 +785,55 @@ trait Checking { * @param enumCtx the context immediately enclosing the corresponding enum */ private def checkEnumCaseRefsLegal(cdef: TypeDef, enumCtx: Context)(implicit ctx: Context): Unit = { - def check(tree: Tree) = { - // allow access to `sym` if a typedIdent just outside the enclosing enum - // would have produced the same symbol without errors - def allowAccess(name: Name, sym: Symbol): Boolean = { - val testCtx = enumCtx.fresh.setNewTyperState() - val ref = ctx.typer.typedIdent(untpd.Ident(name), WildcardType)(testCtx) - ref.symbol == sym && !testCtx.reporter.hasErrors + + def checkCaseOrDefault(stat: Tree, caseCtx: Context) = { + + def check(tree: Tree) = { + // allow access to `sym` if a typedIdent just outside the enclosing enum + // would have produced the same symbol without errors + def allowAccess(name: Name, sym: Symbol): Boolean = { + val testCtx = caseCtx.fresh.setNewTyperState() + val ref = ctx.typer.typedIdent(untpd.Ident(name), WildcardType)(testCtx) + ref.symbol == sym && !testCtx.reporter.hasErrors + } + checkRefsLegal(tree, cdef.symbol, allowAccess, "enum case") } - checkRefsLegal(tree, cdef.symbol, allowAccess, "enum case") + + if (stat.symbol.is(Case)) + stat match { + case TypeDef(_, Template(DefDef(_, tparams, vparamss, _, _), parents, _, _)) => + tparams.foreach(check) + vparamss.foreach(_.foreach(check)) + parents.foreach(check) + case vdef: ValDef => + vdef.rhs match { + case Block((clsDef @ TypeDef(_, impl: Template)) :: Nil, _) + if clsDef.symbol.isAnonymousClass => + impl.parents.foreach(check) + case _ => + } + case _ => + } + else if (stat.symbol.is(Module) && stat.symbol.linkedClass.is(Case)) + stat match { + case TypeDef(_, impl: Template) => + for ((defaultGetter @ + DefDef(DefaultGetterName(nme.CONSTRUCTOR, _), _, _, _, _)) <- impl.body) + check(defaultGetter.rhs) + case _ => + } } + cdef.rhs match { case impl: Template => - for (stat <- impl.body) - if (stat.symbol.is(Case)) - stat match { - case TypeDef(_, Template(DefDef(_, tparams, vparamss, _, _), parents, _, _)) => - tparams.foreach(check) - vparamss.foreach(_.foreach(check)) - parents.foreach(check) - case vdef: ValDef => - vdef.rhs match { - case Block((clsDef @ TypeDef(_, impl: Template)) :: Nil, _) - if clsDef.symbol.isAnonymousClass => - impl.parents.foreach(check) - case _ => - } - case _ => - } - else if (stat.symbol.is(Module) && stat.symbol.linkedClass.is(Case)) - stat match { - case TypeDef(_, impl: Template) => - for ((defaultGetter @ - DefDef(DefaultGetterName(nme.CONSTRUCTOR, _), _, _, _, _)) <- impl.body) - check(defaultGetter.rhs) - case _ => - } + def isCase(stat: Tree) = stat match { + case _: ValDef | _: TypeDef => stat.symbol.is(Case) + case _ => false + } + val cases = for (stat <- impl.body if isCase(stat)) yield untpd.Ident(stat.symbol.name) + val caseImport: Import = Import(ref(cdef.symbol), cases) + val caseCtx = enumCtx.importContext(caseImport, caseImport.symbol) + for (stat <- impl.body) checkCaseOrDefault(stat, caseCtx) case _ => } } diff --git a/tests/run/enums.scala b/tests/run/enums.scala index 7630b174ef38..1a515db40bda 100644 --- a/tests/run/enums.scala +++ b/tests/run/enums.scala @@ -100,6 +100,14 @@ object Test5 { } } +object Test6 { + enum Color(val x: Int) { + case Green extends Color(3) + case Red extends Color(2) + case Violet extends Color(Green.x + Red.x) + } +} + object SerializationTest { object Types extends Enumeration { val X, Y = Value } class A extends java.io.Serializable { val types = Types.values } From f07d9ca4254ae0ba51cd9219817df6862f0c3422 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 20 Feb 2018 22:31:16 +0100 Subject: [PATCH 17/17] Fix creation of importContext for accessibility checking --- compiler/src/dotty/tools/dotc/typer/Checking.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 7a2dec251a33..030e50fdcc3d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -830,7 +830,9 @@ trait Checking { case _: ValDef | _: TypeDef => stat.symbol.is(Case) case _ => false } - val cases = for (stat <- impl.body if isCase(stat)) yield untpd.Ident(stat.symbol.name) + val cases = + for (stat <- impl.body if isCase(stat)) + yield untpd.Ident(stat.symbol.name.toTermName) val caseImport: Import = Import(ref(cdef.symbol), cases) val caseCtx = enumCtx.importContext(caseImport, caseImport.symbol) for (stat <- impl.body) checkCaseOrDefault(stat, caseCtx)