「2週間でできるスクリプト言語の作り方」の4日目「プログラムを表すオブジェクト」と5日目「構文解析器を作る」。 この本ではyaccとか再帰下降法じゃなくてパーサコンビネータを使って構文木を作ってる。

2項演算の優先順位はoperatorsへの登録時に指定していて、実際のパースはParser.Expr.parse() でまず factor.parse() でパースして、続く演算子がある間(nextOperator(lexer) != null) doShift() を呼び出す。doShift() の呼び出しは右結合の処理、またはより優先順位の高い演算子が続く場合(rightIsExpr())。

パースに失敗した時のバックトラックというか、トークンの消費はどうやってるのかは、Parser.match()Lexer.peek() を使って条件に合うかを調べて、Parser.parse()Lexer.read() を使って消費する?よくわかってない。

構文に合った場合にどうやって木を作るか。構文を定義している BasicParser にはアクションは全く書いてない。Javaのリフレクションを使っている。rule(ASTreeの子クラス).somerule() としたときに、そのルールにマッチした場合にその子クラスのインスタンスが作られる。

Factory.get() の中であれこれやっていて、渡されたクラスに(静的メソッドの) create() というメソッドがあればそれを、なければコンストラクタを呼び出す。PrimaryExprはcreate()を定義していて、インスタンスを作るか省略するかを選択している。

複雑な部分はパーサコンビネータのソース Parser.java に押し込んでいて、自分が定義するパーサ BasicParser.java はシンプルになってるけど、あまりよく動作は飲み込めていない。

Javaのリフレクションを知らなかったのでテストした。例えば

class Test {
  public int test() {
    return 123;
  }
  public int add(int x, int y) {
    return x + y;
  }
}

とかいうクラスがあったときに、

Method m = Test.class.getMethod("test", null);

test メソッドが取り出せる。引数のあるメソッドは

Method m2 = Test.class.getMethod(
    "add", new Class<?>[] {int.class, int.class});

と引数の型の配列を渡す。

指定のメソッドがない場合には NoSuchMethodException 例外が発生する。 取り出したメソッドの呼び出しは

m.invoke(t);
m2.invoke(t, 12, 34);

など。動くコードはこちら:http://ideone.com/jmpKh