🗂

ARTLR4を使って四則演算の式を計算する 〜ANTLR入門 その2〜

2024/11/17に公開

以前の記事で四則演算の文法を定義して、式を以下のように認識させました。このような認識結果を解析木と呼びます。

今回は、Javaで式をパースして解析木を生成し、その解析木を以下の図のようにスキャンしていきます。スキャンながら各exprに対応する値を計算していって、式全体の値(一番上のexpr)を計算します。

前回のantlr4 -visitor Arithmetic.g4の実行の結果、以下のソースコードが生成されています。

$ ls *.java
ArithmeticBaseListener.java  ArithmeticLexer.java     ArithmeticParser.java
ArithmeticBaseVisitor.java   ArithmeticListener.java  ArithmeticVisitor.java

上記のクラスを使用しながら、以下のようにコマンドラインから式を受け取って計算するクラスを作成します。


import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;

public class Calc extends ArithmeticBaseVisitor<Double> {
    public static void main(String[] args) throws Exception {
        // 式をパースする
        CharStream input = CharStreams.fromString(args[0]);
        ArithmeticLexer lexer = new ArithmeticLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        ArithmeticParser parser = new ArithmeticParser(tokens);
        parser.setBuildParseTree(true);
        ArithmeticParser.ExprContext expr = parser.expr();

        // 式を計算する
        Calc calc = new Calc();
        double result = calc.visit(expr);
        System.out.println(result);
    }

    @Override
    public Double visitExpr(ArithmeticParser.ExprContext ctx) {
        if (ctx.NUMBER() != null) {
            // 子ノードが数字の場合
            return Double.parseDouble(ctx.NUMBER().getText());
        } else if (ctx.expr().size() == 1) {
            // 括弧で囲まれた式の場合
            return visit(ctx.expr(0));
        } else {
            // 四則演算の場合
            double left = visit(ctx.expr(0));
            double right = visit(ctx.expr(1));
            return switch (ctx.op.getText()) {
                case "+" -> left + right;
                case "-" -> left - right;
                case "*" -> left * right;
                case "/" -> left / right;
                default -> throw new RuntimeException("Unknown operator: " + ctx.op.getText());
            };
        }
    }
}

ここでは以下のように実行される事を想定しています。

java Calc "3 + 5 * (2 - 8)"

以下の部分で、引数の式をパースします。

    public static void main(String[] args) throws Exception {
        // 式を解析する
        CharStream input = CharStreams.fromString(args[0]);
        ArithmeticLexer lexer = new ArithmeticLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        ArithmeticParser parser = new ArithmeticParser(tokens);
        parser.setBuildParseTree(true);
        ArithmeticParser.ExprContext expr = parser.expr();

Calcクラスは、以下のようにArithmeticParserがパースした式(expr)を引数にvisitメソッドを呼び出すと、前述の図のexprをスキャンしていきます。

        // 式を計算する
        Calc calc = new Calc();
        double result = calc.visit(expr);
        System.out.println(result);

スキャン時に以下のメソッドが各expr毎に呼び出されます。ここでctxはexprに対応するオブジェクトです。

    @Override
    public Double visitExpr(ArithmeticParser.ExprContext ctx) {
        if (ctx.NUMBER() != null) {
            // 子ノードが数字の場合
            return Double.parseDouble(ctx.NUMBER().getText());
        } else if (ctx.expr().size() == 1) {
            // 括弧で囲まれた式の場合
            return visit(ctx.expr(0));
        } else {
            // 四則演算の場合
            double left = visit(ctx.expr(0));
            double right = visit(ctx.expr(1));
            return switch (ctx.op.getText()) {
                case "+" -> left + right;
                case "-" -> left - right;
                case "*" -> left * right;
                case "/" -> left / right;
                default -> throw new RuntimeException("Unknown operator: " + ctx.op.getText());
            };
        }
    }

以下のような数値に対応するexprの場合

ctx.NUMBER()3のトークンが返します。以下のように3を返すようにします。

        if (ctx.NUMBER() != null) {
            // 子ノードが数字の場合
            return Double.parseDouble(ctx.NUMBER().getText());

また、以下のような減算に対応するexprの場合

以下のように、2つの子ノード、expr(0), expr(1)から値を取得して、演算子の文字列opから演算子を判定して、減算を実行した結果を返すようにします。

            double left = visit(ctx.expr(0));
            double right = visit(ctx.expr(1));
            return switch (ctx.op.getText()) {
                // 中略
                case "-" -> left - right;

上記のように、ArithmeticBaseVisitorのvisitExprメソッドをオーバーライドしてやると

        double result = calc.visit(expr);

は下図のexprの計算結果(式全体の計算結果)を返すようになります。

ソースコードをコンパイルして実行すると以下のようになります。

$ javac *.java
$ java Calc "3 + 5 * (2 - 8)"
-27.0

以上で、ANTLRを使用して四則演算を計算できるようになりました。

GitHubで編集を提案

Discussion