SE-0308: `#if` for postfix member expressions
* Proposal: [SE-0308](0308-postfix-if-config-expressions.md) * Author: [Rintaro Ishizaki](https://github.com/rintaro) * Review Manager: [Saleem Abdulrasool](https://github.com/compnerd) * Status: **Implemented (Swift 5.5)** * Implementation: [apple/swift#35097](https://github.com/apple/swift/pull/35097) * Decision Notes: [Rationale](https://forums.swift.org/t/accepted-se-0308-postfix-if-config-expressions/47780)
Introduction
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.
Motivation
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.
Proposed solution
This proposal expands #if functionality to postfix member expressions. For example, in the following example:
baseExpr
#if CONDITION
.someOptionalMember?
.someMethod()
#else
.otherMember
#endifIf CONDITION evaluates to true, the expression is parsed as
baseExpr
.someOptionalMember?
.someMethod()Otherwise, it’s parsed as
baseExpr
.otherMemberDetailed design
Grammar changes
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
#endifAlso, 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
#endifExpression kind inside #if/#elseif/#else body
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) {
//...
}
#endifHowever, 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
#endifStarting 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.
#elseif/#else body
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.
#endifIf 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
#endifConsecutive postfix #if expressions
#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
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
#endifPostfix #if expression inside another expression
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.
Source compatibility
This proposal does not have any source breaking changes.
baseExpr
#if CONDITION_1
.someMethod()
#endifThis is currently parsed as
baseExpr
#if CONDITION_1.someMethod()
#endifAnd 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.
Effect on ABI stability
This change is frontend-only and would not impact ABI.
Effect on API resilience
This is not an API-level change and would not impact resilience.
Alternatives considered
Lexer based #if preprocessing
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.