ここのところ極めて体調が悪い日々でしたが、try! Swift でなんとか発表できました。
この発表は、弊社の有志で毎週水曜 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 さんの以下の記事を頼りになんとか環境を整えられました。
そんなこんなで完成したのが、以下のパッチです。
このパッチを使うと、先ほどの 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"
...
このパッチのポイントは、構文木のそれぞれの struct に CustomReflectable と CustomDebugStringConvertible を準拠させることです。前者は、dump() で表示される要素を指示するためのものです。dump() は内部的に Mirror(reflecting: Any) というリフレクション API を利用して要素の列挙をおこないます(該当コード)。通常であれば、この Mirror(reflecting: Any) は struct や class、enum の stored properties を列挙しますが、対象が CustomReflectable に準拠していると任意の properties を列挙する挙動へと変更されます。これを利用して、構文木の computed properties を列挙するように挙動を変更しました。
ただし、この変更だけではまだ不十分です。なぜなら、 dump() のオブジェクトの名前を表示する欄に不要な文字列が入り込むため依然として読みづらい状態になっているからです。これを解決するために、CustomDebugStringConvertible を利用します。この CustomDebugConvertible は debugDescription というデバッグ用文字列表現のプロパティを指示する protocol で、この debugDescription を変更することで dump() のオブジェクトの名前の表示欄を制御できます。今回は、struct の型の名前を返すように変更しました。
あとは、動作確認のためのテストを書きました。このテストは、Parameterized test という、データを追加するだけでテストケースが増えるような効率的な構造で記述しています。Parameterized test には assertion roulette というどこで失敗したかわかりづらくなるという欠点があるのですが、これは #line を利用することで解決できます。これはかなり有用なテクニックで超おすすめです。
嬉しかったコメント
以下が嬉しかったコメントです。
どうやってつかうんだろう。使いたくなった。 #tryswiftconf
— SatoTakeshi 【4/14技術書典6 け01 Fluid Interface本】 (@hatakenokakashi) March 22, 2019
異常な分かりやすさ#tryswiftconf
— 𝙣𝙤𝙥𝙥𝙚 (@noppefoxwolf) March 22, 2019
めちゃくちゃ分かりやすい #tryswiftconf
— kariad/かりあど@技術書典6-け11 (@kariad_uu) March 22, 2019
相変わらず展開が上手いなぁ… #tryswiftconf
— みやび (@miyabi099) March 22, 2019
kuniwak さんの発表はいつも本当にわかりやすい&スライド見やすい#tryswiftconf
— while true { ta_ka_tsu } (@ta_ka_tsu) March 22, 2019
SwiftSyntaxの説明すんごいわかりやすかった('ω') #tryswiftconf
— クウルス@🎹えれぴこ!🎮 (@Qoo_Rus) March 22, 2019
コードと構文木のもっと詳しい話、 @orga_chem さんという人が解説してるので大変参考になりました
— takasek (@takasek) March 22, 2019
事前知識なしで理解する、静的検査のいろは / Introduction about static analysis without previous knowledge - Speaker Deckhttps://t.co/4p1xtJVLfc#tryswiftconf pic.twitter.com/YGSqmDbtys
説明がわかりやすくてできそうって感じる #tryswiftconf
— しゅんくん@すうぃふとや🐹 (@shunkun_san) March 22, 2019
SwiftSyntaxと構文木の説明、めちゃくちゃわかりやすいしスライドがすごく綺麗で惚れる... #tryswiftconf
— 🍺 (@yzkiyuto) March 22, 2019
Great presentation by @orga_chem about how to leverage swift-syntax for better productivity in @tryswiftconf . https://t.co/Y5rFBPdnOE
— Xi Ge (@xge_apple) March 22, 2019
Agreed, this was really well done!
— Marshall Elfstrand (@llahsram) March 22, 2019
Oooh, uses the new parser API 😀👏
— Argyrios Kyrtzidis (@akyrtzi) March 22, 2019