You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
cpp-peglib/tutorial/chap_02.md

154 lines
8.5 KiB

# はじめてのPEG
PEGは言語の文法を定義するための言語で、2004年に[Bryan Ford][Link_Ford]によって発表されました。記法はBNFに似ていますが、言語の構文を定義するだけでなく字句の定義も同一のファイルでに含めることができます。
PEGの文法ファイルは「構文規則名(N) <- (e)」で表される複数の構文規則で構成されます例えば簡単な日付フォーマットはこんな感じで定義できるでしょう
```
# Ex) 12:34 p.m.
DATE <- NUMBER ':' NUMBER ' ' SUFFIX
NUMBER <- [0-9]+
SUFFIX <- 'a.m.' / 'p.m.'
```
PEGパーサーはこの文法ファイルの開始規則DATEに対して入力テキストがマッチするか判定します式の中で他の規則が参照されている場合再帰的にマッチを試みます
式には次のようなものがあります
* `N`構文規則の参照
* `'...'`または`"..."`(文字列
* `.`任意の文字
* `[...]`文字クラス。[a-zA-Z0-9-_])
* `e1 e2`連続
* `e1 / e2`優先順位付き選択。`e1`が失敗した時のみ`e2`を試みる
* `e*`0回以上の繰り返し
* `e+`1回以上の繰り返し
* `e?`オプション
* `(e)`グルーピング
* `&e`肯定先読み
* `!e`否定先読み
ではもう少し複雑な例を見てみましょう
## CSVフォーマットの定義
CSVには様々な方言があるので今回は以下の仕様を満たすものとします
* CSVファイルは一つ以上フィールドでなる一つ以上の行の集まり
* フィールドはダブルクォート無しと有りある
* ダブルクォート無しのフィールドにはカンマとダブルクォートを含むことができない
* ダブルクォート有りのフィールドにはダブルクォート以外の全ての文字と連続したダブルクォートを含むことができる
文法は以下のようになります
```
CSV <- RECORD (NL RECORD)* NL?
RECORD <- FIELD (',' FIELD)*
FIELD <- DQ_FIELD / NO_DQUOTE_FIELD
NO_DQUOTE_FIELD <- (![,"\r\n] .)*
DQ_FIELD <- '"' (!["] . / '""')* '"'
NL <- '\r\n' / '\r' / '\n'
```
個々の規則をボトムアップで見ていきましょう最初はダブルクォート無しのフィールドです
NO_DQUOTE_FIELD <- (![,"\r\n] .)*
この規則はカンマダブルクォート改行以外の任意の文字の0回以上の繰り返しを意味します規則を構成する個々の要素を見てみましょう
`.`は任意の文字にマッチします。`[,"\r\n]`はカンマダブルクォート改行文字のいずれかにマッチします外側の`()*`は0回以上の繰り返しを意味しますどれも正規表現と全く同じですね
`!`否定先読み演算子で後続の`[,"\r\n]`がマッチしなければマッチしたことになりますまたマッチしたとしても現在処理している入力テキストの位置は進めません
続いてダブルクォート有りのフィールドです
DQ_FIELD <- '"' (!["] . / '""')* '"'
この規則は両端にダブルクォートがあってその間はダブルクォートでない任意の文字か2連続するダブルクォートの0以上の繰り返しという意味です
ではこの2つの規則をまとめましょう
FIELD <- DQ_FIELD / NO_DQUOTE_FIELD
これはDQ_FIELDかNO_DQUOTE_FIELDにマッチを意味します。`/`優先順位付き選択Priority Choise)」と呼ばれ左の要素から順番にマッチを試みますある要素がマッチした時点で式の評価は終了します
ですからDQ_FIELDとNO_DQUOTE_FIELDの順序はとても重要ですもしこの順序を逆にするとダブルクォート文字列はNO_DQUOTE_FIELDの規則で誤ってマッチしてしまいDQ_FIELDとしては認識されなくなってしまいます。(ダブルクォート文字列は最初のクオート無しのフィールド規則で長さ0のフィールドとしてマッチしてしまい続くダブルクォート有りのフィールド規則はスキップされてしまいます。)
次に改行文字を定義しましょう
NL <- '\r\n' / '\r' / '\n'
この場合も要素の順序に気をつけてください。`\r\n`を`\r`の後に置くと,`\r\n`は決してマッチしなくなってしまいます。
続いてレコード規則を定義します。
RECORD <- FIELD (',' FIELD)*
これは「1回以上のFIELDの繰り返し」を意味します。この形式は,「要素が最低1回以上出現するリスト」を表現するイディオムです。(「要素が0回以上出現するリスト」は`(ELEM (DELM ELEM)*)?`で表します。)
最後に開始規則であるCSV規則を定義します
CSV <- RECORD (NL RECORD)* NL?
無事CSVフォーマットの文法が完成ですBNFや正規表現に慣れている方は親近感を感じたのではないでしょうか
ここでPEGで文法を定義する際によく直面する問題について考慮しましょう
## 空白の除去
PEGの問題の中で一番よく知られているのは空白扱いですYaccなどではLexなど外部の字句解析器がテキストのトークン分割を行う際に不必要な空白を除去してくれますPEGでは構文解析と字句解析が明確に分離されていないため空白除去の処理も通常の構文規則を用いて行う必要があります
`[123,456,789]`のような数字の配列は次のように定義できます
ARRAY <- '[' (NUM (',' NUM)*)? ']'
NUM <- [0-9]+
しかし括弧やカンマの前後に任意の空白文字を許したい場合は明示的にその規則を追加する必要があります
START <- _ ARRAY
ARRAY <- '[' _ (NUM (',' _ NUM)*)? ']' _
NUM <- [0-9]+ _
_ <- [ \t\r\n]*
`_`が空白規則で開始規則の先頭と括弧やカンマや数字などのトークンの直後にこの規則を指定しています
このようにPEGでは字句解析器を別途用意する必要はありませんが自前で空白除去の規則を適切に散りばめなければならずこの点がPEGの弱点と言えるかもしれません
## 空白の間違った扱い
`if x then y`でも`if(x)then y`でも記述可能なプログラミング言語の文法を考えてみましょう
IF <- 'if' _ ('(' _)? IDENT (')' _)? 'then' _ EXPR
IDENT <- [a-zA-Z][a-zA-Z0-9_-]* _
...
_ <- [ \t\r\n]* # 空白文字が0回以上
この文法で上の2つの例にマッチさせることができますが,`ifxtheny`にも誤ってマッチしてしまいますトークン間に必ず空白が必要ということで'_'の規則を次のように変更してみます
_ <- [ \t\r\n]+ # 空白文字が1回以上
今度は正しく`ifxtheny`がエラーとなりますしかし`if(x)then y`は空白を必要ないにもかかわらずマッチしなくなってしまいましたこうした時は否定先読みを使用してトークンの切れ目を明確にして解決することができます
IF <- 'if' __ ('(' _)? IDENT (')' _)? 'then' __ EXPR
IDENT <- [a-zA-Z][a-zA-Z0-9_-]* __
...
__ <- ![a-zA-Z0-9_-] _
_ <- [ \t\r\n]*
`__`前のトークンの直後の文字が名前文字`[a-zA-Z0-9_-]`」ではないことを確認しつつ0回以上の空白文字を受け入れます
## Unicodeのサポート
FordのPEGの論文ではUnicode文字については想定されていませんしかし多言語のテキストを扱うにはUnicodeのサポートが必須です対応は処理系によってまちまちです
この本で使用するPEGライブラリcpp-peglibは正式にはUnicodeに未対応ですが問題なくUTF8のテキストを扱うことができますU+0800以上の全ての文字を規則名として使用することができます
このように幾つかの弱点はあるもののPEGは十分に実用的な文法定義のための言語です
次章ではついにスクリプト言語の文法デザインに着手しましょう
[Link_Ford]: http://pdos.csail.mit.edu/papers/parsing:popl04.pdf