Javaで簡単なインタプリタを書いてみよう (1)

と思う.手持ちのコードがあればいつかなにかの役に立つかもしれない.大昔にバイト先でlex,yaccを使って,C++で特殊な用途のインタプリタを書いたことがあるのだけど,最近はいろいろと状況が違いそうなので,まずは復習から.

お手軽にやるにはパーザジェネレータを使いたいところだが,何がいいのかよくわからない.検索してみてよく見かけるのは,昔からあるjavaccと国産の¬<><∪∪(notavacc).どっちでもいいような気がするが,とりあえず¬<><∪∪を使ってみよう.そのうち乗り換えるかも.

¬<><∪∪

¬<><∪∪ は,おそらく横倒しに読むとなんとなくjavaccに見えるという理由で名前づけられたパーザジェネレータ.読み方は「ノタバシーシー」でいいんだと思う.javaccLL(k)なのに対して,LALR(∞)でパーズする,らしい.それぞれ記述できる言語の範囲が微妙に違うらしいのだが,よくわからない.勉強不足で恥ずかしい.まあ,これから書くのはいずれにしろおもちゃだから,どちらをつかっても問題は無いだろう.

¬<><∪∪の動作

¬<><∪∪ は,シンタックスを記述した xxxx.notavacc をコンパイルし,単一のjava ファイルを吐き出す.javaファイルには,各ノードに対応するクラスが定義された,パーザが収められる.

パーザはただ抽象構文木を返すだけなので,それに対する解釈を行うメインのプログラムを記述してやる必要がある.

とりあえず簡単な計算機を書いてみる.

まずは,計算機を書いてみよう.¬<><∪∪のチュートリアルに,加減乗除のできる計算機がのっているので,それを参考に,変数を導入できるものを考えてみる.イメージとしては

> pi = 3.14
3.14
> r = 10
10.0
> pi * r * r
314.0

みたいな感じで書けるものを目指す.

文法を定義する

基本的にチュートリアルからのパクリ.数値リテラルを,浮動小数点数を受けられるように拡張し,expression の上にstatementを設け,代入を処理できるようにしてある.

// ExpressionParser.notavacc
$parser ExpressionParser;      // 出力クラス名を定義

$token NUMBER       = SUBNUMBER | SUBNUMBER '.' SUBNUMBER | '.' SUBNUMBER ;
$subtoken SUBNUMBER = '0'..'9'+ ;         // 数値リテラル
$white $token SPACE   = ( ' ' | '\t' | '\n' | '\r')+ ;  // ホワイトスペース

$token IDENTIFIER          = IDENTIFIER_START IDENTIFIER_PART* ;  // 変数
$subtoken IDENTIFIER_START = '$' | 'A'..'Z' | '_' | 'a'..'z' | '\u0080'..'\uffff' ;
$subtoken IDENTIFIER_PART  = '0'..'9' | IDENTIFIER_START ;

$parsable
Root            { cont:statement }

// 文は代入か,式
statement       = Assignment { left:IDENTIFIER "=" right:expression }
                | expression
				;
// 式は加算,減算
expression      = Addition { left:expression "+" right:term }
                | Subtraction { left:expression "-" right:term }
                | term
                ;

// タームは,乗除算
term            = Multiplication { left:term "*" right:factor }
                | Division { left:term "/" right:factor }
                | factor
                ;

// ファクタは,括弧式,数値リテラル,マイナス式,変数
factor          = Paren { "(" cont:expression ")" }
                | NUMBER
                | Negative { "-" cont:factor }
                | IDENTIFIER
                ;

ドライバ

構文木を処理するメインクラスはDriverという名前にした.標準入力と標準出力を使う.

import java.io.*;
import java.util.*;

public class Driver {

    ExpressionParser p = new ExpressionParser();
    PrintStream pw;
    LineNumberReader lnr;
    Map<String, Double> env = new HashMap<String, Double>();

    public Driver(PrintStream pw, LineNumberReader lnr) {
        this.pw = pw;
        this.lnr = lnr;
    }

