Download - Introduction to PEG
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 / VV ← "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 / VV ← "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 MA ← 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/parboiled2
Scalaマクロベースのジェネレータ
Scalaコードの一部として文法を記述
LPeg Lua用
http://www.inf.puc-rio.br/~roberto/lpeg/
文字列とのパターンマッチングライブラリ
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とカバーする範囲が重複する