concreteElement.accept(Visitor visitor)
→ \rightarrow → visitor.visit(concreteElement)
),实现了Element与操作的解耦。创建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;
src/main/java/com/sunrise/calculator
目录下生成如下文件:CalculatorBaseVisitor
中的方法来实现Calculator.g4
编译后,有两个以Visitor为结尾的Java文件:CalculatorVisitor
接口、CalculatorBaseVisitor
类CalculatorVisitor接口的代码如下:
Calculator.g4
中不同parse rule对应的parse tree,创建了相应的visit()方法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 vi,itAssign(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);
}
注意: 使用Void作为返回值类型,方法的返回值应该定义为null
,而不能像void方法一样,使用return;
或者无return语句
Callable<Void> callable = new Callable<Void>() {
@Override
public Void call() {
System.out.println("Hello!");
return null;
}
};
由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是一个抽象类,提供了ParseTreeVisitor接口的默认实现,关键点:
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.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.
grammar Calculator
为例:
CalculatorParser
类,它继承了抽象类Parser,是实现parser rule解析的关键类public class CalculatorParser extends Parser
AddSubContext
、ProgContext
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);
}
}
按照某种方式遍历parse tree,最终将访问TerminalNode,并从TerminalNode获取一些需要的信息或值
使用stat这个paser rule,构建1+2
对应的parse tree
然而,AbstractParseTreeVisitor
对visitTerminal()
方法的默认实现,是直接返回null
值
若使用visit()
方法对上面的parse tree进行访问,按照CalculatorBaseVisitor的访问逻辑,最终TerminalNode均为null
,更别提按照语义进行加法计算
// 遍历stat对应的语法解析树
CalculatorBaseVisitor<Integer> visitor = new CalculatorBaseVisitor<>();
visitor.visit(stat);
因此,想要实现计算器逻辑,则必须按照实际需求重写CalculatorBaseVisitor中的方法
继承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());
}
}
使用visitor遍历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);
visit()
方法,确切地说是AbstractParseTreeVisitor.visit()
方法,是parse tree遍历的入口方法
PrintExprContext
开始的treeMulDivContext
开始的treectx.accept(visitor)
→ \rightarrow → visitor.visitCtx(ctx)
实现了double dispatch,将对某个ParserRuleContext(简写为Ctx)的visit导向了具体的Visitor实现visitCtx()
方法中,会根据情况选择visit(孩子节点)
,还是visitChildren()
,以实现树的DFS
MulDivContext
的visit流程有所省略,直接到了visit孩子节点这一步visitChildren()
方法visit()
, accept()
→ \rightarrow → visitCtx()
的double dispatch,visitCtx()
→ \rightarrow → visit()
或visitCtx()
→ \rightarrow → visitChildren()
的DFS链。Normally, the Parser is responsible for initiating the lexing of the input stream
这句话的含义