SE-0487: Extensible Enums

Hi Swift community,

The review of SE-0487: Extensible Enums begins now and runs through June 5th, 2025.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you,

Ben Cohen
Review Manager

18 Likes

Great to offer this functionality for projects that need it. I like that it will be opt-in with an attribute rather than changing the default behavior.

3 Likes

I like this proposal. It covers the bases.
Extendable enums to me means there is a method to use enums for states, and to have the states themselves be extensible, but centrally managed without a more complex and harder to read system based on String tags. It could be easily picked up by the language server as well.

I read the review pretty carefully, so now I want Hawaiian pizza.

+1

1 Like

I'm very pleased to see this (hopefully first of two or more parts) to address the discrepancy in enum extensibility between language modes.

I want to comment on the proposed design of @preEnumExtensibility. While inspired by @preconcurrency, it has the key difference of modifying (and specifically, modifying the diagnostics associated with) another attribute. This leads to redundancy at the declaration site (@preEnumExtensibility @extensible ...), and to me feels suboptimal for the additional reason that it's not really two independent, co-equal attributes of the type.

By contrast, we have an extensive and active precedent of adopting parenthesized variants of attributes and keywords particularly for diagnostics changes and for transitional staging-in. I'm thinking of nonisolated(nonsending), for example. I wonder if such an approach would work better here—maybe something like:

  • @extensible(preexisting) — adapting the language in the proposal used to describe the types that may want to adopt the double attribute (i.e., pre-existing enums that were intended to be extensible but could not express such a behavior)
  • @extensible(retroactive) — using an existing term that users are already familiar with; this has a drawback in that, if a library like Foundation that's both resilient and non-resilient adopts this attribute on a pre-existing type, the type was extensible all along in one of the two modes and isn't "retroactively" made so (although the same critique could be made of the proposed spelling, as such a type also isn't "pre–enum extensibility" in that mode)
  • @extensible(warn) or @extensible(warning) — using the "tell me what it actually does" approach to naming (i.e., that it downgrades an error that would otherwise arise because of @extensible to a warning)
16 Likes

+1

As someone who maintains a number of libraries this will be a godsend.

Converting public enums to structs with an internal enum has always been a half solution and never worked with associated values.

I'm not certain about the naming of @preEnumExtensibility or even its use. Does the language need an attribute saying I may change this enum in the next major version of a library? Marking an enum as extensible but never extending isn't very useful in itself.

1 Like

Perhaps indicative of the need for a better spelling for this: this attribute (IIUC) has no behavior on its own [except, as an edge case, in the case of already-extensible resilient library enums]—it simply downgrades hard errors to warnings for users of @extensible enums when they don't include @unknown default in exhaustive switches.

1 Like

This is a great addition for libraries and it’s nice to see this come to review. I just have two comments:

  1. I’d like enum extensibility to be tied to access control, allowing for things like having an enum that’s frozen within fileprivate scope, but extensible beyond that. The “Larger compilation units than packages” future direction may benefit from enum extensibility being tied to access control.

  2. We shouldn’t use the term “extensible” for something that’s unrelated to extension. It’s too confusing: why are you allowed to declare an extension of a non-extensible enum? Personally, I think @nonFrozen is much more clear about its nature and relation to @frozen. Other possibilities include @unfrozen, @thawed, or @living.

2 Likes

The raison d'ĂŞtre of this proposal is to unify behavior of extensible enums in resilient and non-resilient libraries. Given that we already have extensible enums in resilient libraries and they behave in a particular way, the only way to achieve this goal is to make the explicit attribute adopt the same behavior.

If we invent a new, different behavior for explicitly-marked @extensible enums, then we would be back to where we started: lacking a spelling for non-resilient libraries to opt into the behavior that existing extensible enums currently have.

1 Like

I’m not sure you understand what I’m proposing.

From the proposal:

Exhaustive switching inside same module/package

Code inside the same module or package can be thought of as one co-developed unit of code. Inside the same module or package, switching exhaustively over an @extensible enum inside will not require an@unknown default, and using one will generate a warning.

What I’m proposing is that you should be able to declare that you want exhaustive switching at access control scopes other than package. If you declare an enum like:

@extensible(beyond: fileprivate)
enum AnimalKind {
  case cat, dog, bird
}

Then you’ll be able to exhaustively switch over an instance of AnimalKind within the same file as its declaration. But in all other files, you’ll have to include a default case.

This way, you can isolate within a single file the code that requires modification if a new case is added to AnimalKind. Code outside the file would be required to handle unknown cases, reducing the impact of adding a new case to AnimalKind.

We wouldn’t lack a spelling for non-resilient libraries to opt into the behavior that resilient non-frozen enums currently have, since @extensible(beyond: package) would be equivalent to what’s proposed here. @extensible with no arguments could be an alias of @extensible(beyond: package), and none of the behavior described in this proposal would change.

In the future, if we added an access control modifier that could refer to larger compilation units than the current package, then that would fulfill the “Larger compilation units than packages” future direction.

I hope this clears things up! :)

