Introduction to PEG
構文解析友の会
水島 宏太
背景
 多様な入力文字列を構文解析する必要性
 (色々なフォーマットの)設定ファイル
 Webのクローリング
 "Cargo cult parsing" (from Yacc is dead)
の流行
 Googleで検索して正規表現を拾ってきて、テキトウにコピー
&ペーストしてパーザを作ること
 「だいたいの入力に対してそれなりにうまく働く」不完全なパー
ザ
 Cargo cult parsingを追放せよ!
構文解析って何?
 一言でいうと:
 入力文字列を木構造(Abstract Syntax Tree)
に組み立てる捜査
 二種類に分けられる
 自然言語の構文解析
 構文解析の結果あいまい性が生じることがある
 非自然言語の構文解析 ←今回扱うもの
 構文解析の結果あいまい性が生じない
様々な構文解析アルゴリズム
 CYK - O(n3)
 (S|LA)?LR(k) - O(n)
 LL(k) - O(n)
 LL(*) - O(n)
 GLR (Generalized LR) - O(n3)
 GLL (Generalized LL) - O(n3)
 PEG(or Packrat Parsing) - O(kn) (O(n))
一長一短がある
構文解析器をどうやって書く?
 手書き
 どんな文法でも大体作れる
 文法のメンテナンス/デバッグが大変
 パーザジェネレータ
 文法定義からパーザを自動生成
 パーザコンビネータ
 プログラミング言語のDSLとしてパーザを組み立
てるものを用意する
 ホスト言語のデバッグ環境をそのまま用意できる
YACC (LALR(1))
 最もメジャーなパーザジェネレータ
 Rubyを初めとする様々な言語で採用
 上位互換のbisonが一般に採用される
 空白の有無等で意味が変わる文法は苦手
 空白は通常字句解析で処理するため
 トークンが再帰的な構造を含む文法は苦手
 式埋め込み可能な文字列リテラル(Ruby)
 "1+2 = #{1+2}"
 ヒアドキュメント
JavaCC (LL(k) + α)
 Javaでメジャーなパーザジェネレータ
 構文木の生成がちょっと楽
 jjtree(プリプロセッサ)を使用した場合
 空白の有無で意味が変わる文法は苦手
 理由はyaccと同じ
 トークンが再帰的な構造を含む文法は苦手
 理由はyaccと同じ
何が言いたいか
 一般的な構文解析アルゴリズムでは字句解析
と構文解析の分離が前提
 プログラミング言語用の字句解析器の限界
 字句解析器は通常正規表現によって記述
 再帰的な構造をうまく扱えない
 パーザの文脈によってトークンの切り出し方が変わる
ような文法は苦手
そこでPEG(Ford04)ですよ
 プログラミング言語等の文法の表記法
 BNFと目的は類似
 構文解析アルゴリズムの一種とも言える
 決定的な任意のLR(k)言語や一部の文脈依存言
語を扱える
 構文規則の集合
 (N ← e)*
 N: 構文規則名
 e: 式(Parsing Expression)
Parsing Expression(1)
 N : 規則(非終端記号)の参照
 "a" : 文字列a
 ε : 空文字列
 . : 任意の一文字
 [...] : 文字クラス
 e1 e2 : e1とe2の並び
 e1 / e2 : e1を試し、失敗したらe2を試す
 e1 / e2 ≠ e2 / e1
Parsing Expression(2)
 e? : 0回または1回の繰り返し
 e* : 0回以上の繰り返し
 e+ : 1回以上の繰り返し
 &e : And-predicate
 eがマッチしたら成功。入力を消費しない
 !e : Not-predicate
 eがマッチしなければ成功。入力を消費しない
シンプル!
Desugaring Parsing Expression
 e? : (e / ε)
 e* : N'; N'← e N' / ε
 e+ : e e*
 !e : !!(e)
PEGの解釈
 入力に対して式がマッチするかを判定する
 結果の値
 マッチに成功した場合: 「消費」した文字列
 失敗した場合: f(失敗したことを意味する値)
 例: (式, 入力) → 結果 という形とする
 ("a", "ab") → "a"
 ("b", "ab") → f
 (&"a", "ab") → ""
 ("a"/"b", "ab") → "a"
 (.*, "ab") → "ab"
PEGの例
 Eが表している言語
 a,b,cの文字と+から成る算術式
 a,a+b,a+b+c,a+c,a+b+c+a, ...
E ← V "+" E / V
V ← "a" / "b" / "c"
PEGで嬉しいこと
 無限長の先読みが可能
 字句解析不要→柔軟な文法が記述し易い
 String interpolation, Here document等
 曖昧性が無い
 文法のconflictが起きない
 C言語のif文のPEGによる記述
 if_stmt ← IF LP expr RP stmt (ELSE stmt)?
PEGで嬉しくないこと
 PEGの利点と表裏一体
 空白の読み飛ばしなどを明示する必要がある
 通常は字句解析で空白が処理される
 マクロを導入すれば軽減できる
 順序を入れ替えると意味が変わってしまう
 if_stmt ← IF LP expr RP stmt
/ IF LP expr RP stmt ELSE stmt
 elseを含む文が解析できないPEG
