New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SwiftIfConfig: A library to evaluate #if
conditionals within a Swift syntax tree.
#1816
base: main
Are you sure you want to change the base?
Conversation
Building on top of the parser and operator-precedence parsing library, introduce a new library that evaluates `#if` conditions against a particular build configuration. The build configuration is described by the aptly named `BuildConfiguration` protocol, which has queries for various build settings (e.g., configuration flags), compiler capabilities (features and attributes), and target information (OS, architecture, endianness, etc.). At present, the only user-facing operation is the `IfConfigState` initializer, which takes in an expression (the `#if` condition) and a build configuration, then evaluates that expression against the build condition to determine whether code covered by that condition is active, inactive, or completely unparsed. This is a fairly low-level API, meant to be a building block for more useful higher-level APIs that query which `#if` clause is active and whether a particular syntax node is active.
`IfConfigDeclSyntax.activeClause(in:)` determines which clause is active within an `#if` syntax node. `SyntaxProtocol.isActive(in:)` determines whether a given syntax node is active in the program, based on the nested stack of `#if` configurations.
These were introduced by SE-0212.
This is the last kind of check! Remove the `default` fallthrough from the main evaluation function.
The `ActiveSyntax(Any)Visitor` visitor classes provide visitors that only visit the regions of a syntax tree that are active according to a particular build configuration, meaning that those nodes would be included in a program that is built with that configuration.
The operation `SyntaxProtocol.removingInactive(in:)` returns a syntax tree derived from `self` that has removed all inactive syntax nodes based on the provided configuration.
Postfix `#if` expressions have a different syntactic form than other `#if` clauses because they don't fit into a list-like position in the grammar. Implement a separate, recursive folding algorithm to handle these clauses.
@swift-ci please test |
I would love to use this in the part of the compiler that strips inactive |
XCTAssertThrowsError(try ifConfigState("3.14159")) { error in | ||
XCTAssertEqual(String(describing: error), "invalid conditional compilation expression") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If #if
condition evaluation produces diagnostics, it would probably be good to have a assertIfConfigEvaluation
that accepts a DiagnosticSpec
and location markers.
And honestly, I think it would be a good idea even in the current design to avoid the repeated definition of ifConfigState
.
func testCanImport() throws { | ||
let buildConfig = TestingBuildConfiguration() | ||
|
||
func ifConfigState(_ condition: ExprSyntax) throws -> IfConfigState { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inside #if
expressions this will get parsed as a CanImportExprSyntax
. So I think you probably need to do something like
let ifConfigDecl = """
#if \(condition)
#else
let expr = extract the parsed condition again.
"""
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have CanImportExprSyntax
at all? It seems like we should leave it as a normal call expression and let SwiftIfConfig
deal with it. There are two diagnostics that would have to move into SwiftIfConfig
(missing major version, wrong number of arguments), but those are straightforward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's because the diagnostics(wrong label, wrong number of arguments..) are currently generated in SwiftParserDiagnostics
and a call expression node is not expressive enough to generate them.
@swift-ci please test |
@swift-ci please test Windows |
…lt types The optional return was used to mean "don't know", but was always treated as false. Instead, make all of the result types non-optional, and allow these operations to throw to indicate failure. While here, drop the "syntax" parameters to all of these functions. We shouldn't be working with syntax inside the build configuration.
Swift provides the ability to conditionally compile parts of a source file based on various built-time conditions, including information about the target (operating system, processor architecture, environment), information about the compiler (version, supported attributes and features), and user-supplied conditions specified as part of the build (e.g.,
DEBUG
), which we collectively refer to as the build configuration. These conditions can occur within a#if
in the source code, e.g.,The syntax tree and its parser do not reason about the build configuration. Rather, the syntax tree produced by parsing this code will include
IfConfigDeclSyntax
nodes wherever there is a#if
, and each such node contains the a list of clauses, each with a condition to check (e.g.,os(Linux)
) and a list of syntax nodes that are conditionally part of the program. Therefore, the syntax tree captures all the information needed to process the source file for any build configuration.The
SwiftIfConfig
library provides utilities to determine which syntax nodes are part of a particular build configuration. Each utility requires that one provide a specific build configuration (i.e., an instance of a type that conforms to the doc:BuildConfiguration protocol), and provides a different view on essentially the same information:#if
clauses.SyntaxProtocol.removingInactive(in:)
produces a syntax node that removes all inactive regions (and their correspondingIfConfigDeclSyntax
nodes) from the given syntax tree, returning a new tree that is free of#if
conditions.IfConfigDeclSyntax.activeClause(in:)
determines which of the clauses of an#if
is active for the given build configuration, returning the active clause.SyntaxProtocol.isActive(in:)
determines whether the given syntax node is active for the given build configuration.There are a few things I'd still like to do before we can call this "complete":
#if
information by only using swift-driver and swift-syntax. (#if canImport(...)
effectively requires a compiler).