文字列定義
それでは文法定義について見ていきましょう。文法定義は「文字列定義」と「生成規則の定義」に分かれます。まず最初に文字列定義を見てみます。JavaCCでは、トークンなど、次の4種類の文字列定義があります。
種類 | 概要 |
TOKEN | トークンの定義 |
SKIP | 空白など、処理では無視される区切り文字 |
MORE | 続くトークンと一緒に処理される |
SPECIAL_TOKEN | コメントなど文法とは独立して現れる |
文字列定義の基本的な形は次のようになります。
TOKEN{ <トークン名 : パターン > | <トークン名 : パターン > }
パターンは簡易正規表現で表します。そのとき文字は「"」で囲んで表します。
サンプルでは次のようにして「HELLO
」「JAVACC
」「JJTREE
」という固定文字列のトークンを定義しています。
<HELLO: "hello"> | <JAVACC: "javacc"> | <JJTREE: "jjtree">
次の部分は、「NAME
」というトークンをアルファベットの繰り返しとして定義しています。
| <NAME: (["A"-"Z", "a"-"z"])+>
「[」「]」で囲んだ文字リストでは、文字をカンマ区切りで列挙します。また「-」を使うと、文字の範囲を指定することができます。
次のようにすると、「大文字か小文字のアルファベット1文字」を表します。
["A"-"Z", "a"-"z"]
また、このとき「~」を付けると、文字リストに列挙していない文字を表します。特に「~[]」としたときには任意の1文字を表します。
回数の指定には次のものが使えます。通常の正規表現とは違い、「(」「)」は省略できません。
記述 | 回数 |
(~)? | 省略可能 |
(~)* | 0回以上の繰り返し |
(~)+ | 1回以上の繰り返し |
次のようにすると「アルファベット1文字以上の繰り返し」を表現できます。
(["A"-"Z", "a"-"z"])+
TOKEN
以外ではトークン名は必要ないので、パターンだけを記述します。
SKIP: { " " | "\r" | "\t" | "\n" }
トークン名に#を付けると、トークン定義だけで使えるローカルなトークンになります。
| <#UPPERCASE : ["A" - "Z"]> | <NAME: ( <UPPERCASE> | ["a"-"z"])+>
また、種別の後に「[IGNORE_CASE]
」を記述すると、大文字小文字を区別しなくなります。
TOKEN[IGNORE_CASE]: { <HELLO: "hello"> }
トークンの優先順位は、一番長くマッチするものが、先に定義された順に当てはまります。
生成規則定義
生成規則は、次のようにメソッド定義のような形で定義します。
戻り値 生成規則名(引数): { ローカル変数定義 } { 生成規則 }
生成規則には、トークン、生成規則、文字列の連結を記述します。
基本的な使い方は次のようになります。
void JavaCC(): {} { <JAVACC> }
また、回数や選択などは文字列定義とほぼ同じように使えます。
記号 | 意味 |
~|~ | 選択(いずれか) |
(~)? | 省略可能 |
[~] | 省略可能 |
(~)* | 0回以上の繰り返し |
(~)+ | 1回以上の繰り返し |
Command
生成規則では次のように、JavaCC
生成規則か、JJTree
生成規則か、Name
生成規則のいずれかを生成するように定義しています。
void Command() #void : {} { JavaCC() | JJTree() | Name() }
ここで「#void
」はノードクラスを生成しないための指定です。JJTreeを使うと、生成規則ごとに「AST生成規則名」という名前のノードクラスが生成されますが、「#void
」を付けるとノードクラスを生成しません。ノードクラスの生成の制御に関しては次回に説明します。
生成規則やトークンの後に「{」「}」で囲んでJavaコードを記述すると、それらの生成規則やトークンが処理された後で実行されるアクションとなります。ローカル変数を定義すると、アクションの中で利用できます。
Name
生成規則は次のようになっています。
void Name(): { Token t;} { t = <NAME> { jjtThis.nodeValue = t.image;} }
ここで、トークンを変数t
に代入していますが、このようにすると、識別したトークンをアクションで利用することができます。
アクション内で使っているjjtThis
はそのノード自身をあらわすオブジェクトです。
リテラルや識別子など、トークンの文字列を処理中で利用したい場合には、このようにしてトークンのimage
フィールドを取得します。
ここで使っているnodeValue
フィールドは、BaseNode
クラスで定義しているString
型のフィールドです。オプションにノードの基底クラスとして指定しているので、ここで利用できます。ノードの基底クラスを指定しない場合には、生成されたノードクラスを編集してフィールドを追加しておく必要があります。
生成規則定義の戻り値は通常void
としますが、戻り値の型を指定することもできます。このときの戻り値は、アクション中でreturn
文を使って指定します。
サンプルでは次のように、この文法定義での開始記号になる生成規則で自分自身のノードオブジェクトを返すようにしています。開始記号はHello
生成規則なので、ノードオブジェクトのクラスはASTHello
です。
ASTHello Hello(): {} { <HELLO> Command() { return jjtThis;} }
このように開始記号の生成規則では、自分自身のノードを返すようにするのが一つのパターンです。
構文の処理
最後に処理の記述について説明します。
構文解析は、パーサーオブジェクトから開始記号のメソッドを呼び出すことで始まります。今回はノードを返すように指定していたので、ノードのオブジェクトが返ってきます。
Node node = parser.Hello();
ノードのオブジェクトは、SimpleNode
クラスを継承した「AST+生成規則名」という名前のクラスになります。また、SimpleNode
クラスはNode
インタフェースを実装しています。今回のオプションにNODE_EXTENDS
を指定すると、SimpleNode
クラスは指定したクラスのサブクラスになります。
これらのクラス・インタフェースは、PARSER_BEGIN
のあとに指定したパッケージに作成されます。
VISITORオプションを指定している場合、jjtAccept
メソッドを呼び出すとそのノードに対応するvisit
メソッドが呼び出されます。jjtAccept
メソッドの第2引数に渡したオブジェクトは、visit
メソッドの第2引数として渡されます。ここでは特に指定する必要がないのでnull
を渡しています。
node.jjtAccept(visitor, null);
また、それぞれのノードの処理では、jjtGetChild
メソッドで子ノードを取得してjjtAccept
メソッドを呼び出すことで再帰的に処理が行われていきます。
public Object visit(ASTHello node, Object data) { String word = node.jjtGetChild(0).jjtAccept(this, null).toString(); System.out.println("Hello " + word +"!!"); return null; }
visit
メソッドでの戻り値は、そのままjjtAccept
の戻り値として引き継がれます。
public Object visit(ASTJavaCC node, Object data) { return "ステキJavaCC"; }
まとめ
今回は、JavaCC+JJTreeでの構文定義について大まかに説明しました。JavaCCの「examples」フォルダにはJava文法の構文定義などサンプルがあるので、構文定義を実際に行うときの参考になると思います。
次回は簡単な式言語を定義して、実用的な構文を定義していこうと思います。