|
| 1 | +# compilerSettings: a top level statement for enabling compiler flags locally in a specific file |
| 2 | + |
| 3 | +* Proposal: [SE-NNNN](NNNN-filename.md) |
| 4 | +* Authors: [Michael Gottesman](https://github.com/gottesmm) |
| 5 | +* Review Manager: TBD |
| 6 | +* Status: **Awaiting implementation** |
| 7 | +* Vision: [[Prospective Vision] Improving the approachability of data-race safety](https://forums.swift.org/t/prospective-vision-improving-the-approachability-of-data-race-safety/76183) |
| 8 | +* Review: ([pitch](https://forums.swift.org/...)) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +We propose introducing a new top level statement called `compilerSettings` that |
| 13 | +allows for compiler flags to be enabled on a single file. This can be used to |
| 14 | +enable warningsAsError, upcoming features, and default isolation among other |
| 15 | +flags. For example: |
| 16 | + |
| 17 | +```swift |
| 18 | +compilerSettings [ |
| 19 | + .warningAsError(.concurrency), |
| 20 | + .strictConcurrency(.complete), |
| 21 | + .enableUpcomingFeature(.existentialAny), |
| 22 | + .defaultIsolation(MainActor.self), |
| 23 | +] |
| 24 | +``` |
| 25 | + |
| 26 | +## Motivation |
| 27 | + |
| 28 | +Today Swift contains multiple ways of changing compiler and language behavior |
| 29 | +from the command line including: |
| 30 | + |
| 31 | +* Enabling experimental and upcoming features. |
| 32 | +* Enabling warnings as errors for diagnostic groups. |
| 33 | +* Enabling strict concurrency when compiling pre-Swift 6 code. |
| 34 | +* Specifying the default actor isolation via [Controlling Default Actor Isolation](). |
| 35 | +* Requiring code to use [Strict Memory Safety](). |
| 36 | + |
| 37 | +Since these compiler and language behaviors can only be manipulated via the |
| 38 | +command line and the compiler compiles one module at a time, users are forced to |
| 39 | +apply these flags one module at a time instead of one file at a time. This |
| 40 | +results in several forms of developer friction that we outline below. |
| 41 | + |
| 42 | +### Unnecessary Module Splitting |
| 43 | + |
| 44 | +Since compiler flags can only be applied to entire modules, one cannot apply a |
| 45 | +compiler flag to a subset of files of a module. When confronted with this, users |
| 46 | +are forced to split the subset of files into a separate module creating an |
| 47 | +unnecessary module split. We foresee that this will occur more often in the |
| 48 | +future if new features like [Controlling Default Actor Isolation]() and [Strict |
| 49 | +MemorySafety]() are accepted by Swift Evolution. Consider the following |
| 50 | +situations where unnecessary module splitting would be forced: |
| 51 | + |
| 52 | +* Consider a framework that by default uses `nonisolated` isolation, but wants |
| 53 | + to expose a set of UI elements for users of the framework. In such a case, it |
| 54 | + is reasonable to expect the author to want to make those specific files |
| 55 | + containing UI elements to have a default isolation of `@MainActor` instead of |
| 56 | + nonisolated. Without this feature one must introduce an additional module to |
| 57 | + contain the UI elements for the framework when semantically there is really |
| 58 | + only one framework. |
| 59 | + |
| 60 | +* Imagine an app that has enabled `@MainActor` isolation by default since it |
| 61 | + contains mostly UI elements but that wishes to implement helper abstractions |
| 62 | + that interact with a database on a background task. This helper code is |
| 63 | + naturally expressed as having a default isolation of nonisolated implying that |
| 64 | + one must either split the database helpers into a separate module or must mark |
| 65 | + most declarations in the database code with nonisolated by hand. |
| 66 | + |
| 67 | +* Visualize a banking framework that contains both UI elements and functionality |
| 68 | + for directly manipulating the account data of a customer. In such a case the |
| 69 | + code that actually modifies customer data may want to enable strict memory |
| 70 | + safety to increase security. In contrast, strict memory safety is not |
| 71 | + necessary for the UI elements that the framework defines. To support both uses |
| 72 | + cases, a module split must be introduced. |
| 73 | + |
| 74 | +### Preventing per-file based migration of large modules using warningsAsErrors |
| 75 | + |
| 76 | +Swift has taken the position that updating modules for a new language mode or |
| 77 | +feature should be accomplished by: |
| 78 | + |
| 79 | +1. Enabling warnings on the entire module by enabling upcoming features from the |
| 80 | + command line. |
| 81 | + |
| 82 | +2. Updating the module incrementally until all of the warnings have been |
| 83 | + eliminated from the module. |
| 84 | + |
| 85 | +3. Updating the language version of the module so from that point on warnings |
| 86 | + will become errors to prevent backsliding. |
| 87 | + |
| 88 | +This creates developer friction when applied to large modules, since the size of |
| 89 | +the module makes it difficult to eliminate all of the warnings at once. This |
| 90 | +results in either the module being updated over time on main during which main |
| 91 | +is left in an intermediate, partially updated state or the module being updated |
| 92 | +on a branch that is merged once all updates have been completed. In the former |
| 93 | +case, it is easy to introduce new warnings as new code is added to the codebase |
| 94 | +since only warnings are being used. In the later case, the branch must be kept |
| 95 | +up to date in the face of changes being made to the codebase on main, forcing |
| 96 | +one to continually need to update the branch to resolve merge conflicts against |
| 97 | +main. |
| 98 | + |
| 99 | +In contrast by allowing for command line flags to be applied one file at a time, |
| 100 | +users can update code using a warningsAsErrors based migration path. This |
| 101 | +involves instead: |
| 102 | + |
| 103 | +1. Enabling warningsAsErrors for the new feature on the specific file. |
| 104 | + |
| 105 | +2. Fixing all of the errors in the file. |
| 106 | + |
| 107 | +3. Commiting the updated file into main. |
| 108 | + |
| 109 | +Since one is only updating one file at a time and using warningsAsErrors, the |
| 110 | +updates can be done incrementally on a per file basis, backsliding cannot occur |
| 111 | +in the file, and most importantly main is never in an intermediate, partially |
| 112 | +updated state. |
| 113 | + |
| 114 | +## Proposed solution |
| 115 | + |
| 116 | +We propose introducing a new top level statement called `compilerSettings` that |
| 117 | +takes a list of enums of type `CompilerSetting`. Each enum case's associated |
| 118 | +values represent arguments that would be passed as an argument to the compiler |
| 119 | +flag on the command line. The compiler upon parsing a `compilerSettings` |
| 120 | +statement updates its internal state to set the appropriate settings before |
| 121 | +compiling the rest of the file. An example of such a `compilerSettings` |
| 122 | +statement is the following: |
| 123 | + |
| 124 | +```swift |
| 125 | +compilerSettings [ |
| 126 | + .warningAsError(.concurrency), |
| 127 | + .strictConcurrency(.complete), |
| 128 | + .enableUpcomingFeature(.existentialAny), |
| 129 | + .defaultIsolation(MainActor.self), |
| 130 | +] |
| 131 | +``` |
| 132 | + |
| 133 | +By specifying compiler options in such a manner, we are able to solve all of the |
| 134 | +problems above: |
| 135 | + |
| 136 | +1. The programmer can avoid module splitting when they wish to use compiler |
| 137 | + flags only on a specific file. |
| 138 | + |
| 139 | +2. The programmer can specify options on a per file basis instead of only on a |
| 140 | + per module basis allowing for warningsAsError to be used per file instead of |
| 141 | + per module migration to use new features. |
| 142 | + |
| 143 | +As an additional benefit since enums are being used, code completion can make |
| 144 | +the compiler options discoverable to the user in the IDE. |
| 145 | + |
| 146 | +## Detailed design |
| 147 | + |
| 148 | +We will update the swift grammar to allow for a new top level statement called |
| 149 | +`compilerSettings` that takes a list of expressions: |
| 150 | + |
| 151 | +```text |
| 152 | +top-level-statement ::= 'compilerSettings' '[' (expr ',')* expr ','? ']' |
| 153 | +``` |
| 154 | + |
| 155 | +Each expression passed to `compilerSettings` is an instance of the enum |
| 156 | +`CompilerSetting` that specifies a command line option that is to be applied to |
| 157 | +the file. `CompilerSetting` will be a resilient enum defined in the standard |
| 158 | +library that provides the following API: |
| 159 | + |
| 160 | +``` |
| 161 | +public enum CompilerSetting { |
| 162 | +case enableUpcomingFeature(UpcomingFeatureCompilerSetting) |
| 163 | +case enableExperimentalFeature(ExperimentalFeatureCompilerSetting) |
| 164 | +case warningAsError(WarningAsErrorCompilerSetting) |
| 165 | +case defaultIsolation(Actor.Type) |
| 166 | +case strictConcurrency(StrictConcurrencyCompilerSetting) |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +We purposely make `CompilerSetting` resilient so additional kinds of compiler |
| 171 | +settings can be added over time without breaking ABI. |
| 172 | + |
| 173 | +`CompilerSetting` provides an enum case for each command line option and each |
| 174 | +case is able to provide associated values to specify arguments that would |
| 175 | +normally be passed on the command line to the option. These associated values |
| 176 | +can be one of: |
| 177 | + |
| 178 | +* an already existing type (for example `Actor.Type`). |
| 179 | + |
| 180 | +* a custom fixed enum defined as a subtype of `CompilerSetting` (for example |
| 181 | + `StrictConcurrencyCompilerSetting`). |
| 182 | + |
| 183 | +* a custom enum defined as a subtype of `CompilerSetting` that the compiler |
| 184 | + synthesizes cases for depending on internal compiler data (for example |
| 185 | + features for `UpcomingFeatureCompilerSetting`). |
| 186 | + |
| 187 | +We purposely keep all custom types defined for `CompilerSetting` as nested types |
| 188 | +within `CompilerSetting` in order to avoid polluting the global namespace. Thus |
| 189 | +we would necessarily define in the stdlib all of the custom types as: |
| 190 | + |
| 191 | +```swift |
| 192 | +extension CompilerSetting { |
| 193 | + // Cases synthesized by the compiler. |
| 194 | + public enum UpcomingFeatureCompilerSetting { } |
| 195 | + |
| 196 | + // Cases synthesized by the compiler. |
| 197 | + public enum ExperimentalFeatureCompilerSetting { } |
| 198 | + |
| 199 | + // Cases synthesized by the compiler. |
| 200 | + public enum WarningsAsErrorsCompilerSetting { } |
| 201 | + |
| 202 | + // We know ahead of time all of the cases, so this is specified explicitly. |
| 203 | + public enum StrictConcurrency { |
| 204 | + case minimal |
| 205 | + case targeted |
| 206 | + case complete |
| 207 | + } |
| 208 | +} |
| 209 | +``` |
| 210 | + |
| 211 | +In order to ensure that compiler settings are easy to find in the file, we |
| 212 | +require that `compilerSettings` be the first statement in the file. |
| 213 | + |
| 214 | +By default command line flags will not be allowed to be passed to |
| 215 | +`compilerSettings`. Instead, we will require a compiler flag to explicitly opt |
| 216 | +in to being supported by `compilerSettings`. We require that since: |
| 217 | + |
| 218 | +1. Certain compiler flags like `-enable-library-evolution` have effects that can |
| 219 | + not be constrained to a single file since they are inherently module wide |
| 220 | + flags. |
| 221 | + |
| 222 | +2. Other compiler flags whose impacts would necessitate exposing |
| 223 | + `compilerSettings` in textual modules. We view supported `compilerSettings` |
| 224 | + as an anti-goal since appearing in the module header makes enabling |
| 225 | + `compilerSettings` affect the way the module is interpreted by users, while |
| 226 | + we are designing `compilerSettings` to have an impact strictly local to the |
| 227 | + file. NOTE: This does not preclude compiler options that modify the way the |
| 228 | + code in the file is exposed in textual modules by modifying the code's |
| 229 | + representation in the textual module directly. |
| 230 | + |
| 231 | +## Source compatibility |
| 232 | + |
| 233 | +The addition of the `compilerSetting` keyword does not impact parsing of other |
| 234 | +constructs since it can only be parsed as part of top level code in a position |
| 235 | +where one must have a keyword preventing any ambiguity. Adding the enum |
| 236 | +`CompilerSetting` cannot cause a name conflict since the name shadowing rules |
| 237 | +added to the compiler for `Result` will also guarantee that any user defined |
| 238 | +`CompilerSetting` will take precedence over the `CompilerSetting` type. In such |
| 239 | +a situation, the user can spell `CompilerSetting` as `Swift.CompilerSetting` as |
| 240 | +needed. All of the nested types within `CompilerSetting` cannot cause any source |
| 241 | +compatibility issues since they are namespaced within `CompilerSetting`. |
| 242 | + |
| 243 | +## ABI compatibility |
| 244 | + |
| 245 | +This proposal does not inherently break ABI. But it can break ABI if a command |
| 246 | +line option is enabled that causes the generated code's ABI to change. |
| 247 | + |
| 248 | +## Implications on adoption |
| 249 | + |
| 250 | +Adopters of this proposal should be aware that while this feature does not |
| 251 | +inherently break ABI, enabling options using `compilerSettings` that break ABI |
| 252 | +can result in ABI breakage. |
| 253 | + |
| 254 | +## Future directions |
| 255 | + |
| 256 | +Even though we purposely chose not to reuse `SwiftSetting` from `SwiftPM` (see |
| 257 | +Alternatives Considered), we could add a new static method onto `SwiftSetting` |
| 258 | +called `SwiftSetting.defineCompilerSetting` or provide an overload for |
| 259 | +`SwiftSetting.define` that takes a `CompilerSetting` enum and uses it to pass |
| 260 | +flags onto a specific target. This would allow for SwiftPM users to take |
| 261 | +advantage of the discoverability provided by using enums in comparison with the |
| 262 | +stringly typed APIs that `SwiftSetting` uses today to enable experimental and |
| 263 | +upcoming features. This generalized API also would ensure that `SwiftSetting` |
| 264 | +does not need to be updated to support more kinds of command line options since |
| 265 | +the compiler would be able to "inject" support for such options into |
| 266 | +`SwiftPM`. This would most likely require out sub-enum cases to be String enums |
| 267 | +so that SwiftPM can just use their string representation. |
| 268 | + |
| 269 | +## Alternatives considered |
| 270 | + |
| 271 | +Instead of using `compilerSettings` and `CompilerSetting`, we could instead use |
| 272 | +`swiftSettings` and `SwiftSetting` respectively. The reason why the authors |
| 273 | +avoided this is that `swiftSetting` and `SwiftSetting` are currently used by |
| 274 | +swiftpm to specify build settings resulting in potential user confusion since |
| 275 | +`SwiftSetting` uses stringly typed APIs instead of the more discoverable enum |
| 276 | +based APIs above. If instead one attempted to move `SwiftSetting` into the |
| 277 | +standard library, one would have to expose many parts of SwiftPM's internal |
| 278 | +types into the standard library such as `BuildSettingData`, |
| 279 | +`BuildSettingCondition`, `Platform`, `BuildConfiguration`, and more. |
| 280 | + |
| 281 | +## Acknowledgments |
| 282 | + |
| 283 | +TODO |
0 commit comments