Many other mainstream languages these days, outside of C/C++/Objective-C, don't have a preprocessor that allows for the exclusion of arbitrary regions of text. The closest ones to Swift that I'm remotely familiar with that offer a form of conditional compilation are:
C#
C# has #if
-style conditional compilation directives that fall somewhere in the middle of C's and Swift's. The untaken branches aren't stripped out completely by a true preprocessor pass; instead, their parsing API appears to take a set of compilation conditions as its input so that it can parse only the branches that are taken into nodes. But the #if
directives themselves are preserved in the parsed AST, and the untaken branches are represented as trivia (unparsed raw text).
So, C# allows bizarre constructs like this that Swift would forbid, because it only actually parses the branches taken (notice how Func
wouldn't be syntactically correct if NOT_DEFINED
was defined):
using System;
public class Program
{
public static void Main()
{
double x = 100.0;
Func(
#if NOT_DEFINED
x +
#else
x -
#endif
10.5);
}
public static void Func(
#if NOT_DEFINED
int x
) {}
#else
double x
#endif
) { Console.WriteLine(x); }
}
Rust
I'm far less familiar with Rust, but it looks like it offers a couple different approaches to conditional compilation:
- A
#[cfg()]
attribute, which tells the compiler to ignore the language element it's attached to.
- A
cfg!
macro that evaluates to true/false at compile-time.
The attribute can handle situations similar to Swift wrapping an entire declaration in #if/#endif
, but also applies to lower-level language elements. For example, it supports array literal exclusion:
fn main() {
println!(
"{:?}",
[1, 2,
#[cfg(target_os = "macos")]
3,
4, 5]);
// prints [1, 2, 4, 5] on a non-Mac system
}
By only being a prefix instead of wrapping the beginning and end of an element, the Rust solution appears to skirt the issue of "should a trailing comma go inside or outside the #if
".
It also works nicely for the example @gwendal.roue linked to, although I'm not sure if it's possible to conditionalize the entire function signature—you can conditionalize individual arguments though, which works better in their example anyway:
fn blah(
#[cfg(target_os = "macos")]
x: SomeType,
#[cfg(not(target_os = "macos"))]
x: SomeDifferentType
) {}
While the previous example highlighted an advantage of Rust's approach being a prefix instead of wrapping, this example highlights a drawback; the condition has to be repeated and inverted instead of just using #else
.
But what's also really interesting here is that since this is just a parameter list where each parameter has an attribute attached, rather than excluding a wrapped region of code, the first one must be terminated by a comma, but the second one may not be.
Separately, the cfg!
macro can be used in expression contexts, but AFAICT it just evaluates to a true/false boolean, so both branches of an if/else
have to be syntactically and semantically valid. For example, you can't mix types:
fn main() {
println!("{:?}", if cfg!(target_os = "macos") {
50
} else {
"x"
// ^^^ expected integer, found `&str`
});
}
Unfortunately, I have no idea what Rust's standard parsing solution is (if it has one), so I don't know if/how these elements are reflected in their syntax tree.
Also, someone please correct me if I've misspoken about any of Rust's capabilities here.
Despite the similarities in C#'s overall parsing and syntax tree API, their approach would lose the benefits that @Douglas_Gregor and @akyrtzi mentioned—being able to parse the entire file irrespective of build conditions is a major benefit, and it makes tools like swift-format possible at all (in their current implementation).
Having researched Rust's approach some more, I'm really fond of it, but I imagine it's a non-starter for Swift since it would be too much of a departure from the #if
syntax we already have. But conceptually, it seems like it solves the problems that folks here want solved. I think @Douglas_Gregor's suggestion of just identifying the places in the grammar where #if
s make sense, and then figure out how to adapt it to a model based on open/close delimiters instead of being prefix-only-based.
Then, it's a matter of figuring out how to update the SwiftSyntax APIs to make it easier to peer through the #if
s to get at the actual nodes for each branch. Right now, if a node can be optionally surrounded by an #if
, its type degrades to the base Syntax
type, and you have to do runtime type-checking/casting to figure out what it actually is. This isn't ideal because losing the strong typing makes it hard to reason about what the tree content is; you can't guarantee that you've covered every possibility exhaustively. If we went this route, then maybe there's a way to represent this with a generic container instead—a IfConfigsAreAllowedHereContainer<ActualNodeType>
wrapper that provides accessors for all of its branches, or for the single "null" branch if it's not actually an #if
but just regular language elements.