Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions content/multi-level-enums.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
---
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
Copy link
Member

Choose a reason for hiding this comment

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

also good to demonstrate how parameters work (e.g. case enum Bird(val call: String):) and optional extends clauses

Copy link
Member

Choose a reason for hiding this comment

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

also the rules of extends clauses, e.g. what class is allowed to be extended

```

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

#### 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
Copy link
Member

Choose a reason for hiding this comment

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

Enums are abstract classes, not traits.

Copy link
Author

Choose a reason for hiding this comment

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

fixed.


object Animal:
sealed abstract class Mammal extends Animal
object Mammal:
case object Dog extends Mammal
Copy link
Member

Choose a reason for hiding this comment

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

to avoid getting into the weeds, because this is the wrong desugaring, its probably better to explain it in a set of recursive steps, where each case enum becomes an enum in the companion that extends the parent enum class (because an enum is a class not a sealed trait)
see the spec here: https://scala-lang.org/files/archive/spec/3.4/05-classes-and-objects.html#lowering-of-enum-definitions

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the pointer!

I'll look into that and update the spec.

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 type** (e.g., `m: 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
Copy link

@JD557 JD557 Sep 19, 2025

Choose a reason for hiding this comment

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

Maybe I missed it, but I think is not explicit how non-leaf ordinals are computed.

Would it be?

enum Animal:        // No ordinal
  case enum Mammal: // Ordinal 0
    case Dog        // Ordinal 0
    case Cat        // Ordinal 1
  case enum Bird:   // Ordinal 1
    case Sparrow    // Ordinal 2
    case Pinguin    // Ordinal 3
  case Fish         // Ordinal 2

If that is the case, I assume Mammal.fromOrdinal(...) won't exist.
If it does exist, how would Bird.fromOrdinal(...) work? Would it take "global ordinals" (2 for Sparrow and 3 for Penguin), or would it expect "local ordinals" (0 for Sparrow and 1 for Penguin)?

Copy link
Author

Choose a reason for hiding this comment

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

No, you didn't miss it. It's currently not specified.

Do Mammal or Bird, i.e. "sub-enums", need their own ordinal? Can we even instantiate them?

enum Animal:        // No ordinal
  case enum Mammal:       // No ordinal
    case Dog        // Ordinal 0
    case Cat        // Ordinal 1
  case enum Bird:       // No ordinal
    case Sparrow    // Ordinal 2
    case Pinguin    // Ordinal 3
  case Fish         // Ordinal 4

- `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 ::= Ids
| 'enum' Id ':' EnumBody
Ids ::= Id {',' Id}
```


#### 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
```

#### 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.
- 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