パーサジェネレータを作る
再帰下降構文解析を手で書いていると、面倒くさい & 結構間違える事があったため、パーサジェネレータを作っていました。EBNF でルールを与えると、再帰下降構文解析のパーサのコードを出力してくれる簡単なものですが、試しに作ってみる簡単な言語であれば十分そうです。
なお、前回までは C# でやっていたのですが、今回はテスト実行に CodeMirror を使いたかったため JavaScript を追加いました。
字句解析と EBNF パーサは手書き
構文をパーサジェネレータのクラスに関数呼び出しで与える方法も良かったのですが、せっかく解析法を覚えたので、EBNF の解析くらいは手書きしました。EBNF の正確な文法は知らないですが、多分こんな感じだと思います。
STRING となっているのは、ダブルクオートで囲った文字リテラルを意味しています。また、改行(先頭は必ず "|" に限っています)や空行も許しています。
EBNF からソースコードに変換
以下の手順でやりました。
- 非終端記号の関数を作る
- 非終端記号の関数内で順番に呼ぶ(list)
{}
は while 文に展開する(repeat)[]
は if 文で 1 トークン目を確認する(option)|
は if 文で分岐する(rule)
変換先のソースコードは、とりあえず JavaScript にしましたが、後々 C# など別の言語にすることも考えて、一旦、抽象木的なものを組み立てて、その出力実装を入れ替えることで、(ある程度仕様が似ていれば)様々な言語のコードを出力できるようにしました。言語の本を読んだおかけで、こういうことも考えられるようになった気がします。
一番面倒だったのが、
getConditionのところです。この関数は、与えたルールを解析して、次に来る可能性のあるトークン(終端記号)を列挙し、それらとマッチするかの条件式を返します。最初に来る可能性のあるトークは、
{}等があるとちょっと厄介で、その場合は 2 番目もチェックするようにしています。このあたりは、どこかに書いてあったわけではなく、自前なのでバグがあるかもしれませんが、今の所、自分としては必要そうな場面があまりないため、ひとまず先の手順で実装を終えています。
演算子順位解析にも対応
簡単な言語を作ると行っても、そこそこ使えるようにするためには、演算子は揃えないといけません。しかしながら、優先順位を考えて EBNF を書くのは大変そうです。そこで、演算子順位解析にも対応しました。
文法定義の前に、二項演算子の定義を書けるようにし、yacc 風に %% で区切って文法定義を書くようにしました。そして、
X { OP X }(X は記号、OP は演算子)のときだけ、演算子順位解析を行うコードを出力するようにしました。
ついでに、定義された演算子は字句解析でちゃんとトークンとして返ってくるように設定されるようにしています。
できたもの
EBNF を書いたら、それをもとにパーサを作成し、さらにそれに文章を入力して、生成された AST を確認できるようなプログラムを作りました。
画面左上が EBNF の定義、その下が出力したパーサのソースコードです。画面右上に作成したパーサへのテスト入力を記述でき、それを実行(?)すると、その下に解析結果の抽象木が出力されます。試しに四則演算を入れてみましたが、無事に動作しました。
JavaScript で書いてあるため、こちらに置いておきました。エラー確認等あまりやっていないので、変な入力を入れると、無限ループに陥る可能性がありますので、ご注意ください。
ファイルの保存なども行えるようにしたいため、本当は Electron でデスクトップにしたいです。
まとめ
ここ数日、再帰下降構文解析をメインにしたパーサジェネレータを作っていました。次からは、抽象木の評価関数を作って、プログラムを実行できるようにしようと思います。(それはそれとして、プログラムの実行以外にも、別言語に出力(トランスパイル?)の方が個人的には使い勝手が良さそうな気がしてきました。)
言語づくりは用語自体は諸々知っていたのですが、やはり実際にやってみると新しい可能性が広がりますね。