Swift parsing 使い始め
Swift parsing 使い始め
Swiftで独自言語やDSL(ドメイン固有言語)っぽい構文を作りたくて、Point-Freeが公開しているswift-parsingを触り始めました。最初にやったのは、字句解析(トークナイザー)を自作すること。
この記事では、実際に手を動かしながら「空白・演算子・コメント」あたりをどうパースしていくか、簡易トークンパーサを組んでいく中で気をつけたことなどをまとめてみます。
Token型を定義する
まずはSwift上で取り扱いたいトークンをenumで定義します。今回は最低限の構成に絞りました。
public enum Token: Equatable {
case keyword(String)
case arrow(String)
case operatorSymbol(String)
case number(String)
case comment(String)
case identifier(String)
case newline
}
最初から「厳密なASTを構築するぞ」と意気込むと、型設計が肥大化してしまう。
まずは**「正しく切れるか」**に集中したいので、扱いやすさ優先で設計しました。
Whitespaceの扱い
static let skipSpaces = Skip(Whitespace())
Point-Free製のWhitespace()
を使うと、空白・改行・タブなどをいい感じに吸収してくれます。重要なのは、すべてのトークンの前に必ず空白スキップを仕込んでおくこと。
static let keywordParser = Parse {
skipSpaces
OneOf {
"if"
"elseif"
}
}.map(Token.keyword)
こうすると、先頭にいくら空白があってもキーワードを認識してくれるようになります。
コメントをパースする
static let commentBlockParser = Parse {
skipSpaces
"/*"
PrefixThrough("*/")
}.map { Token.comment(String($0)) }
static let commentLineParser = Parse {
skipSpaces
"//"
PrefixUpTo("\n")
}.map { Token.comment(String($0)) }
演算子のパース
記号トークンでよく詰まるのが*
や+
。リテラルとしてパースするだけなら以下でOK。
static let operatorMultiplicationParser = Parse {
skipSpaces
"*"
}.map { Token.operatorSymbol("*") }
ただし、以下のようにまとめてしまうのがおすすめです:
static let operatorParser = OneOf {
"+".map { Token.operatorSymbol("+") }
"-".map { Token.operatorSymbol("-") }
operatorMultiplicationParser
"/".map { Token.operatorSymbol("/") }
"<=".map { Token.operatorSymbol("<=") }
}
OneOf
の順序が大事。たとえば<=
のあとに<
を置くと、先に短いほうにマッチしてしまいます。
実際にトークン列を取る
static func lex(_ source: String) throws -> [Token] {
var input = source[...]
return try Many {
OneOf {
commentBlockParser
commentLineParser
keywordParser
operatorParser
// ...他にも必要なparserを追加
}
}.parse(&input)
}
よくあるつまずきポイント
-
Parse { "*" }
が呼ばれてない → 実は前のparserが全部吸ってる(OneOf
の順序を変えてみよう) - 空白があると失敗する →
Skip(Whitespace())
忘れてるかも - コンパイルは通るけど結果がおかしい →
.debugPrint()
を途中で挟むと動きが見える
Swift Parsing と Swift Syntax の違い・関係
-
Swift Parsing:汎用的なパーサー合成ライブラリ。文字列やバイナリなど「任意の入力列」に対して、自分で定義したルールで構文を組み立てられる。対象の文法は自由。自作DSL、テンプレート言語、構成ファイルパーサなどに向いている。
-
Swift Syntax:Swiftコンパイラ公式の構文解析ライブラリ。Swift言語そのものを構文木として扱える。ソース編集やLSP(コード補完・診断)を扱うレベルのツールに必須。
🧩 ざっくり言えば:
- SwiftParsing → 自分の言語を定義してパースしたいとき
- SwiftSyntax → Swiftそのものを解析・改変したいとき
たとえば swift-format
や SwiftLint
は SwiftSyntax を使っている。
自作DSLを作るなら Swift Parsing のほうが自由度が高く、型安全な構文パーサをゼロから構築できる。
ちなみに SwiftSyntax はパーサとしての柔軟性は低い(Swift以外の言語を解析するには不向き)。一方で Swift Parsing は何にでも使えるが「Swiftっぽい構文」を完全に再現するにはそれなりに組み込みが必要。
Swift Parsing の体験から学んだこと
最初に詰まったのは、これ。
Parse { "*" } → Token.operatorSymbol("*")
が全然呼ばれない!
なぜなら、それより前にマッチするparserが「全部飲み込んで」いたから。
static let operatorParser = OneOf {
"<="
"<"
"*"
}
この場合、"<"
に入った時点でマッチ成功 → "<="
はもう見えない。
同じように、//
があるとき "/"
に先にマッチしてしまうと、コメントとして認識されないこともある。
つまり:
順序、大事。
さらにやっかいなのが「空白でパースに失敗する」という罠。
Whitespace()
をskipしていないparserは、先頭にスペースがあるだけでマッチしてくれません。
.debugPrint()
で見えるもの
そんなとき役に立つのがこれ:
let parser = MyParser().debugPrint()
何がパースされていて、何がスルーされたかが丸見えになります。
swift-parsing
では、パーサーの挙動そのものが値として構築されているので、こうしたインスペクションができるのが楽しいところ。
この段階で、ある種のREPL的な感覚が味わえてきます。
「字句解析」ってこんなに動的だったのか、とちょっとびっくりしました。
おわりに
文字列からトークンを切り出すレイヤーを自分で書くと、「何がどこまでパースされていて、次に何が来るのか」がすごくクリアになります。
SwiftParsingは最初とっつきにくいけど、少しずつ組み合わせていけば複雑な構文も分解できる。トークナイザー部分ができたら、次は構文解析(AST)に進んでいこうと思ってます。
というわけで、字句解析編はひとまず完了。次は「演算式の構文解析」や「ネストしたif文」に挑戦してみます。Swiftで言語を作る旅、もう少しだけ続けてみるつもりです。
Discussion