こんにちは。iOSエンジニアの@kitasukeです。
今回は、Swift Compilerのパーサー内部でlibSyntaxが作成する、StringLiteralExpr
に関する改善を行ったので、その内容を簡単に紹介します。
ちなみに、この変更はlibSyntax
内での変更でありSwiftのString APIへの変更ではありません。
libSyntax
には、Stringリテラル用にStringLiteralExpr
とStringInterpolationExpr
の2種類のシンタックスが定義されています。
しかし、両者のリテラル間で文字列を囲むクオートの扱いに差異がありました。
その結果、生成されたシンタックスから文字列のみを取得する実装が共通化できない問題がありました。
詳しい内容は下記のチケットを参照してください。
https://bugs.swift.org/browse/SR-10241
それでは実際にシンタックスを出力してみましょう。
swift -frontend -emit-syntax
で出力するlibSyntax
のシンタックスツリーは情報量が多いので、今回は代わりにSwiftSyntax
を使います。
SwiftSyntax
はlibSyntax
をSwift APIで提供しているツールです。
SwiftSyntax
の詳細について興味がある方は、こちらの本を参考にしてください。
https://kitasuke.booth.pm/items/1310632
今回はSwift AST Explorerを使ってSwiftSyntax
の出力結果を表示します。
"foo"
を入力した結果は、以下のように文字列がクオートに囲まれてます。
文字列のみ取得したい場合は、自分でクオートを除去する必要があります。
- StringLiteralExpr
- \"foo\"
"foo \(bar)"
を入力した結果は、以下のように文字列とクオートが別トークンとして表現されます。文字列のfoo
を取得したい場合は、StringSegment
の値だけ使えば良いです。
- StringInterpolationExpr
- "
- StringInterpolationSegments
- StringSegment
- foo
- ExpressionSegment
- \
- (
- IdentifierExpr
- bar
- )
- "
上記の比較結果から、文字列とクオートの関係性においてStringInterpolationExpr
の方がより良い構造だと分かります。
したがって、StringInterpolationExpr
の構造をStringLiteralExpr
にも適用できれば問題が解決するはずです。
こちらが現状のStringLiteralExpr
の定義です。
StringLiteralExpr
の要素はStringLiteral
のみです。
utils/gyb_syntax_support/ExprNodes.py
Node('StringLiteralExpr', kind='Expr',
children=[
Child("StringLiteral", kind='StringLiteralToken')
]),
こちらが新しいStringLiteralExpr
の定義です。
StringLiteralExpr
の要素はOpenQuote
, Segments
, CloseQuote
があります。
この新しい定義によって、StringLiteralExpr
とStringInterpolationExpr
を同じ構造にするために、一つのシンタックスに統合されます。
utils/gyb_syntax_support/ExprNodes.py
Node('StringLiteralExpr', kind='Expr',
children=[
Child('OpenQuote', kind='Token',
token_choices=[
'StringQuoteToken',
'MultilineStringQuoteToken',
]),
Child('Segments', kind='StringLiteralSegments'),
Child('CloseQuote', kind='Token',
token_choices=[
'StringQuoteToken',
'MultilineStringQuoteToken',
]),
]),
SwiftSyntax
上の定義の違いはこちらで解説をしています。
興味がある方は参考にしてみてください。
上記の新しい定義を使って"foo"
をSwiftSyntax
で出力すると、下記のようにクオートと文字列が区別されています。
- StringLiteralExpr
- "
- StringLiteralSegments
- StringSegment
- foo
- "
"foo \(bar)"
の出力結果は先程と同じなので省略します。
両リテラルで構造が統一され、より良い階層でトークンが構成されるようになりました。
したがって、どちらのリテラルでも文字列を取得したい場合は、StringSegment
の値を見れば良いです。
詳しい実装内容はこちらのPRをご覧ください。
今回のように小さな変更でも大いに便利になることもあります。 良い機会なのでパーサーの仕組みをもっと理解できるようにソースコードを読んでみようと思いました。
今回のPRは、@rintaroさんが丁寧に色々アドバイスしてくれたおかげなので感謝してます!