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 {... // 其他代码省略@Overridepublic void enterRule(ParseTreeListener listener) {if ( listener instanceof CalculatorListener ) ((CalculatorListener)listener).enterProg(this);}@Overridepublic 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; iwalk(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 ctxs = new ParseTreeProperty<>();// 存储变量名和对应的值,若存储ctx会导致变量的值在后续表达式中无法获取private final HashMap memory = new HashMap<>();// 语法规则prog不涉及实际操作,可以无需重写@Overridepublic 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已经发生了变化// 因此不能直接存储,而是应该单独开辟一块内存,缓存变量名和对应的值@Overridepublic void exitAssign(CalculatorParser.AssignContext ctx) {String variable = ctx.ID().getText();// 从ctxs中获取expr对应的ctx的属性Integer value = ctxs.get(ctx.expr());memory.put(variable, value);}@Overridepublic void exitVariable(CalculatorParser.VariableContext ctx) {// 从内存中获取变量的值,作为VariableContext的属性ctxs.put(ctx, memory.getOrDefault(ctx.getText(), 0));}@Overridepublic 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);}}@Overridepublic 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);}}@Overridepublic void exitConstant(CalculatorParser.ConstantContext ctx) {// 获取常量的值,作为ConstantContext的属性ctxs.put(ctx, Integer.valueOf(ctx.INT().getText()));}@Overridepublic 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 annotations = new IdentityHashMap();
对变量的操作是比较特殊且容易出错的地方,这里特别做一下分析
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);// 词法分析,获取tokenCharStream charStream = CharStreams.fromString(input);CalculatorLexer lexer = new CalculatorLexer(charStream);CommonTokenStream tokens = new CommonTokenStream(lexer);// 语法分析,获取parse treeCalculatorParser parser = new CalculatorParser(tokens);ParseTree parseTree = parser.prog();// 使用自定义的Listener访问parse treeParseTreeWalker walker = new ParseTreeWalker();walker.walk(new CalculatorListenerImpl(), parseTree);
}
最终执行结果如下,符合预期:
以4+8/2
为例,得到的parse tree如下:
debug跟踪代码的执行流程,发现listener模式对parse tree的遍历过程大致如下:
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.