From d0cda54ca865106b6a99381ef68819227d47bc42 Mon Sep 17 00:00:00 2001 From: Raphael Bosshard Date: Fri, 19 Sep 2025 00:22:58 +0200 Subject: [PATCH 1/7] initial draft for multi-level enums --- content/multi-level-enums.md | 212 +++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 content/multi-level-enums.md diff --git a/content/multi-level-enums.md b/content/multi-level-enums.md new file mode 100644 index 00000000..4725557c --- /dev/null +++ b/content/multi-level-enums.md @@ -0,0 +1,212 @@ +--- +layout: sip +permalink: /sips/:title.html +stage: implementation +status: waiting-for-implementation +presip-thread: https://contributors.scala-lang.org/t/pre-sip-foo-bar/9999 +title: SIP-NN - Multi-Level Enums +--- + +# SIP: Multi-Level Enums + +**By: Raphael Bosshard** + +## History + +| Date | Version | +| ---------- | ------------------ | +| 2025-09-15 | Initial Draft | + + + +## Summary + +This proposal adds minimal syntax to allow enums with nested enumerations, maintaining full exhaustivity checking while keeping syntax clean and intuitive. + +## Motivation + +Scala 3 introduced `enum` as a concise and type-safe way to define algebraic data types. However, it currently supports only flat enums. Many real-world use cases, such as domain modeling or UI state machines, naturally require **hierarchical enums**, where cases are grouped into logical families or categories. + +Consider modeling animals: + +```scala +enum Animal: + case Dog, Cat, Sparrow, Penguin +``` + +This flat structure works, but grouping `Dog` and `Cat` under `Mammal`, and `Sparrow` and `Penguin` under `Bird` is more expressive and enables cleaner abstraction. + + +## Proposed solution + +### High-level overview + +Allow `enum` cases to contain **nested `enum` definitions**, using a consistent indentation-based syntax. + +```scala +enum Animal: + case enum Mammal: + case Dog, Cat + case enum Bird: + case Sparrow, Pinguin +``` + +Each nested `enum` case defines a group of related subcases. The **nested enum is itself a valid case** of the parent enum, and its members are **also valid cases**, allowing full exhaustivity and pattern matching. + +### Specification + +#### Enum Definition: + +```scala +enum Animal: + case enum Mammal: + case Dog, Cat + case enum Bird: + case Sparrow, Pinguin + case Fish +``` + +- `case enum Mammal:` introduces a **sub-enum case**. +- Nested cases (`Dog`, `Cat`) are **automatically part of the parent enum** (`Animal`), as well as part of the sub-enum (`Mammal`). + +#### Desugaring / Type Relationships + +The above syntax desugars to an enum tree with subtype relationships: + +```scala +sealed trait Animal + +object Animal: + sealed trait Mammal extends Animal + object Mammal: + case object Dog extends Mammal + case object Cat extends Mammal + + sealed trait Bird extends Animal + object Bird: + case object Sparrow extends Bird + case object Pinguin extends Bird + case object FIsh extends Animal +``` + +Results in: + +- `Mammal` and `Bird` are singleton enum cases of `Animal` +- `Dog`, `Cat`, `Sparrow`, and `Pinguin` are **also** values of `Animal`, and they belong to `Mammal` and `Bird` respectively +- Type relationships: + - `Dog <: Mammal <: Animal` + - `Cat <: Mammal <: Animal` + - `Fish <: Animal` + - etc. + +All leaf cases are usable as values of `Animal`, and the nested grouping allows matching at any level of the hierarchy. + + +#### Pattern Matching + +Exhaustive pattern matching on `Animal` must cover all leaf cases: + +```scala +def classify(a: Animal): String = a match + case Dog => "a dog" + case Cat => "a cat" + case Sparrow => "a bird" + case Pinguin => "a penguin" + case Fish => "a fish" +``` + +Matching on intermediate enums is also allowed: + +```scala +def isWarmBlooded(a: Animal): Boolean = a match + case Mammal => true // Covers Dog, Cat + case Bird => true // Covers Sparrow, Pinguin + case Fish => false +``` + +Matching on a **supercase** (e.g., `Mammal`) is shorthand for matching all its subcases. + +#### `values`, `ordinal`, `valueOf` + +- `Animal.values` returns all **leaf cases**: `[Dog, Cat, Sparrow, Pinguin, Fish]` +- `Mammal.values` returns `[Dog, Cat]` +- `Mammal.ordinal` and `Mammal.valueOf(...)` are also available +- `Mammal` and `Bird` are usable as enum **values**, but excluded from `values` (unless explicitly included) + + +#### Sealed-ness and Exhaustivity + +- The parent enum and all nested enums are sealed. +- Pattern matching at any level (e.g. on `Mammal`) is **exhaustive** at that level. +- At the top level (`Animal`), exhaustivity means all leaf cases must be covered. + +#### Reflection / Enum APIs + +- `Animal.values`: All leaf values (`Dog`, `Cat`, `Sparrow`, etc.) +- Each nested `case enum` (e.g., `Mammal`) gets its own `.values`, `.ordinal`, and `.valueOf` API. +- `ordinal` value of leaves are global to the supercase (e.g., `Dog.ordinal == 0`, `Cat.ordinal == 1`, etc.) + +#### Syntax Specification (EBNF-like) + +``` +EnumDef ::= 'enum' Id ':' EnumBody +EnumBody ::= { EnumCase } +EnumCase ::= 'case' EnumCaseDef +EnumCaseDef ::= Id [',' Ids] + | 'enum' Id ':' EnumBody +``` + + +#### Compiler + +- The Scala compiler must treat nested enums inside `case enum` as part of the parent enum’s namespace. +- Exhaustivity checking logic must recursively analyze nested enums to extract leaf cases. +- Type relationships must be modeled to reflect subtyping: e.g. `Dog <: Mammal <: Animal`. + + +#### Examples + +#### Example 1: Basic Structure + +```scala +enum Shape: + case enum Polygon: + case Triangle, Square + + case enum Curve: + case Circle + + case Point +``` + + +### Compatibility +- Fully backwards compatible: does not affect existing flat enums. +- Adds optional expressiveness. +- Libraries using `enum` APIs (e.g., `values`) will continue to function with leaf-only views. +- Mirrors and macros + +### Other Concerns + +#### Macro / Tooling Support + +- IDEs and macros need to understand the nested structure. +- Pattern matching hints and auto-completion should support matching on intermediate cases. + + +### Feature Interactions + + +### Alternatives + +- **Flat enums with traits**: more verbose, less exhaustivity checking, more boilerplate. +- **Nested cases with `extends`**: heavier syntax, harder to teach/read. +- **DSLs or macros**: non-standard, cannot integrate with Scala's `enum` semantics cleanly. + + +## Related Work + + +## Faq + + From 4d306b22b873e41b0868097c293d23e3054e2144 Mon Sep 17 00:00:00 2001 From: Raphael Bosshard Date: Fri, 19 Sep 2025 00:32:03 +0200 Subject: [PATCH 2/7] fix ebnf syntax --- content/multi-level-enums.md | 1 + 1 file changed, 1 insertion(+) diff --git a/content/multi-level-enums.md b/content/multi-level-enums.md index 4725557c..a36999bb 100644 --- a/content/multi-level-enums.md +++ b/content/multi-level-enums.md @@ -154,6 +154,7 @@ EnumBody ::= { EnumCase } EnumCase ::= 'case' EnumCaseDef EnumCaseDef ::= Id [',' Ids] | 'enum' Id ':' EnumBody +Ids ::= Id {',' Id} ``` From 8fb9c823851d70bd7ff9c21e9c135bb216a18441 Mon Sep 17 00:00:00 2001 From: Raphael Bosshard Date: Fri, 19 Sep 2025 00:35:12 +0200 Subject: [PATCH 3/7] simplify grammar --- content/multi-level-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/multi-level-enums.md b/content/multi-level-enums.md index a36999bb..d47124ee 100644 --- a/content/multi-level-enums.md +++ b/content/multi-level-enums.md @@ -152,7 +152,7 @@ Matching on a **supercase** (e.g., `Mammal`) is shorthand for matching all its s EnumDef ::= 'enum' Id ':' EnumBody EnumBody ::= { EnumCase } EnumCase ::= 'case' EnumCaseDef -EnumCaseDef ::= Id [',' Ids] +EnumCaseDef ::= Ids | 'enum' Id ':' EnumBody Ids ::= Id {',' Id} ``` From 9d268bb2e703217690ae67bd9ffea3d60a8f563f Mon Sep 17 00:00:00 2001 From: Raphael Bosshard Date: Fri, 19 Sep 2025 17:09:00 +0200 Subject: [PATCH 4/7] Update content/multi-level-enums.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sébastien Doeraene --- content/multi-level-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/multi-level-enums.md b/content/multi-level-enums.md index d47124ee..1ba6c868 100644 --- a/content/multi-level-enums.md +++ b/content/multi-level-enums.md @@ -77,7 +77,7 @@ The above syntax desugars to an enum tree with subtype relationships: sealed trait Animal object Animal: - sealed trait Mammal extends Animal + sealed abstract class Mammal extends Animal object Mammal: case object Dog extends Mammal case object Cat extends Mammal From 20336633a313bc7e4bf6c43571aa62201655ac41 Mon Sep 17 00:00:00 2001 From: Raphael Bosshard Date: Fri, 19 Sep 2025 17:09:30 +0200 Subject: [PATCH 5/7] Update content/multi-level-enums.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sébastien Doeraene --- content/multi-level-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/multi-level-enums.md b/content/multi-level-enums.md index 1ba6c868..fd5f40f8 100644 --- a/content/multi-level-enums.md +++ b/content/multi-level-enums.md @@ -119,7 +119,7 @@ Matching on intermediate enums is also allowed: ```scala def isWarmBlooded(a: Animal): Boolean = a match - case Mammal => true // Covers Dog, Cat + case _: Mammal => true // Covers Dog, Cat case Bird => true // Covers Sparrow, Pinguin case Fish => false ``` From 985438d61f3530cf4054acf052e28cef5501a34e Mon Sep 17 00:00:00 2001 From: Raphael Bosshard Date: Fri, 19 Sep 2025 19:17:06 +0200 Subject: [PATCH 6/7] add more examples --- content/multi-level-enums.md | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/content/multi-level-enums.md b/content/multi-level-enums.md index fd5f40f8..1840ba91 100644 --- a/content/multi-level-enums.md +++ b/content/multi-level-enums.md @@ -180,6 +180,51 @@ enum Shape: case Point ``` +#### Example 2: Moddeling size information + +``` +enum SizeInfo { + case Bounded(bound: Int) + case enum Atomic { + case Infinite + case Precise(n: Int) + } +} +``` + +#### Example 3: Generalized `Either` + +``` +enum AndOr[+A, +B] { + case Both[+A, +B](left: A, right: B) extends AndOr[A, B] + case enum Either[+A, +B] extends AndOr[A, B] { + case Left[+A, +B](value: A) extends Either[A, B] + case Right[+A, +B](value: B) extends Either[A, B] + } +} + +``` + +#### Example 4: +Grouping JSON values into primitives and non-primitives + +``` +enum JsValue { + case Obj(fields: Map[String, JsValue]) + case Arr(elems: ArraySeq[JsValue]) + case enum Primitive { + case Str(str: String) + case Num(bigDecimal: BigDecimal) + case JsNull + case enum Bool(boolean: Boolean) { + case True extends Bool(true), + case False extends Bool(false) } + } +} +``` + + + ### Compatibility - Fully backwards compatible: does not affect existing flat enums. From 70ec12706acf79d62858cb2e646d90fd8cf93427 Mon Sep 17 00:00:00 2001 From: Raphael Bosshard Date: Fri, 19 Sep 2025 19:31:14 +0200 Subject: [PATCH 7/7] fix some problems with matching on supercase --- content/multi-level-enums.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/content/multi-level-enums.md b/content/multi-level-enums.md index 1840ba91..f4e20fb6 100644 --- a/content/multi-level-enums.md +++ b/content/multi-level-enums.md @@ -51,7 +51,7 @@ enum Animal: case Sparrow, Pinguin ``` -Each nested `enum` case defines a group of related subcases. The **nested enum is itself a valid case** of the parent enum, and its members are **also valid cases**, allowing full exhaustivity and pattern matching. +Each nested `enum` case defines a group of related subcases. The **nested enum is itself a valid subtype** of the parent enum, and its members are **valid cases** of the parent enum, allowing full exhaustivity and pattern matching. ### Specification @@ -124,7 +124,7 @@ def isWarmBlooded(a: Animal): Boolean = a match case Fish => false ``` -Matching on a **supercase** (e.g., `Mammal`) is shorthand for matching all its subcases. +Matching on a **supercase type** (e.g., `m: Mammal`) is shorthand for matching all its subcases. #### `values`, `ordinal`, `valueOf` @@ -216,9 +216,10 @@ enum JsValue { case Str(str: String) case Num(bigDecimal: BigDecimal) case JsNull - case enum Bool(boolean: Boolean) { - case True extends Bool(true), - case False extends Bool(false) } + case enum Bool(boolean: Boolean) { + case True extends Bool(true) + case False extends Bool(false) + } } } ```