構文解析その②
EBNF をコードに変換
昨日は、EBNF をそのままコードに変換してみました。作った感じ、以下のようなルールで機械的にコードに変換できそうです(どこかに書いてあったものではないので、間違っているかも)。
記号 | 意味 |
---|---|
{ } | カッコ内が 0 回以上の繰り返しと一致 |
[ ] | カッコ内が 0 回または 1 回一致 |
a | b | a または b |
() | カッコ内をひとまとまりとする |
- 非終端記号は一つの関数にする
- 関数内では、生成規則の順に読んでいく
- 終端記号の場合、普通にトークンを読み込む
- 非終端記号の場合、対応する非終端記号の関数を呼び出す
- {} [] | のときにトークンを先読みし、条件分岐する
- {} は while 文へ(先読みしたトークンでループの条件を判断)
- [] は if 文へ(先読みしたトークンが条件に)
- | は if 文で分岐になるか、{} や [] の条件判断の OR 文になる
このように書いてみると、LL(1) 法で解析できる文法の理解が進みます。おそらく、以下のことに注意しないと思います。
- 非終端記号の最初の生成規則に、同じ非終端記号が出てこないようにする
- 左再帰になって無限ループが起きてしまう
- 生成規則の最初の非終端記号を考えないといけない
- {} [] | のときに、一つ先読みしただけで判断できるようにしないといけない
- {} や [] の 1 記号目が非終端記号の場合、展開先を考えないといけない
- その上で、一意に決まる必要がある
- 文法全体で考える必要あり(後述)
いずれにしても、非終端記号が出てくるときは、その生成規則もみて判断しないといけません。また、上に書いた「文法全体で考える必要あり」については、例えば
としてしまうと、
factor { ("*" | "/") factor }の後ろの繰り返し部分で、
("*" | "/") factorへ進むべきなのか、それとも factor の
"/" expression "/"へ進むべきなのかわからなくなります。こういったエラーはうっかり見逃しそうです。つまり、メタ記号の 1 記号目は、全体で注意深くチェックする必要がありそうです。
こう考えると、手でコードを書いていくのはなかなか大変そうですので、やはり何らかのジェネレーターは欲しいところで、すくなくとも、手で書く前には、何らかの既存システムに入れて文法の確認はしたほうが良さそうです。一方で、文法のエラーのチェックやジェネレータの作成は思ったより簡単そうです。
演算子順位法
少々浮気。
2 週間でできる ! スクリプト言語の作り方の方に載っていた、中置記法の二項演算子の構文解析を、演算子の優先度をベースに解析できる方法が乗っていました。この方法を使えば、例えば、演算子を OP として、
とだけしておき、普段は LL 法で構文解析し、expression の解析のところだけは演算子順位法を使うということができるようです。演算子はそれなりの数があり、優先順位を EBNF だけで書いていると大変(更にそれを実装するのはしんどい)ですので、とても便利そうです。
本には、アルゴリズムの解説も載っていたのですが、コードを読んだほうが理解が進んだので、とりあえず実装してみることにしました。
上記に加えて、前回のコードの SsASTree クラスの Calc 内の、expression での計算も、掛け算・割り算もやるように修正しました。
こちらに、前回同様、
((6 + 8) * 7 + 8 - 7) / 9 / 3を入れると以下のような感じになります。なんとなく、良さそうです。
6 + 8 * 7 + 8 - 7 / 9 / 3 + 18を入れると、
となります。Google に計算式を入れたところ、同じ結果が返ってきましたので、多分あってます。
まとめ
EBNF →コード変換について考えました。そして、ちょっと浮気して、演算子優先順位法を試してみました。演算子は比較演算を考えるとなかなかの量がありますので、演算子優先順位法はほぼ必須なように思います。
次は、EBNF (風の表記)のパーサを作り、それを使って、EBNF からコードを生成するようなプログラムを作ってみようと思います。EBNF を書きながら EBNF のデバッグをして、出力のデバッグをして…といった作業を考えると、JavaScript で書き直そうかと思います(C# だとテスト入力用のエディタやデバッグ表示の表現を作るのが大変だったためです)。