From 89066d91f5bbe3a2dc86e43a07b2ec5fc405df7d Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sun, 2 Feb 2025 22:41:16 +0100 Subject: [PATCH 01/18] Extensible enums --- proposals/NNNN-extensible-enums.md | 185 +++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 proposals/NNNN-extensible-enums.md diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md new file mode 100644 index 0000000000..f5bcdd00cb --- /dev/null +++ b/proposals/NNNN-extensible-enums.md @@ -0,0 +1,185 @@ +# Extensible enums + +* Proposal: [SE-NNNN](NNNN-extensible-enums.md) +* Authors: [Cory Benfield](https://github.com/lukasa), [Pavel Yaskevich](https://github.com/xedin), [Franz Busch](https://github.com/FranzBusch) +* 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 pitch 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 dialetcs. This will make 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 exisiting +`@frozen` attribute to mark an enumeration as non-extensible. Allowing consumers +of the module to exhaustively switch over the cases. This makes commiting 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`. + +Since enabling a language feature applies to the whole module at once we also +propose adding a new attribute `@extensible` analogous to `@frozen`. This +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`. + +## 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. + +## 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 it must be gated behind a new language mode. \ No newline at end of file From b4f3c6870bea180ea64a696b8a872b9db6b6e549 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Mon, 3 Feb 2025 12:33:23 +0100 Subject: [PATCH 02/18] Review --- proposals/NNNN-extensible-enums.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index f5bcdd00cb..e45232b838 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -1,7 +1,7 @@ # Extensible enums * Proposal: [SE-NNNN](NNNN-extensible-enums.md) -* Authors: [Cory Benfield](https://github.com/lukasa), [Pavel Yaskevich](https://github.com/xedin), [Franz Busch](https://github.com/FranzBusch) +* 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) @@ -26,7 +26,7 @@ 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 pitch would aim to address it. +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 @@ -136,13 +136,13 @@ non-resilient Swift. ## Proposed solution We propose to introduce a new language feature `ExtensibleEnums` that aligns the -behaviour of enumerations in both language dialetcs. This will make enumerations +behaviour of enumerations in both language dialects. This will make 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 exisiting -`@frozen` attribute to mark an enumeration as non-extensible. Allowing consumers -of the module to exhaustively switch over the cases. This makes commiting to the +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 @@ -156,6 +156,13 @@ 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` module 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 From 6fefb2996b9be57da54993a3a30c5fb222d35ce7 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 4 Feb 2025 14:18:44 +0100 Subject: [PATCH 03/18] Reviews and same module/package enums --- proposals/NNNN-extensible-enums.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index e45232b838..b3bf1142f3 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -136,9 +136,9 @@ 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 enumerations -in packages a safe default and leave maintainers the choice of extending them -later on. +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 @@ -147,7 +147,10 @@ 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`. +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. Since enabling a language feature applies to the whole module at once we also propose adding a new attribute `@extensible` analogous to `@frozen`. This @@ -156,9 +159,9 @@ 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` module 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 +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. @@ -189,4 +192,4 @@ This proposal only affects API resilience of non-resilient libraries, by enablin 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 it must be gated behind a new language mode. \ No newline at end of file +source breaking change it must be gated behind a new language mode. \ No newline at end of file From b429d9227b47c31a09c3731843afeba2fed71109 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 4 Feb 2025 14:54:57 +0100 Subject: [PATCH 04/18] Clarify API impact of `@extensible` and add alternatives considered section --- proposals/NNNN-extensible-enums.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index b3bf1142f3..1510576b75 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -176,6 +176,9 @@ 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. ## Effect on ABI stability @@ -192,4 +195,14 @@ This proposal only affects API resilience of non-resilient libraries, by enablin 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. \ No newline at end of file +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. \ No newline at end of file From f718b7dddea1058c8ae3403d0d69c6192b9c66f3 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 4 Feb 2025 17:16:23 +0100 Subject: [PATCH 05/18] More review comments --- proposals/NNNN-extensible-enums.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index 1510576b75..8a209dce5b 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -59,6 +59,7 @@ case .hawaiian: throw BadFlavorError() case .pepperoni: try validateNoVegetariansEating() + return .delicious case .cheese: return .delicious } @@ -149,8 +150,8 @@ 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. +the same modules of the declaring package switches are still required to be +exhaustive and don't require an `@unknown default:` case. Since enabling a language feature applies to the whole module at once we also propose adding a new attribute `@extensible` analogous to `@frozen`. This @@ -166,6 +167,9 @@ right default choice in both resilient and non-resilient modules and the new proposed `@extensible` attribute primiarly exists to give developers a migration path. +In non-resilient modules, adding the `@extensible` attribute to non-public enums +will produce a warning since those enums can only be matched exhaustively. + ## Source compatibility Enabling the language feature `ExtensibleEnums` in a module that contains public @@ -176,9 +180,9 @@ 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. +Adding an `@extensible` annotation to an exisitng public enum is a source +breaking change in modules that have **not** enabled the `ExtensibleEnums` +language features or are compiled with resiliency. ## Effect on ABI stability From 0735a82b82caea758a649d8a9eec54792a36b31b Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 5 Feb 2025 15:13:58 +0100 Subject: [PATCH 06/18] Pitch feedback --- proposals/NNNN-extensible-enums.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index 8a209dce5b..5a5bbadc1c 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -139,7 +139,8 @@ non-resilient Swift. 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. +extending them later on. We also propose to enable this new language feature +by default with the next lagnuage mode. In modules with the language feature enabled, developers can use the existing `@frozen` attribute to mark an enumeration as non-extensible, allowing consumers @@ -194,12 +195,12 @@ This proposal only affects API resilience of non-resilient libraries, by enablin ## Future directions -### Enable `ExtensibleEnums` by default in a future language mode +### `@unkown case` + +Enums can be used for errors. Catching and pattern matching enums could add +support for an `@unknown catch` to make pattern matching of typed throws align +with `switch` pattern matching. -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 From 6a0fc9752f056a6c7f448c80217d8e07c3d137c0 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 5 Feb 2025 17:21:08 +0100 Subject: [PATCH 07/18] Pitch feedback: Adding migration path and expand on implications inside the same package --- proposals/NNNN-extensible-enums.md | 165 ++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 49 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index 5a5bbadc1c..7ec028448f 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -9,6 +9,16 @@ * Upcoming Feature Flag: `ExtensibleEnums` * Review: ([pitch](https://forums.swift.org/...)) +Previously pitched in: +- https://forums.swift.org/t/extensible-enumerations-for-non-resilient-libraries/35900 +- https://forums.swift.org/t/pitch-non-frozen-enumerations/68373 + +> **Differences to previous proposals** + +> This proposal expands on the previous proposals and incorperates the language +> steering groups feedback of exploring language features to solve the +> motivating problem. It also provides a migration path for existing modules. + ## Introduction This proposal addresses the long standing behavioural difference of `enum`s in @@ -139,59 +149,107 @@ non-resilient Swift. 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. We also propose to enable this new language feature -by default with the next lagnuage mode. - -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 are still required to be -exhaustive and don't require an `@unknown default:` case. - -Since enabling a language feature applies to the whole module at once we also -propose adding a new attribute `@extensible` analogous to `@frozen`. This -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. - -In non-resilient modules, adding the `@extensible` attribute to non-public enums -will produce a warning since those enums can only be matched exhaustively. +extending them later on. We also propose to enable this new language feature by +default with the next lagnuage mode. -## Source compatibility +We also propose to introduce two new attributes. +- `@nonExtensible`: For marking an enumeration as not extensible. +- `@extensible`: For marking an enumeration as extensible. -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 to an exisitng public enum is a source -breaking change in modules that have **not** enabled the `ExtensibleEnums` -language features or are compiled with resiliency. +Modules consuming other modules with the language feature enabled will be +required to add an `@unknown default:` case to any switch state for enumerations +that are not marked with `@nonExtensible`. + +An example of using the language feature and the keywords is below: + +```swift +/// Module A +@extensible // or language feature ExtensibleEnums is enabled +enum MyEnum { + case foo + case bar +} + +@nonExtensible +enum MyFinalEnum { + case justMe +} + +/// Module B +switch myEnum { // error: Switch covers known cases, but 'MyEnum' may have additional unknown values, possibly added in future versions + case .foo: break + case .bar: break +} + +// The below produces no warnings since the enum is marked as nonExtensible +switch myFinalEnum { + case .justMe: break +} +``` -## Effect on ABI stability +## Detailed design -This attribute does not affect the ABI, as it is a no-op when used in a resilient library. +### Migration path -## Effect on API resilience +The proposed new language feature is the first langauge feature that has impact +on the consumers of a module and not the module itself. Enabling the langauge +feature in a non-resilient module with public enumerations is a source breaking +change. -This proposal only affects API resilience of non-resilient libraries, by enabling more changes to be made without API breakage. +The two proposed annotations `@extensible/@nonExtensible` give developers tools +to opt-in to the new language feature or in the future language mode without +breaking their consumers. This paves a path for a gradual migration. Developers +can mark all of their exisiting public enumerations as `@nonExtensible` and then +turn on the language feature. Similarly, developers can also mark new +enumerations as `@extensible` without turning on the language feature yet. + +In a future language mode, individual modules can still be opted in one at a +time into the new language mode and apply the annotations as needed to avoid +source breakages. + +When the language feature is turned on and a public enumeration is marked as +`@extensible` it will produce a warning that the annotation isn't required. + +In non-resilient modules without the language feature turned on, adding the +`@extensible` attribute to non-public enums will produce a warning since those +enums can only be matched exhaustively. + +### Implications on code in the same package + +Code inside the same package still needs to exhaustively switch over +enumerations defined in the same package. Switches over enums of the same +package containing an `@unknown default` will produce a compiler warning. + +### Impact on resilient modules & `@frozen` attribute + +Explicitly enabling the language feature in resilient modules will produce a +compiler warning since that is already the default behaviour. Using the +`@nonExtensible` annotation will lead to a compiler error since users of +resilient modules must use the `@frozen` attribute instead. + +Since some modules support compiling in resilient and non-resilient modes, +developers need a way to mark enums as non-extensible for both. `@nonExtensible` +produces an error when compiling with resiliency; hence, developers must use +`@frozen`. To make supporting both modes easier `@frozen` will also work in +non-resilient modules and make enumerations extensible. + +## Source compatibility + +- Enabling the language feature `ExtensibleEnums` in a module that contains +public enumerations is a source breaking change unless all existing public +enumerations are marked with `@nonExtensible` +- Adding an `@extensible` annotation to an exisitng public enum is a source +breaking change in modules that have **not** enabled the `ExtensibleEnums` +language features or are compiled with resiliency. +- Changing the annotation from `@nonExtensible/@frozen` to `@extensible` is a +source breaking change. +- Changing the annotation from `@extensible` to `@nonExtensible/@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. + +## ABI compatibility +The new attributes do not affect the ABI, as it is a no-op when used in a resilient library. ## Future directions @@ -201,7 +259,6 @@ Enums can be used for errors. Catching and pattern matching enums could add support for an `@unknown catch` to make pattern matching of typed throws align with `switch` pattern matching. - ## Alternatives considered ### Only provide the `@extensible` annotation @@ -210,4 +267,14 @@ 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. \ No newline at end of file +future Swift language mode. + +### Usign `@frozen` and introducing `@nonFrozen` + +We considered names such as `@nonFrozen` for `@extensible` and using `@frozen` +for `@nonExtensible`; however, we believe that _frozen_ is a concept that +includes more than exhaustive matching. It is heavily tied to resiliency and +also has ABI impact. That's why decoupled annotations that only focus on the +extensability is better suited. `@exhaustive/@nonExhaustive` would fit that bill +as well but we believe that `@extensible` better expresses the intention of the +author. From d3da8bb83a3356cade99c6b8ffe34064b4e4a8b3 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 5 Feb 2025 17:44:19 +0100 Subject: [PATCH 08/18] Fix small mistake --- proposals/NNNN-extensible-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index 7ec028448f..20f322e376 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -231,7 +231,7 @@ Since some modules support compiling in resilient and non-resilient modes, developers need a way to mark enums as non-extensible for both. `@nonExtensible` produces an error when compiling with resiliency; hence, developers must use `@frozen`. To make supporting both modes easier `@frozen` will also work in -non-resilient modules and make enumerations extensible. +non-resilient modules and make enumerations non extensible. ## Source compatibility From b367bb9fd6c1ea2c3833b9ba119903ecac5800c7 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 5 Feb 2025 17:47:56 +0100 Subject: [PATCH 09/18] Address adding associated values --- proposals/NNNN-extensible-enums.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index 20f322e376..ab67d59d8d 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -259,6 +259,13 @@ Enums can be used for errors. Catching and pattern matching enums could add support for an `@unknown catch` to make pattern matching of typed throws align with `switch` pattern matching. +### Allow adding additional associated values + +Adding additional associated values to an enum can also be seen as extending it +and we agree that this is interesting to explore in the future. However, this +proposal focuses on solving the primary problem of the unusability of public +enumerations in non-resilient modules. + ## Alternatives considered ### Only provide the `@extensible` annotation From e341481cde318715f3ef8283301660f2bcea5f7f Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 6 Feb 2025 13:22:28 +0100 Subject: [PATCH 10/18] Remove new annotations and re-use `@frozen` --- proposals/NNNN-extensible-enums.md | 248 +++++++++++++++++------------ 1 file changed, 150 insertions(+), 98 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index ab67d59d8d..d9748c90b9 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -15,13 +15,28 @@ Previously pitched in: > **Differences to previous proposals** -> This proposal expands on the previous proposals and incorperates the language +> This proposal expands on the previous proposals and incorporates the language > steering groups feedback of exploring language features to solve the -> motivating problem. It also provides a migration path for existing modules. +> motivating problem. It also reuses the existing `@frozen` and documents a +> migration path for existing modules. + +Revisions: +- Introduced a second annotation `@nonExtensible` to allow a migration path into + both directions +- Added future directions for adding additional associated values +- Removed both the `@extensible` and `@nonExtensible` annotation in favour of + re-using the existing `@frozen` annotation +- Added the high level goals that this proposal aims to achieve +- Expanded on the proposed migration path for packages with regards to their + willingness to break API +- Added future directions for exhaustive matching for larger compilation units +- Added alternatives considered section for a hypothetical + `@preEnumExtensibility` +- Added a section for `swift package diagnose-api-breaking-changes` ## Introduction -This proposal addresses the long standing behavioural difference of `enum`s in +This proposal addresses the long standing behavioral 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. @@ -29,7 +44,7 @@ Swift modules compiled with and without library evolution. This makes Swift 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 +support a movable scale between "maximally evolvable" 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 @@ -42,9 +57,9 @@ evolving SE-0192, and this proposal would aim to address it. 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. +behavior of switch statements. -Consider the following simple library to your favourite pizza place: +Consider the following simple library to your favorite pizza place: ```swift public enum PizzaFlavor { @@ -58,7 +73,7 @@ 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. +the behavior in switch statements changes. In the _standard_, "non-resilient" mode, users of the library can write exhaustive switch statements over the enum `PizzaFlavor`: @@ -78,7 +93,7 @@ case .cheese: 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 +behavior 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. @@ -117,10 +132,10 @@ 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. +exhaustive switching behavior 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 +dialect to opt-in to the extensible enumeration behavior. 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 @@ -146,114 +161,119 @@ non-resilient Swift. ## Proposed solution +With the following proposed solution we want to achieve the following goals: +1. Align the differences between the two language dialects in a future language + mode +2. Provide developers a path to opt-in to the new behavior before the new + language mode so they can start declaring **new** extensible enumerations +3. Provide a migration path to the new behavior without forcing new SemVer + majors + We propose to introduce a new language feature `ExtensibleEnums` that aligns the -behaviour of enumerations in both language dialects. This will make **public** +behavior 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. We also propose to enable this new language feature by -default with the next lagnuage mode. - -We also propose to introduce two new attributes. -- `@nonExtensible`: For marking an enumeration as not extensible. -- `@extensible`: For marking an enumeration as extensible. +extending them later on. This language feature will become enabled by default in +the next language mode. Modules consuming other modules with the language feature enabled will be -required to add an `@unknown default:` case to any switch state for enumerations -that are not marked with `@nonExtensible`. +required to add an `@unknown default:`. An example of using the language feature and the keywords is below: ```swift /// Module A -@extensible // or language feature ExtensibleEnums is enabled -enum MyEnum { - case foo - case bar -} - -@nonExtensible -enum MyFinalEnum { - case justMe +public enum PizzaFlavor { + case hawaiian + case pepperoni + case cheese } /// Module B -switch myEnum { // error: Switch covers known cases, but 'MyEnum' may have additional unknown values, possibly added in future versions - case .foo: break - case .bar: break -} - -// The below produces no warnings since the enum is marked as nonExtensible -switch myFinalEnum { - case .justMe: break +switch pizzaFlavor { // error: Switch covers known cases, but 'MyEnum' may have additional unknown values, possibly added in future versions +case .hawaiian: + throw BadFlavorError() +case .pepperoni: + try validateNoVegetariansEating() + return .delicious +case .cheese: + return .delicious } ``` -## Detailed design - -### Migration path +Additionally, we propose to re-use the existing `@frozen` annotation to allow +developers to mark enumerations as non-extensible in non-resilient modules +similar to how it works in resilient modules already. -The proposed new language feature is the first langauge feature that has impact -on the consumers of a module and not the module itself. Enabling the langauge -feature in a non-resilient module with public enumerations is a source breaking -change. - -The two proposed annotations `@extensible/@nonExtensible` give developers tools -to opt-in to the new language feature or in the future language mode without -breaking their consumers. This paves a path for a gradual migration. Developers -can mark all of their exisiting public enumerations as `@nonExtensible` and then -turn on the language feature. Similarly, developers can also mark new -enumerations as `@extensible` without turning on the language feature yet. - -In a future language mode, individual modules can still be opted in one at a -time into the new language mode and apply the annotations as needed to avoid -source breakages. +```swift +/// Module A +@frozen +public enum PizzaFlavor { + case hawaiian + case pepperoni + case cheese +} -When the language feature is turned on and a public enumeration is marked as -`@extensible` it will produce a warning that the annotation isn't required. +/// Module B +// The below doesn't require an `@unknown default` since PizzaFlavor is marked as frozen +switch pizzaFlavor { +case .hawaiian: + throw BadFlavorError() +case .pepperoni: + try validateNoVegetariansEating() + return .delicious +case .cheese: + return .delicious +} +``` -In non-resilient modules without the language feature turned on, adding the -`@extensible` attribute to non-public enums will produce a warning since those -enums can only be matched exhaustively. +Turning on the new language feature will be a semantically breaking change for +consumers of their module; hence, requiring a new SemVer major release of the +containing package. Some packages can release a new major and adopt the new +language feature right away; however, the ecosystem also contains packages that +try to avoid breaking API if at all possible. Such packages are often at the +very bottom of the dependency graph e.g. `swift-collections` or `swift-nio`. If +any of such packages releases a new major version it would effectively split the +ecosystem until all packages have adopted the new major. + +Packages that want to avoid breaking their API can use the new language feature +and the `@frozen` attribute in combination to unlock to possibility to declare +**new extensible** public enumerations but stay committed to the non-extensible +API of the already existing public enumerations. This is achieved by marking all +existing public enumerations with `@frozen` before turning on the language +feature. ### Implications on code in the same package Code inside the same package still needs to exhaustively switch over -enumerations defined in the same package. Switches over enums of the same -package containing an `@unknown default` will produce a compiler warning. - -### Impact on resilient modules & `@frozen` attribute +enumerations defined in the same package when the language feature is enabled. +Switches over enums of the same package containing an `@unknown default` will +produce a compiler warning. -Explicitly enabling the language feature in resilient modules will produce a -compiler warning since that is already the default behaviour. Using the -`@nonExtensible` annotation will lead to a compiler error since users of -resilient modules must use the `@frozen` attribute instead. +### API breaking checker -Since some modules support compiling in resilient and non-resilient modes, -developers need a way to mark enums as non-extensible for both. `@nonExtensible` -produces an error when compiling with resiliency; hence, developers must use -`@frozen`. To make supporting both modes easier `@frozen` will also work in -non-resilient modules and make enumerations non extensible. +The behavior of `swift package diagnose-api-breaking-changes` is also updated +to understand if the language feature is enabled and only diagnose new enum +cases as a breaking change in non-frozen enumerations. ## Source compatibility -- Enabling the language feature `ExtensibleEnums` in a module that contains -public enumerations is a source breaking change unless all existing public -enumerations are marked with `@nonExtensible` -- Adding an `@extensible` annotation to an exisitng public enum is a source -breaking change in modules that have **not** enabled the `ExtensibleEnums` -language features or are compiled with resiliency. -- Changing the annotation from `@nonExtensible/@frozen` to `@extensible` is a -source breaking change. -- Changing the annotation from `@extensible` to `@nonExtensible/@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. +- Enabling the language feature `ExtensibleEnums` in a module compiled without +resiliency that contains public enumerations is a source breaking change unless +all existing public enumerations are marked with `@frozen` +- Disabling the language feature `ExtensibleEnums` in a module compiled without +resiliency is a source compatible change since it implicitly marks all +enumerations as `@frozen` +- Adding a `@frozen` annotation to an existing public enumeration is a source + compatible change ## ABI compatibility -The new attributes do not affect the ABI, as it is a no-op when used in a resilient library. + +The new language feature dos not affect the ABI, as it is already how modules +compiled with resiliency behave. ## Future directions -### `@unkown case` +### `@unknown case` Enums can be used for errors. Catching and pattern matching enums could add support for an `@unknown catch` to make pattern matching of typed throws align @@ -263,25 +283,57 @@ with `switch` pattern matching. Adding additional associated values to an enum can also be seen as extending it and we agree that this is interesting to explore in the future. However, this -proposal focuses on solving the primary problem of the unusability of public +proposal focuses on solving the primary problem of the usability of public enumerations in non-resilient modules. +### Larger compilation units than packages + +During the pitch it was brought up that a common pattern for application +developers is to split an application into multiple smaller packages. Those +packages are versioned together and want to have the same exhaustive matching +behavior as code within a single package. As a future direction, build and +package tooling could allow to define larger compilation units to express this. +Until then developers are encouraged to use `@frozen` attributes on their +enumerations to achieve the same effect. + +### Swift PM allowing multiple conflicting major versions in a single dependency graph + +To reduce the impact of an API break on the larger ecosystem Swift PM could +allow multiple conflicting major versions of the same dependency in a single +dependency graph. This would allow a package to adopt the new language feature, +break their existing, and release a new major while having minimal impact on +the larger ecosystem. + ## Alternatives considered -### Only provide the `@extensible` annotation +### Provide an `@extensible` annotation -We believe that the default behaviour in both language dialects should be that +We believe that the default behavior 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. -### Usign `@frozen` and introducing `@nonFrozen` - -We considered names such as `@nonFrozen` for `@extensible` and using `@frozen` -for `@nonExtensible`; however, we believe that _frozen_ is a concept that -includes more than exhaustive matching. It is heavily tied to resiliency and -also has ABI impact. That's why decoupled annotations that only focus on the -extensability is better suited. `@exhaustive/@nonExhaustive` would fit that bill -as well but we believe that `@extensible` better expresses the intention of the -author. +### Introducing a new annotation instead of using `@frozen` + +An initial pitch proposed an new annotation instead of using `@frozen. The +problem with that approach was coming up with a reasonable behavior of how the +new annotation works in resilient modules and what the difference to `@frozen` +is. Feedback during this and previous pitches was that `@frozen` has more +implications than just the non-extensibility of enumerations but also impact on +ABI. We understand the feedback but still believe it is better to re-use the +same annotation and clearly document the additional behavior when used in +resilient modules. + +### Introduce a `@preEnumExtensibility` annotation + +We considered introducing an annotation that allows developers to mark +enumerations as pre-existing to the new language feature similar to how +`@preconcurrency` works. The problem with such an annotation is how the compiler +would handle this in consuming modules. It could either downgrade the warning +for the missing `@unknown default` case or implicitly synthesize one. However, +the only reasonable behavior for synthesized `@unknown default` case is to +`fatalError`. Furthermore, such an attribute becomes even more problematic to +handle when the module then extends the annotated enum; thus, making it possible +to hit the `@unknown default` case during runtime leading to potentially hitting +the `fatalError`. \ No newline at end of file From 010feb1d000662032548cfaa3986c45e69f75e7e Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 8 Mar 2025 11:46:37 +0100 Subject: [PATCH 11/18] Update metadata with implementation and pitch --- proposals/NNNN-extensible-enums.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index d9748c90b9..8e848f3d43 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -5,9 +5,9 @@ * 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) +* Implementation: [apple/swift#79580](https://github.com/swiftlang/swift/pull/79580) * Upcoming Feature Flag: `ExtensibleEnums` -* Review: ([pitch](https://forums.swift.org/...)) +* Review: ([pitch](https://forums.swift.org/t/pitch-extensible-enums-for-non-resilient-modules/77649)) Previously pitched in: - https://forums.swift.org/t/extensible-enumerations-for-non-resilient-libraries/35900 From 8268dd9f1e6211d1e9fcfe7f59ff5bf8d05eaf20 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 8 Mar 2025 11:54:56 +0100 Subject: [PATCH 12/18] Small fixups --- proposals/NNNN-extensible-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index 8e848f3d43..05c730e59f 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -316,7 +316,7 @@ future Swift language mode. ### Introducing a new annotation instead of using `@frozen` -An initial pitch proposed an new annotation instead of using `@frozen. The +An initial pitch proposed a new annotation instead of using `@frozen`. The problem with that approach was coming up with a reasonable behavior of how the new annotation works in resilient modules and what the difference to `@frozen` is. Feedback during this and previous pitches was that `@frozen` has more From 5f8e7776d7c6d0fce678433d24593f1c1050332b Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 13 Mar 2025 09:11:35 +0100 Subject: [PATCH 13/18] Add migration paths --- proposals/NNNN-extensible-enums.md | 168 +++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 8 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index 05c730e59f..1b61d683ad 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -168,6 +168,7 @@ With the following proposed solution we want to achieve the following goals: language mode so they can start declaring **new** extensible enumerations 3. Provide a migration path to the new behavior without forcing new SemVer majors +4. Provide tools for developers to treat dependencies as source stable We propose to introduce a new language feature `ExtensibleEnums` that aligns the behavior of enumerations in both language dialects. This will make **public** @@ -255,6 +256,114 @@ The behavior of `swift package diagnose-api-breaking-changes` is also updated to understand if the language feature is enabled and only diagnose new enum cases as a breaking change in non-frozen enumerations. +### Migration paths + +The following section is outlining the migration paths and tools we propose to +provide for different kinds of projects to adopt the proposed feature. The goal +is to reduce churn across the ecosystem while still allowing us to align the +default behavior of enums. There are many scenarios why these migration paths +must exist such as: + +- Projects split up into multiple packages +- Projects build with other tools than Swift PM +- Projects explicitly vendoring packages without wanting to modify the original + source +- Projects that prefer to deal with source breaks as they come up rather than + writing source-stable code + +#### Semantically versioned packages + +Semantically versioned packages are the primary reason for this proposal. The +expected migration path for packages when adopting the proposed feature is one +of the two: + +- API stable adoption by turning on the feature and marking all existing public + enums with `@frozen` +- API breaking adoption by turning on the feature and tagging a new major if the + public API contains enums + +### Projects with multiple non-semantically versioned packages + +A common project setup is splitting the code base into multiple packages that +are not semantically versioned. This can either be done by using local packages +or by using _revision locked_ dependencies. The packages in such a setup are +often considered part of the same logical collection of code and would like to +follow the same source stability rules as same module or same package code. We +propose to extend then package manifest to allow overriding the package name +used by a target. + +```swift +extension SwiftSetting { + /// Defines the package name used by the target. + /// + /// This setting is passed as the `-package-name` flag + /// to the compiler. It allows overriding the package name on a + /// per target basis. The default package name is the package identity. + /// + /// - Important: Package names should only be aligned across co-developed and + /// co-released packages. + /// + /// - Parameters: + /// - name: The package name to use. + /// - condition: A condition that restricts the application of the build + /// setting. + public static func packageName(_ name: String, _ condition: PackageDescription.BuildSettingCondition? = nil) -> PackageDescription.SwiftSetting +} +``` + +This allows to construct arbitrary package _domains_ across multiple targets +inside a single package or across multiple packages. When adopting the +`ExtensibleEnums` feature across multiple packages the new Swift setting can be +used to continue allowing exhaustive matching. + +While this setting allows treating multiple targets as part of the same package. +This setting should only be used across packages when the packages are +both co-developed and co-released. + +### Other build systems + +Swift PM isn't the only system used to create and build Swift projects. Build +systems and IDEs such as Bazel or Xcode offer support for Swift projects as +well. When using such tools it is common to split a project into multiple +targets/modules. Since those targets/modules are by default not considered to be +part of the package, when adopting the `ExtensibleEnums` feature it would +require to either add an `@unknown default` when switching over enums defined in +other targets/modules or marking all public enums as `@frozen`. Similarly, to +the above to avoid this churn we recommend specifying the `-package-name` flag +to the compiler for all targets/modules that should be considered as part of the +same unit. + +### Escape hatch + +There might still be cases where developers need to consume a module that is +outside of their control which adopts the `ExtensibleEnums` feature. For such +cases we propose to introduce a flag `--assume-source-stable-package` that +allows assuming modules of a package as source stable. When checking if a switch +needs to be exhaustive we will check if the code is either in the same module, +the same package, or if the defining package is assumed to be source stable. +This flag can be passed multiple times to define a set of assumed-source-stable +packages. + +```swift +// a.swift inside Package A +public enum MyEnum { + case foo + case bar +} + +// b.swift inside Package B compiled with `--assume-source-stable-package A` + +switch myEnum { // No @unknown default case needed +case .foo: + print("foo") +case .bar: + print("bar") +} +``` + +In general, we recommend to avoid using this flag but it provides an important +escape hatch to the ecosystem. + ## Source compatibility - Enabling the language feature `ExtensibleEnums` in a module compiled without @@ -304,6 +413,14 @@ dependency graph. This would allow a package to adopt the new language feature, break their existing, and release a new major while having minimal impact on the larger ecosystem. +### Using `--assume-source-stable-packages` for other diagnostics + +During the pitch it was brought up that there are more potential future +use-cases for assuming modules of another package as source stable such as +borrowing from a declaration which distinguishes between a stored property and +one written with a `get`. Such features would also benefit from the +`--assume-source-stable-packages` flag. + ## Alternatives considered ### Provide an `@extensible` annotation @@ -329,11 +446,46 @@ resilient modules. We considered introducing an annotation that allows developers to mark enumerations as pre-existing to the new language feature similar to how -`@preconcurrency` works. The problem with such an annotation is how the compiler -would handle this in consuming modules. It could either downgrade the warning -for the missing `@unknown default` case or implicitly synthesize one. However, -the only reasonable behavior for synthesized `@unknown default` case is to -`fatalError`. Furthermore, such an attribute becomes even more problematic to -handle when the module then extends the annotated enum; thus, making it possible -to hit the `@unknown default` case during runtime leading to potentially hitting -the `fatalError`. \ No newline at end of file +`@preconcurrency` works. Such an annotation seems to work initially when +existing public enumerations are marked as `@preEnumExtensibility` instead of +`@frozen`. It would result in the error about the missing `@unknown default` +case to be downgraded as a warning. However, such an annotation still doesn't +allow new cases to be added since there is no safe default at runtime when +encountering an unknown case. Below is an example how such an annotation would +work and why it doesn't allow existing public enums to become extensible. + +```swift +// Package A +public enum Foo { + case foo +} + +// Package B +switch foo { +case .foo: break +} + +// Package A adopts ExtensibleEnums feature and marks enum as @preEnumExtensibility +@preEnumExtensibility +public enum Foo { + case foo +} + +// Package B now emits a warning downgraded from an error +switch foo { // warning: Enum might be extended later. Add an @unknown default case. +case .foo: break +} + +// Later Package A decides to extend the enum +@preEnumExtensibility +public enum Foo { + case foo + case bar +} + +// Package B didn't add the @unknown default case yet. So now we we emit a warning and an error +switch foo { // error: Unhandled case bar & warning: Enum might be extended later. Add an @unknown default case. +case .foo: break +} + +``` \ No newline at end of file From 40e622c6c6616c8c6abeb46c4b2397ef894f925e Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Mon, 17 Mar 2025 15:33:17 +0100 Subject: [PATCH 14/18] Minor spelling fix ups --- proposals/NNNN-extensible-enums.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index 1b61d683ad..16619c8ba6 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -289,7 +289,7 @@ are not semantically versioned. This can either be done by using local packages or by using _revision locked_ dependencies. The packages in such a setup are often considered part of the same logical collection of code and would like to follow the same source stability rules as same module or same package code. We -propose to extend then package manifest to allow overriding the package name +propose to extend the package manifest to allow overriding the package name used by a target. ```swift @@ -337,7 +337,7 @@ same unit. There might still be cases where developers need to consume a module that is outside of their control which adopts the `ExtensibleEnums` feature. For such -cases we propose to introduce a flag `--assume-source-stable-package` that +cases we propose to introduce a new flag `--assume-source-stable-package` that allows assuming modules of a package as source stable. When checking if a switch needs to be exhaustive we will check if the code is either in the same module, the same package, or if the defining package is assumed to be source stable. From fd0d3a4efa5e5bdfeddca2c3f5fdf122b37ffdc0 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 24 Apr 2025 10:29:49 +0200 Subject: [PATCH 15/18] Update proposal to focus on `@extensible` attribute --- proposals/NNNN-extensible-enums.md | 277 +++++------------------------ 1 file changed, 44 insertions(+), 233 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index 16619c8ba6..f79065a74f 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -13,14 +13,9 @@ Previously pitched in: - https://forums.swift.org/t/extensible-enumerations-for-non-resilient-libraries/35900 - https://forums.swift.org/t/pitch-non-frozen-enumerations/68373 -> **Differences to previous proposals** - -> This proposal expands on the previous proposals and incorporates the language -> steering groups feedback of exploring language features to solve the -> motivating problem. It also reuses the existing `@frozen` and documents a -> migration path for existing modules. - Revisions: +- Re-focused this proposal on introducing a new `@extensible` attribute and + moved the language feature to a future direction - Introduced a second annotation `@nonExtensible` to allow a migration path into both directions - Added future directions for adding additional associated values @@ -36,9 +31,9 @@ Revisions: ## Introduction -This proposal addresses the long standing behavioral 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. +This proposal provides developers the capabilities to mark public enums in +non-resilient Swift libraries as extensible. This makes Swift `enum`s vastly +more useful in public API of such libraries. ## Motivation @@ -161,28 +156,15 @@ non-resilient Swift. ## Proposed solution -With the following proposed solution we want to achieve the following goals: -1. Align the differences between the two language dialects in a future language - mode -2. Provide developers a path to opt-in to the new behavior before the new - language mode so they can start declaring **new** extensible enumerations -3. Provide a migration path to the new behavior without forcing new SemVer - majors -4. Provide tools for developers to treat dependencies as source stable - -We propose to introduce a new language feature `ExtensibleEnums` that aligns the -behavior 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. This language feature will become enabled by default in -the next language mode. - -Modules consuming other modules with the language feature enabled will be -required to add an `@unknown default:`. +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. -An example of using the language feature and the keywords is below: +An example of using the new attribute is below: ```swift /// Module A +@extensible public enum PizzaFlavor { case hawaiian case pepperoni @@ -190,7 +172,7 @@ public enum PizzaFlavor { } /// Module B -switch pizzaFlavor { // error: Switch covers known cases, but 'MyEnum' may have additional unknown values, possibly added in future versions +switch pizzaFlavor { // error: Switch covers known cases, but 'MyEnum' may have additional unknown values, possibly added in future versions case .hawaiian: throw BadFlavorError() case .pepperoni: @@ -201,187 +183,52 @@ case .cheese: } ``` -Additionally, we propose to re-use the existing `@frozen` annotation to allow -developers to mark enumerations as non-extensible in non-resilient modules -similar to how it works in resilient modules already. +### Exhaustive switching inside same module/package -```swift -/// Module A -@frozen -public enum PizzaFlavor { - case hawaiian - case pepperoni - case cheese -} +Code inside the same module or package can be thought of as one co-developed +unit of code. Switching over an `@extensible` enum inside the same module or +package will require exhaustive matching to avoid unnecessary `@unknown default` +cases. -/// Module B -// The below doesn't require an `@unknown default` since PizzaFlavor is marked as frozen -switch pizzaFlavor { -case .hawaiian: - throw BadFlavorError() -case .pepperoni: - try validateNoVegetariansEating() - return .delicious -case .cheese: - return .delicious -} -``` +### `@extensible` and `@frozen` -Turning on the new language feature will be a semantically breaking change for -consumers of their module; hence, requiring a new SemVer major release of the -containing package. Some packages can release a new major and adopt the new -language feature right away; however, the ecosystem also contains packages that -try to avoid breaking API if at all possible. Such packages are often at the -very bottom of the dependency graph e.g. `swift-collections` or `swift-nio`. If -any of such packages releases a new major version it would effectively split the -ecosystem until all packages have adopted the new major. - -Packages that want to avoid breaking their API can use the new language feature -and the `@frozen` attribute in combination to unlock to possibility to declare -**new extensible** public enumerations but stay committed to the non-extensible -API of the already existing public enumerations. This is achieved by marking all -existing public enumerations with `@frozen` before turning on the language -feature. - -### Implications on code in the same package - -Code inside the same package still needs to exhaustively switch over -enumerations defined in the same package when the language feature is enabled. -Switches over enums of the same package containing an `@unknown default` will -produce a compiler warning. +An enum cannot be `@frozen` and `@extensible` at the same time. Thus, marking an +enum both `@extensible` and `@frozen` is not allowed and will result in a +compiler error. ### API breaking checker The behavior of `swift package diagnose-api-breaking-changes` is also updated -to understand if the language feature is enabled and only diagnose new enum -cases as a breaking change in non-frozen enumerations. - -### Migration paths - -The following section is outlining the migration paths and tools we propose to -provide for different kinds of projects to adopt the proposed feature. The goal -is to reduce churn across the ecosystem while still allowing us to align the -default behavior of enums. There are many scenarios why these migration paths -must exist such as: - -- Projects split up into multiple packages -- Projects build with other tools than Swift PM -- Projects explicitly vendoring packages without wanting to modify the original - source -- Projects that prefer to deal with source breaks as they come up rather than - writing source-stable code - -#### Semantically versioned packages - -Semantically versioned packages are the primary reason for this proposal. The -expected migration path for packages when adopting the proposed feature is one -of the two: +to understand the new `@extensible` attribute. -- API stable adoption by turning on the feature and marking all existing public - enums with `@frozen` -- API breaking adoption by turning on the feature and tagging a new major if the - public API contains enums - -### Projects with multiple non-semantically versioned packages - -A common project setup is splitting the code base into multiple packages that -are not semantically versioned. This can either be done by using local packages -or by using _revision locked_ dependencies. The packages in such a setup are -often considered part of the same logical collection of code and would like to -follow the same source stability rules as same module or same package code. We -propose to extend the package manifest to allow overriding the package name -used by a target. - -```swift -extension SwiftSetting { - /// Defines the package name used by the target. - /// - /// This setting is passed as the `-package-name` flag - /// to the compiler. It allows overriding the package name on a - /// per target basis. The default package name is the package identity. - /// - /// - Important: Package names should only be aligned across co-developed and - /// co-released packages. - /// - /// - Parameters: - /// - name: The package name to use. - /// - condition: A condition that restricts the application of the build - /// setting. - public static func packageName(_ name: String, _ condition: PackageDescription.BuildSettingCondition? = nil) -> PackageDescription.SwiftSetting -} -``` - -This allows to construct arbitrary package _domains_ across multiple targets -inside a single package or across multiple packages. When adopting the -`ExtensibleEnums` feature across multiple packages the new Swift setting can be -used to continue allowing exhaustive matching. - -While this setting allows treating multiple targets as part of the same package. -This setting should only be used across packages when the packages are -both co-developed and co-released. - -### Other build systems - -Swift PM isn't the only system used to create and build Swift projects. Build -systems and IDEs such as Bazel or Xcode offer support for Swift projects as -well. When using such tools it is common to split a project into multiple -targets/modules. Since those targets/modules are by default not considered to be -part of the package, when adopting the `ExtensibleEnums` feature it would -require to either add an `@unknown default` when switching over enums defined in -other targets/modules or marking all public enums as `@frozen`. Similarly, to -the above to avoid this churn we recommend specifying the `-package-name` flag -to the compiler for all targets/modules that should be considered as part of the -same unit. - -### Escape hatch - -There might still be cases where developers need to consume a module that is -outside of their control which adopts the `ExtensibleEnums` feature. For such -cases we propose to introduce a new flag `--assume-source-stable-package` that -allows assuming modules of a package as source stable. When checking if a switch -needs to be exhaustive we will check if the code is either in the same module, -the same package, or if the defining package is assumed to be source stable. -This flag can be passed multiple times to define a set of assumed-source-stable -packages. - -```swift -// a.swift inside Package A -public enum MyEnum { - case foo - case bar -} - -// b.swift inside Package B compiled with `--assume-source-stable-package A` +## Source compatibility -switch myEnum { // No @unknown default case needed -case .foo: - print("foo") -case .bar: - print("bar") -} -``` +### Resilient modules -In general, we recommend to avoid using this flag but it provides an important -escape hatch to the ecosystem. +- Adding or removing the `@extensible` attribute has no-effect since it is the default in this language dialect. -## Source compatibility +### Non-resilient modules -- Enabling the language feature `ExtensibleEnums` in a module compiled without -resiliency that contains public enumerations is a source breaking change unless -all existing public enumerations are marked with `@frozen` -- Disabling the language feature `ExtensibleEnums` in a module compiled without -resiliency is a source compatible change since it implicitly marks all -enumerations as `@frozen` -- Adding a `@frozen` annotation to an existing public enumeration is a source - compatible change +- Adding the `@extensible` attribute to a public enumeration is an API breaking change. +- Removing the `@extensible` attribute from a public enumeration is an API stable change. ## ABI compatibility -The new language feature dos not affect the ABI, as it is already how modules -compiled with resiliency behave. +The new attribute does not affect the ABI of an enum since it is already the +default in resilient modules. ## Future directions +### Aligning the language dialects + +In a previous iteration of this proposal, we proposed to add a new language +feature to align the language dialects in a future language mode. The main +motivation behind this is that the current default of non-extensible enums is a +common pitfall and results in tremendous amounts of unnoticed API breaks in the +Swift package ecosystem. We still believe that a future proposal should try +aligning the language dialects. This proposal is focused on providing a first +step to allow extensible enums in non-resilient modules. + ### `@unknown case` Enums can be used for errors. Catching and pattern matching enums could add @@ -405,50 +252,15 @@ package tooling could allow to define larger compilation units to express this. Until then developers are encouraged to use `@frozen` attributes on their enumerations to achieve the same effect. -### Swift PM allowing multiple conflicting major versions in a single dependency graph - -To reduce the impact of an API break on the larger ecosystem Swift PM could -allow multiple conflicting major versions of the same dependency in a single -dependency graph. This would allow a package to adopt the new language feature, -break their existing, and release a new major while having minimal impact on -the larger ecosystem. - -### Using `--assume-source-stable-packages` for other diagnostics - -During the pitch it was brought up that there are more potential future -use-cases for assuming modules of another package as source stable such as -borrowing from a declaration which distinguishes between a stored property and -one written with a `get`. Such features would also benefit from the -`--assume-source-stable-packages` flag. - ## Alternatives considered -### Provide an `@extensible` annotation - -We believe that the default behavior 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. - -### Introducing a new annotation instead of using `@frozen` - -An initial pitch proposed a new annotation instead of using `@frozen`. The -problem with that approach was coming up with a reasonable behavior of how the -new annotation works in resilient modules and what the difference to `@frozen` -is. Feedback during this and previous pitches was that `@frozen` has more -implications than just the non-extensibility of enumerations but also impact on -ABI. We understand the feedback but still believe it is better to re-use the -same annotation and clearly document the additional behavior when used in -resilient modules. - ### Introduce a `@preEnumExtensibility` annotation We considered introducing an annotation that allows developers to mark -enumerations as pre-existing to the new language feature similar to how +enumerations as pre-existing to the `@extensible` annotation similar to how `@preconcurrency` works. Such an annotation seems to work initially when existing public enumerations are marked as `@preEnumExtensibility` instead of -`@frozen`. It would result in the error about the missing `@unknown default` +`@extensible`. It would result in the error about the missing `@unknown default` case to be downgraded as a warning. However, such an annotation still doesn't allow new cases to be added since there is no safe default at runtime when encountering an unknown case. Below is an example how such an annotation would @@ -465,8 +277,8 @@ switch foo { case .foo: break } -// Package A adopts ExtensibleEnums feature and marks enum as @preEnumExtensibility -@preEnumExtensibility +// Package A wants to make the existing enum extensible +@preEnumExtensibility @extensible public enum Foo { case foo } @@ -477,7 +289,7 @@ case .foo: break } // Later Package A decides to extend the enum -@preEnumExtensibility +@preEnumExtensibility @extensible public enum Foo { case foo case bar @@ -487,5 +299,4 @@ public enum Foo { switch foo { // error: Unhandled case bar & warning: Enum might be extended later. Add an @unknown default case. case .foo: break } - ``` \ No newline at end of file From 8876f9939f5a7c28c5eca0b4ae0ff9de35c65db1 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 29 Apr 2025 10:43:46 +0200 Subject: [PATCH 16/18] Change implementation link --- proposals/NNNN-extensible-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index f79065a74f..7659d4a1cf 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -5,7 +5,7 @@ * Review Manager: TBD * Status: **Awaiting review** * Bug: [apple/swift#55110](https://github.com/swiftlang/swift/issues/55110) -* Implementation: [apple/swift#79580](https://github.com/swiftlang/swift/pull/79580) +* Implementation: [apple/swift#80503](https://github.com/swiftlang/swift/pull/80503) * Upcoming Feature Flag: `ExtensibleEnums` * Review: ([pitch](https://forums.swift.org/t/pitch-extensible-enums-for-non-resilient-modules/77649)) From 79533ec39ecb1e709607c505af0b3b91ee1706a7 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 29 Apr 2025 22:29:59 +0200 Subject: [PATCH 17/18] Add `@preEnumExtensibility` to the proposal --- proposals/NNNN-extensible-enums.md | 98 ++++++++++++++++-------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index 7659d4a1cf..b6f01545e9 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -201,6 +201,55 @@ compiler error. The behavior of `swift package diagnose-api-breaking-changes` is also updated to understand the new `@extensible` attribute. +### Staging in using `@preEnumExtensibility` + +We also propose adding a new `@preEnumExtensibility` attribute that can be used +to mark enumerations as pre-existing to the `@extensible` attribute. This allows +developers to mark existing public enumerations as `@preEnumExtensibility` in +addition to `@extensible`. This is useful for developers that want to stage in +changing an existing non-extensible enum to be extensible over multiple +releases. Below is an example of how this can be used: + +```swift +// Package A +public enum Foo { + case foo +} + +// Package B +switch foo { +case .foo: break +} + +// Package A wants to make the existing enum extensible +@preEnumExtensibility @extensible +public enum Foo { + case foo +} + +// Package B now emits a warning downgraded from an error +switch foo { // warning: Enum might be extended later. Add an @unknown default case. +case .foo: break +} + +// Later Package A decides to extend the enum and releases a new major version +@preEnumExtensibility @extensible +public enum Foo { + case foo + case bar +} + +// Package B didn't add the @unknown default case yet. So now we we emit a warning and an error +switch foo { // error: Unhandled case bar & warning: Enum might be extended later. Add an @unknown default case. +case .foo: break +} +``` + +While the `@preEnumExtensibility` attribute doesn't solve the need of requiring +a new major when a new case is added it allows developers to stage in changing +an existing non-extensible enum to become extensible in a future release by +surfacing a warning about this upcoming break early. + ## Source compatibility ### Resilient modules @@ -254,49 +303,8 @@ enumerations to achieve the same effect. ## Alternatives considered -### Introduce a `@preEnumExtensibility` annotation - -We considered introducing an annotation that allows developers to mark -enumerations as pre-existing to the `@extensible` annotation similar to how -`@preconcurrency` works. Such an annotation seems to work initially when -existing public enumerations are marked as `@preEnumExtensibility` instead of -`@extensible`. It would result in the error about the missing `@unknown default` -case to be downgraded as a warning. However, such an annotation still doesn't -allow new cases to be added since there is no safe default at runtime when -encountering an unknown case. Below is an example how such an annotation would -work and why it doesn't allow existing public enums to become extensible. - -```swift -// Package A -public enum Foo { - case foo -} - -// Package B -switch foo { -case .foo: break -} +### Different names for the attribute -// Package A wants to make the existing enum extensible -@preEnumExtensibility @extensible -public enum Foo { - case foo -} - -// Package B now emits a warning downgraded from an error -switch foo { // warning: Enum might be extended later. Add an @unknown default case. -case .foo: break -} - -// Later Package A decides to extend the enum -@preEnumExtensibility @extensible -public enum Foo { - case foo - case bar -} - -// Package B didn't add the @unknown default case yet. So now we we emit a warning and an error -switch foo { // error: Unhandled case bar & warning: Enum might be extended later. Add an @unknown default case. -case .foo: break -} -``` \ No newline at end of file +We considered different names for the attribute such as `@nonFrozen`; however, +we felt that `@extensible` communicates the idea of an extensible enum more +clearly. \ No newline at end of file From e15d0445fe43ebb25305effefef688b217fc59bb Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 30 Apr 2025 11:45:16 +0200 Subject: [PATCH 18/18] Source compat section for preEnumExtensibility --- proposals/NNNN-extensible-enums.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/proposals/NNNN-extensible-enums.md b/proposals/NNNN-extensible-enums.md index b6f01545e9..27ac6352d3 100644 --- a/proposals/NNNN-extensible-enums.md +++ b/proposals/NNNN-extensible-enums.md @@ -255,11 +255,15 @@ surfacing a warning about this upcoming break early. ### Resilient modules - Adding or removing the `@extensible` attribute has no-effect since it is the default in this language dialect. +- Adding the `@preEnumExtensibility` attribute has no-effect since it only downgrades the error to a warning. +- Removing the `@preEnumExtensibility` attribute is an API breaking since it upgrades the warning to an error again. ### Non-resilient modules -- Adding the `@extensible` attribute to a public enumeration is an API breaking change. -- Removing the `@extensible` attribute from a public enumeration is an API stable change. +- Adding the `@extensible` attribute is an API breaking change. +- Removing the `@extensible` attribute is an API stable change. +- Adding the `@preEnumExtensibility` attribute has no-effect since it only downgrades the error to a warning. +- Removing the `@preEnumExtensibility` attribute is an API breaking since it upgrades the warning to an error again. ## ABI compatibility