什么是Antlr?引用官方的说明就是:
What is ANTLR?
ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files. It’s widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build and walk parse trees.
从编译器的角度来看,Antlr可以用来帮助我们完成编译器前端所需要完成的一些工作:词法分析(Lexer Analysis)、语法分析(Syntax Analysis)、生成抽象语法树(Abstract Syntax Tree, AST)等等。语义分析(Semantic Analysis),例如类型检查,就需要我们自己来完成。
要使用Antlr生成语言的词法分析器(Lexer)和语法分析器(Parser),我们需要告诉Antlr语言的文法(Grammar)。Antlr采用的是上下文无关文法(Context Free Grammar),使用类似BNF的符号集来描述。使用上下文无关文法的语言比较常用的Parser有两种,LL Parser和LR Parser,而Antlr帮我们生成的是前者。
安装好Java,在这里就不再赘述了
下载Antlr的jar包——Antlr Download
将antlr-4.7-complete.jar添加到系统环境变量的CLASSPATH中:
D:\antlr-v4.7\antlr-4.7-complete.jar;
为Antlr Tool和TestRig建立batch命令:
为了方便管理,我就在antlr-4.7-complete.jar所在文件夹下建立一个bat文件夹
在里面创建两个bat文件,一个叫antlr4.bat,内容如下:
java org.antlr.v4.Tool %*
另一个叫grun.bat,内容如下:
java org.antlr.v4.runtime.misc.TestRig %*
测试是否安装成功:
打开IDEA,File -> Settings -> Plugins -> 导航栏搜索Antlr -> Search in repositories -> Install
这样就安装完成了,方便快捷。
首先我们先给出一个能识别Hello World那样的简单语法:
grammar Hello; // 定义文法的名字
s : 'hello' ID; // 匹配关键字hello,后面跟着一个标志符
ID : [a-z]+; // 匹配小写字母标志符
WS : [ \t\r\n]+ -> skip; // 跳过空格、制表符、回车符和换行符
将这个语法保存为Hello.g4,然后执行以下命令来生成识别器:
antlr4 Hello.g4
Anltr会为我们在当前目录下生成6个文件:
Hello.tokens HelloLexer.java HelloParser.java
HelloLexer.tokens HelloBaseListener.java HelloListener.java
执行javac Hello*.java
将所有java文件编译。这样,我们已经有了一个可以被HelloParser和HelloLexer利用的可执行的识别器,只缺一个主程序去触发语言识别了。
ANTLR运行库有提供称之为TestRig的测试工具,可以让你不创建主程序就能测试语法。TestRig使用Java反射调用编译后的识别器,它能显示关于识别器如何匹配输入的大量信息。还记得我们之前创建的grun.bat文件吗?那就是用来调用TestRig的。
现在我们来执行一下:
grun Hello s -tokens
接着输入以下内容:
hello world #输入后回车
EOF #Windows下按Ctrl+Z再按回车
TestRig会打印出记号列表,每一行输出表示一个记号以及它的有关信息:
[@0,0:4='hello',<1>,1:0]
[@1,6:10='world',<2>,1:6]
[@2,13:12='',<-1>,2:0]
这里详细讲解下[@1,6:10=’world’,\<2>,1:6]的意义。@1表示记号索引(从0开始);6:10表示
记号开始与结束的位置(从0开始);\<2>表示记号类型,具体数值和类型存储在后缀名为
tokens的文件中;最后的1:6表示记号在第一行(从1开始),从第6个字符开始(从0开始,
制表符作为单个字符计算)。
以下是TestRig可用的所有参数:
现在让我们来用Antlr来实现一个简单的四则运算计算器吧!
使用IDEA安装好Antlr插件,并创建一个Maven项目,在pom.xml下加入如下内容:
<dependency>
<groupId>org.antlrgroupId>
<artifactId>antlr4-runtimeartifactId>
<version>4.7version>
dependency>
创建Cal.g4文件,内容如下
grammar Cal;
prog: stat+; //一个程序由至少一条语句组成
/*为了以后的运算的方便性,我们需要给每一步规则打上标签,标签以”#”开头,出现在每一条规则的右边。打上标签后,antlr会为每一个规则都生成一个事件*/
stat: ID '=' expr ';' #Assign //变量赋值语句
| 'print' '(' expr ')' ';' #printExpr //输出语句
;
expr: expr op=('*'|'/') expr #MulDiv //表达式可以是表达式之间乘除
| expr op=('+'|'-') expr #AddSub //表达式可以是表达式之间加减
| NUM #NUM //表达式可以是一个数字
| ID #ID //表达式可以是一个变脸
| '(' expr ')' #parens //表达式可以被括号括起来
;
MUL:'*';
DIV:'/';
ADD:'+';
SUB:'-';
ID: [a-zA-Z][a-zA-Z0-9]*; //变量可以是数字和字母,但必须以字母开头
//负数必须要用"()"括起来
NUM: [0-9]+ //正整数
| '(' '-' [0-9]+ ')' //负整数
| [0-9]+'.'[0-9]+ //正浮点数
| '(' '-' [0-9]+'.'[0-9]+ ')' //负浮点数
;
WS: [ \t\r\n] -> skip; //跳过空格、制表符、回车、换行
在IDEA中配置文件,右击文件 -> Configure ANTLR…,如下图所示:
(注意要勾选generate parse tree visitor,后来的程序要用到)
配置完成后右键文件 -> Generate ANNTLR Recognizer
我们先看一下刚才生成的两个Visitor:
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
public interface CalVisitor<T> extends ParseTreeVisitor<T> {
T visitProg(CalParser.ProgContext ctx);
T visitAssign(CalParser.AssignContext ctx);
T visitPrintExpr(CalParser.PrintExprContext ctx);
T visitParens(CalParser.ParensContext ctx);
T visitMulDiv(CalParser.MulDivContext ctx);
T visitAddSub(CalParser.AddSubContext ctx);
T visitNUM(CalParser.NUMContext ctx);
T visitID(CalParser.IDContext ctx);
}
Visitor的作用顾名思义就是对整个语法树进行遍历,然后进行相关操作。我们需要重写每一个方法来实现我们自己的遍历,在这之前,我们先声明一个map。用于存放变量与其对应的值,即符号表
Map memory = new HashMap();
在这里,我们选择统一使用DOUBLE来表示数字,在输出结果的时候再做进一步的转换
visitAssign():
@Override
public Double visitAssign(CalParser.AssignContext ctx) {
//获取ID的名字
String id=ctx.ID().getText();
//调用expr()遍历子树,获取结果
Double value=visit(ctx.expr());
//赋值给ID,存放入符号表中
table.put(id,value);
//返回dummy value
return null;
}
visitPrint():
@Override
public Double visitPrintExpr(CalParser.PrintExprContext ctx) {
Double value=visit(ctx.expr());
String str=value.toString();
int index=str.indexOf('.');
//检查是否为整数,截取小数点后的字符串,与"0"比较
if(str.substring(index+1).equals("0"))
//是整数,只输出小数点之前的字符串
System.out.println(str.substring(0,index));
else
//不是整数,直接输出
System.out.println(str);
return null;
}
visitMulDiv():
@Override
public Double visitMulDiv(CalParser.MulDivContext ctx) {
Double left=visit(ctx.expr(0)); //左值
Double right=visit(ctx.expr(1)); //右值
//如果逻辑错误,获取出错地方的行号和列数
int line,column;
if(ctx.op.getType()==CalParser.DIV){ //如果是除法
if(right==0.0){
line=ctx.expr(1).start.getLine();
column=ctx.expr(1).start.getStartIndex();
//除数为0,抛出异常
try{
throw new CalException(line,column,"Divided by zero");
}catch (CalException e){
System.out.println(e.toString());
}
return null;
}else
return left/right;
}else
return left*right;
}
CalException的定义如下:
public class CalException extends Exception {
private int line;
private int column;
private String msg;
public CalException(int line,int column,String msg){
this.line=line;
this.column=column;
this.msg=msg;
}
@Override
public String toString() {
return msg+",line:"+line+",column:"+column;
}
}
visitAddSub():
@Override
public Double visitAddSub(CalParser.AddSubContext ctx) {
Double left=visit(ctx.expr(0));
Double right=visit(ctx.expr(1));
if(ctx.op.getType()==CalParser.ADD)
return left+right;
else
return left-right;
}
以上这两个就是我们加减乘除的核心函数了
visitNum():
@Override
public Double visitNUM(CalParser.NUMContext ctx) {
return Double.valueOf(ctx.getText()); //直接将字符串转为数字并返回
}
visitID():
@Override
public Double visitID(CalParser.IDContext ctx) {
String id=ctx.getText();
int line,column;
//符号表中存在这个变量并且已经赋值,直接返回
if(table.containsKey(id))
return table.get(id);
else{
//变量未初始化,抛出异常
line=ctx.start.getLine();
column=ctx.start.getStartIndex();
try{
throw new CalException(line,column,"Undefined variable:"+id);
}catch (CalException e){
System.out.println(e.toString());
}
return null;
}
}
visitParens():
@Override
public Double visitParens(CalParser.ParensContext ctx) {
return visit(ctx.expr());
}
全部代码一览:
import java.util.HashMap;
import java.util.Map;
public class EvalVisitor extends CalBaseVisitor<Double> {
private Map table;
public EvalVisitor(){
table=new HashMap<>();
}
@Override
public Double visitPrintExpr(CalParser.PrintExprContext ctx) {
Double value=visit(ctx.expr());
String str=value.toString();
int index=str.indexOf('.');
//check if it is an integer
if(str.substring(index+1).equals("0"))
System.out.println(str.substring(0,index));
else
System.out.println(str);
return null;
}
@Override
public Double visitAssign(CalParser.AssignContext ctx) {
String id=ctx.ID().getText();
Double value=visit(ctx.expr());
table.put(id,value);
return null;
}
@Override
public Double visitMulDiv(CalParser.MulDivContext ctx) {
Double left=visit(ctx.expr(0));
Double right=visit(ctx.expr(1));
//if divided by zero,get the position of zero
int line,column;
if(ctx.op.getType()==CalParser.DIV){
if(right==0.0){
line=ctx.expr(1).start.getLine();
column=ctx.expr(1).start.getStartIndex();
try{
throw new CalException(line,column,"Divided by zero");
}catch (CalException e){
System.out.println(e.toString());
}
return null;
}else
return left/right;
}else
return left*right;
}
@Override
public Double visitAddSub(CalParser.AddSubContext ctx) {
Double left=visit(ctx.expr(0));
Double right=visit(ctx.expr(1));
if(ctx.op.getType()==CalParser.ADD)
return left+right;
else
return left-right;
}
@Override
public Double visitNUM(CalParser.NUMContext ctx) {
return Double.valueOf(ctx.getText());
}
@Override
public Double visitID(CalParser.IDContext ctx) {
String id=ctx.getText();
int line,column;
if(table.containsKey(id))
return table.get(id);
else{
line=ctx.start.getLine();
column=ctx.start.getStartIndex();
try{
throw new CalException(line,column,"Undefined variable:"+id);
}catch (CalException e){
System.out.println(e.toString());
}
return null;
}
}
@Override
public Double visitParens(CalParser.ParensContext ctx) {
return visit(ctx.expr());
}
}
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.ParseTree;
import java.io.FileInputStream;
import java.io.IOException;
public class MyCalculator {
public static void main(String[] args) throws IOException {
String file="test_case\\test.in";
//创建输入文件流
FileInputStream inputStream=new FileInputStream(file);
//转化为字符流
CharStream input= CharStreams.fromStream(inputStream);
//创建词法分析器
CalLexer lexer=new CalLexer(input);
//获取Token集
CommonTokenStream tokenStream=new CommonTokenStream(lexer);
//创建语法分析器
CalParser parser=new CalParser(tokenStream);
//分析语法
ParseTree tree=parser.prog();
//遍历语法树,输出结果
EvalVisitor visitor=new EvalVisitor();
//当遍历树时如果出错,会返回空指针,在这里捕获
try{
visitor.visit(tree);
}catch (NullPointerException e){
System.out.println("oops, we have some problem");
}
}
}
样例输入:
a=(10.44*356+1.28)/2-1024*1.6;
b=a*2-a/2;
c123=a+b*b/5-(a-a*2)/b;
print(a);
print(b);
print(c123);
print(1+2-3*4/5);
输出结果:
220.55999999999995
330.8399999999999
22112.24778666666
0.6000000000000001
参考链接:antlr入门实例——计算器