Hi all,
Swift has #if
to affect to allow different build configurations to impact what code is part of the final program. The conditions in #if
are a simplified form of the Swift expression syntax, so you can have code like this:
#if DEBUG
#if compiler(>=5.7) && hasFeature(ParameterPacks)
func f<each Arg>(_ args: repeat each Arg) { }
#endif
#else
func g() { }
#endif
If DEBUG
has been set (e.g., via -D
on the command line), the compiler is version 5.7 or greater, and it has the feature ParameterPacks
, then the function f
will be part of the program. If one of these conditions fails, f
will not be part of the program. If DEBUG
has not been set, g
will be part of the program.
I'm working on a new module for swift-syntax, SwiftIfConfig
, which provides APIs for working with #if
s in Swift sources. It's meant to make it easier to reason about #if
s: what they evaluate to, what code is in the program, and so on.
The core protocol of this new module is BuildConfiguration
. A build configuration answers queries about the build configuration, e.g.,
- Is
DEBUG
defined? - Is the architecture x86_64?
- Can we import the module
XYZ
?
I expect that there will be a few implementations of BuildConfiguration
in practice. I've added one to the compiler itself that reflects the compiler's knowledge of the current build configuration into the form SwiftIfConfig
understands, so that we can use SwiftIfConfig
directly in the compiler to replace some of the C++ code there. Ideally, we'd also implement a lighter-weight BuildConfiguration
that can answer most queries based only on the command-line options provided to the Swift driver, so that IDE clients can use these APIs without spinning up the full compiler.
In SwiftIfConfig
, I've included all of the client APIs I could think of uses for:
- Evaluation of a single
#if
condition expression viaConfiguredRegionState.init(condition:configuration:)
. - Determining the active condition for a specific
#if
clause viaIfConfigDeclSyntax.activeClause(in:)
. - Walking only the “active” regions of a source tree with the
ActiveSyntaxVisitor
, which is aSyntaxVisitor
that uses the givenBuildConfiguration
evaluate any#if
s along the way. - Rewriting a syntax tree by removing all inactive regions according to a built configuration,
SyntaxProtocol.removingInactive(in:)
. - Querying whether a specific syntax node is part of a build configuration,
SyntaxProtocol.isActive(in:)
. - Collecting all the active/inactive
#if
regions in a source tree according to a configuration withSyntaxProtocol.configuredRegions(in:)
. This is the same information as the prior API, but in a "batch" form.
All of these APIs are stateless: there are no mutable data structures behind them, it's just a query on the AST. The lower-level APIs like ConfiguredRegionState.init(condition:configuration:)
will throw to indicate when they can't make a determination, while the higher-level APIs that expect most clients to use will make a reasonable assumption (invalid condition -> inactive code). All of the APIs can provide diagnostics to indicate any errors, such as ill-formed #if
conditions or errors that come from the BuildConfiguration
.
The aforementioned integration in the compiler currently uses the configuredRegions(in:)
API to replace some existing compiler functionality that backs profiling coverage information and the SourceKit "inactive regions" query. There are a few other obvious places in the compiler to adopt more of these APIs, including outright replacement of the C++ parser's handling of #if
.
This library is another step in the Swift'ification of the Swift compiler itself, building little focused libraries that are easy to work with but complete enough to power the compiler itself in the days to come. I'd love your thoughts on the API provided by SwiftIfConfig
!
Doug