
前面的文章介绍了如何写一个非常简单的解释器去完成四则运算。文章的最后也提到用 Antlr 可以帮助你完成 Lexer 和 Parser。(我老是分不清词法分析和语法分析,不知为何我国的翻译的专业词汇总是这样的) 。这篇文章就是介绍如何用这些 Antlr 工具完成四则运算(当然你要按官网的步骤先完成安装)。Antlr 真心大赞!
官网的其实就有一个例子了,正好就是四则运算。
创建一个 Expr.g4 文件
grammar Expr;
prog: (expr NEWLINE)* ;
expr: expr ('*'|'/') expr
| expr ('+'|'-') expr
| INT
| '(' expr ')'
;
NEWLINE : [\r\n]+ ;
INT : [0-9]+ ;
用 antlr 根据文法生成 java 文件
antlr4 Expr.g4
用 javac Expr*.java
编译 java 文件后
用 grun Expr prog -gui
后,输入四则运算的表达式1*2+4/2
,再回车按CTRL-D。可以可视化地看到抽象树了

但官网的Sample的例子,没有遍历树和求值的。下面我们一起来完成一下。
首先给表达式的后面标记一些东西,这样 antlr 会在生成的用于遍历抽象树的接口文件中抛出这些函数,方便我们求值。
其次,引入 op 变量更友好地标记运算符。在Visitor 的上下文(ctx)中可以,ctx.op
这样直接获取到运算符。
最后,添加些常量(你可以 ExprParser.MUL),方便对比
grammar Expr;
prog: expr NEWLINE ;
expr: expr op=('*'|'/') expr # MulDiv
| expr op=('+'|'-') expr # AddSub
| INT # int
| '(' expr ')' # parens
;
NEWLINE : [\r\n]+ ;
INT : [0-9]+ ;
/*创建常量方便引入*/
MUL: '*';
DIV: '/';
Add: '+';
SUB: '-';
然后用 Expr.g4 文件生成 java 文件。
antlr4 -no-listener -visitor Expr.g4
生成的 ExprVisitor 文件中就有我们刚才标记的函数
public interface ExprVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link ExprParser#prog}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitProg(ExprParser.ProgContext ctx);
/**
* Visit a parse tree produced by the {@code MulDiv}
* labeled alternative in {@link ExprParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitMulDiv(ExprParser.MulDivContext ctx);
/**
* Visit a parse tree produced by the {@code AddSub}
* labeled alternative in {@link ExprParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitAddSub(ExprParser.AddSubContext ctx);
/**
* Visit a parse tree produced by the {@code prens}
* labeled alternative in {@link ExprParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitPrens(ExprParser.PrensContext ctx);
/**
* Visit a parse tree produced by the {@code int}
* labeled alternative in {@link ExprParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitInt(ExprParser.IntContext ctx);
}
然后我们创建用于求值的文件 EvalVisitor
public class EvalVisitor extends ExprBaseVisitor<Integer> {
@Override
public Integer visitProg(ExprParser.ProgContext ctx) {
Integer value = visit(ctx.expr());
System.out.println(value);
return value;
}
/* expr ('*'|'/') expr */
@Override
public Integer visitMulDiv(ExprParser.MulDivContext ctx) {
int left = visit(ctx.expr(0));
int right = visit(ctx.expr(1));
if (ctx.op.getType() == ExprParser.MUL){
return left*right;
}
return left/right;
}
/* expr ('+'|'-') expr */
@Override
public Integer visitAddSub(ExprParser.AddSubContext ctx) {
int left = visit(ctx.expr(0));
int right = visit(ctx.expr(1));
if (ctx.op.getType() == ExprParser.Add){
return left+right;
}
return left-right;
}
/* INT */
@Override
public Integer visitInt(ExprParser.IntContext ctx) {
return Integer.valueOf(ctx.INT().getText());
}
/* '(' expr ')' */
@Override
public Integer visitParens(ExprParser.ParensContext ctx) {
return visit(ctx.expr());
}
}
具体的 REPL 实现如下:
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
while (true) {
System.out.print(">");
String command = scan.nextLine() + "\n";
try {
if (command.length() == 0) {
continue;
}
ExprLexer lexer = new ExprLexer(CharStreams.fromString(command));
CommonTokenStream tokens = new CommonTokenStream(lexer);
ExprParser parser = new ExprParser(tokens);
ExprParser.ProgContext tree = parser.prog();
EvalVisitor eval = new EvalVisitor();
eval.visit(tree);
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
}
}
}
当然来到这里,可能会觉得一脸懵逼,不知道在干啥。
其实 Antlr 是做了 Parser 的部分,和生成一些可以快捷遍历抽象树的接口。我们实现了接口了就可以遍历抽象树求值了。
至于为何基本只要写规则就可以完成,因为这些规则就是本身就是用来描述语言的,对规则进行 Lexer、Parser 得到的中间表示(IR),遍历中间表示就可以将之转换成其他的语言(java、c),其实相当于是一个翻译。
这些不需要太在意。可以自动完成的东西就自动完成就好,编译原理很多人就是在 Parser 上栽倒就不想学后面的 IR、codegen 之类,其实那些部分更有意思,更重要。Parser 这种东西可以明白原理就好了,自动生成就可以了。
以上就是这篇文章的全部内容。
参考资料