SE-0382: Expression Macros

Something that I’m noticing in all of the example macros is a pervasive use of (macro-)runtime verification of arguments. I feel like while doing all this checking explicitly in the macro body allows for maximum flexibility in macro interfaces, there could be some benefit to offering an alternative, more-strongly-typed API. This could be implemented in a similar way to how any Swift type can either define one or more callAsFunction methods with statically typed parameters, or be @dynamicCallable and define a dynamicallyCall method that does validation at runtime. Here’s one way this could work for macros, taking the example WarningMacro:

protocol StaticExpressionMacro: ExpressionMacro {}
extension StaticExpressionMacro {
  // compiler auto-generates this based on your `expandCall` implementations
  static func expansion(of macro: MacroExpansionExprSyntax, in context: inout MacroExpansionContext) throws -> ExprSyntax
}

struct StaticWarningMacro: StaticExpressionMacro {
  static func expandCall(
    // context (+ other args) provided by the compiler go here:
    to macro: MacroExpansionExprSyntax,
    in context: inout MacroExpansionContext,
    // arguments passed by the macro invocation go here
    _ stringLiteral: StringLiteralExprSyntax
  ) throws -> ExprSyntax {
    guard
      // these checks are still necessary once we have the `StringLiteralExprSyntax`
      stringLiteral.segments.count == 1,
      case let .stringSegment(messageString)? = stringLiteral.segments.first
    else {
      throw CustomError.message("#myWarning macro requires a string literal")
    }
    // [same macro implementation]
  }

  // version for value-like macros
  // static func expandReference(to macro: MacroExpansionExprSyntax, in context: inout MacroExpansionContext) throws → ExprSyntax {}
}

// potentially allow more concise declaration since the valid
// overloads can be inferred from the macro struct declaration?
public macro myWarning: StaticWarningMacro

Does this seem like a reasonable approach? I’m not entirely sold on allowing the parameters of the expandCall method to be declared as anything other than ExprSyntax since I imagine most macros will do semantic verification of their arguments rather than plain syntactic verification (but maybe it’s worth it to make very simple macros easier to write?)

Another reason I’m proposing this is that most of the checks in these macros are somewhat untestable — if you only declare a macro as taking one parameter, there’s no way to test that it behaves correctly when passed zero or more than one parameters, so that code would not be covered by unit tests (if the unit tests use the #blah syntax rather than invoking the macro directly).

1 Like