@@ -15,21 +15,36 @@ Previously pitched in:
15
15
16
16
> ** Differences to previous proposals**
17
17
18
- > This proposal expands on the previous proposals and incorperates the language
18
+ > This proposal expands on the previous proposals and incorporates the language
19
19
> steering groups feedback of exploring language features to solve the
20
- > motivating problem. It also provides a migration path for existing modules.
20
+ > motivating problem. It also reuses the existing ` @frozen ` and documents a
21
+ > migration path for existing modules.
22
+
23
+ Revisions:
24
+ - Introduced a second annotation ` @nonExtensible ` to allow a migration path into
25
+ both directions
26
+ - Added future directions for adding additional associated values
27
+ - Removed both the ` @extensible ` and ` @nonExtensible ` annotation in favour of
28
+ re-using the existing ` @frozen ` annotation
29
+ - Added the high level goals that this proposal aims to achieve
30
+ - Expanded on the proposed migration path for packages with regards to their
31
+ willingness to break API
32
+ - Added future directions for exhaustive matching for larger compilation units
33
+ - Added alternatives considered section for a hypothetical
34
+ ` @preEnumExtensibility `
35
+ - Added a section for ` swift package diagnose-api-breaking-changes `
21
36
22
37
## Introduction
23
38
24
- This proposal addresses the long standing behavioural difference of ` enum ` s in
39
+ This proposal addresses the long standing behavioral difference of ` enum ` s in
25
40
Swift modules compiled with and without library evolution. This makes Swift
26
41
` enum ` s vastly more useful in public API of non-resilient Swift libraries.
27
42
28
43
## Motivation
29
44
30
45
When Swift was enhanced to add support for "library evolution" mode (henceforth
31
46
called "resilient" mode), the Swift project had to make a number of changes to
32
- support a movable scale between "maximally evolveable " and "maximally
47
+ support a movable scale between "maximally evolvable " and "maximally
33
48
performant". This is because it is necessary for an ABI stable library to be
34
49
able to add new features and API surface without breaking pre-existing compiled
35
50
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.
42
57
enumerations. It has a wide ranging number of effects, including exposing their
43
58
size directly as part of the ABI and providing direct access to stored
44
59
properties. However, on enumerations it happens to also exert effects on the
45
- behaviour of switch statements.
60
+ behavior of switch statements.
46
61
47
- Consider the following simple library to your favourite pizza place:
62
+ Consider the following simple library to your favorite pizza place:
48
63
49
64
``` swift
50
65
public enum PizzaFlavor {
@@ -58,7 +73,7 @@ public func bakePizza(flavor: PizzaFlavor)
58
73
59
74
Depending on whether the library is compiled with library evolution mode
60
75
enabled, what the caller can do with the ` PizzaFlavor ` enum varies. Specifically,
61
- the behaviour in switch statements changes.
76
+ the behavior in switch statements changes.
62
77
63
78
In the _ standard_ , "non-resilient" mode, users of the library can write
64
79
exhaustive switch statements over the enum ` PizzaFlavor ` :
@@ -78,7 +93,7 @@ case .cheese:
78
93
This code will happily compile. If the author of the above switch statement was
79
94
missing a case (perhaps they forgot ` .hawaiian ` is a flavor), the compiler will
80
95
error, and force the user to either add a ` default: ` clause, or to express a
81
- behaviour for the missing case. The term for this is "exhaustiveness": in the
96
+ behavior for the missing case. The term for this is "exhaustiveness": in the
82
97
default "non-resilient" dialect, the Swift compiler will ensure that all switch
83
98
statements over enumerations cover every case that is present.
84
99
@@ -117,10 +132,10 @@ wants to improve the performance of using it, the author can annotate the enum
117
132
with ` @frozen ` . This annotation has a wide range of effects, but one of its
118
133
effects is to enable callers to perform exhaustive switches over the frozen
119
134
enumeration. Thus, resilient library authors that are interested in the
120
- exhaustive switching behaviour are able to opt-into it.
135
+ exhaustive switching behavior are able to opt-into it.
121
136
122
137
However, in Swift today it is not possible for the default, "non-resilient"
123
- dialect to opt-in to the extensible enumeration behaviour . That is, there is no
138
+ dialect to opt-in to the extensible enumeration behavior . That is, there is no
124
139
way for a Swift package to be able to evolve a public enumeration without
125
140
breaking the API. This is a substantial limitation, and greatly reduces the
126
141
utility of enumerations in non-resilient Swift. Over the past years, many
@@ -146,114 +161,119 @@ non-resilient Swift.
146
161
147
162
## Proposed solution
148
163
164
+ With the following proposed solution we want to achieve the following goals:
165
+ 1 . Align the differences between the two language dialects in a future language
166
+ mode
167
+ 2 . Provide developers a path to opt-in to the new behavior before the new
168
+ language mode so they can start declaring ** new** extensible enumerations
169
+ 3 . Provide a migration path to the new behavior without forcing new SemVer
170
+ majors
171
+
149
172
We propose to introduce a new language feature ` ExtensibleEnums ` that aligns the
150
- behaviour of enumerations in both language dialects. This will make ** public**
173
+ behavior of enumerations in both language dialects. This will make ** public**
151
174
enumerations in packages a safe default and leave maintainers the choice of
152
- extending them later on. We also propose to enable this new language feature by
153
- default with the next lagnuage mode.
154
-
155
- We also propose to introduce two new attributes.
156
- - ` @nonExtensible ` : For marking an enumeration as not extensible.
157
- - ` @extensible ` : For marking an enumeration as extensible.
175
+ extending them later on. This language feature will become enabled by default in
176
+ the next language mode.
158
177
159
178
Modules consuming other modules with the language feature enabled will be
160
- required to add an ` @unknown default: ` case to any switch state for enumerations
161
- that are not marked with ` @nonExtensible ` .
179
+ required to add an ` @unknown default: ` .
162
180
163
181
An example of using the language feature and the keywords is below:
164
182
165
183
``` swift
166
184
/// Module A
167
- @extensible // or language feature ExtensibleEnums is enabled
168
- enum MyEnum {
169
- case foo
170
- case bar
171
- }
172
-
173
- @nonExtensible
174
- enum MyFinalEnum {
175
- case justMe
185
+ public enum PizzaFlavor {
186
+ case hawaiian
187
+ case pepperoni
188
+ case cheese
176
189
}
177
190
178
191
/// Module B
179
- switch myEnum { // error: Switch covers known cases, but 'MyEnum' may have additional unknown values, possibly added in future versions
180
- case .foo : break
181
- case . bar : break
182
- }
183
-
184
- // The below produces no warnings since the enum is marked as nonExtensible
185
- switch myFinalEnum {
186
- case . justMe : break
192
+ switch pizzaFlavor { // error: Switch covers known cases, but 'MyEnum' may have additional unknown values, possibly added in future versions
193
+ case .hawaiian :
194
+ throw BadFlavorError ()
195
+ case . pepperoni :
196
+ try validateNoVegetariansEating ()
197
+ return . delicious
198
+ case . cheese :
199
+ return . delicious
187
200
}
188
201
```
189
202
190
- ## Detailed design
191
-
192
- ### Migration path
203
+ Additionally, we propose to re-use the existing ` @frozen ` annotation to allow
204
+ developers to mark enumerations as non-extensible in non-resilient modules
205
+ similar to how it works in resilient modules already.
193
206
194
- The proposed new language feature is the first langauge feature that has impact
195
- on the consumers of a module and not the module itself. Enabling the langauge
196
- feature in a non-resilient module with public enumerations is a source breaking
197
- change.
198
-
199
- The two proposed annotations ` @extensible/@nonExtensible ` give developers tools
200
- to opt-in to the new language feature or in the future language mode without
201
- breaking their consumers. This paves a path for a gradual migration. Developers
202
- can mark all of their exisiting public enumerations as ` @nonExtensible ` and then
203
- turn on the language feature. Similarly, developers can also mark new
204
- enumerations as ` @extensible ` without turning on the language feature yet.
205
-
206
- In a future language mode, individual modules can still be opted in one at a
207
- time into the new language mode and apply the annotations as needed to avoid
208
- source breakages.
207
+ ``` swift
208
+ /// Module A
209
+ @frozen
210
+ public enum PizzaFlavor {
211
+ case hawaiian
212
+ case pepperoni
213
+ case cheese
214
+ }
209
215
210
- When the language feature is turned on and a public enumeration is marked as
211
- ` @extensible ` it will produce a warning that the annotation isn't required.
216
+ /// Module B
217
+ // The below doesn't require an `@unknown default` since PizzaFlavor is marked as frozen
218
+ switch pizzaFlavor {
219
+ case .hawaiian :
220
+ throw BadFlavorError ()
221
+ case .pepperoni :
222
+ try validateNoVegetariansEating ()
223
+ return .delicious
224
+ case .cheese :
225
+ return .delicious
226
+ }
227
+ ```
212
228
213
- In non-resilient modules without the language feature turned on, adding the
214
- ` @extensible ` attribute to non-public enums will produce a warning since those
215
- enums can only be matched exhaustively.
229
+ Turning on the new language feature will be a semantically breaking change for
230
+ consumers of their module; hence, requiring a new SemVer major release of the
231
+ containing package. Some packages can release a new major and adopt the new
232
+ language feature right away; however, the ecosystem also contains packages that
233
+ try to avoid breaking API if at all possible. Such packages are often at the
234
+ very bottom of the dependency graph e.g. ` swift-collections ` or ` swift-nio ` . If
235
+ any of such packages releases a new major version it would effectively split the
236
+ ecosystem until all packages have adopted the new major.
237
+
238
+ Packages that want to avoid breaking their API can use the new language feature
239
+ and the ` @frozen ` attribute in combination to unlock to possibility to declare
240
+ ** new extensible** public enumerations but stay committed to the non-extensible
241
+ API of the already existing public enumerations. This is achieved by marking all
242
+ existing public enumerations with ` @frozen ` before turning on the language
243
+ feature.
216
244
217
245
### Implications on code in the same package
218
246
219
247
Code inside the same package still needs to exhaustively switch over
220
- enumerations defined in the same package. Switches over enums of the same
221
- package containing an ` @unknown default ` will produce a compiler warning.
222
-
223
- ### Impact on resilient modules & ` @frozen ` attribute
248
+ enumerations defined in the same package when the language feature is enabled.
249
+ Switches over enums of the same package containing an ` @unknown default ` will
250
+ produce a compiler warning.
224
251
225
- Explicitly enabling the language feature in resilient modules will produce a
226
- compiler warning since that is already the default behaviour. Using the
227
- ` @nonExtensible ` annotation will lead to a compiler error since users of
228
- resilient modules must use the ` @frozen ` attribute instead.
252
+ ### API breaking checker
229
253
230
- Since some modules support compiling in resilient and non-resilient modes,
231
- developers need a way to mark enums as non-extensible for both. ` @nonExtensible `
232
- produces an error when compiling with resiliency; hence, developers must use
233
- ` @frozen ` . To make supporting both modes easier ` @frozen ` will also work in
234
- non-resilient modules and make enumerations non extensible.
254
+ The behavior of ` swift package diagnose-api-breaking-changes ` is also updated
255
+ to understand if the language feature is enabled and only diagnose new enum
256
+ cases as a breaking change in non-frozen enumerations.
235
257
236
258
## Source compatibility
237
259
238
- - Enabling the language feature ` ExtensibleEnums ` in a module that contains
239
- public enumerations is a source breaking change unless all existing public
240
- enumerations are marked with ` @nonExtensible `
241
- - Adding an ` @extensible ` annotation to an exisitng public enum is a source
242
- breaking change in modules that have ** not** enabled the ` ExtensibleEnums `
243
- language features or are compiled with resiliency.
244
- - Changing the annotation from ` @nonExtensible/@frozen ` to ` @extensible ` is a
245
- source breaking change.
246
- - Changing the annotation from ` @extensible ` to ` @nonExtensible/@frozen ` is a
247
- source compatible change and will only result in a warning code that used
248
- ` @unknown default: ` clause. This allows developers to commit to the API of an
249
- enum in a non-source breaking way.
260
+ - Enabling the language feature ` ExtensibleEnums ` in a module compiled without
261
+ resiliency that contains public enumerations is a source breaking change unless
262
+ all existing public enumerations are marked with ` @frozen `
263
+ - Disabling the language feature ` ExtensibleEnums ` in a module compiled without
264
+ resiliency is a source compatible change since it implicitly marks all
265
+ enumerations as ` @frozen `
266
+ - Adding a ` @frozen ` annotation to an existing public enumeration is a source
267
+ compatible change
250
268
251
269
## ABI compatibility
252
- The new attributes do not affect the ABI, as it is a no-op when used in a resilient library.
270
+
271
+ The new language feature dos not affect the ABI, as it is already how modules
272
+ compiled with resiliency behave.
253
273
254
274
## Future directions
255
275
256
- ### ` @unkown case `
276
+ ### ` @unknown case `
257
277
258
278
Enums can be used for errors. Catching and pattern matching enums could add
259
279
support for an ` @unknown catch ` to make pattern matching of typed throws align
@@ -263,25 +283,57 @@ with `switch` pattern matching.
263
283
264
284
Adding additional associated values to an enum can also be seen as extending it
265
285
and we agree that this is interesting to explore in the future. However, this
266
- proposal focuses on solving the primary problem of the unusability of public
286
+ proposal focuses on solving the primary problem of the usability of public
267
287
enumerations in non-resilient modules.
268
288
289
+ ### Larger compilation units than packages
290
+
291
+ During the pitch it was brought up that a common pattern for application
292
+ developers is to split an application into multiple smaller packages. Those
293
+ packages are versioned together and want to have the same exhaustive matching
294
+ behavior as code within a single package. As a future direction, build and
295
+ package tooling could allow to define larger compilation units to express this.
296
+ Until then developers are encouraged to use ` @frozen ` attributes on their
297
+ enumerations to achieve the same effect.
298
+
299
+ ### Swift PM allowing multiple conflicting major versions in a single dependency graph
300
+
301
+ To reduce the impact of an API break on the larger ecosystem Swift PM could
302
+ allow multiple conflicting major versions of the same dependency in a single
303
+ dependency graph. This would allow a package to adopt the new language feature,
304
+ break their existing, and release a new major while having minimal impact on
305
+ the larger ecosystem.
306
+
269
307
## Alternatives considered
270
308
271
- ### Only provide the ` @extensible ` annotation
309
+ ### Provide an ` @extensible ` annotation
272
310
273
- We believe that the default behaviour in both language dialects should be that
311
+ We believe that the default behavior in both language dialects should be that
274
312
public enumerations are extensible. One of Swift's goals, is safe defaults and
275
313
the current non-extensible default in non-resilient modules doesn't achieve that
276
314
goal. That's why we propose a new language feature to change the default in a
277
315
future Swift language mode.
278
316
279
- ### Usign ` @frozen ` and introducing ` @nonFrozen `
280
-
281
- We considered names such as ` @nonFrozen ` for ` @extensible ` and using ` @frozen `
282
- for ` @nonExtensible ` ; however, we believe that _ frozen_ is a concept that
283
- includes more than exhaustive matching. It is heavily tied to resiliency and
284
- also has ABI impact. That's why decoupled annotations that only focus on the
285
- extensability is better suited. ` @exhaustive/@nonExhaustive ` would fit that bill
286
- as well but we believe that ` @extensible ` better expresses the intention of the
287
- author.
317
+ ### Introducing a new annotation instead of using ` @frozen `
318
+
319
+ An initial pitch proposed an new annotation instead of using `@frozen. The
320
+ problem with that approach was coming up with a reasonable behavior of how the
321
+ new annotation works in resilient modules and what the difference to ` @frozen `
322
+ is. Feedback during this and previous pitches was that ` @frozen ` has more
323
+ implications than just the non-extensibility of enumerations but also impact on
324
+ ABI. We understand the feedback but still believe it is better to re-use the
325
+ same annotation and clearly document the additional behavior when used in
326
+ resilient modules.
327
+
328
+ ### Introduce a ` @preEnumExtensibility ` annotation
329
+
330
+ We considered introducing an annotation that allows developers to mark
331
+ enumerations as pre-existing to the new language feature similar to how
332
+ ` @preconcurrency ` works. The problem with such an annotation is how the compiler
333
+ would handle this in consuming modules. It could either downgrade the warning
334
+ for the missing ` @unknown default ` case or implicitly synthesize one. However,
335
+ the only reasonable behavior for synthesized ` @unknown default ` case is to
336
+ ` fatalError ` . Furthermore, such an attribute becomes even more problematic to
337
+ handle when the module then extends the annotated enum; thus, making it possible
338
+ to hit the ` @unknown default ` case during runtime leading to potentially hitting
339
+ the ` fatalError ` .
0 commit comments