From 9cc485303bbe00092e594d25d3438be1a0238b06 Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 12 Aug 2015 15:13:11 -0400 Subject: [PATCH] Updated tutorial. --- tutorial/chap_01.md | 12 +++---- tutorial/chap_02.md | 87 +++++++++++++++++++++++++++++++-------------- tutorial/intro.md | 11 ++---- tutorial/toc.md | 2 +- 4 files changed, 68 insertions(+), 44 deletions(-) diff --git a/tutorial/chap_01.md b/tutorial/chap_01.md index 77b2d4f..54480d0 100644 --- a/tutorial/chap_01.md +++ b/tutorial/chap_01.md @@ -20,7 +20,7 @@ const auto grammar = R"( int main(void) { // 文法を読み込んでパーサーを生成 - peglib::peg parser(grammar); + peg::parser parser(grammar); // 文法に誤りがあったかチェック if (!parser) { @@ -58,7 +58,7 @@ g++ 5.1でもほとんど同じで, Visual C++ 14 (Visual Studio 2015) では, - cl -DUNICODE /EHsc hello.cc User32.lib + cl -DUNICODE /EHsc hello.cc とするとコンパイルできました。 @@ -94,13 +94,11 @@ const auto grammar = R"( )"; ``` -脇にそれますが,この文法には一つ大きなバグがあります。実は「helloworld!」のように言葉をくっつけて入力しても結果が「OK」になってしまいます。このバグの解決法は後の章で説明します。 - この文法を理解するPEGパーサーを生成しましょう。`peglib::peg`がパーサーです。先ほどの定義した文法をコンストラクタに渡してパーサーを生成します。 ```cpp // 文法を読み込んでパーサーを生成 - peglib::peg parser(grammar); + peg::parser parser(grammar); // 文法に誤りがあったかチェック if (!parser) { @@ -111,7 +109,7 @@ const auto grammar = R"( 文法にエラーがあると、上記のように`parser`オブジェクトの真偽値が'false'になります。 -最後に`parser.parse`メソッドを呼び、ユーザーの入力した文字列をパースします。成功すると`true`が返ります。 +最後に`parse`メソッドを呼び、ユーザーの入力した文字列をパースします。成功すると`true`が返ります。 ```cpp // ユーザーからの入力をパース @@ -133,4 +131,4 @@ const auto grammar = R"( PEGライブラリはステップ2と3のみ扱い,残りは自分で扱わなければなりません。しかし,この文法定義とインタープリタ作成の部分が個性を出せる一番面白いところで,デザインセンスと実装技術の見せ所です。 -では最初に,PEG記法を使ってどのように言語を定義していくのか見てみましょう。 +次章では,PEGがどんなものかを見てみましょう。 diff --git a/tutorial/chap_02.md b/tutorial/chap_02.md index 12beb7f..dbc44cb 100644 --- a/tutorial/chap_02.md +++ b/tutorial/chap_02.md @@ -1,35 +1,68 @@ -# PEGで言語をデザインしてみよう +# はじめてのPEG -PEGは「言語を定義するための言語」です。言語としてのPEGは,C++やJavaScriptなどの言語とはずいぶん毛色が違います。むしろ正規表現,XML Schemer,DBのテーブルスキーマのように「規則を定義」するタイプの言語です。 +PEGは言語の文法を定義するための言語で、2004年に[Bryan Ford][Link_Ford]によって発表されました。記法はBNFに似ていますが、言語の構文を定義するだけでなく字句の定義も同一のファイルでに含めることができます。 -PEGで文法を書き表すことによって,人間に読みやすくかつ厳密に文法を定義できます。さらにこの文法ファイルを使って,その言語のパーサーをプログラムに自動生成させることもできます。 +PEGの文法ファイルは「構文規則名(N) <- 式(e)」で表される複数の構文規則で構成されます。例えば簡単な日付フォーマットはこんな感じで定義できるでしょう。 -PEGの記法はとてもシンプルで習得しやすいものです。BNFや正規表現をご存知の方でしたら,ほんのわずかな時間で覚えられるでしょう。 +``` +# Ex) 12:34 p.m. +DATE <- NUMBER ':' NUMBER ' ' SUFFIX +NUMBER <- [0-9]+ +SUFFIX <- 'a.m.' / 'p.m.' +``` -PEGの理論的背景についてもっと知りたい方は,Bryan Fordさんの[2004年の論文][Link_Ford]を読みましょう。このTechnical Paperの2番めのセクションには,PEGをPEGで定義するとても興味深いコードが載せられています。(私もこれを参照しながらパーサジェネレータを実装しました。) +PEGパーサーは,この文法ファイルの開始規則(DATE)に対して入力テキストがマッチするか判定します。式の中で他の規則が参照されている場合,再帰的にマッチを試みます。 -「習うより慣れろ」と古くから言われるように,実際に手を動かしてコードを書くことは理解を深めるための早道です。その際[「PEG Playground」](http://yhirose.github.io/peglint/)を使ってみてください。このWebアプリは,PEGの文法定義コードと定義される言語のコードをリアルタイムにチェックしてくれます。 +式には次のようなものがあります。 -では始めましょう。最初にデザインするのはCSVフォーマットです。 + * `N`(構文規則の参照) -## CSVフォーマット + * `'...'`または`"..."`(文字列) + * `.`(任意の文字) + * `[...]`(文字クラス。[a-zA-Z0-9-_]) -おなじみカンマ区切りフォーマットの文法を定義してみましょう。CSVには色んな方言がありますが,以下の仕様を満たすものとします。 + * `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回以上の繰り返し」を意味します。理解を深めるために,規則を構成する個々の要素を見てみましょう。 +この規則は「カンマ,ダブルクォート,改行以外の任意の文字の0回以上の繰り返し」を意味します。規則を構成する個々の要素を見てみましょう。 -`.`は任意の文にマッチします。`[,"\r\n]`はカンマ,ダブルクォート,改行文字のいずれかにマッチします。外側の`()*`は0回以上の繰り返しを意味します。どれも正規表現と全く同じですね。 +`.`は任意の文字にマッチします。`[,"\r\n]`はカンマ,ダブルクォート,改行文字のいずれかにマッチします。外側の`()*`は0回以上の繰り返しを意味します。どれも正規表現と全く同じですね。 -`!`は「否定先読み」演算子で,後続の`[,"\r\n]`の評価が偽であればマッチしたことになります。マッチしたとしても,現在処理している入力テキストの位置は進めません。 +`!`は「否定先読み」演算子で,後続の`[,"\r\n]`がマッチしなければ,マッチしたことになります。またマッチしたとしても,現在処理している入力テキストの位置は進めません。 続いてダブルクォート有りのフィールドです。 @@ -41,9 +74,15 @@ PEGの理論的背景についてもっと知りたい方は,Bryan Fordさん FIELD <- DQ_FIELD / NO_DQUOTE_FIELD -これは「DQ_FIELDかNO_DQUOTE_FIELDにマッチ」を意味します。`/`は「Priority Choise」と呼ばれ,左から右に式の要素を評価します。評価が真になった時点でマッチに成功します。 +これは「DQ_FIELDかNO_DQUOTE_FIELDにマッチ」を意味します。`/`は「優先順位付き選択(Priority Choise)」と呼ばれ,左の要素から順番にマッチを試みます。ある要素がマッチした時点で式の評価は終了します。 -このDQ_FIELDとNO_DQUOTE_FIELDの順序はとても重要です。もしこの順序を逆にすると,ダブルクォート文字列はNO_DQUOTE_FIELDの規則で誤ってマッチしてしまい,DQ_FIELDとしては認識されなくなってしまいます。(ダブルクォート文字列は,最初のクオート無しのフィールド規則で長さ0のフィールドとしてマッチしてしまい,続くダブルクォート有りのフィールド規則にたどりつかないからです。) +ですからDQ_FIELDとNO_DQUOTE_FIELDの順序はとても重要です。もしこの順序を逆にすると,ダブルクォート文字列はNO_DQUOTE_FIELDの規則で誤ってマッチしてしまい,DQ_FIELDとしては認識されなくなってしまいます。(ダブルクォート文字列は,最初のクオート無しのフィールド規則で長さ0のフィールドとしてマッチしてしまい,続くダブルクォート有りのフィールド規則はスキップされてしまいます。) + +次に改行文字を定義しましょう。 + + NL <- '\r\n' / '\r' / '\n' + +この場合も要素の順序に気をつけてください。`\r\n`を`\r`の後に置くと,`\r\n`は決してマッチしなくなってしまいます。 続いてレコード規則を定義します。 @@ -51,25 +90,19 @@ PEGの理論的背景についてもっと知りたい方は,Bryan Fordさん これは「1回以上のFIELDの繰り返し」を意味します。この形式は,「要素が最低1回以上出現するリスト」を表現するイディオムです。(「要素が0回以上出現するリスト」は`(ELEM (DELM ELEM)*)?`で表します。) -開始規則であるCSV規則は, +最後に開始規則であるCSV規則を定義します。 CSV <- RECORD (NL RECORD)* NL? -と定義できます。改行文字を定義を忘れていましたね… +無事CSVフォーマットの文法が完成です。BNFや正規表現に慣れている方は,親近感を感じたのではないでしょうか? - NL <- '\r\n' / '\r' / '\n' +ここでPEGで文法を定義する際によく直面する問題について考慮しましょう。これは構文解析と字句解析が一緒に行われるPEG特有の問題です。 -この場合も要素の順序に気をつけてください。`\r\n`を`\r`の後に置くなら,`\r\n`は決してマッチしなくなります。 +## 空白の除去 -CSVの文法が定義できました! +## キーワードの処理 - CSV <- RECORD (NL RECORD)* NL? - RECORD <- FIELD (',' FIELD)* - FIELD <- DQ_FIELD / NO_DQUOTE_FIELD - NO_DQUOTE_FIELD <- (![,"\r\n] .)* - DQ_FIELD <- '"' (!["] . / '""')* '"' -では,この文法をテストしてみましょう。 +次章では,より複雑なスクリプト言語の文法デザインしてみましょう。 -[Link_BasicGrammar]: http://kmizu.hatenablog.com/entry/20100203/1265183754 [Link_Ford]: http://pdos.csail.mit.edu/papers/parsing:popl04.pdf diff --git a/tutorial/intro.md b/tutorial/intro.md index 6854170..40cd3a7 100644 --- a/tutorial/intro.md +++ b/tutorial/intro.md @@ -8,7 +8,7 @@ はからずも「プログラミング言語のようなもの」を作ることになりました。まずは設定ファイルをパースしなければなりません。「さて,パーサをどう実装する? Lex+Yaccを使う? パーサーを手書きで書く? いずれにしても面倒で難しそう…」と悩みつつWebで検索していたところ,偶然[『PEG』][Link_PEG]という言葉を目にしました。PEGとは「Parsing Expression Grammar」の略号です。調べてみると,PEGの文法ファイル一つで,字句解析と構文解析を行えること,普段良く使っている正規表現に似ていること,自分でもPEGパーサコンビネータやジェネレータを作成できそうなことがわかりました。 -さっそく日々の仕事で使っているC++で,PEGコンビネータとパーサジェネレータを作り始めました。はい,コンパイラ作成の経験などなかった私でも,意外にあっさり実装できてしまいました。その後,先の設定ファイルの文法をPEGでデザインし(これがまたとても面白い!),パーサジェネレータでいとも簡単にパーサを生成することができました。このプロジェクトはとても上手くいきました。 +さっそく日々の仕事で使っているC++で,PEGコンビネータとパーサジェネレータを作り始めました。はい,コンパイラ作成の経験などなかった私でも,意外にあっさり実装できてしまいました。その後,先の設定ファイルの文法をPEGでデザインし(これがまたとても面白い!),パーサジェネレータでいとも簡単にパーサを生成することができました。 これが契機となって、「もう少し頑張れば、自分にもプログラミング言語を作ることができるかも」と思えるようになりました。文法定義とパーサーの生成がとても簡単になったので,言語作成の敷居がぐっと下がったように感じたのです。さらにこのPEGライブラリに改良を重ね,実際にプログラミング言語を作って動かすことができるようになりました! @@ -16,14 +16,7 @@ 皆さんにもこの同じ喜びを味わっていただきたと思い,この文章を書いています。想定している読者は,ある程度のプログラミングの経験を持っていて,自分で言語を創りあげたいとの熱意を持っている方です。 -必要な道具はC++11対応のコンパイラだけです。最新の主要C++コンパイラは,Clang,GCC,Visual C++にかかわらず,全てC++11に対応しています。以下のおなじみHello Worldコードをコンパイル・実行できる環境があれば,準備は完了です。 - -```cpp -#include -int main(void) { - std::cout << "hello world!\n"; -} -``` +必要な道具はC++11対応のコンパイラだけです。最新の主要C++コンパイラは,Clang,GCC,Visual C++にかかわらず,全てC++11に対応しています。 ではさっそく始めましょう! diff --git a/tutorial/toc.md b/tutorial/toc.md index 9b9e378..d12e19c 100644 --- a/tutorial/toc.md +++ b/tutorial/toc.md @@ -7,4 +7,4 @@ ## 章 1. [まずは,Hello world](chap_01.md) - 2. [PEGで言語をデザインしてみよう](chap_02.md) + 2. [はじめてのPEG](chap_02.md)