JavaCC は、主に入力テキストを解析し、その構文構造に対応する構文ツリーを生成するために使用されるコンパイラ開発ツールであることがわかっています。JavaCC によって生成される構文ツリーは、開発者がその処理と操作を定義して実装する必要がある下位レベルの抽象化です。JJTree は JavaCC の拡張機能であり、より高いレベルの抽象化を提供します。JavaCC と比較すると、JJTree によって生成される構文ツリー ノードには属性とメソッドが含まれているため、特に複雑な構文構造や構文ツリー ノードの操作要件に対して、構文ツリーの構築と処理が容易になります。
まず次の例を見てみましょう。
PARSER_BEGIN(Eg1)
package com.github.gambo.javacc.jjtree.eg1;
/** An Arithmetic Grammar. */
public class Eg1 {
/** Main entry point. */
public static void main(String args[]) {
System.out.println("Reading from standard input...");
Eg1 t = new Eg1(System.in);
try {
SimpleNode n = t.Start();
n.dump("");
System.out.println("Thank you.");
} catch (Exception e) {
System.out.println("Oops.");
System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
PARSER_END(Eg1)
SKIP :
{
" "
| "\t"
| "\n"
| "\r"
| <"//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")>
| <"/*" (~["*"])* "*" (~["/"] (~["*"])* "*")* "/">
}
TOKEN : /* LITERALS */
{
< INTEGER_LITERAL:
<DECIMAL_LITERAL> (["l","L"])?
| <HEX_LITERAL> (["l","L"])?
| <OCTAL_LITERAL> (["l","L"])?
>
|
< #DECIMAL_LITERAL: ["1"-"9"] (["0"-"9"])* >
|
< #HEX_LITERAL: "0" ["x","X"] (["0"-"9","a"-"f","A"-"F"])+ >
|
< #OCTAL_LITERAL: "0" (["0"-"7"])* >
}
TOKEN : /* IDENTIFIERS */
{
< IDENTIFIER: <LETTER> (<LETTER>|<DIGIT>)* >
|
< #LETTER: ["_","a"-"z","A"-"Z"] >
|
< #DIGIT: ["0"-"9"] >
}
/** Main production. */
SimpleNode Start() : {}
{
Expression() ";"
{ return jjtThis; }
}
/** An Expression. */
void Expression() : {}
{
AdditiveExpression()
}
/** An Additive Expression. */
void AdditiveExpression() : {}
{
MultiplicativeExpression() ( ( "+" | "-" ) MultiplicativeExpression() )*
}
/** A Multiplicative Expression. */
void MultiplicativeExpression() : {}
{
UnaryExpression() ( ( "*" | "/" | "%" ) UnaryExpression() )*
}
/** A Unary Expression. */
void UnaryExpression() : {}
{
"(" Expression() ")" | Identifier() | Integer()
}
/** An Identifier. */
void Identifier() : {}
{
<IDENTIFIER>
}
/** An Integer. */
void Integer() : {}
{
<INTEGER_LITERAL>
}
これは算術式を解析するための文法ファイルで、以前の javacc 文法ファイルと比較して、SimpleNode の dump メソッドによって構文ツリーを出力するための階層構造が追加されています。
Ant のビルド構成を見てみましょう。
<target name="eg1" description="Builds example 'eg1'">
<delete dir="${build.home}/jjtree"/>
<mkdir dir="${build.home}/jjtree"/>
<copy file="eg1.jjt" todir="${build.home}/jjtree"/>
<jjtree target="eg1.jjt" outputdirectory="${build.home}/jjtree" javacchome="${javacc.home}"/>
<javacc target="${build.home}/jjtree/eg1.jj" outputdirectory="${build.home}/jjtree" javacchome="${javacc.home}"/>
<javac deprecation="false" srcdir="${build.home}/jjtree" destdir="${build.class.home}" includeantruntime='false'/>
<echo message="*******"/>
<echo message="******* Now cd into the eg1 directory and run 'java Eg1' ******"/>
<echo message="*******"/>
</target>
xxx.jj ファイルが最初に jjtree コマンドによって生成され、次に java コードが javacc コマンドによって生成されることがわかります。最終的に生成されるファイルは次のとおりです。
Eg1 の main メソッドを実行し、(a + b) * (c + 1) と入力します。セミコロンを入力する必要があることに注意してください。実行結果は次のとおりです。
Reading from standard input...
(a + b) * (c + 1);
Start
Expression
AdditiveExpression
MultiplicativeExpression
UnaryExpression
Expression
AdditiveExpression
MultiplicativeExpression
UnaryExpression
Identifier
MultiplicativeExpression
UnaryExpression
Identifier
UnaryExpression
Expression
AdditiveExpression
MultiplicativeExpression
UnaryExpression
Identifier
MultiplicativeExpression
UnaryExpression
Integer
Thank you.
プロダクションが呼び出される順序に従って階層構造が生成されることがわかります。
ノード
デフォルトでは、JJTree は非終端記号ごとに解析ツリー ノードを構築するコードを生成します。この動作を変更して、特定の非終端シンボルがノードを生成しないか、実稼働拡張のノードの一部になるようにすることもできます。
JJTree は、すべての解析ツリー ノードが実装する必要がある Java インターフェイス `Node` を定義します。このインターフェイスは、現在のノードに親ノードを追加する方法、および子ノードを追加して取得する方法など、いくつかのメソッドを提供します。生成したコードには Node という名前の別のインターフェイスがあり、その構造は次のとおりです。
public interface Node {
// 此方法在节点成为当前节点后调用。它表明当前节点现在可以添加子节点。
public void jjtOpen();
// 子节点添加完毕后,将调用此方法。
public void jjtClose();
//这对方法分别用于设置节点的父节点和获取节点的父节点
public void jjtSetParent(Node n);
public Node jjtGetParent();
//方法将指定的节点添加到当前节点的子节点列表中
public void jjtAddChild(Node n, int i);
//获取指定索引的子节点
public Node jjtGetChild(int i);
//获取子节点的数量
public int jjtGetNumChildren();
public int getId();
}
Nodeインターフェースを実装したSimpleNode.classを自分で実装することもできますが、存在しない場合はJJTreeによって自動生成されます。このクラスをノード実装のテンプレートまたは親クラスとして使用したり、変更したりできます。SimpleNode は、ノードとその子を再帰的にダンプするための基本メカニズムも提供します。生成された SimpleNode の dump メソッドを確認できます。
public void dump(String prefix) {
System.out.println(toString(prefix));
if (children != null) {
for (int i = 0; i < children.length; ++i) {
SimpleNode n = (SimpleNode)children[i];
if (n != null) {
n.dump(prefix + " ");
}
}
}
}
このため、main 関数で dump メソッドを実行した後に階層構造を出力します。
ノード名と条件を定義する
上記の実行結果を観察してみましょう。単純な算術式では 20 を超えるノードが生成されます。実際には、UnaryExpression、express などの多くの中間遷移ノードは不要です。算術式に直接関連するノードのみを生成したいと考えています。また、ノード名もすべて製品名にちなんで命名されており、どれがプラスノードでどれが乗算ノードなのかが直感的にわかりにくく、一般的に可読性は高くありません。
上記の例を改良してみましょう。
options {
MULTI=true;
KEEP_LINE_COLUMN = false;
}
PARSER_BEGIN(Eg2)
package com.github.gambo.javacc.jjtree.eg2;
/** An Arithmetic Grammar. */
public class Eg2 {
/** Main entry point. */
public static void main(String args[]) {
System.out.println("Reading from standard input...");
Eg2 t = new Eg2(System.in);
try {
ASTStart n = t.Start();
n.dump("");
System.out.println("Thank you.");
} catch (Exception e) {
System.out.println("Oops.");
System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
PARSER_END(Eg2)
SKIP :
{
" "
| "\t"
| "\n"
| "\r"
| <"//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")>
| <"/*" (~["*"])* "*" (~["/"] (~["*"])* "*")* "/">
}
TOKEN : /* LITERALS */
{
< INTEGER_LITERAL:
<DECIMAL_LITERAL> (["l","L"])?
| <HEX_LITERAL> (["l","L"])?
| <OCTAL_LITERAL> (["l","L"])?
>
|
< #DECIMAL_LITERAL: ["1"-"9"] (["0"-"9"])* >
|
< #HEX_LITERAL: "0" ["x","X"] (["0"-"9","a"-"f","A"-"F"])+ >
|
< #OCTAL_LITERAL: "0" (["0"-"7"])* >
}
TOKEN : /* IDENTIFIERS */
{
< IDENTIFIER: <LETTER> (<LETTER>|<DIGIT>)* >
|
< #LETTER: ["_","a"-"z","A"-"Z"] >
|
< #DIGIT: ["0"-"9"] >
}
/** Main production. */
ASTStart Start() : {}
{
Expression() ";"
{ return jjtThis; }
}
/** An Expression. */
void Expression() #void : {}
{
AdditiveExpression()
}
/** An Additive Expression. */
void AdditiveExpression() #void : {}
{
(
MultiplicativeExpression() ( ( "+" | "-" ) MultiplicativeExpression() )*
) #Add(>1)
}
/** A Multiplicative Expression. */
void MultiplicativeExpression() #void : {}
{
(
UnaryExpression() ( ( "*" | "/" | "%" ) UnaryExpression() )*
) #Mult(>1)
}
/** A Unary Expression. */
void UnaryExpression() #void : {}
{
"(" Expression() ")" | MyID() | Integer()
}
/** An Identifier. */
void MyID() :
{
Token t;
}
{
t=<IDENTIFIER>
{
jjtThis.setName(t.image);
}
}
/** An Integer. */
void Integer() : {}
{
<INTEGER_LITERAL>
}
同じ式 (a + b) * (c + 1) を入力すると、結果は次のようになります。
Reading from standard input...
(a + b) * (c + 1);
Start
Mult
Add
Identifier: a
Identifier: b
Add
Identifier: c
Integer
Thank you.
以前の jjtree 文法ファイルと比較すると、ここでの変更は大きくありません。1 つずつ見てみましょう。
void Expression() #void : {}
{
AdditiveExpression()
}
ここには以前より #void が 1 つ増えています。現在のプロダクションでノードが生成されないようにしたい場合は、この構文を使用できます。
void AdditiveExpression() #void : {}
{
(
MultiplicativeExpression() ( ( "+" | "-" ) MultiplicativeExpression() )*
) #Add(>1)
}
この生成は、複数の乗算式の加算と減算を表します。ここでの #Add は後置演算子として機能し、そのスコープは直前の展開単位です (ここでは、前の括弧内の式を表します)。
#Add(>1) は、条件付きノードを記述する方法です。条件が 'true' と評価された場合にのみ、現在のノードとその子ノードが構築されます。計算結果が 'false' の場合、現在のノードはそしてその子ノードは構築されません。#Add の後に条件がない場合は #Add(true) を意味し、#Add(>1) は #Add(jjtree.arity() > 1) の略であり、jjtree.arity() は現在のノード範囲: ノード スタックにプッシュされたノードの数は、Add ノードに生成された子ノードがあるかどうかとして単純に理解できます。jjtreeのクラス構造は後ほど追加します。
void MyID() :
{
Token t;
}
{
t=<IDENTIFIER>
{
jjtThis.setName(t.image);
}
}
これは、解析されたトークン文字をノード名として出力するために使用されるカスタム ノードのアプリケーションです。jjtThis.setName(t.image); は、現在のノードの名前の設定を表します。ここでは、SimpleNode を展開する方法を確認できます。
package com.github.gambo.javacc.jjtree;
/**
* An ID.
*/
public class ASTMyID extends SimpleNode {
private String name;
/**
* Constructor.
* @param id the id
*/
public ASTMyID(int id) {
super(id);
}
/**
* Set the name.
* @param n the name
*/
public void setName(String n) {
name = n;
}
/**
* {@inheritDoc}
* @see org.javacc.examples.jjtree.eg2.SimpleNode#toString()
*/
public String toString() {
return "Identifier: " + name;
}
}
上のノードツリーの印刷を見比べてみると、使い方が一目瞭然!カスタム ノードのクラス名には AST というプレフィックスが付いており、文法ファイル内のこのノードへの参照名は ASTxxx であり、xxx はノード参照であることに注意してください。
上記の例では、ノードの生成を避けるためにいくつかのプロダクションの後に #void を追加しましたが、実際には、このようなアクションをデフォルトの動作として設定することができ、ファイルヘッダーのオプション領域に設定を追加できます。
NODE_DEFAULT_VOID=true
この方法では、すべてのプロダクションがノードを生成するわけではありません。ノードを生成するためにいくつかのプロダクションが必要な場合は、最後に #xxx を追加します。具体的なコードは次のとおりです。
options {
MULTI=true;
NODE_DEFAULT_VOID=true;
}
PARSER_BEGIN(Eg)
package com.github.gambo.javacc.jjtree;
/** An Arithmetic Grammar. */
public class Eg {
/** Main entry point. */
public static void main(String args[]) {
System.out.println("Reading from standard input...");
Eg t = new Eg(System.in);
try {
ASTStart n = t.Start();
n.dump("");
System.out.println("Thank you.");
} catch (Exception e) {
System.out.println("Oops.");
System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
PARSER_END(Eg)
SKIP :
{
" "
| "\t"
| "\n"
| "\r"
| <"//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")>
| <"/*" (~["*"])* "*" (~["/"] (~["*"])* "*")* "/">
}
TOKEN : /* LITERALS */
{
< INTEGER_LITERAL:
<DECIMAL_LITERAL> (["l","L"])?
| <HEX_LITERAL> (["l","L"])?
| <OCTAL_LITERAL> (["l","L"])?
>
|
< #DECIMAL_LITERAL: ["1"-"9"] (["0"-"9"])* >
|
< #HEX_LITERAL: "0" ["x","X"] (["0"-"9","a"-"f","A"-"F"])+ >
|
< #OCTAL_LITERAL: "0" (["0"-"7"])* >
}
TOKEN : /* IDENTIFIERS */
{
< IDENTIFIER: <LETTER> (<LETTER>|<DIGIT>)* >
|
< #LETTER: ["_","a"-"z","A"-"Z"] >
|
< #DIGIT: ["0"-"9"] >
}
/** Main production. */
ASTStart Start() #Start : {}
{
Expression() ";"
{ return jjtThis; }
}
/** An Expression. */
void Expression() : {}
{
AdditiveExpression()
}
/** An Additive Expression. */
void AdditiveExpression() : {}
{
(
MultiplicativeExpression() ( ( "+" | "-" ) MultiplicativeExpression() )*
) #Add(>1)
}
/** A Multiplicative Expression. */
void MultiplicativeExpression() : {}
{
(
UnaryExpression() ( ( "*" | "/" | "%" ) UnaryExpression() )*
) #Mult(>1)
}
/** A Unary Expression. */
void UnaryExpression() : {}
{
"(" Expression() ")" | Identifier() | Integer()
}
/** An Identifier. */
void Identifier() #MyID :
{
Token t;
}
{
t=<IDENTIFIER>
{
jjtThis.setName(t.image);
}
}
/** An Integer. */
void Integer() #Integer : {}
{
<INTEGER_LITERAL>
}
ビジター
前の例では、simpleNode の dump メソッドを使用してノード ツリーを出力し、関連するノードの toString メソッドを変更して、対応するノードの名前を出力しましたが、これは洗練されたアプローチではありません。優れたプログラム設計では、単一の責任と分離された操作が必要です。この章の例では、ノードの種類に関係なく、そのノード クラスは独自のビジネス ロジックに重点を置く必要があり、ノード ツリーへのアクセスはノード自体から分離して外部で定義する必要があります。jjtree が提供する Visitor はこの要件を満たすことができます。
Visitor パターンを使用すると、開発者は、元のノード コードを変更せずに、構文ツリー内のノードをトラバースして新しい操作や関数を追加できるビジター オブジェクトを定義できます。
上記のコードにいくつかの変更を加えてみましょう。
options {
MULTI=true;
VISITOR=true;
NODE_DEFAULT_VOID=true;
}
PARSER_BEGIN(Eg2)
package com.github.gambo.javacc.jjtree;
/** An Arithmetic Grammar. */
public class Eg2 {
/** Main entry point. */
public static void main(String args[]) {
System.out.println("Reading from standard input...");
Eg2 t = new Eg2(System.in);
try {
ASTStart n = t.Start();
Eg2Visitor v = new Eg2DumpVisitor();
n.jjtAccept(v, null);
System.out.println("Thank you.");
} catch (Exception e) {
System.out.println("Oops.");
System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
PARSER_END(Eg2)
SKIP :
{
" "
| "\t"
| "\n"
| "\r"
| <"//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")>
| <"/*" (~["*"])* "*" (~["/"] (~["*"])* "*")* "/">
}
TOKEN : /* LITERALS */
{
< INTEGER_LITERAL:
<DECIMAL_LITERAL> (["l","L"])?
| <HEX_LITERAL> (["l","L"])?
| <OCTAL_LITERAL> (["l","L"])?
>
|
< #DECIMAL_LITERAL: ["1"-"9"] (["0"-"9"])* >
|
< #HEX_LITERAL: "0" ["x","X"] (["0"-"9","a"-"f","A"-"F"])+ >
|
< #OCTAL_LITERAL: "0" (["0"-"7"])* >
}
TOKEN : /* IDENTIFIERS */
{
< IDENTIFIER: <LETTER> (<LETTER>|<DIGIT>)* >
|
< #LETTER: ["_","a"-"z","A"-"Z"] >
|
< #DIGIT: ["0"-"9"] >
}
/** Main production. */
ASTStart Start() #Start : {}
{
Expression() ";"
{ return jjtThis; }
}
/** An Expression. */
void Expression() : {}
{
AdditiveExpression()
}
/** An Additive Expression. */
void AdditiveExpression() : {}
{
(
MultiplicativeExpression() ( ( "+" | "-" ) MultiplicativeExpression() )*
) #Add(>1)
}
/** A Multiplicative Expression. */
void MultiplicativeExpression() : {}
{
(
UnaryExpression() ( ( "*" | "/" | "%" ) UnaryExpression() )*
) #Mult(>1)
}
/** A Unary Expression. */
void UnaryExpression() : {}
{
"(" Expression() ")" | Identifier() | Integer()
}
/** An Identifier. */
void Identifier() #MyOtherID :
{
Token t;
}
{
t=<IDENTIFIER>
{
jjtThis.setName(t.image);
}
}
/** An Integer. */
void Integer() #Integer : {}
{
<INTEGER_LITERAL>
}
まず、オプション領域の VISITOR=true; はビジター モードをオンにすることを意味し、このとき、JJTree は生成するすべてのノード クラスに 'jjtAccept()' メソッドを挿入し、実装して渡すことができるビジター インターフェイスを生成します。受け入れ用のノードです。生成されたコードを見てみましょう。
生成された訪問者インターフェイスは次のとおりです。
package com.github.gambo.javacc.jjtree;
public interface Eg2Visitor
{
public Object visit(SimpleNode node, Object data);
public Object visit(ASTStart node, Object data);
public Object visit(ASTAdd node, Object data);
public Object visit(ASTMult node, Object data);
public Object visit(ASTMyOtherID node, Object data);
public Object visit(ASTInteger node, Object data);
}
#voild 以外のすべてのノードに対して、対応する Visit メソッドが生成されていることがわかります。次に、Visitor の実装を見てみましょう。
package com.github.gambo.javacc.jjtree;
public class Eg2DumpVisitor implements Eg2Visitor
{
private int indent = 0;
private String indentString() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < indent; ++i) {
sb.append(' ');
}
return sb.toString();
}
public Object visit(SimpleNode node, Object data) {
System.out.println(indentString() + node +
": acceptor not unimplemented in subclass?");
++indent;
data = node.childrenAccept(this, data);
--indent;
return data;
}
public Object visit(ASTStart node, Object data) {
System.out.println(indentString() + node);
++indent;
data = node.childrenAccept(this, data);
--indent;
return data;
}
public Object visit(ASTAdd node, Object data) {
System.out.println(indentString() + node);
++indent;
data = node.childrenAccept(this, data);
--indent;
return data;
}
public Object visit(ASTMult node, Object data) {
System.out.println(indentString() + node);
++indent;
data = node.childrenAccept(this, data);
--indent;
return data;
}
public Object visit(ASTMyOtherID node, Object data) {
System.out.println(indentString() +"Identifier:"+ node.getName());
++indent;
data = node.childrenAccept(this, data);
--indent;
return data;
}
public Object visit(ASTInteger node, Object data) {
System.out.println(indentString() + node);
++indent;
data = node.childrenAccept(this, data);
--indent;
return data;
}
}
/*end*/
現在のノードツリーの印刷が各ノードの visit メソッドに実装されていることがわかります。
data = node.childrenAccept(this, data); このコード行は、現在のノードの子ノードの jjAccept メソッドを呼び出して、子ノードの訪問をトリガーするメソッドを表します。
public Object jjtAccept(Eg2Visitor visitor, Object data){
return visitor.visit(this, data);
}
/** Accept the visitor. **/
public Object childrenAccept(Eg2Visitor visitor, Object data){
if (children != null) {
for (int i = 0; i < children.length; ++i) {
children[i].jjtAccept(visitor, data);
}
}
return data;
}
この記事の導入例は、ソース コードの例を基にしていますが、後続の章では、jjtree を使用して、より詳細なケースをいくつか示します。
記事内のサンプルコード: GitHub - ziyiyu/javacc-tutorial: javacc チュートリアル