2 Likes

Another alternative to @extensible would be @nonExhaustive, which is in line with Swift’s existing nomenclature.

It would work especially nicely with my proposed access control addition. The meaning of @nonExhaustive(beyond: fileprivate) seems very clear to me.

4 Likes

Ah, got it—you're talking about a future direction rather than a modification to this proposal. Thanks for clarifying!

1 Like

I think @nonFrozen or @nonExhaustive are better names. When I saw extensible enums, I thought of Apple’s CF/NS_TYPED_EXTENSIBLE_ENUM, which is a macro for __attribute__((swift_wrapper(struct))). C enums with this attribute map to a Swift struct with static constants for common “cases.” These are extensible in the sense that additional “cases” can be added by other modules.

If I’ve understood correctly, extensible enums aren’t extensible from the consumer’s perspective. This is just a hint that additional cases may be added in the future, and thus exhaustiveness checking should be done differently. That suggests to me that a different name would be better.

4 Likes

I agree, @extensible looks like somethings related to extensions. Since there's already the frozen term related to this I don't see the need to introduce this clash of meaning. I like the options @nonfrozen or @unfrozen.

2 Likes

I really like the proposal. My only concern is the use of @extensible. I don't dislike the name at all, but to echo a concern that others have raised, it seems likely that we might one day want to add built-in sugar (perhaps via macros) for the struct-based approach to extensible enums, e.g.

@Extensible
enum Foo: Int {
  case bar = 1
  case baz = 2
}

// would desugar to
struct Foo: RawRepresentable {
  var rawValue: Int

  static let bar = Foo(rawValue: 1)
  static let baz = Foo(rawValue: 2)
}

And while the attributes wouldn't actually clash due to differing case, it seems a bad idea to conflate these two concepts.

I can also see a future where Swift provided the capability for enum cases to be extended via extensions, e.g:

// Module 1
enum Foo {
  case bar
}

// Module 2
extension Foo {
  case baz // only usable in module 2 but must be guarded against in module 1
}

And such a feature would presumably also required an opt-in attribute with a name synonymous to @extensibleat the point of declaration (since supporting it would require adding @unknown defaults in the declaring module), rendering this proposed usage retrospectively ambiguous.

4 Likes

I've never really understood the need for frozen enums apart from system provided frameworks. I've never come across a case where a dependency is updated but the app/software hasn't been rebuilt and any necessary code changes made. Is there use case I'm missing? I wouldn't even know how to use SPM say to update a dependency and not have to rebuild the app/tool/project

1 Like

Seems to me the best way to express things would be: @unfrozen(nextMajor).

@extensible does not sound related to frozen, and I think that's a problem. I don't really understand why we need a different name for the same concept in reverse. And @preEnumExtensibility doesn't really tell you anything you should know about the declaration: that it will become non-frozen in the next major version.

If the goal is to unfreeze the enum at the next major version, just say it like it is.

3 Likes

It's worth mentioning Rust's attribute naming here for precedent #[non_exhaustive]: Type System - The Rust Reference

Generally speaking +1 as this solves a huge pain point for Swift packages, but I have a question regarding performance.

We propose to introduce a new @extensible attribute that can be applied to enumerations to mark them as extensible. Such enums will behave the same way as non-frozen enums from resilient Swift libraries.

Does this mean @extensible enums will inhibit the same performance behaviors as non-frozen enums in resilient libraries? Because if so, that is extremely disappointing because the performance cliff from using non-frozen enums in resilient libraries is so abysmal it's almost not worth using them instead of hand rolled structs. Extensible enums should be able to communicate their layout information to outside modules because the issue in adding/removing cases would require a complete recompile of downstream dependents because of new source changes in the extensible enum right?

2 Likes

No this statement was intended to convey that they behave in same way with regards to exhaustive switching. It isn’t expected that there are any performance differences between non-extensible and extensible enums in non-resilient libraries.

2 Likes