8.8 KiB
PEG記法を学ぼう
PEGは言語の文法を定義するための言語で、2004年にBryan Fordによって発表されました。記法はEBNFに似ていますが、言語の構文を定義するだけでなく字句の定義も同一のファイルでに含めることができます。
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
(否定先読み)
幾つか短い例をみてみましょう。
# Ex) 大文字で始まる英単語
WORD <- [A-Z][A-Za-z-]+
# Ex) 数列 [1,2,3...]
NUMBER_LIST <- '[' (NUMBER (',' NUMBER)*)? ']'
NUMBER <- [0-9]+
正規表現ととても似ていますね。ではもう少し複雑な例として、CSVファイルための文法を定義してみましょう。
abc,"def ghi", jkl
...
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のサポートが必須です。Unicodeへの対応は、PEG処理系によってまちまちです。
この本で使用するPEGライブラリ(cpp-peglib)は正式にはUnicodeに未対応ですが,問題なくUTF8のテキストを扱うことができます。U+0800以上の全ての文字を規則名として使用することができます。
このように幾つかの弱点はあるものの,PEGは十分に実用的な文法定義のための言語です。
次章では,いよいよスクリプト言語のデザインに着手しましょう。