Macro expression with compiler control statements

Hi,

I was wondering if it is possible to incorporate compiler directives into the expression syntax. This could greatly reduce the amount of repetitive code caused by compiler control statements or preprocessor macros (e.g. #if DEBUG statements). However, Xcode currently is unable to evaluate compiler control statements that are expanded from macros, resulting in the following error:

Error: Expected macro expansion to produce an expression

One potential workaround is to directly apply the compiler control statements within the macro. However, the conditional compilation behavior will not be visible at call site in this case, leading to ambiguous code generations.

Example: The following macro appends a Swift version suffix to a given string based on the compiler version.

/// A macro that adds a suffix to the given string.
@freestanding(expression)
public macro addSuffix(_ string: String) -> String = #externalMacro(module: "StringSuffixMacros", type: "AddSuffixMacro")

public struct AddSuffixMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        guard let argument = node.argumentList.first?.expression,
              let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
              segments.count == 1,
              case .stringSegment(let literalSegment)? = segments.first
        else {
            fatalError("#addSuffix requires a string literal")
        }
        
        // Xcode fails to evaluate the expanded macro
        let exprSyntax: ExprSyntax =
            """
            #if compiler(>=6)
            "\(raw: literalSegment.content.text)_Swift6"
            #elseif compiler(>=5)
            "\(raw: literalSegment.content.text)_Swift5"
            #else
            "\(raw: literalSegment.content.text)_LegacySwift"
            #endif
            """
        
        // Workaround
        let exprSyntax: ExprSyntax
        #if compiler(>=6)
        exprSyntax = "\"\(raw: literalSegment.content.text)_Swift6\""
        #elseif compiler(>=5)
        exprSyntax = "\"\(raw: literalSegment.content.text)_Swift5\""
        #else
        exprSyntax = "\"\(raw: literalSegment.content.text)_LegacySwift\""
        #endif
        
        return exprSyntax
    }
}
1 Like

The issue here is that the syntax tree you create must be a valid substitution for the macro invocation. In pretty much all locations except for single-expression statements, the grammar doesn't allow for #if at arbitrary syntax positions. So for example, let x = #addSuffix("y") would produce

let x = #if compiler(>=6)
  "..."
#elseif // and so on

which isn't valid Swift.

One thing you can try instead is to wrap the whole expression in an immediately-called closure, which would let you nest the #if directives inside the body:

let exprSyntax: ExprSyntax =
  """
  {
    #if compiler(>=6)
    "\(raw: literalSegment.content.text)_Swift6"
    #elseif compiler(>=5)
    "\(raw: literalSegment.content.text)_Swift5"
    #else
    "\(raw: literalSegment.content.text)_LegacySwift"
    #endif
  }()
  """
3 Likes

That explains about #if, but I don't understand why a macro that returns, as an ExprSyntax string:

while true {}

fails to compile, with an "Expected macro expansion to produce an expression" error, but (as you so helpfully suggest):

{
    while true {}
}()

compiles (and runs) just fine. The macro expands to exactly what I want it to expand to, a while loop, and yet the compiler balks. I mean, what could be more of an "expression" than a while loop?

A while loop is a statement, not an expression. Expressions have types, and they can be used in more syntactic positions than statements, which don't have types and usually represent control flow.

The line is blurred somewhat these days now that Swift supports if and switch expressions, but loops are still unequivocally statements.

2 Likes