Skip to content

Commit e812ae3

Browse files
committed
Initial draft of proposal for compilerSettings.
1 parent 8b38f17 commit e812ae3

File tree

1 file changed

+283
-0
lines changed

1 file changed

+283
-0
lines changed

proposals/0000-compiler-settings.md

+283
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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

Comments
 (0)