- Proposal: SE-0308
- Author: Rintaro Ishizaki
- Review Manager: Saleem Abdulrasool
- Status: Implemented (Swift 5.5)
- Implementation: apple/swift#35097
- Decision Notes: Rationale
Swift has conditional compilation block #if ... #endif
which allows code to be conditionally compiled depending on the value of one or more compilation conditions. Currently, unlike #if
in C family languages, the body of each clause must surround complete statements. However, in some cases, especially in result builder contexts, demand for applying #if
to partial expressions has emerged. This proposal expands #if ... #endif
to be able to surround postfix member expressions.
For example, when you have some SwiftUI code like this:
VStack {
Text("something")
#if os(iOS)
.iOSSpecificModifier()
#endif
.commonModifier()
}
This doesn’t parse today, so you end up having to do something like:
VStack {
let basicView = Text("something")
#if os(iOS)
basicView
.iOSSpecificModifier()
.commonModifier()
#else
basicView
.commonModifier()
#endif
}
which is ugly and has duplicated .commonModifier()
. If you want to eliminate the duplication:
VStack {
let basicView = Text("something")
#if os(iOS)
let tmpView = basicView.iOSSpecificModifier()
#else
let tmpView = basicView
#endif
tmpView.commonModifier()
}
...which is even uglier.
This proposal expands #if
functionality to postfix member expressions. For example, in the following example:
baseExpr
#if CONDITION
.someOptionalMember?
.someMethod()
#else
.otherMember
#endif
If CONDITION
evaluates to true
, the expression is parsed as
baseExpr
.someOptionalMember?
.someMethod()
Otherwise, it’s parsed as
baseExpr
.otherMember
This proposal adds postfix-ifconfig-expression
to postfix-expression
. postfix-ifconfig-expression
is a postfix-expression followed by a #if ... #endif
clause.
+ postfix-expression → postfix-ifconfig-expression
+ postfix-ifconfig-expression → postfix-expression conditional-compilation-block
postfix-ifconfig-expression
is parsed only if the body of the #if
clause starts with a period (.
) followed by a identifier, a keyword or an integer-literal. For example:
// OK
baseExpr
#if CONDITION_1
.someMethod()
#else
.otherMethod()
#endif
But the following is not a postfix-ifconfig-expression
because it does not start with .
. In such cases, #if ... #endif
is not considered a part of the expression, but is parsed as a normal compiler control statement.
// ERROR
baseExpr // warning: expression of type 'BaseExpr' is unused.
#if CONDITION
{ $0 + 1 } // error: closure expression is unused
#endif
baseExpr // warning: expression of type 'BaseExpr' is unused.
#if CONDITION
+ otherExpr // error: unary operator cannot be separated from its operand
#endif
Also, the body must not contain any other characters after the expression.
// ERROR
baseExpr
#if CONDITION_1
.someMethod()
print("debug") // error: unexpected tokens in '#if' expression body
#endif
There are several kinds of postfix expressions in Swift grammar.
- initializer expression
- postfix self expression
- explicit member expression
- function call expression
- subscript expression
- forced value expression
- optional chaining expression
- postfix operator expression
The body of a postfix #if
expression must start with an explicit member expression, initializer expression, or postfix self expression (that is, the suffixes that begin with .
). Once started this way, you can continue the expression with any other postfix expression suffixes. For example:
// OK
baseExpr
#if CONDITION_1
.someMember?.otherMethod()![idx]++
#else
.otherMethod(arg) {
//...
}
#endif
However, you cannot continue the expression within the #if
with non-postfix suffixes. For example, you cannot continue it with a binary operator, because a binary expression is not a postfix expression:
// ERROR
baseExpr
#if CONDITION_1
.someMethod() + 12 // error: unexpected tokens in '#if' expression body
#endif
Starting with other postfix expression suffixes besides those beginning with .
is not allowed because this would be ambiguous with starting a new statement. These suffixes are generally required to start on the same line as the base expression.
While the body of the #if
clause must begin with .
, the body of any #elseif
or #else
clauses can be empty.
// OK
baseExpr
#if CONDITION_1
.someMethod()
#elseif CONDITION_2
// OK. Do nothing.
#endif
If the clause is not empty, then it has the same requirements as the #if
clause: it must begin with a postfix expression suffix starting with .
, it may not continue into a non-postfix expression, and it must not contain an unrelated statement.
// ERROR
baseExpr
#if CONDITION_1
.someMethod()
#else
return 1 // error: unexpected tokens in '#if' expression body
#endif
#if ... #endif
blocks for postfix expression can be followed by an additional postfix expression including another #if ... #endif
:
// OK
baseExpr
#if CONDITION_1
.someMethod()
#endif
#if CONDITION_2
.otherMethod()
#endif
.finalizeMethod()
Nested #if
blocks are supported as long as the first body starts with an explicit member-like expression. Each inner #if
must follow the rule for postfix-ifconfig-expression
too.
// OK
baseExpr
#if CONDITION_1
#if CONDITION_2
.someMethod()
#endif
#if CONDITION_3
.otherMethod()
#endif
#else
.someMethod()
#if CONDITION_4
.otherMethod()
#endif
#endif
Postfix #if
expressions can be nested inside another expression or statement.
// OK
someFunc(
baseExpr
.someMethod()
#if CONDITION_1
.otherMethod()
#endif
)
This is parsed as someFunc(baseExpr.someMethod().otherMethod())
or someFunc(baseExpr.someMethod())
depending on the condition.
This proposal does not have any source breaking changes.
baseExpr
#if CONDITION_1
.someMethod()
#endif
This is currently parsed as
baseExpr
#if CONDITION_1.someMethod()
#endif
And it is error because CONDITION_1.someMethod()
is not a valid compilation condition. This proposal changes the parser behavior so .someMethod()
is not parsed as a part of the condition. As a bonus, this new behavior applies to non-postfix #if
expressions too. Consequently,
enum MyEnum { case foo, bar, baz }
func test() -> MyEnum {
#if CONDITION_1
.foo
#elseif CONDITION_2
.bar
#else
.baz
#endif
}
Now becomes valid swift code. This change doesn’t break anything because explicit member expressions have always been invalid at the compilation condition position.
This change is frontend-only and would not impact ABI.
This is not an API-level change and would not impact resilience.
Like C-family languages, we could pre-process conditional compilation directives purely in Lexer level as discussed in https://forums.swift.org/t/allow-conditional-inclusion-of-elements-in-array-dictionary-literals/16171/29. Although it is certainly a design we should explore some day, in this proposal, we would like to focus on expanding #if
to postfix expressions.