    public static void main(String[] args) {
        Driver d = new Driver(System.out, 
                              new LineNumberReader(new InputStreamReader(System.in)));
        d.start();
    }
...

startがメインループ.いわゆるeval-printループになっている.一行読んで評価し,出力する.

  void start() {
    while (true) {
      try {
        // プロンプトの出力
        pw.print("> "); pw.flush();
        String line = lnr.readLine();
        if (line == null)
          break;
        // 構文木の作成
        ExpressionParser.Node expr = p.parseRoot("test", line, 8);
        // 評価
        double v = eval(expr);
        // 結果の表示
        pw.println(v); pw.flush();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

evalは,式を再起的に評価する.

    public double eval(ExpressionParser.Node node) {
        if (node instanceof ExpressionParser.Token) {
            ExpressionParser.Token token = (ExpressionParser.Token) node;
                if (token.getSymbolID() == ExpressionParser.TOKEN_NUMBER) {
                    String image = token.getImage();
                    return Double.parseDouble(image);
               } else if (token.getSymbolID() == ExpressionParser.TOKEN_IDENTIFIER) {
                    String image = token.getImage();
                    if (!env.containsKey(image))
                         return 0.0;
                    return env.get(image);
               }
        } else if (node instanceof ExpressionParser.Addition) {
            ExpressionParser.Addition n = (ExpressionParser.Addition) node;
            return eval(n.left()) + eval(n.right());
        } else if (node instanceof ExpressionParser.Subtraction) {
            ExpressionParser.Subtraction n = (ExpressionParser.Subtraction) node;
            return eval(n.left()) - eval(n.right());
        } else if (node instanceof ExpressionParser.Multiplication) {
            ExpressionParser.Multiplication n = (ExpressionParser.Multiplication) node;
            return eval(n.left()) * eval(n.right());
        } else if (node instanceof ExpressionParser.Division) {
            ExpressionParser.Division n = (ExpressionParser.Division) node;
            return eval(n.left()) / eval(n.right());
        } else if (node instanceof ExpressionParser.Paren) {
            ExpressionParser.Paren n = (ExpressionParser.Paren) node;
            double v = eval(n.cont());
            return v;
        } else if (node instanceof ExpressionParser.Negative) {
            ExpressionParser.Negative n = (ExpressionParser.Negative) node;
            double v = - eval(n.cont());
            return v;
        } else if (node instanceof ExpressionParser.Assignment) {
            ExpressionParser.Assignment n = (ExpressionParser.Assignment) node;
            double v = eval(n.right());
			assign(n.left(), v);
            return v;
        } else if (node instanceof ExpressionParser.Root) {
            ExpressionParser.Root n = (ExpressionParser.Root) node;
            double v = eval(n.cont());
            return v;
        } else {
            throw new IllegalArgumentException("" + node);
        }
		return 0;
    }

代入式の処理.mapで実装されたenv に対してputするだけ.

    public void assign(ExpressionParser.Node node, double val) {
         if (node instanceof ExpressionParser.Token) {
             ExpressionParser.Token token = (ExpressionParser.Token) node;
             if (token.getSymbolID() == ExpressionParser.TOKEN_IDENTIFIER) {
                  String image = token.getImage();
                  env.put(image, val);
                  return;
             }
         }
         System.err.println("left side of assignment have to be a variable.");
    }

これで一応完成.
実行してみる.

> $ java Driver
> a = 10 
10.0
> b = 20
20.0
> a + b - 0.2 / 3
29.933333333333334

どうやら動いているようだ.

所感

notavaccはよくできている. yaccだと,文法の中に,コードの断片を埋め込んで自前で構文木を構築しなければならなかったが,その辺の手間が省けて大変に楽だ.

ただ,演算子の優先順位,結合則に関しては,yaccのほうが楽だったかもしれない.yacc では,トークンの定義する順番と明示的なleft,rightのノーテーションで記述するが,notavaccでは,中間ノードを導入することで実現しなければならないようだ.expression, term, factorと分けているが,yaccだったら全部expressionにできたんじゃないだろうか.

次回は関数定義を導入する予定.