-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Pitch for extensible enums #2679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
airspeedswift
merged 19 commits into
swiftlang:main
from
FranzBusch:fb-extensible-enums
May 25, 2025
Merged
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
89066d9
Extensible enums
FranzBusch b4f3c68
Review
FranzBusch 6fefb29
Reviews and same module/package enums
FranzBusch b429d92
Clarify API impact of `@extensible` and add alternatives considered s…
FranzBusch f718b7d
More review comments
FranzBusch 0735a82
Pitch feedback
FranzBusch 6a0fc97
Pitch feedback: Adding migration path and expand on implications insi…
FranzBusch d3da8bb
Fix small mistake
FranzBusch b367bb9
Address adding associated values
FranzBusch e341481
Remove new annotations and re-use `@frozen`
FranzBusch 010feb1
Update metadata with implementation and pitch
FranzBusch 8268dd9
Small fixups
FranzBusch 5f8e777
Add migration paths
FranzBusch 40e622c
Minor spelling fix ups
FranzBusch fd0d3a4
Update proposal to focus on `@extensible` attribute
FranzBusch 8876f99
Change implementation link
FranzBusch 79533ec
Add `@preEnumExtensibility` to the proposal
FranzBusch e15d044
Source compat section for preEnumExtensibility
FranzBusch 3566297
Kick off review
airspeedswift File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| # Extensible enums | ||
|
|
||
| * Proposal: [SE-NNNN](NNNN-extensible-enums.md) | ||
| * Authors: [Pavel Yaskevich](https://github.com/xedin), [Franz Busch](https://github.com/FranzBusch), [Cory Benfield](https://github.com/lukasa) | ||
| * Review Manager: TBD | ||
| * Status: **Awaiting review** | ||
| * Bug: [apple/swift#55110](https://github.com/swiftlang/swift/issues/55110) | ||
| * Implementation: [apple/swift#NNNNN](https://github.com/apple/swift/pull/NNNNN) | ||
| * Upcoming Feature Flag: `ExtensibleEnums` | ||
| * Review: ([pitch](https://forums.swift.org/...)) | ||
|
|
||
| ## Introduction | ||
|
|
||
| This proposal addresses the long standing behavioural difference of `enum`s in | ||
| Swift modules compiled with and without library evolution. This makes Swift | ||
| `enum`s vastly more useful in public API of non-resilient Swift libraries. | ||
|
|
||
| ## Motivation | ||
|
|
||
| When Swift was enhanced to add support for "library evolution" mode (henceforth | ||
| called "resilient" mode), the Swift project had to make a number of changes to | ||
| support a movable scale between "maximally evolveable" and "maximally | ||
| performant". This is because it is necessary for an ABI stable library to be | ||
| able to add new features and API surface without breaking pre-existing compiled | ||
| binaries. While by-and-large this was done without introducing feature | ||
| mismatches between the "resilient" and default "non-resilient" language | ||
| dialects, the `@frozen` attribute when applied to enumerations managed to | ||
| introduce a difference. This difference was introduced late in the process of | ||
| evolving SE-0192, and this proposal would aim to address it. | ||
|
|
||
| `@frozen` is a very powerful attribute. It can be applied to both structures and | ||
| enumerations. It has a wide ranging number of effects, including exposing their | ||
| size directly as part of the ABI and providing direct access to stored | ||
| properties. However, on enumerations it happens to also exert effects on the | ||
| behaviour of switch statements. | ||
|
|
||
| Consider the following simple library to your favourite pizza place: | ||
|
|
||
| ```swift | ||
| public enum PizzaFlavor { | ||
| case hawaiian | ||
| case pepperoni | ||
| case cheese | ||
| } | ||
|
|
||
| public func bakePizza(flavor: PizzaFlavor) | ||
| ``` | ||
|
|
||
| Depending on whether the library is compiled with library evolution mode | ||
| enabled, what the caller can do with the `PizzaFlavor` enum varies. Specifically, | ||
| the behaviour in switch statements changes. | ||
|
|
||
| In the _standard_, "non-resilient" mode, users of the library can write | ||
| exhaustive switch statements over the enum `PizzaFlavor`: | ||
|
|
||
| ```swift | ||
| switch pizzaFlavor { | ||
| case .hawaiian: | ||
| throw BadFlavorError() | ||
| case .pepperoni: | ||
| try validateNoVegetariansEating() | ||
| case .cheese: | ||
| return .delicious | ||
| } | ||
| ``` | ||
|
|
||
| This code will happily compile. If the author of the above switch statement was | ||
| missing a case (perhaps they forgot `.hawaiian` is a flavor), the compiler will | ||
| error, and force the user to either add a `default:` clause, or to express a | ||
| behaviour for the missing case. The term for this is "exhaustiveness": in the | ||
| default "non-resilient" dialect, the Swift compiler will ensure that all switch | ||
| statements over enumerations cover every case that is present. | ||
|
|
||
| There is a downside to this mode. If the library wants to add a new flavour | ||
| (maybe `.veggieSupreme`), they are in a bind. If any user anywhere has written | ||
| an exhaustive switch over `PizzaFlavor`, adding this flavor will be an API and | ||
| ABI breaking change, as the compiler will error due to the missing case | ||
| statement for the new enum case. | ||
|
|
||
| Because of the implications on ABI and the requirement to be able to evolve | ||
| libraries with public enumerations in their API, the resilient language dialect | ||
| behaves differently. If the library was compiled with `enable-library-evolution` | ||
| turned on, when a user attempts to exhaustively switch over the `PizzaFlavor` | ||
| enum the compiler will emit a warning, encouraging users to add an `@unknown | ||
| default:` clause. Thus, to avoid the warning the user would be forced to | ||
| consider how new enumeration cases should be treated. They may arrive at | ||
| something like this: | ||
|
|
||
| ```swift | ||
| switch pizzaFlavor { | ||
| case .hawaiian: | ||
| throw BadFlavorError() | ||
| case .pepperoni: | ||
| try validateNoVegetariansEating() | ||
| return .delicious | ||
| case .cheese: | ||
| return .delicious | ||
| @unknown default: | ||
| try validateNoVegetariansEating() | ||
| return .delicious | ||
| } | ||
| ``` | ||
|
|
||
| When a resilient library knows that an enumeration will not be extended, and | ||
| wants to improve the performance of using it, the author can annotate the enum | ||
| with `@frozen`. This annotation has a wide range of effects, but one of its | ||
| effects is to enable callers to perform exhaustive switches over the frozen | ||
| enumeration. Thus, resilient library authors that are interested in the | ||
| exhaustive switching behaviour are able to opt-into it. | ||
|
|
||
| However, in Swift today it is not possible for the default, "non-resilient" | ||
| dialect to opt-in to the extensible enumeration behaviour. That is, there is no | ||
| way for a Swift package to be able to evolve a public enumeration without | ||
| breaking the API. This is a substantial limitation, and greatly reduces the | ||
| utility of enumerations in non-resilient Swift. Over the past years, many | ||
| packages ran into this limitation when trying to express APIs using enums. As a | ||
| non-exhaustive list of problems this can cause: | ||
|
|
||
| - Using enumerations to represent `Error`s is inadvisable, as if new errors need | ||
| to be introduced they cannot be added to existing enumerations. This leads to | ||
| a proliferation of `Error` enumerations. "Fake" enumerations can be made using | ||
| `struct`s and `static let`s, but these do not work with the nice `Error` | ||
| pattern-match logic in catch blocks, requiring type casts. | ||
| - Using an enumeration to refer to a group of possible ideas without entirely | ||
| exhaustively evaluating the set is potentially dangerous, requiring a | ||
| deprecate-and-replace if any new elements appear. | ||
| - Using an enumeration to represent any concept that is inherently extensible is | ||
| tricky. For example, `SwiftNIO` uses an enumeration to represent HTTP status | ||
| codes. If new status codes are added, SwiftNIO needs to either mint new | ||
| enumerations and do a deprecate-and-replace, or it needs to force these new | ||
| status codes through the .custom enum case. | ||
|
|
||
| This proposal plans to address these limitations on enumerations in | ||
| non-resilient Swift. | ||
|
|
||
| ## Proposed solution | ||
|
|
||
| We propose to introduce a new language feature `ExtensibleEnums` that aligns the | ||
| behaviour of enumerations in both language dialects. This will make **public** | ||
| enumerations in packages a safe default and leave maintainers the choice of | ||
| extending them later on. | ||
|
|
||
| In modules with the language feature enabled, developers can use the existing | ||
| `@frozen` attribute to mark an enumeration as non-extensible, allowing consumers | ||
| of the module to exhaustively switch over the cases. This makes committing to the | ||
| API of an enum an active choice for developers. | ||
|
|
||
| Modules consuming other modules with the language feature enabled will be forced | ||
| to add an `@unknown default:` case to any switch state for enumerations that are | ||
| not marked with `@frozen`. Importantly, this only applies to enums that are | ||
| imported from other modules that are not in the same package. For enums inside | ||
| the same modules of the declaring package switches is still required to be | ||
| exhaustive and doesn't require an `@unknown default:` case. | ||
|
FranzBusch marked this conversation as resolved.
Outdated
|
||
|
|
||
| Since enabling a language feature applies to the whole module at once we also | ||
| propose adding a new attribute `@extensible` analogous to `@frozen`. This | ||
|
FranzBusch marked this conversation as resolved.
Outdated
|
||
| attribute allows developers to make a case-by-case decision on each enumeration | ||
| if it should be extensible or not by applying one of the two attributes. The | ||
| language feature `ExtensibleEnums` can be thought of as implicitly adding | ||
| `@extensible` to all enums that are not explicitly marked as `@frozen`. | ||
|
|
||
| In resilient modules, the `@extensible` attribute doesn't affect API nor ABI | ||
| since the behaviour of enumerations in modules compiled with library evolution | ||
| mode are already extensible by default. We believe that extensible enums are the | ||
| right default choice in both resilient and non-resilient modules and the new | ||
| proposed `@extensible` attribute primiarly exists to give developers a migration | ||
| path. | ||
|
|
||
| ## Source compatibility | ||
|
|
||
| Enabling the language feature `ExtensibleEnums` in a module that contains public | ||
| enumerations is a source breaking change. | ||
| Changing the annotation from `@frozen` to `@extensible` is a source breaking | ||
| change. | ||
| Changing the annotation from `@extensible` to `@frozen` is a source compatible | ||
| change and will only result in a warning code that used `@unknown default:` | ||
| clause. This allows developers to commit to the API of an enum in a non-source | ||
| breaking way. | ||
| Adding an `@extensible` annotation is a source breaking change in modules that | ||
| have **not** enabled the `ExtensibleEnums` language features or are compiled | ||
| with resiliency. | ||
|
FranzBusch marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Effect on ABI stability | ||
|
|
||
| This attribute does not affect the ABI, as it is a no-op when used in a resilient library. | ||
|
|
||
| ## Effect on API resilience | ||
|
|
||
| This proposal only affects API resilience of non-resilient libraries, by enabling more changes to be made without API breakage. | ||
|
|
||
| ## Future directions | ||
|
|
||
| ### Enable `ExtensibleEnums` by default in a future language mode | ||
|
|
||
| We believe that extensible enums should be default in the language to remove the | ||
| common pitfall of using enums in public API and only later on realising that | ||
| those can't be extended in an API compatible way. Since this would be a large | ||
| source breaking change it must be gated behind a new language mode. | ||
|
|
||
| ## Alternatives considered | ||
|
|
||
| ### Only provide the `@extensible` annotation | ||
|
|
||
| We believe that the default behaviour in both language dialects should be that | ||
| public enumerations are extensible. One of Swift's goals, is safe defaults and | ||
| the current non-extensible default in non-resilient modules doesn't achieve that | ||
| goal. That's why we propose a new language feature to change the default in a | ||
| future Swift language mode. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.