😊

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-formatSwiftLint は 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