前の投稿:分析式を作成するための計算機
目次
1.訪問者とリスナー
前回の記事では、AntlrとVisitorを使用して、式を計算できるプログラムMarvelCalcを実装しました。このプログラムは非常にシンプルで、AntlrのHelloWorldと同等です。ただし、Antlrは、訪問者モードに加えて、リスナーモード、つまりリスナーモードもサポートしています。どちらの方法でも、目的はAST(抽象構文ツリー)をトラバースすることですが、Visitorメソッドは(visitメソッドを介してアクセスされる)子ノードへの明示的なアクセスを必要とします。たとえば、次のコードはMulDivの2つの子ノード(MulDiv)にアクセスします左右のオペランド(ctx.expr(0)およびctx.expr(1))。
// expr op=('*'|'/') expr # MulDiv
public Integer visitMulDiv(CalcParser.MulDivContext ctx) {
int left = visit(ctx.expr(0)); // 访问MulDiv的左操作数
int right = visit(ctx.expr(1)); // 访问MulDiv的右操作数
if ( ctx.op.getType() == CalcParser.MUL ) return left * right;
return left / right;
}
}
リスナーモードでは、システムは自動的に現在のノードの子ノードにアクセスし、子ノードに明示的にアクセスする必要はありません。また、リスナーは現在のノードの開始および終了処理アクションをインターセプトできます。アクションの処理を開始するイベントメソッドはEnterで始まり、処理アクションを終了するイベントメソッドはExitで始まります。たとえば、MulDivアクションを処理すると、enterMulDivとexitMulDivの2つのイベントメソッドが生成されます。これらは、それぞれMulDivの処理の開始と終了を表します。これら2つのメソッドのコードは次のとおりです。
@Override
public void enterMulDiv(CalcParser.MulDivContext ctx) {
}
@Override
public void exitMulDiv(CalcParser.MulDivContext ctx) {
}
それでは、開始処理アクションと終了処理アクションの違いは何ですか?id、valueなどのアトミック式(内部に他の式を含まない式)の場合、2つのイベントメソッドに違いはありません(どちらを使用しても式を処理できます)。ただし、それが非アトミック式である場合は、Enterを使用するかExitを使用するかを検討してください。たとえば、次の式:
3 * (20 / x * 43)
この表現は明らかに非原子的です。コンパイラーは式全体を左から右にスキャンします。最初の乗算記号(*)がスキャンされると、右側のすべてのコンテンツ(20 / x * 43)が全体として処理されます。これは初めてです。 enterMulDivメソッドとexitMulDivメソッドを呼び出します。enterMulDivメソッドを呼び出した後にのみ、他の多くの作業が行われ、最後にexitMulDivメソッドが呼び出されます。では、途中で何が行われるのでしょうか。もちろん式(20 / x * 43)の処理です。この式には変数xがあるため、xをスキャンするときに、変数の存在を検索する必要があります。存在する場合は、変数の値を抽出する必要があります。つまり、最初にenterMulDivメソッドが呼び出されたとき、変数xは処理されていません。(xの値が決定されていないため)enterMulDivメソッドで式全体の値を計算することは明らかに不可能なので、正しいアプローチメソッドが呼び出されると、式全体の各サブ式の値が計算されているため、式全体の値は、exitMulDivメソッドで計算する必要があります。
enterXxxメソッドとexitXxxメソッドもスコープの処理によく使用されます。たとえば、次の関数がスキャンされると、現在のスコープは、関数に対応するenterXxxメソッドとexitXxxでmyfun関数(通常はStackによって処理されます)に切り替えられますこのメソッドでは、myfun関数の親スコープが復元されます。クラス、条件文、ループ文にもスコープの問題があります。スコープの問題については、スコープの実装方法については後の記事で詳しく紹介します。
void myfun() {
}
前の紹介から、リスナーはビジターよりも柔軟性があります。リスナーは、ASTをトラバースするための推奨される方法でもあります。以下の記事では、基本的にリスナーを使用してコンパイラーを実装します。
2.リスナー対応インターフェースと基本クラス
ここで、この記事の主題に戻ります。この記事の目的は、ビジターではなくリスナーを使用して計算機を実装することです。Calc.g4をコンパイルすると、CalcVisitor.javaとCalcBaseVisitor.javaの生成に加えて、CalcListener.javaとCalcBaseListener.javaの2つのファイルも生成されます。CalcListener.javaファイルは、リスナーのインターフェースファイルです。インターフェースのメソッドは、Calc.g4ファイルの生成に基づいて生成されます。ファイルのコードは次のとおりです。
import org.antlr.v4.runtime.tree.ParseTreeListener;
public interface CalcListener extends ParseTreeListener {
void enterProg(CalcParser.ProgContext ctx);
void exitProg(CalcParser.ProgContext ctx);
void enterPrintExpr(CalcParser.PrintExprContext ctx);
void exitPrintExpr(CalcParser.PrintExprContext ctx);
void enterAssign(CalcParser.AssignContext ctx);
void exitAssign(CalcParser.AssignContext ctx);
void enterBlank(CalcParser.BlankContext ctx);
void exitBlank(CalcParser.BlankContext ctx);
void enterParens(CalcParser.ParensContext ctx);
void exitParens(CalcParser.ParensContext ctx);
void enterMulDiv(CalcParser.MulDivContext ctx);
void exitMulDiv(CalcParser.MulDivContext ctx);
void enterAddSub(CalcParser.AddSubContext ctx);
void exitAddSub(CalcParser.AddSubContext ctx);
void enterId(CalcParser.IdContext ctx);
void exitId(CalcParser.IdContext ctx);
void enterInt(CalcParser.IntContext ctx);
void exitInt(CalcParser.IntContext ctx);
}
一般的に言って、すべてのメソッドをCalcListenerインターフェースに実装する必要はないので、antlrは、CalcBaseListener.javaファイルにあるデフォルトの実装クラスCalcBaseListenerも生成します。CalcListenerインターフェースの各メソッドは、CalcBaseListenerクラスに空の実装を提供するため、Listenerメソッドを使用してASTをトラバースするには、CalcBaseListenerクラスから継承して必要なメソッドをオーバーライドするだけで済みます。
3.リスナー方式で電卓を実現
MyCalcParser.javaファイルを作成し、ファイルにMyCalcParserという名前の空のクラスを書き込みます。コードは次のとおりです。
public class MyCalcParser extends CalcBaseListener{
... ...
}
ここでの問題は、CalcBaseListenerのどのメソッドをMyCalcParserクラスでオーバーライドする必要があるか、そしてこれらのメソッドをどのように実装するかです。
この質問に答えるには、前の記事で作成したEvalVisitorクラスのコードを最初に分析する必要があります。実際、EvalVisitorでカバーされているアクションに対応するメソッドは、MyCalcParserクラスのアクションに対応するメソッドもカバーする必要があります。違いは、enterXxx、exitXxx、またはその両方を使用することだけです。
次に、EvalVisitorクラスの重要なポイントを提示します。
(1)EvalVisitorクラスには、memoryという名前のMapオブジェクトがあり、変数の値を格納するために使用されます。これは、リスナーにも必要です。
(2)EvalVisitorクラスにエラー変数があり、分析プロセスにエラーがあるかどうかを識別するために使用されます。これはリスナーにも必要です。
(3)すべてのvisitXxxメソッドには戻り値があります。実際、この戻り値は、上位層のノードに渡される値です。リスナーのメソッドは値を返しませんが、値を上位ノードに渡す必要があるため、値をアップロードする他の方法を考える必要があります。
なぜそれに値をアップロードするのですか?最初に例を示すために、次の式を見てください。
4 * 5
これは乗算式です。コンパイラがこの式をスキャンすると、最初に2つの整数(4と5)が識別されます。これらの2つの整数は2つのアトミック式です。リスナーメソッドを使用する場合、これらの2つの整数に対応するenterIntメソッドで、 '4'と '5'を整数に変換する必要があります(exitIntメソッドも可能です)。これは、値のタイプに関係なく、コンパイラが値を読み取るためです。文字列なので、型変換が必要です。
4と5を含む式はMulDivであり、対応するアクションメソッドはexitMulDivです(4と5がまだスキャンされていないため、enterMulDivは使用できません)。exitMulDivメソッドでは、乗算記号(*)の左と右のオペランド(ctx.expr(0)とctx.expr(1))の値を取得する必要があります。これら2つのオペランドの値は、enterIntメソッドで取得されています。取得した値を前の式(MulDiv式)に渡す必要があります。上位層に値を渡すには多くの方法があります。ここに私が非常に推奨する方法を示します。Mapオブジェクトを使用して渡す必要のあるすべての値を格納することにより、キーは上位層ノードのParseTreeオブジェクトです(各enterXxxおよびexitXxxメソッドのctxパラメータ)タイプはParseTreeインターフェースを実装し、valueは渡される値です。このMapオブジェクトは次のように定義できます。
private Map<ParseTree,Integer> values = new HashMap<>();
同時に、値を設定および取得するには、setValueとgetValueの2つのメソッドが必要です。コードは次のとおりです。
public void setValue(ParseTree node, int value) {
values.put(node,value);
}
public int getValue(ParseTree node) {
try {
return values.get(node);
} catch (Exception e) {
return 0;
}
}
MyCalcParserクラスの完全なコードを以下に示します。
import org.antlr.v4.runtime.tree.ParseTree;
import java.util.HashMap;
import java.util.Map;
public class MyCalcParser extends CalcBaseListener{
private Map<ParseTree,Integer> values = new HashMap<>(); // 用于保存向上一层节点传递的值
Map<String, Integer> memory = new HashMap<String, Integer>(); // 用于保存变量的值
boolean error = false; // 用于标识分析的过程是否出错
// 设置值
public void setValue(ParseTree node, int value) {
values.put(node,value);
}
// 获取值
public int getValue(ParseTree node) {
try {
return values.get(node);
} catch (Exception e) {
return 0;
}
}
@Override public void enterPrintExpr(CalcParser.PrintExprContext ctx) {
// 当开始处理表达式时,默认没有错误
error = false;
}
@Override public void exitPrintExpr(CalcParser.PrintExprContext ctx) {
if(!error) {
// 只有在没有错误的情况下,才会输出表达式的值
System.out.println(getValue(ctx.expr()));
}
}
// 必须要放在exitAssign里
@Override public void exitAssign(CalcParser.AssignContext ctx) {
String id = ctx.ID().getText(); // 获取变量名
int value = getValue(ctx.expr()); // 获取右侧表达式的值
memory.put(id, value); // 保存变量
}
// 必须在exitParens中完成
@Override public void exitParens(CalcParser.ParensContext ctx) {
setValue(ctx,getValue(ctx.expr()));
}
// 计算乘法和除法(必须在exitMulDiv中完成)
@Override public void exitMulDiv(CalcParser.MulDivContext ctx) {
int left = getValue(ctx.expr(0)); // 获取左操作数的值
int right = getValue(ctx.expr(1)); // 获取右操作数的值
if ( ctx.op.getType() == CalcParser.MUL )
setValue(ctx,left * right); // 向上传递值
else
setValue(ctx,left / right); // 向上传递值
}
// 计算加法和减法(必须在exitAddSub中完成)
@Override public void exitAddSub(CalcParser.AddSubContext ctx) {
int left = getValue(ctx.expr(0)); // 获取左操作数的值
int right = getValue(ctx.expr(1)); // 获取右操作数的值
if ( ctx.op.getType() == CalcParser.ADD )
setValue(ctx,left + right);
else
setValue(ctx,left - right);
}
// 在enterId方法中也可以
@Override public void exitId(CalcParser.IdContext ctx) {
String id = ctx.ID().getText();
if ( memory.containsKey(id) ) {
setValue(ctx,memory.get(id)); // 将变量的值向上传递
} else {
// 变量不存在,输出错误信息(包括行和列),
System.err.println(String.format("行:%d, 列:%d, 变量<%s> 不存在!",ctx.getStart().getLine(),ctx.getStart().getCharPositionInLine() + 1, id));
error = true;
}
}
// 处理int类型的值
@Override public void enterInt(CalcParser.IntContext ctx) {
int value = Integer.valueOf(ctx.getText());
setValue(ctx, value); // 将整数值向上传递
}
}
次に、ASTをトラバースして結果を計算するためのMarvelListenerCalcクラスを記述します。コードは次のとおりです。
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import java.io.FileInputStream;
import java.io.InputStream;
public class MarvelListenerCalc {
public static void main(String[] args) throws Exception {
String inputFile = null;
if ( args.length>0 ) {
inputFile = args[0];
} else {
System.out.println("语法格式:MarvelCalc inputfile");
return;
}
InputStream is = System.in;
if ( inputFile!=null ) is = new FileInputStream(inputFile);
CharStream input = CharStreams.fromStream(is);
// 创建词法分析器
CalcLexer lexer = new CalcLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
//
CalcParser parser = new CalcParser(tokens);
ParseTree tree = parser.prog();
MyCalcParser calc = new MyCalcParser();
ParseTreeWalker walker = new ParseTreeWalker();
// 开始遍历AST
walker.walk(calc, tree);
}
}
前の記事で使用したテストケースを引き続き使用します。
1+3 * 4 - 12 /6;
x = 40;
y = 13;
x * y + 20 - 42/6;
z = 12;
4;
x + 41 * z - y;
MarvelListenerCalcの実行結果は以下のとおりです。
この記事で実装されているプログラムはエラーキャプチャもサポートしています。たとえば、最後の式の変数xをxxに変更してプログラムを実行すると、例外がスローされます。エラー式は値を出力せず、例外はエラーの場所を示します(行および列)、次の図に示すように: