Skip to content
Open
Changes from 3 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
213 changes: 213 additions & 0 deletions content/multi-level-enums.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
---
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 case** of the parent enum, and its members are **also valid cases**, allowing full exhaustivity and pattern matching.
Copy link
Member

Choose a reason for hiding this comment

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

The nested enum is itself a valid case of the parent enum

This does not make sense to me. It's a subtype, sure. But it cannot be a valid (term) case of the parent enum while at the same time having several "sub-terms". A term must be unique.


### 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 trait 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** (e.g., `Mammal`) is shorthand for matching all its subcases.
Copy link
Member

Choose a reason for hiding this comment

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

This does not make sense. Matching on Mammal itself already has meaning, which is to test if it is the Mammal companion object itself. That cannot be changed to means case Cat | Dog.

Copy link
Author

Choose a reason for hiding this comment

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

True, which is a shame. We can still match on supercase type though.

def laysEggs(a: Animal) = a match {
  case _: Bird | Fish => true
  case _ => false 
}


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


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