ANTLR的运行库提供了两种遍历树的机制:语法分析树监听器与访问器。通过它们,我们可以在遍历树的时候实现相应逻辑。在本章中,我们将通过编写一个简单的计算器来探究三种在事件方法中共享信息的途径。
按照上一章“Antlr4入门(三)如何编写语法文件”所学的内容,我们可以很轻松的写出一个只支持加法和乘法的计算器语法文件。
grammar ExprTest;
cal : expr;
expr : expr MUL expr # Mul
| expr ADD expr # Add
| INT # Int
;
MUL : '*';
ADD : '+';
INT : '0' | [1-9][0-9]*;
NEWLINE : '\r'?'\n';
WS : [ \t\n] -> skip;
使用ANTLR工具来测试下语法规则是否正确。
测试无误后,我们接着使用ANTLR工具来生成语法分析树等代码。
ExprTestVisitor.java是个访问器接口,定义了一些访问RuleNode的接口方法,可以通过实现它来完成自定义的功能。而ExprTestBaseVisitor.java是该接口的默认实现,它为每个接口方法提供了空实现。
为构建一个基于访问器的计算器程序,最简单的方法是令expr规则中的事件方法返回子表达式的值。例如,visitAdd()将返回两个子表达式相加的结果,visitInt()方法返回整数元素的值。按照说明,我们实现的访问器如下所示:
package exprtest;
public class CalculatorVisitor extends ExprTestBaseVisitor {
@Override
public Integer visitAdd(ExprTestParser.AddContext ctx) {
return visit(ctx.expr(0)) + visit(ctx.expr(1));
}
@Override
public Integer visitMul(ExprTestParser.MulContext ctx) {
return visit(ctx.expr(0)) * visit(ctx.expr(1));
}
@Override
public Integer visitInt(ExprTestParser.IntContext ctx) {
// return int value
return Integer.valueOf(ctx.getText());
}
}
让我们编写一个简单的main方法去调用它
import exprtest.*;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
public class ExprTestMain {
public static void main(String[] args){
ANTLRInputStream inputStream = new ANTLRInputStream("1+2*3+4");
ExprTestLexer lexer = new ExprTestLexer(inputStream);
CommonTokenStream tokenStream = new CommonTokenStream(lexer);
ExprTestParser parser = new ExprTestParser(tokenStream);
ParseTree parseTree = parser.cal();
System.out.println(parseTree.toStringTree(parser));
CalculatorVisitor visitor = new CalculatorVisitor();
int result = visitor.visit(parseTree);
System.out.println("Visitor calculate result: "+result);
}
}
运行结果如下:
到这里,我们已经使用访问器实现了加法和乘法功能,下面,让我们来看看如何使用监听器来实现它。
ANTLR的运行库提供了两种遍历树的机制:监听器机制与访问器机制。与访问器不同的是,监听器的方法会被ANTLR提供的遍历器对象(比如ParseTreeWalker)自动调用,而在访问器的方法中,必须显示调用visit方法来访问子节点。如果没有调用visit方法就会导致对应的子树不被访问。而且监听器方法是没有返回值的(即返回类型是void)。因此我们需要一种额外的数据结构来存储我们的计算结果,供下一次计算调用。与Java虚拟机使用栈来临时存储返回值一样,我们可以使用栈来存储中间计算结果。
package exprtest;
import java.util.Stack;
// 监听器方法是没有返回值的,因此需要一个成员变量来存储局部变量——栈(JVM也是使用栈来临时存储返回值的)
// 监听器会自动访问子树
public class CalculatorListener extends ExprTestBaseListener {
// 定义一个栈(先进后出),存放中间计算结果
private Stack result = new Stack();
public int getResult(){
// 将最后的结果返回
return result.pop();
}
@Override
public void exitAdd(ExprTestParser.AddContext ctx) {
// 右边的值会先出栈
int right = result.pop();
int left = result.pop();
// 再将计算后的值放入栈中
result.push(left + right);
}
@Override
public void exitMul(ExprTestParser.MulContext ctx) {
int right = result.pop();
int left = result.pop();
result.push(left * right);
}
@Override
public void exitInt(ExprTestParser.IntContext ctx) {
// 将INT的值放入栈中
result.push(Integer.valueOf(ctx.getText()));
}
}
以上就是一个完整的使用监听器来实现计算功能的代码,下面在main方法中测试它。
ParseTreeWalker walker = new ParseTreeWalker();
CalculatorListener listener = new CalculatorListener();
walker.walk(listener,parseTree);
int listenerResult = listener.getResult();
System.out.println("Listener calculate result: "+listenerResult);
这种使用栈的方法不够优雅,但是非常有效。通过它,我们可以保证事件方法在所有的监听器事件之间的执行顺序是正确的。除此之外,我们还能将局部变量存储在树节点中。
最简单的标注语法分析树节点的方法是使用Map来将任意值和节点一一对应起来。出于这个目的,ANTLR提供了一个名为ParseTreeProperty的辅助类,通过查看它的源代码,我们可以知道它实际上是一个Map
package org.antlr.v4.runtime.tree;
import java.util.IdentityHashMap;
import java.util.Map;
public class ParseTreeProperty {
protected Map annotations = new IdentityHashMap();
public ParseTreeProperty() {
}
public V get(ParseTree node) {
return this.annotations.get(node);
}
public void put(ParseTree node, V value) {
this.annotations.put(node, value);
}
public V removeFrom(ParseTree node) {
return this.annotations.remove(node);
}
}
需要注意的是,如果你想使用自己的Map来替代ParseTreeProperty,那么需要保证它是从IdentityHashMap而不是普通的HashMap派生。而且不管是监听器还是访问器,它们都支持树的标注。
package exprtest;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeProperty;
public class CalculatorWithProps extends ExprTestBaseListener{
// 使用Map将节点映射到对应的结果值
ParseTreeProperty result = new ParseTreeProperty();
public void setValues(ParseTree node, int value){
result.put(node, value);
}
public int getValues(ParseTree node){
return result.get(node);
}
@Override
public void exitCal(ExprTestParser.CalContext ctx) {
setValues(ctx, getValues(ctx.expr()));
}
@Override
public void exitAdd(ExprTestParser.AddContext ctx) {
// 子树节点有三个,两个操作数和一个操作符,1 + 2
int left = getValues(ctx.getChild(0));
int right = getValues(ctx.getChild(2));
// int left = getValues(ctx.expr(0));
// int right = getValues(ctx.expr(1));
setValues(ctx, left + right);
}
@Override
public void exitMul(ExprTestParser.MulContext ctx) {
int left = getValues(ctx.getChild(0));
int right = getValues(ctx.getChild(2));
setValues(ctx, left * right);
}
@Override
public void exitInt(ExprTestParser.IntContext ctx) {
setValues(ctx, Integer.valueOf(ctx.getText()));
}
}
显而易见,不管是使用Stack或者ParseTreeProperty,它们的代码总是相似的,毕竟只是换了个存储方式。
CalculatorWithProps calculatorWithProps = new CalculatorWithProps();
walker.walk(calculatorWithProps, parseTree);
int propsValues = calculatorWithProps.getValues(parseTree);
System.out.println("ParseTreeProperty calculate result: "+propsValues);
到这里,我们已经知道了如何使用树监听器和访问器来实现基本的语言类应用程序,后面,我们将先通过几个实战例子来巩固下所学的内容。