CalculatorListener
接口,及其默认的、空实现类CalculatorBaseListener
CalculatorListener接口的代码如下:
ParseTreeListener
接口public interface CalculatorListener extends ParseTreeListener {
// 进入由CalculatorParser.prog()方法生成的parse tree
// 向下递归遍历ProgContext的子节点前,将调用该方法
void enterProg(CalculatorParser.ProgContext ctx);
// 退出由alculatorParser.prog()方法生成的parse tree
// 完成子节点的递归遍历后、退出ProgContext前,将调用该方法
void exitProg(CalculatorParser.ProgContext ctx);
void enterPrintExpr(CalculatorParser.PrintExprContext ctx);
void exitPrintExpr(CalculatorParser.PrintExprContext ctx);
... // 其他的enterCtx()和exitCtx()方法,与上面的含义相似,不过多展示
}
ParseTreeWalker
触发的方法的最小核心public interface ParseTreeListener {
// 定义访问叶子节点和ErrorNode时需要执行的操作
void visitTerminal(TerminalNode node);
void visitErrorNode(ErrorNode node);
// 定义进入或退出每个rule都需要执行的通用操作
void enterEveryRule(ParserRuleContext ctx);
void exitEveryRule(ParserRuleContext ctx);
}
为什么叶子节点和ErrorNode没有enter、exit方法?
叶子节点作为最底层节点,没有子节点可以向下递归遍历,更别提递归后的退出
因此,对叶子节点的遍历没有enter和exit事件,只有一个访问操作
至于ErrorNode,看源码定义,它应该是一种典型的叶子节点,所以只有一个访问操作
public class ErrorNodeImpl extends TerminalNodeImpl implements ErrorNode {
@SuppressWarnings("CheckReturnValue")
public class CalculatorBaseListener implements CalculatorListener {
// CalculatorListener中为Ctx定义的监听器方法,只展示部分
@Override public void enterProg(CalculatorParser.ProgContext ctx) { }
@Override public void exitProg(CalculatorParser.ProgContext ctx) { }
... // 其他方法省略
// CalculatorListener从ParseTreeListener接口继承来的通用方法
@Override public void enterEveryRule(ParserRuleContext ctx) { }
@Override public void exitEveryRule(ParserRuleContext ctx) { }
@Override public void visitTerminal(TerminalNode node) { }
@Override public void visitErrorNode(ErrorNode node) { }
}
从对Parser的学习可知,parser rule或者添加了label的rule element都将对应一个Ctx,且它们都将直接或间接继承ParserRuleContext类
ParserRuleContext类中有两个与listener模式有关的空方法:
public void enterRule(ParseTreeListener listener) { }
public void exitRule(ParseTreeListener listener) { }
以ProgContext为例,它重写了上述两个方法:调用listener的监听器方法对enter和exit事件进行处理
public static class ProgContext extends ParserRuleContext {
... // 其他代码省略
@Override
public void enterRule(ParseTreeListener listener) {
if ( listener instanceof CalculatorListener ) ((CalculatorListener)listener).enterProg(this);
}
@Override
public void exitRule(ParseTreeListener listener) {
if ( listener instanceof CalculatorListener ) ((CalculatorListener)listener).exitProg(this);
}
}
listener本身并不负责parse tree的遍历,parse tree的遍历由ParseTreeWalker.walk()
方法实现
以listener模式遍历parse tree,触发遍历的核心代码如下:
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(listener, parseTree);
walk()方法会在遍历parse tree的过程中,主动地、间接地触发每个Ctx对应的监听器方法
public void walk(ParseTreeListener listener, ParseTree t) {
if ( t instanceof ErrorNode) {
listener.visitErrorNode((ErrorNode)t);
return;
}
else if ( t instanceof TerminalNode) {
listener.visitTerminal((TerminalNode)t);
return;
}
RuleNode r = (RuleNode)t;
// 触发enter事件,最终将调用listener.enterCtx()方法处理enter事件
enterRule(listener, r);
int n = r.getChildCount();
for (int i = 0; i<n; i++) {
walk(listener, r.getChild(i)); // 递归遍历子节点
}
// 触发exit事件,最终将调用listener.exitCtx()方法处理exit事件
exitRule(listener, r);
}
enterRule()方法的实现如下:
protected void enterRule(ParseTreeListener listener, RuleNode r) {
ParserRuleContext ctx = (ParserRuleContext)r.getRuleContext();
// 若进入每个parser rule都需要执行一些通用操作,则可以在自定义的listener中重写该方法
listener.enterEveryRule(ctx);
// 实际将调用listener.enterCtx()方法
ctx.enterRule(listener);
}
至此,触发并监听enter事件的调用链正式形成:walker.walk()
→ \rightarrow → walker.enterRule(listener, r)
→ \rightarrow → ctx.enterRule(listener)
→ \rightarrow → listener.enterCtx()
同样的,触发并监听exit事件的调用链为:walker.walk()
→ \rightarrow → walker.exitRule(listener, r)
→ \rightarrow → ctx.exitRule(listener)
→ \rightarrow → listener.exitCtx()
4 - 3
,得到的parse tree如下:enterRule(listener, addSubCtx)
→ \rightarrow → walk(listener, constantLeft)
→ \rightarrow → walk(listener, subTerminalNode)
→ \rightarrow → walk(listener, constantRight)
→ \rightarrow → exitRule(listener, addSubCtx)
enterAddSub()
之后,因此要想进行4 - 3
的计算,就必须放在exitAddSub()
中进行继承Antlr4编译得到的CalculatorBaseListener
类,按需重写某些方法,主要是exitCtx()方法,以实现一个简单整数计算器
public class CalculatorListenerImpl extends CalculatorBaseListener {
// 记录每个ctx对应的属性,这里的属性是double类型的数值,可以用于实现表达式的计算
private final ParseTreeProperty<Integer> ctxs = new ParseTreeProperty<>();
// 存储变量名和对应的值,若存储ctx会导致变量的值在后续表达式中无法获取
private final HashMap<String, Integer> memory = new HashMap<>();
// 语法规则prog不涉及实际操作,可以无需重写
@Override
public void exitPrintExpr(CalculatorParser.PrintExprContext ctx) {
// 获取expr的属性并打印,保留4位小数
Integer value = ctxs.get(ctx.expr());
// 常量,直接打印值,无需打印表达式
if (ctx.expr() instanceof CalculatorParser.ConstantContext) {
System.out.printf("常量的值: %d\n", value);
} else {
System.out.printf("计算结果: %s = %d\n", ctx.expr().getText(), value);
}
// System.out.printf("exit %s\n", getCtxString(ctx));
}
// 变量可能在后续的表达式中被使用,虽然变量名相同,但ctx已经发生了变化
// 因此不能直接存储,而是应该单独开辟一块内存,缓存变量名和对应的值
@Override
public void exitAssign(CalculatorParser.AssignContext ctx) {
String variable = ctx.ID().getText();
// 从ctxs中获取expr对应的ctx的属性
Integer value = ctxs.get(ctx.expr());
memory.put(variable, value);
}
@Override
public void exitVariable(CalculatorParser.VariableContext ctx) {
// 从内存中获取变量的值,作为VariableContext的属性
ctxs.put(ctx, memory.getOrDefault(ctx.getText(), 0));
}
@Override
public void exitMulDiv(CalculatorParser.MulDivContext ctx) {
// 获取左右expr对应的ctx的属性,并将乘除运算的结果作为MulDivContext的属性
Integer left = ctxs.get(ctx.expr(0));
Integer right = ctxs.get(ctx.expr(1));
if (ctx.op.getType() == CalculatorParser.MUL) {
ctxs.put(ctx, left * right);
} else {
ctxs.put(ctx, left / right);
}
}
@Override
public void exitAddSub(CalculatorParser.AddSubContext ctx) {
// 获取左右expr对应的ctx的属性,并将乘除运算的结果作为AddSubContext的属性
Integer left = ctxs.get(ctx.expr(0));
Integer right = ctxs.get(ctx.expr(1));
if (ctx.op.getType() == CalculatorParser.ADD) {
ctxs.put(ctx, left + right);
} else {
ctxs.put(ctx, left - right);
}
}
@Override
public void exitConstant(CalculatorParser.ConstantContext ctx) {
// 获取常量的值,作为ConstantContext的属性
ctxs.put(ctx, Integer.valueOf(ctx.INT().getText()));
}
@Override
public void exitParentheses(CalculatorParser.ParenthesesContext ctx) {
// 获取expr对应的ctx的属性,作为ParenthesesContext的属性
ctxs.put(ctx, ctxs.get(ctx.expr()));
}
// 必要时,可以在enterCtx()和exitCtx()方法中,打印enter和exit事件
public String getCtxString(ParserRuleContext ctx) {
return ctx.getClass().getSimpleName() + "@" + Integer.toHexString(ctx.hashCode());
}
}
从实现代码可以看出:
ProgContext
和AssignContext
外,其他Ctx都是在exitCtx()方法中得到当前节点的属性,并存入ParseTreeProperty
中protected Map<ParseTree, V> annotations = new IdentityHashMap<ParseTree, V>();
对变量的操作是比较特殊且容易出错的地方,这里特别做一下分析
0
0
,并将VariableContext及其值存入ParseTreeProperty中Integer left = ctxs.get(ctx.expr(0));
Integer right = ctxs.get(ctx.expr(1));
与visitor模式一样,首先需要将输入字符流,通过词法分析、语法分析转化为一棵parse tree
与visitor模式不同的是,listener模式对parse tree进行遍历的入口方法是ParseTreeWalker.walk()
使用自定义的Listener遍历parse tree的代码如下:
public static void main(String[] args) {
String input = "b=(4+8)/2\n"
+ "b+6\n"
+ "24";
System.out.println("输入的字符流:\n" + input);
// 词法分析,获取token
CharStream charStream = CharStreams.fromString(input);
CalculatorLexer lexer = new CalculatorLexer(charStream);
CommonTokenStream tokens = new CommonTokenStream(lexer);
// 语法分析,获取parse tree
CalculatorParser parser = new CalculatorParser(tokens);
ParseTree parseTree = parser.prog();
// 使用自定义的Listener访问parse tree
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(new CalculatorListenerImpl(), parseTree);
}
visit()
或visitChildern()
方法递归遍历子节点。listener模式中,parse tree的DFS由antlr-runtime提供的ParseTreeWalker.walk()
方法实现,开发人员只需要关注enter/exit rule
事件的处理逻辑The biggest difference between the listener and visitor mechanisms is that listener methods are called independently by an ANTLR-provided walker object, whereas visitor methods must walk their children with explicit visit calls. Forgetting to invoke visitor methods on a node’s children, means those subtrees don’t get visited.
ParseTreeWalker.walk()
)负责调用监听器方法,以实现节点的递归遍历;而visitor模式中,必须由开发者显式调用visit方法才能实现节点的递归遍历In visitor pattern you have the ability to direct tree walking while in listener you are only reacting to the tree walker.