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(); } ;
- 増えたこと
- program(プログラム全体)
- statementの列
- statement(文)
- 代入文: 変数に式の評価結果を代入
- 出力文: 式の評価結果を出力
- VARIABLE(変数)
- 代入文の左辺に来たら、その変数に右辺の評価結果を代入
- 式の中に出たら、その時点で代入されている値を利用
- コメント
- $channel = HIDDEN じゃなくて skip() でいいらしい。違いはあるのかな?*1
- options{greedy=false}:
で、指定のルールを最短一致にする。これやらないと /* */ ... /* */ がひどいことに。
- program(プログラム全体)
他は目新しいものはないかと。
インタプリタの実装
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内にメンバ変数を作れる
- 変数を保存する環境を作った
- @member と書いてもコンパイルエラーにならない。でもenvironmentが作られなくてはまってた
- @init でルール開始前に処理を実行できる
- 環境を初期化した
変数が初期化されてなかったら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チャネルに乗せた方がよさそうだ。