ANTLR(5) - インタープリタ

ここまでのおさらいで、簡単なインタープリタを。

機能は次の2つ。主に変数を導入。関数呼び出しとかは面倒だからスキップ。

  • 変数 = 式;
  • print(式);

前のエントリからどのくらいでできるだろ。

ASTの生成

grammar Simple;

options {
  output=AST;
  ASTLabelType=CommonTree;
}

tokens {
  PROGRAM;
  NEGATE;
}

@header {
package introduction;
}

@lexer::header {
package introduction;
}

program
  : statement+ EOF -> ^(PROGRAM statement+)
  ;

statement
  : VARIABLE '=' expr ';'    -> ^('=' VARIABLE expr)
  | 'print' '(' expr ')' ';' -> ^('print' expr)
  ;

expr
  : term (( '+'^ | '-'^ ) term)*
  ;

term
  : unary  (( '*'^ | '/'^ ) unary)*
  ;

unary
  : factor      -> factor
  | '+' factor  -> factor
  | '-' factor  -> ^(NEGATE factor)
  ;

factor
  : NUMBER       -> NUMBER
  | VARIABLE     -> VARIABLE
  | '(' expr ')' -> expr
  ;

NUMBER
  : ('0'..'9')+ ( '.' ('0'..'9')+ )?
  ;

VARIABLE
  : ('A'..'Z'|'a'..'z') ('A'..'Z'|'a'..'z'|'0'..'9'|'_')*
  ;

SKIP
  : ( ' ' | '\t' | '\r' | '\n' )+ { skip(); }
  | '//' ( options{greedy=false;}: . )* '\n' { skip(); }
  | '/*' ( options{greedy=false;}: . )* '*/' { skip(); }
  ;
  • 増えたこと
    1. program(プログラム全体)
      • statementの列
    2. statement(文)
      • 代入文: 変数に式の評価結果を代入
      • 出力文: 式の評価結果を出力
    3. VARIABLE(変数)
      • 代入文の左辺に来たら、その変数に右辺の評価結果を代入
      • 式の中に出たら、その時点で代入されている値を利用
    4. コメント
      • $channel = HIDDEN じゃなくて skip() でいいらしい。違いはあるのかな?*1
      • options{greedy=false}: で、指定のルールを最短一致にする。これやらないと /* */ ... /* */ がひどいことに。

他は目新しいものはないかと。

インタプリタの実装

tree grammarで実装。

Simple.tokensを利用するので、同じディレクトリに持ってきておく。

tree grammar SimpleInterpreter;

options {
  tokenVocab = Simple;
  ASTLabelType = CommonTree;
}

@header {
package introduction;

import java.util.HashMap;
import java.util.Map;
}

@members {
private java.util.Map<String, Double> environment;
}

program
  @init {
    environment = new HashMap<String, Double>();
  }
  : ^(PROGRAM statement+)
  ;

statement
  : ^('=' VARIABLE expr) { environment.put($VARIABLE.text, $expr.result); }
  | ^('print' expr) { System.out.println($expr.result); }
  ;

expr returns [double result]
  : ^('+' left=expr right=expr) { $result = left + right; }
  | ^('-' left=expr right=expr) { $result = left - right; }
  | ^('*' left=expr right=expr) { $result = left * right; }
  | ^('/' left=expr right=expr) { $result = left / right; }
  | ^(NEGATE operand=expr) { $result = - operand; }
  | NUMBER { $result = Double.parseDouble($NUMBER.text); }
  | VARIABLE
    {
      if (environment.containsKey($VARIABLE.text)) {
        $result = environment.get($VARIABLE.text);
      }
      else {
        throw new RuntimeException("Missing " + $VARIABLE.text);
      }
    }
  ;
  • @members でparser内にメンバ変数を作れる
    1. 変数を保存する環境を作った
    2. @member と書いてもコンパイルエラーにならない。でもenvironmentが作られなくてはまってた
  • @init でルール開始前に処理を実行できる
    1. 環境を初期化した

変数が初期化されてなかったらRuntimeException。

確認

複数行になるとechoではつらいので、ファイルから読みます。

起動のためのコード:

public class SimpleInterpreterTest {
  public static void main(String[] args)
        throws IOException, RecognitionException {
    InputStream source = new FileInputStream(args[0]);
    try {
      ANTLRInputStream in =
          new ANTLRInputStream(source, "UTF-8");
      Lexer lexer = new SimpleLexer(in);
      CommonTokenStream tokens =
          new CommonTokenStream(lexer);
      SimpleParser parser = new SimpleParser(tokens);
      CommonTree root = parser.program().tree;
      
      CommonTreeNodeStream ast =
          new CommonTreeNodeStream(root);
      SimpleInterpreter interpreter =
          new SimpleInterpreter(ast);
      
      System.out.println("tree: " + root.toStringTree());
      interpreter.program();
    }
    finally {
      source.close();
    }
  }
}

サンプルプログラム(simple.txt):

/* simple.txt */
print(1);
a = 10;
b = 15;
c = a + b / 5;
print(c * 2); // (10 + 15 / 5) * 2 = 26

実行結果:

> java -cp bin;lib\antlr-runtime-3.0.1.jar \
    introduction.SimpleInterpreterTest simple.txt
tree: (PROGRAM (print 1) (= a 10) (= b 15) (= c (+ a (/ b 5))) (print (* c 2)))
1.0
26.0

ここまでは基本機能の習得で、以降はQueryParserのエラーリカバリ関係について調査。
やや遊びすぎた感。

*1:19:50追記:skipはTokenチャネル自体に乗せないらしい。HIDDENは乗せるけど不可視な状態にする。コメントを後で使いそうな場合などはHIDDENチャネルに乗せた方がよさそうだ。