概述
这节以四则运算语句的解析为例学习语法导入和Visitor模式。相比笔记1,这里的语法更通用,允许加减乘除、圆括号、整数出现,并且允许赋值表达式。
1 四则运算解析
1.1 语法规则文件
从下面的文件中可以看到,整体是要匹配若干条语句,每条语句都是以NEWLINE
换行符结束的。然后语句可以是表达式语句、赋值语句、空语句。
表达式的语法规则定义比较自然,因为这里没有手动消除左递归,ANTLR4可以自己消除直接左递归(文件中13/14行分支expr左侧直接调用自身),这是相比其它工具的一大优势,让语法编写更简单易懂。
grammar Expr;
// 顶层规则:一条至多条语句
prog: stat+ ;
// 语句
stat: expr NEWLINE // 表达式语句(表达式后跟换行)
| ID '=' expr NEWLINE // 赋值语句(左值是标识符,右值是表达式)
| NEWLINE // 空语句(直接一个换行)
;
// 表达式
expr: expr ('*'|'/') expr // 表达式乘除表达式
| expr ('+'|'-') expr // 表达式加减表达式
| INT // 一个整形值
| ID // 一个标识符
| '(' expr ')' // 表达式外加一对括号
;
ID : [a-zA-Z]+ ; // 标识符:一个到多个英文字母
INT : [0-9]+ ; // 整形值:一个到多个数字
NEWLINE:'r'? 'n' ; // 换行符
WS : [ t]+ -> skip ; // 跳过空格和tab
词法符号NEWLINE
匹配换行符,其中符号?
也就是正则里的匹配出现一次或多次,在这里就表示整个NEWLINE
匹配的是r
或者rn
。这样做的目的是因为Windows中的换行符是rn
, 而Linux/Unix下的换行符是n
。
最后的-> skip
在前面学习中也有接触到,这个是一条ANTLR指令,告诉词法分析器匹配时忽略这些字符,这样就不用嵌入代码来做忽略了(书上的意思是不用嵌入代码也就不用和特定编程语言绑定)。
1.2 从主类调用
import anrlr.ExprLexer;
import anrlr.ExprParser;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
public class ExprJoyRide {
public static void main(String[] args) {
CharStream input = CharStreams.fromString("1+(2*3)+4n");
// 词法分析->Token流->生成语法分析器对象
ExprLexer lexer = new ExprLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ExprParser parser = new ExprParser(tokens);
// 真正启动语法分析,并将语法树输出
ParseTree tree = parser.prog();
System.out.println(tree.toStringTree(parser));
}
}
2 语法导入
实际使用时经常会遇到非常大的语法,可以考虑将它拆分成多个小的语法文件。例如,可以将语法规则和词法符号规则拆分开,因为不同语言的词法符号规则很大部分是重复的,这样就可以把它抽象成一个单独的模块,以应用于多个语言的分析器。
2.1 CommonLexerRules.g4
注意这个文件中的的第一行用lexer grammar,表示这里只存放词法符号规则。
lexer grammar CommonLexerRules;
ID : [a-zA-Z]+ ; // 标识符:一个到多个英文字母
INT : [0-9]+ ; // 整形值:一个到多个数字
NEWLINE:'r'? 'n' ; // 换行符
WS : [ t]+ -> skip ; // 跳过空格和tab
2.2 LibExpr.g4
这个就是从最开始的语法规则文件里把词法符号规则去掉,再把2.1
文件导入。测试语法和生成代码的功能都是直接在这个文件上做,而不用在被导入的文件上操作。
grammar LibExpr;
// 导入单独分离出去的词法符号规则文件
import CommonLexerRules;
// 顶层规则:一条至多条语句
prog: stat+ ;
// 语句
stat: expr NEWLINE // 表达式语句(表达式后跟换行)
| ID '=' expr NEWLINE // 赋值语句(左值是标识符,右值是表达式)
| NEWLINE // 空语句(直接一个换行)
;
// 表达式
expr: expr ('*'|'/') expr // 表达式乘除表达式
| expr ('+'|'-') expr // 表达式加减表达式
| INT // 一个整形值
| ID // 一个标识符
| '(' expr ')' // 表达式外加一对括号
;
3 访问者(Visitor)模式
在笔记3中使用的是监听器(Listener)模式,这里尝试ANTLR支持的另外一种遍历语法树的模式,访问者模式。
3.1 LabeledExpr.g4
为了让每条备选分支都能有一个访问方法,这里为每个备选分支都加上标签(类似Python的注释,用# 标签名
表示)。
另外,加减乘除等符号在之前的语法是是字面值,现在也给它们设置名字,其实也就是让它们也成为词法符号。这样就可以直接用这个词法符号名以Java常量的方式引用这些符号。
grammar LabeledExpr;
prog: stat+ ;
// -------------给每个备选分支打标签
stat: expr NEWLINE # printExpr
| ID '=' expr NEWLINE # assign
| NEWLINE # blank
;
expr: expr op=('*'|'/') expr # MulDiv
| expr op=('+'|'-') expr # AddSub
| INT # int
| ID # id
| '(' expr ')' # parens
;
// -------------给运算符号设置名字,也形成词法符号
MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
// -------------剩下的是和之前一样的词法符号
ID : [a-zA-Z]+ ; // 标识符:一个到多个英文字母
INT : [0-9]+ ; // 整形值:一个到多个数字
NEWLINE:'r'? 'n' ; // 换行符
WS : [ t]+ -> skip ; // 跳过空格和tab
3.2 生成解析器代码
在生成的时候勾选上generate parse tree visitor,这样才能生成访问者模式相关的类和接口。
如果是用命令行,就要用antlr4 -visitor LabeledExpr.g4
命令来生成。不过默认还是会带有Listener的,如果想要去掉Listener,还要加上-no-listener
参数。
3.2.1 LabeledExprVisitor.java
首先生成了访问器泛型接口,并为每个未标签的语法或带标签的备选分支生成了一个方法:
// LabeledExpr语法的访问器接口
public interface LabeledExprVisitor<T> extends ParseTreeVisitor<T> {
// 访问顶层语法
T visitProg(LabeledExprParser.ProgContext ctx);
// 访问stat语法的第一个分支(对应# PrintExpr)
T visitPrintExpr(LabeledExprParser.PrintExprContext ctx);
// 访问stat语法的第二个分支(对应# Assign)
T visitAssign(LabeledExprParser.AssignContext ctx);
...
}
这些方法都是泛型返回值的T visit*(*Context)
格式,以便让实现类为具体功能去实现不同的返回值类型。
3.2.2 LabeledExprBaseVisitor.java
另外还生成了一个默认的实现类,在访问每个结点时直接调用访问孩子结点的方法:
// 生成的默认实现类
public class LabeledExprBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements LabeledExprVisitor<T> {
@Override public T visitProg(LabeledExprParser.ProgContext ctx) { return visitChildren(ctx); }
@Override public T visitPrintExpr(LabeledExprParser.PrintExprContext ctx) { return visitChildren(ctx); }
@Override public T visitAssign(LabeledExprParser.AssignContext ctx) { return visitChildren(ctx); }
...
}
这个访问孩子结点的方法visitChildren()
就是继承自它所继承的抽象类AbstractParseTreeVisitor<T>
。
3.3 实现计算功能的访问器类
为了实现自定义的计算功能,要去继承刚刚生成的LabeledExprBaseVisitor
泛型类,因为是整数计算器(计算时候返回整数),所以泛型参数这里指定为Integer
即可。
/*
* Excerpted from "The Definitive ANTLR 4 Reference",
* published by The Pragmatic Bookshelf.
* Copyrights apply to this code. It may not be used to create training material,
* courses, books, articles, and the like. Contact us if you are in doubt.
* We make no guarantees that this code is fit for any purpose.
* Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
*/
import antlr.LabeledExprBaseVisitor;
import antlr.LabeledExprParser;
import java.util.HashMap;
// 实现计算功能的访问器类
public class EvalVisitor extends LabeledExprBaseVisitor<Integer> {
// 模拟计算器的内存,存放"变量名->值"的映射,即在赋值时候往这里写
HashMap<String, Integer> memory = new HashMap<>();
// 访问赋值语句:ID '=' expr NEWLINE
@Override
public Integer visitAssign(LabeledExprParser.AssignContext ctx) {
String id = ctx.ID().getText(); // 获取左值标识符
int value = visit(ctx.expr()); // 对右值表达式访问求值
memory.put(id, value); // 存储赋值
return value;
}
// 访问表达式语句:expr NEWLINE
@Override
public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
Integer value = visit(ctx.expr()); // 对表达式访问求值
System.out.println(value); // 把值打印出来
return 0; // 反正用不到这个返回值,这里返回假值
}
// 访问单个整数构成的表达式:INT
@Override
public Integer visitInt(LabeledExprParser.IntContext ctx) {
return Integer.valueOf(ctx.INT().getText()); // 把这个数返回
}
// 访问单个标识符构成的表达式:ID
@Override
public Integer visitId(LabeledExprParser.IdContext ctx) {
String id = ctx.ID().getText(); // 获取标识符名字
if (memory.containsKey(id)) // 查表,找到就返回
return memory.get(id);
return 0; // 找不到返回0
}
// 访问乘除法表达式:expr op=('*'|'/') expr
@Override
public Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) {
int left = visit(ctx.expr(0)); // 被除数,或乘法因子1
int right = visit(ctx.expr(1)); // 除数,或乘法因子2
if (ctx.op.getType() == LabeledExprParser.MUL) // 检查操作符
return left * right; // 乘法
return left / right; // 除法
}
// 访问加减法表达式:expr op=('+'|'-') expr
@Override
public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {
int left = visit(ctx.expr(0)); // 项1
int right = visit(ctx.expr(1)); // 项2
if (ctx.op.getType() == LabeledExprParser.ADD) // 检查操作符
return left + right; // 加法
return left - right; // 减法
}
// 访问表达式加括号:'(' expr ')'
@Override
public Integer visitParens(LabeledExprParser.ParensContext ctx) {
return visit(ctx.expr()); // 其实就是把括号里表达式的值算出来返回
}
}
注意这里按照书上的代码visitAssign()
方法也把赋值后的值返回了,实际上它也和visitPrintExpr()
一样是语句stat
的一个分支罢了,而语句的返回值是用不到的,所以这里返回0也可以。因为这里的语法里不会出现连续赋值的情况,赋值就是语句,赋值后的值不会再被用到了。
当然实际的程序语言里则可能会用到,比如
a=b=3
这种连续赋值。
当需要计算表达式的值的时候,代码里是直接调用了visit()
方法,这个方法的源码没看到,不过看起来就是直接去调用传入的结点的访问方法就可以了。
还有就是检查操作符的地方值得注意,ctx.op.getType()
这里可以通过op
属性获取到操作符,这个就是需要在语法里给操作符设置op=('*'|'/')
而不是直接('*'|'/')
的原因了。紧随其后的判断== LabeledExprParser.MUL
就是在3.1
中要为原本的操作符字面值设置名字的一大好处。
另外就是除法除0的检查,这里没做检查,我觉得是可以的,相当于除0的时候靠JVM给报错,也没什么大问题。
3.4 从主类调用
import antlr.LabeledExprLexer;
import antlr.LabeledExprParser;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
public class Calc {
public static void main(String[] args) {
CharStream input = CharStreams.fromString("a=2*(3+4)-5nb=2na+bn");
// 词法分析->Token流->生成语法分析器对象
LabeledExprLexer lexer = new LabeledExprLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
LabeledExprParser parser = new LabeledExprParser(tokens);
// 启动语法分析,获取语法树(根节点)
ParseTree tree = parser.prog();
// 创建自定义的能进行四则运算的访问者类
EvalVisitor evalVisitor = new EvalVisitor();
// 访问这棵语法树,在访问同时即可进行计算获取结果
evalVisitor.visit(tree);
}
}
运行结果是11,符合预期。因为前两条是赋值语句:
a
=
2
×
(
3
+
4
)
−
5
=
9
b
=
2
begin{aligned} a&=2times(3+4)-5=9 \ b&=2 end{aligned}
ab=2×(3+4)−5=9=2
最后一条是表达式语句,要把计算值打印出来, a a a和 b b b相加计算得到的值就是11。
最后
以上就是高贵向日葵为你收集整理的【ANTLR学习笔记】4:语法导入和访问者(Visitor)模式的全部内容,希望文章能够帮你解决【ANTLR学习笔记】4:语法导入和访问者(Visitor)模式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复