若くない何かの悩み

何かをdisっているときは、たいていツンデレですので大目に見てやってください。

try! Swift Tokyo 2019 で発表した「SwiftSyntax で便利を実現する基礎」

ここのところ極めて体調が悪い日々でしたが、try! Swift でなんとか発表できました。

speakerdeck.com

この発表は、弊社の有志で毎週水曜 19:00 - 開催している SwiftWednesday という勉強会での活動からきています。この SwiftWednesday には「Swift へ何かの形で貢献したい」というふんわりとした目標がありました。そこで、たまたま構文解析を得意としていた私が「SwiftSyntax でなんかやってみますか」と提案して SwiftSyntax をモブプロ形式で読み解く会が不定期で始まりました。私自身も SwiftSyntax はまったく知らない状態からのスタートだったので、勉強会中にプロジェクタを映しながら皆で README やコードを解読していきました。この過程で、今回発表したような SwiftSyntax の使い方を学びました。

そして、この過程で SwiftSyntax で生成される構文木dump() があまりに読みづらく、説明 & デバッグに苦労するということがわかりました。というのも、構文木struct で本来見るべき properties はすべて computed で定義されており、通常は stored properties のみを列挙する dump() ではあまりデバッグの役に立たない表示がされていたのです(以下が具体例):

▿ // A closure without a signature. The test will ensure it stays the same after
// applying a rewriting pass.
let x: () -> Void = {}
  ▿ data: SwiftSyntax.SyntaxData
    - parent: nil
    ▿ absoluteRaw: SwiftSyntax.AbsoluteRawSyntax
      - raw: // A closure without a signature. The test will ensure it stays the same after
// applying a rewriting pass.
let x: () -> Void = {} #0
        ▿ super: Swift.ManagedBuffer<SwiftSyntax.RawSyntaxBase, Swift.UInt64>
          ▿ header: SwiftSyntax.RawSyntaxBase
      ...

とりあえずは @k_katsumi さん作の Swift AST Explorer で難を乗り越えたのですが、ふと「これは貢献のチャンスでは?」と気づきました。

そして、SwiftWednesday の SwiftSyntax 編はこの貢献のパッチを送るということへ具体的な目標を改めました。パッチを送るには、修正方法を検討するだけでなく、実際に動作させることが重要になります。しかし、ちょうどこの時期に SwiftSyntax は内部的な構文解析器を lib_InternalSwiftSyntaxParser.dylib へ移行するといった変更がおこなわれている真っ最中であり、ビルド環境の構築にはかなり苦労しました。最終的には @rintaro さんの以下の記事を頼りになんとか環境を整えられました。

qiita.com

そんなこんなで完成したのが、以下のパッチです。

github.com

このパッチを使うと、先ほどの dump() の結果が次のようになります:

▿ SwiftSyntax.SourceFileSyntax
  ▿ statements: SwiftSyntax.CodeBlockItemListSyntax
    ▿ SwiftSyntax.CodeBlockItemSyntax
      ▿ item: SwiftSyntax.VariableDeclSyntax
        - attributes: nil
        - modifiers: nil
        ▿ letOrVarKeyword: SwiftSyntax.TokenSyntax
          - text: "let"
          ▿ leadingTrivia: SwiftSyntax.Trivia
            ▿ pieces: 4 elements
              ▿ TriviaPiece
                - lineComment: "// A closure without a signature. The test will ensure it stays the same after"
                ...

このパッチのポイントは、構文木のそれぞれの structCustomReflectableCustomDebugStringConvertible を準拠させることです。前者は、dump() で表示される要素を指示するためのものです。dump() は内部的に Mirror(reflecting: Any) というリフレクション API を利用して要素の列挙をおこないます(該当コード)。通常であれば、この Mirror(reflecting: Any)structclassenum の stored properties を列挙しますが、対象が CustomReflectable に準拠していると任意の properties を列挙する挙動へと変更されます。これを利用して、構文木の computed properties を列挙するように挙動を変更しました

ただし、この変更だけではまだ不十分です。なぜなら、 dump() のオブジェクトの名前を表示する欄に不要な文字列が入り込むため依然として読みづらい状態になっているからです。これを解決するために、CustomDebugStringConvertible を利用します。この CustomDebugConvertibledebugDescription というデバッグ用文字列表現のプロパティを指示する protocol で、この debugDescription を変更することで dump() のオブジェクトの名前の表示欄を制御できます。今回は、struct の型の名前を返すように変更しました

あとは、動作確認のためのテストを書きました。このテストは、Parameterized test という、データを追加するだけでテストケースが増えるような効率的な構造で記述しています。Parameterized test には assertion roulette というどこで失敗したかわかりづらくなるという欠点があるのですが、これは #line を利用することで解決できます。これはかなり有用なテクニックで超おすすめです。

嬉しかったコメント

以下が嬉しかったコメントです。