PEGパーザの実装(関数型)
 各構文規則を純粋な関数として実装
 入力: 入力文字列
 出力: 成功したかどうか
 成功した場合は残りの文字列も一緒に返す
 非終端記号の参照を関数呼び出しとして実装
PEGパーザの実装(関数型)
規則 V ← "a" V / "c"の実装を考える
def parse_V(input)
r1 = match("a", input)
if r1.succeed?
r2 = parse_V(r1.output)
if r2.succeed? then r2 else match("c", input) end
else
match("c", input)
end
end
PEGパーザの実装(手続き型)
 パーザが状態を持つ
 入力文字列
 今何文字目を解析しているか(カーソル)
 構文規則を副作用のある関数として実装
 入力: 無し
 出力: 成功したかどうか
 関数の中でカーソルを破壊的に更新
PEGパーザの実装(手続き型)
規則 V ← "a" V / "c"の実装を考える
def parse_V
backup_pos = current_pos
if match("a")
if parse_V then return true
rewind(backup_pos); return match("c")
else
return match("c")
end
end
Packrat Parsing(Ford02)
 PEGをベースにした構文解析アルゴリズム
 入力の長さに対して線形時間で解析可能
 実用的
 アルゴリズムが非常に単純
 Backtrack parsing + メモ化
 プログラマが挙動を理解しやすい
メモ化(memoize)
 ≠memorize
 一度計算した関数の結果を記憶しておく
 同じ引数で呼び出された場合、結果を再利用
 原則的に副作用の無い関数にしか使えない
 副作用があるとメモ化した場合に結果が異なる
フィボナッチ関数
 再帰的な定義:
 fib(n) = 1 if n = 1 or 2
 fib(n) = fib(n – 1) + fib(n – 2)
 定義にしたがって計算すると指数関数時間
メモ化されたフィボナッチ関数
 同じ引数に対して、計算結果を再利用
 fib(n) = m[n] if m[n] != null
 fib(n) = m[n]:=1 ; m[n] if n = 1 or 2
 fib(n) = m[n]:=fib(n–1)+fib(n–2); m[n]
 入力の大きさに対して線形時間
Backtrack parsing
E ← V "+" E / V
V ← "a" / "b" / "c"
E(1)
V(1)
"a"
"+" E(3)
V(3)
"a" "b"
V(3)
"a" "b"
"+"
 式: E, 入力:"a+b"
同じ計算を二度行う
Packrat parsing
E ← V "+" E / V
V ← "a" / "b" / "c"
E(1)
V(1)
"a"
"+" E(3)
V(3)
"a" "b"
"+"
 式: E, 入力:"a+b"
Vの解析結果を再利用
Backtrack Parsingと
Packrat Parsingの性能比較
 次の文法の言語を解析する時の性能を比較
 Backtrackが頻発する入力を与える
 入力: '(1)', '((1))', '(((1)))', …
E ← M
M ← A Spacing MS / A
MS ← "*" Spacing M / "/" Spacing M / "%" Spacing M
A ← P Spacing AS / P
AS ← "+" Spacing A / "-" Spacing A
P ← Number / "(" Spacing E Spacing ")"
Backtrack Parsingと
Packrat Parsingの性能比較
Packrat Parsingの欠点
 必要な記憶領域が多い
 入力文字列の長さに対して線形の記憶領域
 大規模なファイルの解析には向かない
 実行効率がイマイチ
 バックトラックやメモ化が影響
 ほとんどのメモ化は無駄になっている可能性
PEG/Packrat Parser Generator
 PEGが提案されて10年以上
 様々なものがある
 Rats! (Java)
 Parboiled2 (Scala)
 Treetop (Ruby)
 Lpeg (Lua)
 PEG.js (JavaScript)
Rats!
 Javaのソースコードを生成
 http://cs.nyu.edu/rgrimm/xtc/
 (状態つき)Packrat Parserを生成
 文法定義をモジュールに分割可能
 ASTの自動生成
Parboiled2
 Scala用
 https://github.com/sirthias/parboiled
2
 Scalaマクロベースのジェネレータ
 Scalaコードの一部として文法を記述
LPeg
 Lua用
 http://www.inf.puc-rio.br/~roberto/lp
eg/
 文字列とのパターンマッチングライブラリ
Treetop
 Ruby用のPEGパーザジェネレータ
 http://treetop.rubyforge.org/
 Rubyプログラムに似たシンプルな文法記述
 既存のgrammarを取り込んで新しいパーザ
を合成できる
PEG.js
 JavaScript用
 http://pegjs.org/
 Node.js対応
PEGに関する有名な未解決問題
 PEL ⊃ CFL or not
 PEGはいくつかの文脈依存言語を表現可能
 a^n b^n c^n
 CFGでは表現できない
 では、逆は?
 不明
 回文がそれに相当するのではないかと考えられて
いる
PEGに関するその他の研究
 左再帰の導入(Warth et al, 2007)
 PEGの仮想機械(Medeiros et al, 2008)
 カットおよびカット自動挿入アルゴリズムの導
入 (Mizushima, et al., 2010)
 LL(*)アルゴリズム (Parr, et al., 2011)
 PEGとカバーする範囲が重複する


Introduction to PEG