0. 序言
- antlr支持Visitor和Listener两种设计模式,本文将介绍如何使用antlr4的visitor模式实现一个简单的整数计算器
- Visitor模式通过double dispatch(
concreteElement.accept(Visitor visitor)
→ \rightarrow →visitor.visit(concreteElement)
),实现了Element与操作的解耦。 - Visitor模式的具体介绍,可以参考之前的博客:《访问者模式(visitor)模式》
1. Antlr4的语法文件
1.1 定义语法文件
-
创建
Calculator.g4
文件,使用Antlr4的语法规则定义一个简单的、支持整数的四则运算的计算器grammar Calculator; // combined grammars that can contain both lexical and parser rules // parser rules,以小写字母开头 prog: stat+; // 允许一次执行多个表达式 // 定义语句,在每个可选方案后面,使用#开头定义label stat: expr EOF # PrintExpr | ID '=' expr EOF # Assign ; // 定义表达式,支持加减乘除、括号 expr: expr op=(MUL|DIV) expr # MulDiv | expr op=(ADD|SUB) expr # AddSub | INT # Constant | ID # Variable | '(' expr ')' # Parentheses ; // lexer rules,以大写字母开头,一般全为大写 // 定义运算符 MUL: '*'; DIV: '/'; ADD: '+'; SUB: '-'; // 定义常量、变量 INT: [0-9]+; ID: [a-zA-Z]+; // 定义可以skip的换行符、tab等 WS: [ \t\r\n]+ -> skip;
1.2 编译语法文件,生成Java代码
- 使用IDEA的antlr4插件,编译Calculator.g4,生成对应的Java代码
- 最终将在
src/main/java/com/sunrise/calculator
目录下生成如下文件:
- 基于visitor模式实现计算器,主要通过继承并重写
CalculatorBaseVisitor
中的方法来实现
2. 重要的类或接口
- 在介绍如何基于visitor模式实现计算器前,先简单介绍将要使用到的重要类或接口
- 如果读者觉得铺垫太多,可以直接跳到第3小节,Visitor模式的代码实现
2.1 visitor接口或类
Calculator.g4
编译后,有两个以Visitor为结尾的Java文件:CalculatorVisitor
接口、CalculatorBaseVisitor
类
CalculatorVisitor接口
-
CalculatorVisitor接口的代码如下:
- 继承了ParseTreeVisitor接口,并为
Calculator.g4
中parse rule对应的parse tree创建了相应的visit()方法 - 泛型通配符T,表示visit操作的返回值类型,如果没有返回值,可以使用Void作为返回值类型
public interface CalculatorVisitor<T> extends ParseTreeVisitor<T> { // 访问由CalculatorParser.prog()方法生成的parse tree,即访问prog这个parser rule对应的parse tree T visitProg(CalculatorParser.ProgContext ctx); // 访问CalculatorParser.stat()方法生成的、label为PrintExpr的解析树 T visitPrintExpr(CalculatorParser.PrintExprContext ctx); // 访问CalculatorParser.stat()方法生成的、label为Assign的解析树 T visitAssign(CalculatorParser.AssignContext ctx); // 访问CalculatorParser.expr()方法生成的、label为Variable的解析树 T visitVariable(CalculatorParser.VariableContext ctx); // 访问CalculatorParser.expr()方法生成的、label为MulDiv的解析树 T visitMulDiv(CalculatorParser.MulDivContext ctx); // 访问CalculatorParser.expr()方法生成的、label为AddSub的解析树 T visitAddSub(CalculatorParser.AddSubContext ctx); // 访问CalculatorParser.expr()方法生成的、label为Constant的解析树 T visitConstant(CalculatorParser.ConstantContext ctx); // 访问CalculatorParser.expr()方法生成的、label为Parentheses的解析树 T visitParentheses(CalculatorParser.ParenthesesContext ctx); }
- 继承了ParseTreeVisitor接口,并为
-
注意: 使用Void作为返回值类型,方法的返回值应该定义为
null
,而不能像void方法一样,使用return;
或者无return语句Callable<Void> callable = new Callable<Void>() { @Override public Void call() { System.out.println("Hello!"); return null; } };
ParseTreeVisitor接口
-
由antlr-runtime提供的、访问parse tree的基础接口,其中的每个方法都会返回类型为T的、用户自定义的操作结果(
user-defined result of the operation
)public interface ParseTreeVisitor<T> { // visit一棵parse tree的入口方法 T visit(ParseTree tree); // 访问一个节点的所有孩子节点,这里的孩子特指儿子节点,不包括孙子节点 // parse tree由一系列的节点(RuleNode)组成,一个节点代表的不只有节点本身,还有其下的子节点 T visitChildren(RuleNode node); // 访问terminal节点,是parse tree的叶子节点,也是词法解析后得到的tokens T visitTerminal(TerminalNode node); // 访问一个error节点 T visitErrorNode(ErrorNode node); }
抽象类AbstractParseTreeVisitor
-
AbstractParseTreeVisitor是一个抽象类,提供了ParseTreeVisitor接口的默认实现,关键点:
- visit()方法:内含accept()方法,实现了double dispatch逻辑
- visitChildren()方法:基于visitor模式、使用accept()方法实现对所有孩子节点的访问
public abstract class AbstractParseTreeVisitor<T> implements ParseTreeVisitor<T> { // 以visit模式访问parse tree时,通过ParseTree.accept(this) --> ParseTreeVisitor.visit(this)实现double dispatch @Override public T visit(ParseTree tree) { return tree.accept(this); } @Override public T visitChildren(RuleNode node) { T result = defaultResult(); // result的初始值为null int n = node.getChildCount(); for (int i = 0; i < n; i++) { // 访问孩子节点前,先判断是否可以访问孩子节点。若返回false表示不能访问,则直接退出循环 if (!shouldVisitNextChild(node, result)) { break; } // 获取孩子节点,通过accept()方法,实现孩子节点及其所有子节点的访问,从而获得操作结果 ParseTree c = node.getChild(i); T childResult = c.accept(this); result = aggregateResult(result, childResult); // 聚合结果,默认将childResult作为最新的result } return result; } @Override public T visitTerminal(TerminalNode node) { return defaultResult(); } @Override public T visitErrorNode(ErrorNode node) { return defaultResult(); } protected T defaultResult() { return null; } // 一个RuleContext可能拥有多个孩子节点,需要对每个孩子节点的操作结果做聚合 protected T aggregateResult(T aggregate, T nextResult) { return nextResult; } // 返回值true,表示可以继续访问孩子节点;false,则表示需要立即停止访问孩子节点 protected boolean shouldVisitNextChild(RuleNode node, T currentResult) { return true; } }
-
疑问: AbstractParseTreeVisitor为何被定义为抽象类?
- AbstractParseTreeVisitor对ParseTreeVisitor接口的所有方法都进行了实现,且没有定义任何的abstract方法,完全可以将其定义一个普通类
- 不靠谱的猜测: AbstractParseTreeVisitor是所有Visitor类的父类,将其定义为抽象类完全OK 😂 😂 😂
CalculatorBaseVisitor类
- CalculatorBaseVisitor类,继承了抽象类AbstractParseTreeVisitor,并实现了CalculatorVisitor
- 准确地说,CalculatorBaseVisitor基于
AbstractParseTreeVisitor.visitChildren()
方法,为CalculatorVisitor接口中所有的方法提供了一个默认实现@SuppressWarnings("CheckReturnValue") public class CalculatorBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements CalculatorVisitor<T> { @Override public T visitProg(CalculatorParser.ProgContext ctx) { return visitChildren(ctx); } @Override public T visitPrintExpr(CalculatorParser.PrintExprContext ctx) { return visitChildren(ctx); } ... // 其他方法的实现类似,这里不予展示 }
Generated visitors implement this interface and the XVisitor interface for grammar X.
- 这是ParseTreeVisitor接口注释中的原话,大意:基于grammar X生成的Visitor类,都将实现ParseTreeVisitor接口和对应的XVisitor接口
- 以
grammar Calculator
为例:- Visitor类就是CalculatorBaseVisitor,XVisitor接口就是CalculatorVisitor接口
- CalculatorBaseVisitor对ParseTreeVisitor接口的实现,则是依靠AbstractParseTreeVisitor这个抽象类完成的
2.2 Parser
- 介绍Visitor类时,出现的最多的则是
CalculatorParser
类,它继承了抽象类Parser,是实现parser rule解析的关键类public class CalculatorParser extends Parser
2.2.1 ParserRuleContext
- CalculatorParser类中包含多种ParserRuleContext,它们都是public static类型的inner class,如
AddSubContext
、ProgContext
- 每个parser rule或者加了label的rule element,都将对应一个
ParserRuleContext
- 或者说,每个ParserRuleContext都对应语法解析树中的parser rule或带标签的rule element节点
- 以ProgContext的层次图为例,ParserRuleContext的类层次关系如下:
2.2.2 ParserRuleContext的关键方法
-
以ProgContext为例,对应的prog规则只有一个rule element,因此未使用label
-
ProgContext的代码如下,内含获取child节点(
StatContext
)的getter方法,重写了RuleContext或ParserRuleContext中的一些关键方法@SuppressWarnings("CheckReturnValue") public static class ProgContext extends ParserRuleContext { // 获取孩子节点的getter方法 public List<StatContext> stat() { return getRuleContexts(StatContext.class); } public StatContext stat(int i) { return getRuleContext(StatContext.class,i); } public ProgContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } // 重写RuleContext.getRuleIndex()方法,返回prog的index,值为0 @Override public int getRuleIndex() { return RULE_prog; } // 重写ParserRuleContext中,与listener模式有关的、进入或退出rule的方法 @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); } // 重写RuleContext.accept()方法,是实现visitor模式的关键 @Override public <T> T accept(ParseTreeVisitor<? extends T> visitor) { if ( visitor instanceof CalculatorVisitor ) return ((CalculatorVisitor<? extends T>)visitor).visitProg(this); else return visitor.visitChildren(this); } }
-
对于stat这种包含多个rule element,且使用了label的parser rule,会先基于rule创建一个Conext,然后再创建每个rule element的Context
@SuppressWarnings("CheckReturnValue") public static class StatContext extends ParserRuleContext { public StatContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_stat; } public StatContext() { } public void copyFrom(StatContext ctx) { super.copyFrom(ctx); } } @SuppressWarnings("CheckReturnValue") public static class AssignContext extends StatContext { public TerminalNode ID() { return getToken(CalculatorParser.ID, 0); } public ExprContext expr() { return getRuleContext(ExprContext.class,0); } public AssignContext(StatContext ctx) { copyFrom(ctx); } @Override public void enterRule(ParseTreeListener listener) { if ( listener instanceof CalculatorListener ) ((CalculatorListener)listener).enterAssign(this); } @Override public void exitRule(ParseTreeListener listener) { if ( listener instanceof CalculatorListener ) ((CalculatorListener)listener).exitAssign(this); } @Override public <T> T accept(ParseTreeVisitor<? extends T> visitor) { if ( visitor instanceof CalculatorVisitor ) return ((CalculatorVisitor<? extends T>)visitor).visitAssign(this); else return visitor.visitChildren(this); } }
2.3 TerminalNode
-
所谓TerminalNode,就是parse tree的叶子节点,也是经过词法解析后得到的一系列tokens
-
这点从CalculatorParser中ParserRuleContext的定义也能看出
-
按照某种方式遍历parse tree,最终将访问TerminalNode,并从TerminalNode获取一些需要的信息或值
3. 基于visitor模式实现计算器
3.1 为什么需要自定义visitor ?
-
使用stat这个paser rule,构建
1+2
对应的parse tree -
按照箭头所示的方向遍历这棵树,需要从TerminalNode获取常量的值和运算符,才能完成表达式的计算
-
然而,
AbstractParseTreeVisitor
对visitTerminal()
方法的默认实现,是直接返回null
值 -
若使用
visit()
方法对上面的parse tree进行访问,按照CalculatorBaseVisitor的访问逻辑,最终TerminalNode均为null
,更别提按照语义进行加法计算// 遍历stat对应的语法解析树 CalculatorBaseVisitor<Integer> visitor = new CalculatorBaseVisitor<>(); visitor.visit(stat);
-
因此,想要实现计算器逻辑,则必须按照实际需求重写CalculatorBaseVisitor中的方法
3.2 自定义visitor
-
继承CalculatorBaseVisitor,并设置泛型类型为Integer,之后对所有ParserRuleContext的访问,都将返回Integer类型的值
public class CalculatorVisitorImpl extends CalculatorBaseVisitor<Integer> { private final HashMap<String, Integer> variables = new HashMap<>(); // 无需重写该方法,多个孩子节点,通过CalculatorBaseVisitor.visitProg() --> AbstractParseTreeVisitor.visitChildren()遍历整棵树 @Override public Integer visitProg(CalculatorParser.ProgContext ctx) { return super.visitProg(ctx); } @Override public Integer visitPrintExpr(CalculatorParser.PrintExprContext ctx) { // 通过visit()遍历ExprContext,获取表达式的值并打印计算结果 Integer result = visit(ctx.expr()); // 如果为constant类型的表达式,则直接打印;否则,打印原始的表达式或变量名 if (ctx.expr() instanceof CalculatorParser.ConstantContext) { System.out.println(result); } else { System.out.printf("%s = %d\n", ctx.expr().getText(), result); } return result; } @Override public Integer visitAssign(CalculatorParser.AssignContext ctx) { // 访问ID类型的TerminalNode,获取变量名 String variable = ctx.ID().getText(); // 通过visit()遍历ExprContext,获取表达式的值,从而为变量赋值 Integer value = visit(ctx.expr()); variables.put(variable, value); return value; } @Override public Integer visitVariable(CalculatorParser.VariableContext ctx) { String variable = ctx.getText(); // 从内存中获取变量的值,没有返回0 return variables.getOrDefault(variable, 0); } @Override public Integer visitMulDiv(CalculatorParser.MulDivContext ctx) { // 通过visit()遍历左右两个ExprContext,从而获取左右操作数的值 Integer left = visit(ctx.expr(0)); Integer right = visit(ctx.expr(1)); // 根据运算符,进行相应的计算并返回计算结果 if (ctx.op.getType() == CalculatorParser.MUL) { return left * right; } return left / right; } @Override public Integer visitAddSub(CalculatorParser.AddSubContext ctx) { Integer left = visit(ctx.expr(0)); Integer right = visit(ctx.expr(1)); if (ctx.op.getType() == CalculatorParser.ADD) { return left + right; } return left - right; } @Override public Integer visitConstant(CalculatorParser.ConstantContext ctx) { // 直接获取常量的值,原本为String,通过Integer.valueOf()转为Integer return Integer.valueOf(ctx.INT().getText()); } @Override public Integer visitParentheses(CalculatorParser.ParenthesesContext ctx) { // 括号表达式,需要返回括号内表达式的值。通过visit()访问ExprContext,获取表达式的值 return visit(ctx.expr()); } }
3.3 使用自定义的visitor遍历parse tree
-
使用visitor遍历parse tree,需要分为以下几步:
- 输入字符流,CalculatorLexer将字符流转为tokenStream
- 输入tokenStream,CalculatorParser基于指定的parser rule,将tokenStream转为对应的parse tree
- 使用自定义的visitor遍历stat对应的parse tree,该parse tree对应的操作可能是为变量赋值,也可能是打印表达式的计算结果
public static void main(String[] args) { // 多个stat,只能识别到第一个stat,属于PrintExpr String input = "1 + 2 * 3\n" + "b = (5 - 2) / 2\n" + "c = a + b\n" + "c\n"; // 1. 词法分析,解析出token;这时tokenStream中还未填充token,因为词法分析由语法分析触发 // Normally, the Parser is responsible for initiating the lexing of the input stream CharStream charStream = CharStreams.fromString(input); CalculatorLexer lexer = new CalculatorLexer(charStream); CommonTokenStream tokenStream = new CommonTokenStream(lexer); // 2. 基于tokenStream进行语法分析,得到parse tree CalculatorParser parser = new CalculatorParser(tokenStream); ParseTree stat = parser.stat(); // 打印语法解析树,但并不直观 System.out.printf("stat对应的语法解析树如下:\n%s\n", stat.toStringTree(parser)); // 3. 使用自定义的visitor遍历parse tree CalculatorBaseVisitor<Integer> visitor = new CalculatorVisitorImpl(); visitor.visit(stat); }
-
执行结果如下:
-
对输入字符流进行语法分析后,只能识别出
1 + 2 * 3
这个PrintExpr,与ANTLR Preview得到的结果一致 -
同时,对parse tree的visit遍历,打印的表达式计算结果也符合预期
-
注意: main()是基于stat对字符流进行分析,得到是一个不太完善的计算器,要想得到一个功能完备的计算器,可以基于prog对字符流进行分析
CalculatorBaseVisitor<Integer> visitor = new CalculatorVisitorImpl(); visitor.visit(prog);
3.4 visitor模式的遍历过程详解
- 使用visitor模式遍历stat规则的parse tree,其大致遍历路径如下图所示,实际就是对一棵树进行DFS(深度优先搜索)
- 完整的遍历路径如图所示,可见parse tree的遍历离不开以下要素:
visit()
方法,确切地说是AbstractParseTreeVisitor.visit()
方法,是parse tree遍历的入口方法- parse tree可能是一棵完整的tree,如上图以
PrintExprContext
开始的tree - 也可能是一棵子树,如上图以
MulDivContext
开始的tree
- parse tree可能是一棵完整的tree,如上图以
ctx.accept(visitor)
→ \rightarrow →visitor.visitCtx(ctx)
实现了double dispatch,将对某个ParserRuleContext(简写为Ctx)的visit导向了具体的Visitor实现visitCtx()
方法中,会根据情况选择visit(孩子节点)
,还是visitChildren()
,以实现树的DFS- 如果Ctx需要遍历的孩子节点个数是固定的,如MulDivContext有2个孩子节点需要遍历:左expr、右expr,这时直接
visit(孩子节点)
即可 - 如果Ctx的孩子节点个数不固定,则需要使用
visitChildren()
依次遍历每个孩子节点。对ProgContext的孩子节点的遍历,实际就是采用的visitChildren()
方法
- 如果Ctx需要遍历的孩子节点个数是固定的,如MulDivContext有2个孩子节点需要遍历:左expr、右expr,这时直接
- 注意: 上图,对
MulDivContext
的visit流程有所省略,直接到了visit孩子节点这一步
3.5 对ProgContext的visit
- 输入字符流如左边所示,使用prog规则进行解析,将获得如右边所示的parse tree
- 这时,visit该parse tree,在visit根节点ProgContext时,最终将调用
visitChildren()
方法
4. 后记
- 使用visitor模式实现计算器,有如下感受:
- 所谓的ParseTree实际是包含父节点、子节点和payload的tree node,这些tree node相互关联形成了parse tree
- 基于Antlr4的visitor模式实现某个需求,关键点:继承BaseVisitor并按需重写其中的方法
- visit一棵parse tree的过程非常复杂,但离不开入口方法
visit()
,accept()
→ \rightarrow →visitCtx()
的double dispatch,visitCtx()
→ \rightarrow →visit()
或visitCtx()
→ \rightarrow →visitChildren()
的DFS链。 - 结合ANLTR Preview提供的parse tree,并以debug方式了解遍历过程,是最佳实践
- TODO:
- 学习如何打印词法分析得到的tokens,并体会
Normally, the Parser is responsible for initiating the lexing of the input stream
这句话的含义 - 学习如何使用listener模式实现一个简单的整数计算器
- 学习如何打印词法分析得到的tokens,并体会