Antlr さん、C 用のやつはランタイム必要なのがうざくて使ったことなかったけど、Java ならまぁありかなというところで試す。

`brew install antlr4` とかすると antlr4 コマンドが使えるようになる。

以下のような、サンプルのものをちょっと変えたような文法を書きます。



grammar Expr;
prog:	expr NEWLINE*;
expr:	left=expr op=('*'|'/') right=expr
    |	left=expr op=('+'|'-') right=expr
    |	atom=INT
    |	'(' expr_=expr ')'
    ;
NEWLINE : [\r\n]+ ;
INT     : [0-9]+ ;



antlr4 コマンドで java ファイルを生成させる。maven plugin でもできるけど、maven plugin はなんかうまく動かなかった。maven plugin はなんか大体うまく動かないので、最初から Makefile でも使った方がいい感じある。



今回はMakfefile に以下のように書いた。



src/main/java/com/example/ExprParser.java: src/main/java/com/example/Expr.g4
        antlr4 -package com.example src/main/java/com/example/Expr.g4

あとは Visitor で、巡回しつつ、適当にスタックに積んだり計算したりすればいい。簡単ですね。

package antlrsample;

import static org.junit.Assert.assertEquals;

import java.util.Stack;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.misc.NotNull;
import org.junit.Test;

import com.example.ExprBaseListener;
import com.example.ExprLexer;
import com.example.ExprParser;

public class ExprTest {

	class Listener extends ExprBaseListener {
		public int ret;
		public Stack<Integer> stack = new Stack<>();

		@Override
		public void exitProg(@NotNull ExprParser.ProgContext ctx) {
		}

		@Override
		public void exitExpr(@NotNull ExprParser.ExprContext ctx) {
			if (ctx.expr_ != null) {
				// do nothing
			} else if (ctx.op != null) {
				int r = stack.pop();
				int l = stack.pop();

				switch (ctx.op.getText()) {
				case "*":
					stack.push(l * r);
					break;
				case "+":
					stack.push(l + r);
					break;
				case "/":
					stack.push(l / r);
					break;
				case "-":
					stack.push(l - r);
					break;
				default:
					throw new RuntimeException("Should not reach here");
				}
			} else if (ctx.atom != null) {
				stack.push(Integer.parseInt(ctx.atom.getText()));
			}
		}

		public int getResult() {
			if (stack.size() != 1) {
				throw new RuntimeException("Should not reach here");
			}
			return stack.firstElement();
		}
	}

	private int calc(String src) {
		ExprLexer lexer = new ExprLexer(new ANTLRInputStream(src));
		ExprParser parser = new ExprParser(new CommonTokenStream(lexer));

		Listener listener = new Listener();
		parser.addParseListener(listener);
		parser.prog();
		return listener.getResult();
	}

	@Test
	public void test() {
		assertEquals(116, calc("58*2"));
		assertEquals(3, calc("1+2"));
		assertEquals(11, calc("3+2*4"));
		assertEquals(7, calc("14/2"));
		assertEquals(20, calc("(3+2)*4"));
	}

}

Antlr4 は、一式揃ってる感じあるので、DSL 的なものとか設定ファイルとかをちゃちゃっと書くにはよさそう。

複雑な正規表現を Java で書くのつらいなーとか思い始めたら、気軽に Antlr4 に fallback するのが良さげ(具体的にはPerl で //x を使うようなケースでは Antlr4 に行くのがよさそう)。