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