JavaCCでJVM上で動作するインタプリタを作成

RPGゲームを作ろうとすると、描画処理等の下回りを作成した後、スクリプト 言語を実装する必要が出てくると思います。ゲームのイベント処理等はイベン トエディタ等でスクリプトを記述した方が効率が良いからです。 今回はシェルスクリプトっぽいインタプリタをJavaCCを用いて実装します。

 

1. やりたいこと

インタプリタでやりたいことを以下にまとめます。

* 変数を用いたい
* 整数と文字列を用いたい
* 簡単な算術演算を用いたい
* スクリプトファイルで定義した関数を用いたい
* ビルトインコマンドを呼び出したい
* if文を用いたい
* while文を用いたい

特にif文について解説します。 算術演算についてはCodeZineの記事を参考にしました。

1. TOKEN

TOKENを以下のように定義します。

TOKEN:
{
    < LBLOCK: "{" >
  | < RBLOCK: "}" >
  | < LPAREN: "(" >
  | < RPAREN: ")" >
  | < LBRACKET: "[" >
  | < RBRACKET: "]" >
  | < IF: "if" >
  | < THEN: "then" >
  | < ELIF: "elif" >
  | < ELSE: "else" >
  | < FI: "fi" >
  | < FOR: "for" >
  | < WHILE: "while" >
  | < DO: "do" >
  | < DONE: "done" >
  | < BREAK: "break" >
  | < RETURN: "return" >
  | < ID: (["a"-"z"] | ["A"-"Z"]) (["a"-"z"] | ["A"-"Z"] | ["0"-"9"])* >
  | < REF: "$" >
  | < MUL: "*" >
  | < DIV: "/" >
  | < ADD: "+" >
  | < SUB: "-" >
  | < ASSIGN: "=" >
  | < GE: ">=" >
  | < GT: ">" >
  | < LE: "<=" >
  | < LT: "<" >
  | < EQ: "==" >
  | < NE: "!=" >
  | < QUESTION: "?" >
  | < NUM: (["0"-"9"])+ >
}

2. if文

シェルスクリプトのif文は以下のような構造になります。

if [ $a < $b ]
then
  return $a
elif [ $a == $b ]
then
  return $a
else
  return $b
fi

3. if文のjjtファイル

以下のIfStmt()という構文木のノードを定義します。javaファイルでは ASTIfStmtというクラスで表現されます。

void IfStmt():
{}
{
  <IF> <LBRACKET> Cond() <RBRACKET>
  <THEN>
    Block()
  [
  (
  <ELIF> <LBRACKET> Cond() <RBRACKET>
  <THEN>
    Block()
  )+
  ]
  [
  <ELSE>
    Block()
  ] 
  <FI>
}
 

[<statement>]は省略可能なパターンを表します。また、(<statement>)+は1回 以上の繰り返しを表します。つまり、ifは必ず必要ですが、elifは複数個あっ てもなくてもよくて、elseはひとつあるかないかとなります。

JavaCCはIfStmtのパターンを見つけてくれると、<IF>や<LBRACKET>等のTOKEN は切り取ってくれて(というよりパターンマッチングにしか使わない)、 Cond()とBlock()のみをASTIfStmtでアクセスできるようになります。

Cond()とBlock()はそれぞれASTCondとASTBlockというクラスで表現されます。

4. IfStmtのVisitor

ASTXXXクラスはSimpleNodeクラスを継承しており、ASTIfStmtのjjtGetChild() でASTCondやASTBlockにアクセスできます。jjtGetNumChildren()で jjtGetChild()で取得可能なノードの数が分かります。それぞれのノードの jjtAccept()を実行すると、それぞれのノードのvisit()が呼ばれます。

  @Override
  public Object visit(ASTIfStmt node, Object data) {
    /**
     * First child points if condition, second points if body, third points elif
     * condition, fourth points elif body, and the last child points else body.
     */
    int i, size = node.jjtGetNumChildren();
    for (i = 0; i < size - 1; i += 2) {
      ShellValue value = (ShellValue) node.jjtGetChild(i).jjtAccept(this, data);
      String getValue = value.getValue();
      /** Check condition is 1 */
      if (getValue.equals("1")) {
        node.jjtGetChild(i + 1).jjtAccept(this, null);
        return null;
      }
    }
    
    /** Check which else condition is exists. */
    if (i >= size - 1 && size % 2 != 0)
      node.jjtGetChild(size - 1).jjtAccept(this, null);
    return null;
  }
  

jjtGetChild(0)はif文の判定文、jjtGetChild(1)はif文が真の場合に実行され るブロック、jjtGetChild(2)はelif文の判定文、jjtGetChild(3)はelif文が真 の場合に実行されるブロック、・・・、sizeが奇数の場合にjjtGetChild(size - 1)はelse文の実行されるブロックとなります。

それぞれ判定文を取得し、得られた値が真(ここでは文字列が"1")の場合に 続くブロックのvisitを実行するようにします。

5. コード

GitHubで公開してます。