Swift Digest
SE-0397 | Swift Evolution

Freestanding Declaration Macros

Proposal
SE-0397
Authors
Doug Gregor, Richard Wei, Holly Borla
Review Manager
John McCall
Status
Implemented (Swift 5.9)
Vision
Macros

01 何が問題だったのか

SE-0382 で導入された式マクロ(expression macro)は、#名前(...) の形で呼び出されてその場の「式」に展開される仕組みでした。@freestanding(expression) 属性で宣言し、マクロ実装は与えられた構文木を別の構文木に書き換えて、展開位置に式として差し込みます。

しかし、# 構文で呼び出される類似の機能の中には、式ではなく宣言を生成したいものも存在します。たとえばSE-0196 で導入された #warning / #error ディレクティブは、式でも文でもなく「宣言が書ける場所」に置かれてコンパイル時に診断を出すコンパイラ組み込み機能として実装されていました。同様のパターンで、

  • テンプレート文字列から複数の型や関数などの宣言を生成する(gyb のような用途をマクロに置き換える)
  • JSONなどのサンプル文字列を解析して、対応するデータモデルの struct 群を宣言する

といった「宣言を生み出すライブラリ」を書きたくなりますが、式マクロは定義上「式の位置」にしか展開できないため、これらのユースケースはカバーできませんでした。結果として、#warning / #error はコンパイラ側の特別扱いを続けるしかなく、利用者側がマクロとしてそれに相当する拡張を提供する手段もありませんでした。

02 どのように解決されるのか

式マクロと同じ # 構文を使いつつ、展開結果が「ゼロ個以上の宣言」となるマクロ役割 @freestanding(declaration) を追加します。これにより、#warning / #error をはじめ、テンプレートからの宣言生成やJSONからのデータモデル生成といった用途をマクロとしてライブラリ化できるようになります。

宣言マクロの宣言

宣言マクロは、式マクロと同じく macro キーワードで宣言し、役割を @freestanding(declaration) で示します。マクロが導入する名前を指定する必要があれば、SE-0389 と同じ names: 引数を添えます。宣言マクロに対しては、名前の指定方法として named(...)arbitrary のみが使えます(overloaded / prefixed / suffixed は、元となる宣言が無いため意味がありません)。

// 名前を導入しないケース(#warning / #error など)
@freestanding(declaration)
macro warning(_ message: String) =
    #externalMacro(module: "MyMacros", type: "WarningMacro")

// CodingKeys という固定名の enum を導入するケース
@freestanding(declaration, names: named(CodingKeys))
macro codingKeys(...) = ...

宣言マクロは、関数やクロージャの本体、トップレベル、型定義やextensionの中など、宣言が書ける場所ならどこでも使えます。戻り値は持たず、値を生みません。

宣言マクロの実装

実装は DeclarationMacro プロトコルに適合する型で提供します。FreestandingMacro を介して Macro に連なる点は式マクロと共通で、expansion(of:in:) が返す型だけが [DeclSyntax](宣言の構文木のリスト)になっています。

public protocol DeclarationMacro: FreestandingMacro {
    static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax]
}

たとえば #warning 相当のマクロ実装は次のように書けます。引数から文字列リテラルを取り出し、MacroExpansionContextdiagnose で警告を出したうえで、宣言は1つも生成しないので空配列を返します。

public struct WarningMacro: DeclarationMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let messageExpr = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self),
              messageExpr.segments.count == 1,
              let firstSegment = messageExpr.segments.first,
              case let .stringSegment(message) = firstSegment else {
            throw SimpleError(node, "warning macro requires a non-interpolated string literal")
        }

        context.diagnose(Diagnostic(node: Syntax(node), message: SimpleDiagnosticMessage(
            message: message.description,
            diagnosticID: .init(domain: "test", id: "error"),
            severity: .warning)))
        return []
    }
}

利用側は、これまでの #warning と同じ感覚で書けます。

#warning("unsupported configuration")

属性・修飾子の適用

宣言マクロ展開には、通常の宣言と同様に属性や修飾子を前置できます。前置した属性・修飾子は、マクロが生成した各宣言すべてに自動的に適用されます。たとえば、次のようなテンプレート展開マクロ呼び出しに @availablepublic を付けると、

@available(toasterOS 2.0, *)
public #gyb(
    """
    struct Int${0} { ... }
    struct UInt${0} { ... }
    """,
    [8, 16, 32, 64]
)

生成される複数の struct それぞれに @available(toasterOS 2.0, *) public が付与された形に展開されます。

パース規則と制約

トップレベルや関数スコープのように式も宣言も書ける場所では、#foo(...) はまずマクロ展開としてパースされます。そのマクロが宣言マクロに解決された場合に、型検査の段階でマクロ展開宣言に置き換えられます。これは #line + 1#line as Int? のように、マクロ展開式が部分式になるケースと曖昧にならないようにするための規則です。そのため、宣言マクロが式の中に現れていて最外側の式でない場合はエラーになります。

また、1つのマクロ宣言が持てる freestanding な役割は1つだけで、@freestanding(expression)@freestanding(declaration) を同じマクロに重ねることはできません。

@freestanding(expression)
@freestanding(declaration) // error: a macro cannot have multiple freestanding macro roles
macro foo()

宣言マクロが生成できる宣言の種類や、マクロが導入する名前の可視性、arbitrary 名に対する制約などは、attached peer macro(SE-0389)と同じルールに従います。

使いどころの例

#warning / #error

SE-0196#warning / #error は、単なる宣言マクロとして書き直せるようになります。

@freestanding(declaration) macro warning(_ message: String)
@freestanding(declaration) macro error(_ message: String)

テンプレートによるコード生成

標準ライブラリで使われている gyb ツールのような「テンプレートから大量のボイラープレートを生成する」処理を、別ビルドフェーズを挟まずにマクロとしてインラインに書けます。

@freestanding(declaration, names: arbitrary)
macro gyb(String, [Any]) = #externalMacro(module: "MyMacros", type: "GYBMacro")

#gyb(
    """
    public struct Int${0} { ... }
    public struct UInt${0} { ... }
    """,
    [8, 16, 32, 64]
)

データモデルの生成

サンプルのJSONを与えると、それに対応する Codablestruct 群を自動生成するような使い方もできます。

@freestanding(declaration, names: arbitrary)
macro jsonModel(String) = #externalMacro(module: "MyMacros", type: "JSONModelMacro")

struct JSONValue: Codable {
    #jsonModel("""
    "name": "Produce",
    "shelves": [
      {
        "name": "Discount Produce",
        "product": {
          "name": "Banana",
          "points": 200,
          "description": "A banana that's perfectly ripe."
        }
      }
    ]
    """)
}

この展開結果として、JSONValue の中に name / shelves や、ネストされた Shelves / Product といった struct が定義されます。

Future Directions(speculative)

本Proposalの宣言マクロは、展開結果を「宣言」に限定しています。今後の拡張方向として、宣言・文・式を混ぜて生成できる code item macro@freestanding(codeItem))が示されています。関数やクロージャの本体、トップレベルに展開され、たとえば関数の入口と出口でログを出すようなユーティリティをマクロとして書けるようになる想定です。code item macro は一意な名前のみ導入でき、名前付きの宣言は導入できないなどの制約が議論されていますが、本Proposal時点では実験的機能の段階で、正式採用は別途の提案に委ねられます。