構文解析をやってみる
字句解析ができたので、次は構文解析器の制作に取り掛かります。
どうやって作ろう?
買った 2 冊の書籍には、
- 再帰下降構文解析
- パーサコンビネータのソースコード
が載っていました。ちなみに、開発言語が C# であることと、実際の動きを見ながらやってみたいこともあり、yacc などのツールは使わないことにしました。作る言語のことを考えると、パーサコンビネータを C# でまるっと実装するのが良さそうですが、こちらもまずは動作の理解を深めるために、簡単な構文を再帰下降構文解析を使って解析するコードを作成しようと思います。
解析の前に、終端記号と非終端記号の用語の定義を…
終端記号 | それ以上分解されない記号 ≒ トークン |
非終端記号 | 置換されうる記号 ≒ 定義していくやつ |
再帰下降構文解析
文法として、左再帰を持たず、一個のトークンの先読みで判断できるような文法になっていれば、解析できるようです。そもそもこの辺をあまり理解していないのですが、いろいろやってみて、だめになったら考えます。(ちなみに、2 週間でできる ! の方では、四則演算のところだけ順位を決めて解析する方法が載っていました。これが終わったらやってみようと思います。)なお、世の中のパーサジェネレータを見ると、LL 法のものが結構あり、先読みの数を増やせばそれなりに実用性があるようです。
左再帰は厳密な定義はよくわからないのですが、こちらによると「A → Aa|b」のように、同じ非終端記号が一番左にいきなり現れるようなやつです。これは、文法の定義の仕方で解消できるようです(やりかたが書いてありましたが、ぱっと見では理解できませんでした)。一個のトークンの先読みで判断できるような文法は、文字通りです。(ぱっと思いついた感じですと、ジェネリクスの構文で List<string が、List<string> なのか、List < string という比較演算なのかの判断つかないように思いましたが、やってみないとわからないですね。ちょっと調べた参考はこちら)
ここまでを心に留めて、以下の構文解析を作ってみようと思います。
左再帰をもうちょっと調べてみた
Wikipedia にアルゴリズムが載っていました(こちら)。アルゴリズム自体は意味がわからなかったのですが、手続きはこちらの感じでできました。ただ、生成される木の結合方向が変わるそうで、あまり良くないようです。Wikipedia には、" 再帰ではなくループで繰り返しを実現する文法に書き換える(拡張 BNF などによる構文規則では * を使って表現できる。手続き型の実装には、綺麗に演算子の左結合性も含め変換できる)。" と書いてあったため、ひとまずは、それにしたがおうと思います。(左再帰が起きないように注意する方針)
構文の話にもどる
さて、ここでもう一度、読んでいる本を見比べてみました。
再帰下降構文解析のアルゴリズムについては、2. の方で詳しく説明されていたのですが、残念ながらそちらの本では構文が左再帰バリバリの BNF で解説されていました(この本は yacc を使うので良いのですが)。アルゴリズムの解説のところでは、BNF が一度構文図に置き換えられて、その後構文図に書き直すときになんか表現が変わっていました。
一方、1. の方では繰り返し規則などを使って構文が定義されていたため、構文については、1. の方を見つつ、プログラムを書くときは 2. の方と Wikipedia を参考にしながら進めることにしました。なお、1. の方でも、演算子の優先度は別の方法で解決していたため、再帰下降構文解析をバリバリ手書きでやるのは、最初の四則演算 + αくらいまでにしておき、感覚的にわかったところで 1. の本のパーサコンビネータのところを読みながらシステムを作ろうかと思います。
つくる
以前、ブログに貼ったこちらの構文を作ってみます。忘れているので、メタ記号も再掲
記号 | 意味 |
---|---|
{ } | カッコ内が 0 回以上の繰り返しと一致 |
[ ] | カッコ内が 0 回または 1 回一致 |
a | b | a または b |
() | カッコ内をひとまとまりとする |
こちらを実装したのが以下のコードです。前回作った字句解析器を少し改造して、先読みができるようになっています。また、ファイルの終端では、終端のトークンが返るように変更されています。
解析のコード自体は、上の EBNF の内容をそのまま関数にしただけでした。明日にでも、変換の手順をまとめようと思います。これくらいなら、パーサジェネレータを書けそうです。
はじめは繰り返し部分をそのままの順で子供に入れていたのですが、貼ったコードでは、左側のから枝になるようにしてみました。演算の実行は、本当は ASTree クラスのサブクラスとかにしてオーバーライドでやるべきなんでしょうが、とりあえず、if 文で分岐でやっています。
この Parser に、
((6 + 8) * 7 + 8 - 7) / 9 / 3を入れると以下のような感じになります。計算は合っているようです。
超見づらいので、木のビジュアライザも欲しくなりますね。