|
| 1 | +# Extensible enums |
| 2 | + |
| 3 | +* Proposal: [SE-NNNN](NNNN-extensible-enums.md) |
| 4 | +* Authors: [Pavel Yaskevich](https://github.com/xedin), [Franz Busch](https://github.com/FranzBusch), [Cory Benfield](https://github.com/lukasa) |
| 5 | +* Review Manager: [Ben Cohen](https://github.com/airspeedswift) |
| 6 | +* Status: **In active review (May 25—Jun 5, 2025)** |
| 7 | +* Bug: [apple/swift#55110](https://github.com/swiftlang/swift/issues/55110) |
| 8 | +* Implementation: [apple/swift#80503](https://github.com/swiftlang/swift/pull/80503) |
| 9 | +* Upcoming Feature Flag: `ExtensibleAttribute` |
| 10 | +* Review: ([pitch](https://forums.swift.org/t/pitch-extensible-enums-for-non-resilient-modules/77649)) |
| 11 | + |
| 12 | +Previously pitched in: |
| 13 | + |
| 14 | +- https://forums.swift.org/t/extensible-enumerations-for-non-resilient-libraries/35900 |
| 15 | +- https://forums.swift.org/t/pitch-non-frozen-enumerations/68373 |
| 16 | + |
| 17 | +Revisions: |
| 18 | +- Re-focused this proposal on introducing a new `@extensible` attribute and |
| 19 | + moved the language feature to a future direction |
| 20 | +- Introduced a second annotation `@nonExtensible` to allow a migration path into |
| 21 | + both directions |
| 22 | +- Added future directions for adding additional associated values |
| 23 | +- Removed both the `@extensible` and `@nonExtensible` annotation in favour of |
| 24 | + re-using the existing `@frozen` annotation |
| 25 | +- Added the high level goals that this proposal aims to achieve |
| 26 | +- Expanded on the proposed migration path for packages with regards to their |
| 27 | + willingness to break API |
| 28 | +- Added future directions for exhaustive matching for larger compilation units |
| 29 | +- Added alternatives considered section for a hypothetical |
| 30 | + `@preEnumExtensibility` |
| 31 | +- Added a section for `swift package diagnose-api-breaking-changes` |
| 32 | + |
| 33 | +## Introduction |
| 34 | + |
| 35 | +This proposal provides developers the capabilities to mark public enums in |
| 36 | +non-resilient Swift libraries as extensible. This makes Swift `enum`s vastly |
| 37 | +more useful in public API of such libraries. |
| 38 | + |
| 39 | +## Motivation |
| 40 | + |
| 41 | +When Swift was enhanced to add support for ABI-stable libraries that were built with |
| 42 | +"library evolution" enabled ("resilient" libraries as we call them in this proposal), |
| 43 | +the Swift language had to support these libraries vending enums that might have cases |
| 44 | +added to them in a later version. Swift supports exhaustive switching over cases. |
| 45 | +When binaries are compiled against a ABI-stable library they need to be able to handle the |
| 46 | +addition of a new case by that library later on, without needing to be rebuilt. |
| 47 | + |
| 48 | +Consider the following simple library to your favorite pizza place: |
| 49 | + |
| 50 | +```swift |
| 51 | +public enum PizzaFlavor { |
| 52 | + case hawaiian |
| 53 | + case pepperoni |
| 54 | + case cheese |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +In the standard "non-resilient" mode, users of the library can write exhaustive switch |
| 59 | +statements over the enum `PizzaFlavor`: |
| 60 | + |
| 61 | +```swift |
| 62 | +switch pizzaFlavor { |
| 63 | +case .hawaiian: |
| 64 | + throw BadFlavorError() |
| 65 | +case .pepperoni: |
| 66 | + try validateNoVegetariansEating() |
| 67 | + return .delicious |
| 68 | +case .cheese: |
| 69 | + return .delicious |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +Swift requires switches to be exhaustive i.e. the must handle every possibility. |
| 74 | +If the author of the above switch statement was missing a case (perhaps they forgot |
| 75 | +`.hawaiian` is a flavor), the compiler will error, and force the user to either add a |
| 76 | +`default:` clause, or to add the missing case. |
| 77 | + |
| 78 | +If later a new case is added to the enum (maybe `.veggieSupreme`), exhaustive switches |
| 79 | +over that enum might no longer be exhaustive. This is often _desirable_ within a single |
| 80 | +codebase (even one split up into multiple modules). A case is added, and the compiler will |
| 81 | +assist in finding all the places where this new case must be handled. |
| 82 | + |
| 83 | +But it presents a problem for authors of both resilient and non-resilient libraries: |
| 84 | + |
| 85 | +- For non-resilient libraries, adding a case is a source-breaking API change: clients |
| 86 | +exhaustively switching over the enum will no longer compile. So can only be done with |
| 87 | +a major semantic version bump. |
| 88 | +- For resilient libraries, even that is not an option. An ABI-stable library cannot allow |
| 89 | +a situation where a binary that has not yet been recompiled can no longer rely on its |
| 90 | +switches over an enum are exhaustive. |
| 91 | + |
| 92 | +Because of the implications on ABI and the requirement to be able to evolve |
| 93 | +libraries with public enumerations in their API, the resilient language dialect introduced |
| 94 | +"non-exhaustive enums" in [SE-0192](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0192-non-exhaustive-enums.md). |
| 95 | + |
| 96 | +If the library was compiled with `-enable-library-evolution`, when a user attempts to |
| 97 | +exhaustively switch over the `PizzaFlavor` enum the compiler will emit an error |
| 98 | +(when in Swift 6 language mode, a warning in prior language modes), requiring users |
| 99 | +to add an `@unknown default:` clause: |
| 100 | + |
| 101 | +```swift |
| 102 | +switch pizzaFlavor { |
| 103 | +case .hawaiian: |
| 104 | + throw BadFlavorError() |
| 105 | +case .pepperoni: |
| 106 | + try validateNoVegetariansEating() |
| 107 | + return .delicious |
| 108 | +case .cheese: |
| 109 | + return .delicious |
| 110 | +@unknown default: |
| 111 | + try validateNoVegetariansEating() |
| 112 | + return .delicious |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +The user is forced to specify how cases are handled if they are introduced later. This |
| 117 | +allows ABI-stable libraries to add cases without risking undefined behavior in client |
| 118 | +binaries that haven't yet been recompiled. |
| 119 | + |
| 120 | +When a resilient library knows that an enumeration will never be extended, the author |
| 121 | +can annotate the enum with `@frozen`, which in the case of enums is a guarantee that no |
| 122 | +further cases can be added. For example, the `Optional` type in the standard library is |
| 123 | +frozen, as no third option beyond `some` and `none` will ever be added. This brings |
| 124 | +performance benefits, and also the convenience of not requiring an `@unknown default` case. |
| 125 | + |
| 126 | +`@frozen` is a powerful attribute that can be applied to both structs and enums. It has a |
| 127 | +wide ranging number of effects, including exposing their size directly as part of the ABI |
| 128 | +and providing direct access to stored properties. However, on enums it happens to |
| 129 | +have source-level effects on the behavior of switch statements by clients of a library. |
| 130 | +This difference was introduced late in the process of reviewing SE-0192. |
| 131 | + |
| 132 | +Extensibility of enums is also desirable for non-resilient libraries. Without it, there is no |
| 133 | +way for a Swift package to be able to evolve a public enumeration without breaking the API. |
| 134 | +However, in Swift today it is not possible for the default, "non-resilient" dialect to opt-in |
| 135 | +to the extensible enumeration behavior. This is a substantial limitation, and greatly reduces |
| 136 | +the utility of enumerations in non-resilient Swift. |
| 137 | + |
| 138 | +Over the past years, many packages have run into this limitation when trying to express APIs |
| 139 | +using enums. As a non-exhaustive list of problems this can cause: |
| 140 | + |
| 141 | +- Using enumerations to represent `Error`s is inadvisable, as if new errors need |
| 142 | + to be introduced they cannot be added to existing enumerations. This leads to |
| 143 | + a proliferation of `Error` enumerations. "Fake" enumerations can be made using |
| 144 | + `struct`s and `static let`s, but these do not work with the nice `Error` |
| 145 | + pattern-match logic in catch blocks, requiring type casts. |
| 146 | +- Using an enumeration to refer to a group of possible ideas without entirely |
| 147 | + exhaustively evaluating the set is potentially dangerous, requiring a |
| 148 | + deprecate-and-replace if any new elements appear. |
| 149 | +- Using an enumeration to represent any concept that is inherently extensible is |
| 150 | + tricky. For example, `SwiftNIO` uses an enumeration to represent HTTP status |
| 151 | + codes. If new status codes are added, SwiftNIO needs to either mint new |
| 152 | + enumerations and do a deprecate-and-replace, or it needs to force these new |
| 153 | + status codes through the .custom enum case. |
| 154 | + |
| 155 | +This proposal plans to address these limitations on enumerations in |
| 156 | +non-resilient Swift. |
| 157 | + |
| 158 | +## Proposed solution |
| 159 | + |
| 160 | +We propose to introduce a new `@extensible` attribute that can be applied to |
| 161 | +enumerations to mark them as extensible. Such enums will behave the same way as |
| 162 | +non-frozen enums from resilient Swift libraries. |
| 163 | + |
| 164 | +An example of using the new attribute is below: |
| 165 | + |
| 166 | +```swift |
| 167 | +/// Module A |
| 168 | +@extensible |
| 169 | +public enum PizzaFlavor { |
| 170 | + case hawaiian |
| 171 | + case pepperoni |
| 172 | + case cheese |
| 173 | +} |
| 174 | + |
| 175 | +/// Module B |
| 176 | +switch pizzaFlavor { // error: Switch covers known cases, but 'MyEnum' may have additional unknown values, possibly added in future versions |
| 177 | +case .hawaiian: |
| 178 | + throw BadFlavorError() |
| 179 | +case .pepperoni: |
| 180 | + try validateNoVegetariansEating() |
| 181 | + return .delicious |
| 182 | +case .cheese: |
| 183 | + return .delicious |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +### Exhaustive switching inside same module/package |
| 188 | + |
| 189 | +Code inside the same module or package can be thought of as one co-developed |
| 190 | +unit of code. Inside the same module or package, switching exhaustively over an |
| 191 | +`@extensible` enum inside will not require an`@unknown default`, and using |
| 192 | +one will generate a warning. |
| 193 | + |
| 194 | +### `@extensible` and `@frozen` |
| 195 | + |
| 196 | +An enum cannot be `@frozen` and `@extensible` at the same time. Thus, marking an |
| 197 | +enum both `@extensible` and `@frozen` is not allowed and will result in a |
| 198 | +compiler error. |
| 199 | + |
| 200 | +### API breaking checker |
| 201 | + |
| 202 | +The behavior of `swift package diagnose-api-breaking-changes` is also updated |
| 203 | +to understand the new `@extensible` attribute. |
| 204 | + |
| 205 | +### Staging in using `@preEnumExtensibility` |
| 206 | + |
| 207 | +We also propose adding a new `@preEnumExtensibility` attribute that can be used |
| 208 | +to mark enumerations as pre-existing to the `@extensible` attribute. This allows |
| 209 | +developers to mark existing public enumerations as `@preEnumExtensibility` in |
| 210 | +addition to `@extensible`. This is useful for developers that want to stage in |
| 211 | +changing an existing non-extensible enum to be extensible over multiple |
| 212 | +releases. Below is an example of how this can be used: |
| 213 | + |
| 214 | +```swift |
| 215 | +// Package A |
| 216 | +public enum Foo { |
| 217 | + case foo |
| 218 | +} |
| 219 | + |
| 220 | +// Package B |
| 221 | +switch foo { |
| 222 | +case .foo: break |
| 223 | +} |
| 224 | + |
| 225 | +// Package A wants to make the existing enum extensible |
| 226 | +@preEnumExtensibility @extensible |
| 227 | +public enum Foo { |
| 228 | + case foo |
| 229 | +} |
| 230 | + |
| 231 | +// Package B now emits a warning downgraded from an error |
| 232 | +switch foo { // warning: Enum might be extended later. Add an @unknown default case. |
| 233 | +case .foo: break |
| 234 | +} |
| 235 | + |
| 236 | +// Later Package A decides to extend the enum and releases a new major version |
| 237 | +@preEnumExtensibility @extensible |
| 238 | +public enum Foo { |
| 239 | + case foo |
| 240 | + case bar |
| 241 | +} |
| 242 | + |
| 243 | +// Package B didn't add the @unknown default case yet. So now we we emit a warning and an error |
| 244 | +switch foo { // error: Unhandled case bar & warning: Enum might be extended later. Add an @unknown default case. |
| 245 | +case .foo: break |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +While the `@preEnumExtensibility` attribute doesn't solve the need of requiring |
| 250 | +a new major when a new case is added it allows developers to stage in changing |
| 251 | +an existing non-extensible enum to become extensible in a future release by |
| 252 | +surfacing a warning about this upcoming break early. |
| 253 | + |
| 254 | +## Source compatibility |
| 255 | + |
| 256 | +### Resilient modules |
| 257 | + |
| 258 | +- Adding or removing the `@extensible` attribute has no-effect since it is the default in this language dialect. |
| 259 | +- Adding the `@preEnumExtensibility` attribute has no-effect since it only downgrades the error to a warning. |
| 260 | +- Removing the `@preEnumExtensibility` attribute is an API breaking since it upgrades the warning to an error again. |
| 261 | + |
| 262 | +### Non-resilient modules |
| 263 | + |
| 264 | +- Adding the `@extensible` attribute is an API breaking change. |
| 265 | +- Removing the `@extensible` attribute is an API stable change. |
| 266 | +- Adding the `@preEnumExtensibility` attribute has no-effect since it only downgrades the error to a warning. |
| 267 | +- Removing the `@preEnumExtensibility` attribute is an API breaking since it upgrades the warning to an error again. |
| 268 | + |
| 269 | +## ABI compatibility |
| 270 | + |
| 271 | +The new attribute does not affect the ABI of an enum since it is already the |
| 272 | +default in resilient modules. |
| 273 | + |
| 274 | +## Future directions |
| 275 | + |
| 276 | +### Aligning the language dialects |
| 277 | + |
| 278 | +In a previous iteration of this proposal, we proposed to add a new language |
| 279 | +feature to align the language dialects in a future language mode. The main |
| 280 | +motivation behind this is that the current default of non-extensible enums is a |
| 281 | +common pitfall and results in tremendous amounts of unnoticed API breaks in the |
| 282 | +Swift package ecosystem. We still believe that a future proposal should try |
| 283 | +aligning the language dialects. This proposal is focused on providing a first |
| 284 | +step to allow extensible enums in non-resilient modules. |
| 285 | + |
| 286 | +Regardless of whether a future language mode changes the default for non-resilient |
| 287 | +libraries, a way of staging in this change will be required (similar to how the |
| 288 | +`@preconcurency` attribute facilitated incremental adoption of Swift concurrency). |
| 289 | + |
| 290 | +### `@unknown catch` |
| 291 | + |
| 292 | +Enums can be used for errors. Catching and pattern matching enums could add |
| 293 | +support for an `@unknown catch` to make pattern matching of typed throws align |
| 294 | +with `switch` pattern matching. |
| 295 | + |
| 296 | +### Allow adding additional associated values |
| 297 | + |
| 298 | +Adding additional associated values to an enum can also be seen as extending it |
| 299 | +and we agree that this is interesting to explore in the future. However, this |
| 300 | +proposal focuses on solving the primary problem of the usability of public |
| 301 | +enumerations in non-resilient modules. |
| 302 | + |
| 303 | +### Larger compilation units than packages |
| 304 | + |
| 305 | +During the pitch it was brought up that a common pattern for application |
| 306 | +developers is to split an application into multiple smaller packages. Those |
| 307 | +packages are versioned together and want to have the same exhaustive matching |
| 308 | +behavior as code within a single package. As a future direction, build and |
| 309 | +package tooling could allow to define larger compilation units to express this. |
| 310 | +Until then developers are encouraged to use `@frozen` attributes on their |
| 311 | +enumerations to achieve the same effect. |
| 312 | + |
| 313 | +## Alternatives considered |
| 314 | + |
| 315 | +### Different names for the attribute |
| 316 | + |
| 317 | +We considered different names for the attribute such as `@nonFrozen`; however, |
| 318 | +we felt that `@extensible` communicates the idea of an extensible enum more |
| 319 | +clearly. |
0 commit comments