parent
fa08f98c6e
commit
a0bee1d20b
@ -1,136 +0,0 @@ |
|||||||
# まずは"Hello world!" |
|
||||||
|
|
||||||
はじめに取り組むプログラムは,もちろん**Hello world!**ですよね。 |
|
||||||
|
|
||||||
では「hello world!」と正しく入力できたら「OK」,そうでなければ「NG」と表示するプログラムを作ってみましょう!以下がソースコードです。 |
|
||||||
|
|
||||||
```cpp |
|
||||||
// hello.cc |
|
||||||
#include <iostream> |
|
||||||
#include "peglib.h" |
|
||||||
#include "linenoise.hpp" |
|
||||||
using namespace std; |
|
||||||
|
|
||||||
int main(void) |
|
||||||
{ |
|
||||||
// 文法を読み込んでパーサーを生成 |
|
||||||
peg::parser parser(R"( |
|
||||||
PROGRAM <- _ HELLO _ WORLD '!' _ |
|
||||||
HELLO <- [hH] 'ello' |
|
||||||
WORLD <- [wW] 'orld' |
|
||||||
_ <- [ \t]* |
|
||||||
)"); |
|
||||||
|
|
||||||
if (!parser) { // 文法に誤りがあったかチェック |
|
||||||
cerr << "grammar error..." << endl; |
|
||||||
return -1; |
|
||||||
} |
|
||||||
|
|
||||||
while (true) { |
|
||||||
auto line = linenoise::Readline("> "); // 文字列を一行読み込み |
|
||||||
|
|
||||||
if (line == "exit") { break; } // 終了 |
|
||||||
|
|
||||||
if (parser.parse(line.c_str())) { // ユーザーからの入力をパース |
|
||||||
cout << "OK" << endl; |
|
||||||
} else { |
|
||||||
cout << "NG" << endl; |
|
||||||
} |
|
||||||
|
|
||||||
linenoise::AddHistory(line.c_str()); // 入力履歴に追加 |
|
||||||
} |
|
||||||
|
|
||||||
return 0; |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
このコードを`hello.cc`として保存してください。このコードは以下の2つのライブラリを使用するので,ダウンロードして`hello.cc`と同じディレクトリに保存してください。 |
|
||||||
|
|
||||||
* cpp-peglib C++ PEG parser library - [peblib.h](https://raw.githubusercontent.com/yhirose/cpp-peglib/master/peglib.h) |
|
||||||
* cpp-linenoise C++ Readline library - [linenoise.hpp](https://raw.githubusercontent.com/yhirose/cpp-linenoise/master/linenoise.hpp) |
|
||||||
|
|
||||||
ではコンパイルしましょう。コンパイル時にはC++11の機能を有効にする必要があります。`clang++`のパージョン3.5ではこんな感じになります。 |
|
||||||
|
|
||||||
clang++ -std='c++11' -o hello hello.cc |
|
||||||
|
|
||||||
g++ 5.1でもほとんど同じで, |
|
||||||
|
|
||||||
g++ -std='c+11' -o hello hello.cc' |
|
||||||
|
|
||||||
Visual C++ 14 (Visual Studio 2015) では, |
|
||||||
|
|
||||||
cl /EHsc hello.cc |
|
||||||
|
|
||||||
とするだけでコンパイルできます。 |
|
||||||
|
|
||||||
では生成された実行ファイルを実行してみましょう。画面に`>`が表示され,ユーザーに入力を促します。何か文字列を入力してリターンキーを押してみましょう。正確に「hello world!」と入力すると「OK」と表示されます。何かおかしな入力をすると「NG」になります。 |
|
||||||
|
|
||||||
``` |
|
||||||
> はろーわーるど! |
|
||||||
NG |
|
||||||
> hello world! |
|
||||||
OK |
|
||||||
> hello world! |
|
||||||
OK |
|
||||||
``` |
|
||||||
|
|
||||||
見事にPEG版Hello worldをクリアです!(プログラムを終了したい時は`exit`を入力してください。) |
|
||||||
|
|
||||||
-- |
|
||||||
|
|
||||||
では,このプログラムのPEGに関する部分を順を追ってみましょう。 |
|
||||||
|
|
||||||
まずはPEGライブラリを読み込みます。`peglib.h`はC++ header-only libraryですので,他にファイルは必要ありません。 |
|
||||||
|
|
||||||
```cpp |
|
||||||
#include "peglib.h" |
|
||||||
``` |
|
||||||
|
|
||||||
続いてPEGで文法を定義します。この文法は「hello world!」という文字列を受け付けるだけのとても簡単なものです。入力文字列の前後や「hello」と「world」の間には,任意の長さのスペースやタブを入れることができます。(ちなみに「world」と「!」の間には入れることができません。) |
|
||||||
|
|
||||||
``` |
|
||||||
PROGRAM <- _ 'hello' _ 'world' '!' _ |
|
||||||
_ <- [ \t]* |
|
||||||
``` |
|
||||||
|
|
||||||
この文法を理解するPEGパーサーを生成しましょう。`peglib::peg`がパーサーです。先ほどの定義した文法をコンストラクタに渡してパーサーを生成します。 |
|
||||||
|
|
||||||
```cpp |
|
||||||
// 文法を読み込んでパーサーを生成 |
|
||||||
peg::parser parser(R"( |
|
||||||
PROGRAM <- _ HELLO _ WORLD '!' _ |
|
||||||
HELLO <- [hH] 'ello' |
|
||||||
WORLD <- [wW] 'orld' |
|
||||||
_ <- [ \t]* |
|
||||||
)"); |
|
||||||
|
|
||||||
if (!parser) { // 文法に誤りがあったかチェック |
|
||||||
cerr << "grammar error..." << endl; |
|
||||||
return -1; |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
文法にエラーがあると、上記のように`parser`オブジェクトの真偽値が'false'になります。 |
|
||||||
|
|
||||||
最後に`parse`メソッドを呼び、ユーザーの入力した文字列をパースします。成功すると`true`が返ります。 |
|
||||||
|
|
||||||
```cpp |
|
||||||
if (parser.parse(line.c_str())) { // ユーザーからの入力をパース |
|
||||||
cout << "OK" << endl; |
|
||||||
} else { |
|
||||||
cout << "NG" << endl; |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
これでパーサジェネレータを使う準備ができました。 |
|
||||||
|
|
||||||
実用的な言語の文法はずっと複雑ですし,パーサだけでコードを実行することできません。実際に動作する状態に持って行くには,さらに行うべきことがあります。インタープリタ型言語の場合,次のようなステップが必要です。 |
|
||||||
|
|
||||||
1. 言語の文法を定義する |
|
||||||
2. パーサーを生成する |
|
||||||
3. ソースコードをパースして、AST(抽象構文木)を生成する |
|
||||||
4. ASTを実行するインタープリタを作成する |
|
||||||
|
|
||||||
PEGライブラリはステップ2と3のみ扱い,残りは自分で扱わなければなりません。しかし,この文法定義とインタープリタ作成の部分が個性を出せる一番面白いところで,デザインセンスと実装技術の見せ所です。 |
|
||||||
|
|
||||||
次章では,PEGがどんなものかを見てみましょう。 |
|
@ -1,170 +0,0 @@ |
|||||||
# PEG記法を学ぼう |
|
||||||
|
|
||||||
PEGは言語の文法を定義するための言語で、2004年に[Bryan Ford][Link_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は十分に実用的な文法定義のための言語です。 |
|
||||||
|
|
||||||
次章では,いよいよスクリプト言語のデザインに着手しましょう。 |
|
||||||
|
|
||||||
[Link_Ford]: http://www.brynosaurus.com/pub/lang/peg.pdf |
|
@ -1,26 +0,0 @@ |
|||||||
# 前書き |
|
||||||
|
|
||||||
今この文章をに目を通してくださっているみなさんは、「いつか自分でプログラミング言語を作ってみたい!」と思ったことがあるかもしれません。でもほとんどの人は「正直言って,言語を作るなんて自分には敷居が高過ぎる」とあきらめてしまいます。ご安心を! |
|
||||||
|
|
||||||
何を隠そうこれまでの私も,「コンパイラ」とか「インタープリタ」名の付く技術書を幾つも眺めてきました。「字句解析,構文解析,インタープリタ,VM…」など,本に書かれている概念自体はある程度理解できるものの,それを実装して実際に動くものを作るのは別問題です。 |
|
||||||
|
|
||||||
でもしばらく前のこと,仕事で数十万行にも及ぶ大きなデータファイルを作成する必要がありました。エディタを使って手作業するなら軽く数ヶ月かかってしまいます。それでは大変なので,設定ファイルを定義してプログラムにデータを生成させようということになりました。数千行の特別な書式の設定ファイルを準備すれば,ものの数分で膨大かつ正確なデータを生成してくれるのです。 |
|
||||||
|
|
||||||
はからずも「プログラミング言語のようなもの」を作ることになりました。まずは設定ファイルをパースしなければなりません。「さて,パーサをどう実装する? Lex+Yaccを使う? パーサーを手書きで書く? いずれにしても面倒で難しそう…」と悩みつつWebで検索していたところ,偶然[『PEG』][Link_PEG]という言葉を目にしました。PEGとは「Parsing Expression Grammar」の略号です。調べてみると,PEGの文法ファイル一つで,字句解析と構文解析を行えること,普段良く使っている正規表現に似ていること,自分でもPEGパーサコンビネータやジェネレータを作成できそうなことがわかりました。 |
|
||||||
|
|
||||||
さっそく日々の仕事で使っているC++で,PEGコンビネータとパーサジェネレータを作り始めました。はい,コンパイラ作成の経験などなかった私でも,意外にあっさり実装できてしまいました。その後,先の設定ファイルの文法をPEGでデザインし(これがまたとても面白い!),パーサジェネレータでいとも簡単にパーサを生成することができました。 |
|
||||||
|
|
||||||
これが契機となって、「もう少し頑張れば、自分にもプログラミング言語を作ることができるかも」と思えるようになりました。文法定義とパーサーの生成がとても簡単になったので,言語作成の敷居がぐっと下がったように感じたのです。さらにこのPEGライブラリに改良を重ね,実際にプログラミング言語を作って動かすことができるようになりました! |
|
||||||
|
|
||||||
正直なところ,今でも「コンパイラ技術」についての深い知識は持ち合わせていません。PEGの[オリジナルの論文][Link_Paper]に書かれている理論も完璧に理解しているわけではありません。それでも言語処理系を作ることの楽しさを十分味わい,この分野についてもっと勉強したいというモチベーションを得ることもできたことは素晴らしい経験でした。 |
|
||||||
|
|
||||||
皆さんにもこの同じ喜びを味わっていただきたと思い,この文章を書いています。想定している読者は,ある程度のプログラミングの経験を持っていて,自分で言語を創りあげたいとの熱意を持っている方です。 |
|
||||||
|
|
||||||
必要な道具はC++11対応のコンパイラだけです。最新の主要C++コンパイラは,Clang,GCC,Visual C++にかかわらず,全てC++11に対応しています。 |
|
||||||
|
|
||||||
ではさっそく始めましょう! |
|
||||||
|
|
||||||
Happy Hacking! |
|
||||||
|
|
||||||
[Link_PEG]: https://ja.wikipedia.org/wiki/Parsing_Expression_Grammar |
|
||||||
[Link_Paper]: http://bford.info/pub/lang/peg |
|
@ -1,12 +0,0 @@ |
|||||||
目次 |
|
||||||
==== |
|
||||||
|
|
||||||
* [序文](intro.md) |
|
||||||
* [目次](toc.md) |
|
||||||
|
|
||||||
## 章 |
|
||||||
|
|
||||||
1. [まずは"Hello world!"](chap_01.md) |
|
||||||
2. [PEG記法を学ぼう](chap_02.md) |
|
||||||
3. [言語"Elements"](chap_03.md) |
|
||||||
4. [四則計算式](chap_04.md) |
|
Loading…
Reference in new issue