利⽤ANTLR4实现⼀个简单的四则运算计算器
利⽤ANTLR4实现⼀个简单的四则运算计算器
ANTLR4介绍
ANTLR能够⾃动地帮助你完成词法分析和语法分析的⼯作, 免去了⼿写去写词法分析器和语法分析器的⿇烦
它是基于LL(k)的, 以递归下降的⽅式进⾏⼯作.ANTLR v4还⽀持多种⽬标语⾔。本⽂⽤java来写代码。
总结⼀下:ANTRL能⾃动完成语法分析和词法分析过程,并⽣产框架代码,让我们写相关过程的时候只需要往固定位置添加代码即可。⼤⼤简便了语法分析词法分析的过程。
ANTLR4安装配置
因为⽤IDEA,所以直接介绍在IDEA中怎么安装,在IDEA中安装ANTLR4相关插件即可。然后MAVEN引⽤下
```
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4</artifactId>
<version>4.5.2</version>
</dependency>
```
ANTLR4 语法描述⽂件
ANTLR4有专门的语法来构建整个过程
grammar Expr;
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 : '*' ; // assigns token name to '*' used above in grammar
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
ID : [a-zA-Z]+ ;
INT : [0-9]+ ;
NEWLINE:'\r'? '\n' ;
WS : [ \t]+ -> skip;
相关语法很简单,整体来说⼀个原则,递归下降。即定义⼀个表达式(如expr),可以循环调⽤直接也可以调⽤其他表达式,但是最终肯定会有⼀个最核⼼的表达式不能再继续往下调⽤了。
以上代码在真正执⾏的时候会⽣成⼀棵抽象语法树,选择“prog”然后->"Test Rule prog", 输⼊测试数据“(1 + 2)+3-4*5”,然后我们会就可以看到⼀棵语法树了。
相关⽣成的java代码
整个语法⽂件的⽬的是为了让antlr⽣产相关的java代码。我们先设置下⽣成visitor,然后,他会⽣成如下
⼏个⽂件:
1. ExprParser
2. ExprLexer
3. ExprBaseVistor
4. ExprVisitor
ExprLexer 是词法分析器, ExprParser是语法分析器。⼀个语⾔的解析过程⼀般过程是词法分析-->语法分析。这是ANTLR4为我们⽣成的框架代码,⽽我们唯⼀要做的是⾃⼰实现⼀个Vistor,⼀般从ExprBaseVistor继承即可。
ANTLR 会为ExprBaseVistor 从定义的symoble⽂件如“#printExpr, #assign” ,⾃动⽣成相应的还是,然后就实现这些还是就可以实现我们的功能了。如:
@Override
public Integer visitAssign(ExprParser.AssignContext ctx) {
String id = ctx.ID().getText();
Integer value = pr());
return value;
}
@Override
public Integer visitInt(ExprParser.IntContext ctx) {
return Integer.valueOf(ctx.INT().getText());
}
@Override
public Integer visitMulDiv(ExprParser.MulDivContext ctx) {
Integer left = pr(0));
Integer right = pr(1));
if (Type() == ExprParser.MUL){
return left * right;
}else{
return left / right;
}
}
解释下Context的应⽤, Context 可以通过 expr(i) 取上下⽂的⼦内容。
然后就可以⽤如下⽅式是使⽤了:
public static void main(String [] args) throws IOException {
ANTLRInputStream inputStream = new ANTLRInputStream("1 + 2 + 3 * 4+ 6 / 2");
ExprLexer lexer = new ExprLexer(inputStream);
CommonTokenStream tokenStream = new CommonTokenStream(lexer);
ExprParser parser = new ExprParser(tokenStream);
ParseTree parseTree = parser.prog();
EvalVisitor visitor = new EvalVisitor();
Integer rtn = visitor.visit(parseTree);
System.out.println("#result#"+String());
}
运⾏⼀下,可以得到正确的结果了。
分析下整个过程
好神奇。我们来分析下整个过程是如何实现的。
⾸先Antlr4会根据相关的语法⽂件⽣成ExprParser类,其内容是由其语法内容决定的。如上的语法中有三个表达式:prog,stat,expr,所以就⽣成了三个函数:
public final ProgContext prog() throws RecognitionException {
ProgContext _localctx = new ProgContext(_ctx, getState());
enterRule(_localctx, 0, RULE_prog);
try {
enterOuterAlt(_localctx, 1);
{
setState(6);
stat();
}
}
catch (RecognitionException re) {
_ption = re;
_portError(this, re);
_ver(this, re);
}
finally {
exitRule();
}
return _localctx;
}
public final StatContext stat() throws RecognitionException {
StatContext _localctx = new StatContext(_ctx, getState());
enterRule(_localctx, 2, RULE_stat);
...
stat过程是真正的语法分析过程,他会把相应的token填上不同的StatContext.
整个语法解析的过程就是 prop -> stat ->expr。
在语法⽂件中有MUL,DIV 等⼏个关键字, Antlr会⾃动识别其是否有⼦项调⽤如果没有则这样定义:
public static final int
T__0=1, T__1=2, T__2=3, MUL=4, DIV=5, ADD=6, SUB=7, ID=8, INT=9, NEWLINE=10,
WS=11;
public static final int
RULE_prog = 0, RULE_stat = 1, RULE_expr = 2;
有了parser,下⼀个疑问就是parser如何和我们写的visitor联系起来的。这就要借助于⼀个⾮常重要的概念:Context.
因为语法⽂件中有8个symbol ,所以会对于⽣成不同的Context.
最终返回出去:
ParseTree parseTree = parser.prog();
EvalVisitor visitor = new EvalVisitor();
Integer rtn = visitor.visit(parseTree);
⼀个典型的Context是这样实现的:
public static class IntContext extends ExprContext {
public TerminalNode INT() { return getToken(ExprParser.INT, 0); }
public IntContext(ExprContext ctx) { copyFrom(ctx); }
@Override
public <T> T accept(ParseTreeVisitor<? extends T> visitor) {
if ( visitor instanceof ExprVisitor ) return ((ExprVisitor<? extends T>)visitor).visitInt(this);
else return visitor.visitChildren(this);
}
}
特别关注 accept 的实现。
看下 visitor的实现
public T visit(ParseTree tree) {
return tree.accept(this);
}
典型的visitor模式的实现。以上这个流程是:
1. 通过parser返回⼀个xxContext的树
2. 在visitor中调⽤ xxContent的accept⽅法
3. xxContext 调⽤visitor的具体实现⽅法:如:visitMulDiv
4. 在实现vistor⽅法时候,注意如果还有chilContent,继续往下。
总结用java编写一个简单的计算器
Antlr4 屏蔽了语法分析和词法分析的细节。⼤⼤简化了开发的⼯作量。⽽且使⽤简单⽅便。对⽐ Boost.Spirit,简直⼀个是⾃动挡的汽车,⼀个是飞机。