antlr4读书笔记
在第一周的学习中主要阅读了《Pragmatic.The Definitive ANTLR 4 Reference.2013》的一到九章,《Language Implementation Patterns》的六到八章,查询了各种网络教程并最终实现了一个简单的计算器,计算器包括简单的赋值操作,print操作,局部变量,错误处理,自定义错误信息等,下面主要记录一些在实现计算器的过程中学习到的关键点。
1-antlr4的安装与配置
(1)在使用intellij+antlr4时,只需要在settings中的plugin里搜索antlr v4并安装即可。
(2)在intellij中可以很方便的进行语法树的查看和自定义语法的检查,创建.g4文件并输入antlr4语法,可以通过点击鼠标右键选择test rule对特定的语法规则进行测试
(3)但是仅仅安装intellij的antlr4是不够的,所以需要对电脑环境进行配置,将antlr4加入到环境变量中,并添加命令行命令,方便后面的调试。下面是添加的步骤。
- 下载antlr4的jar包,将它保存到一个系统下的文件夹中,我保存在C:\javalib\antlr-4.5.3-complete.jar
- 在环境变量中CLASSPATH后面加C:\javalib\antlr-4.5.3-complete.jar;
- 在环境变量中PATH后面加C:\javalib;
- 在C:\javalib中添加两个文件antlr4.txt和grun.txt
- antlr4.txt中写入java org.antlr.v4.Tool %*
- grun中写入java org.antlr.v4.runtime.misc.TestRig %*
- 然后将两个文件后缀改为.bat,打开命令行,输入antlr4看是否配置成功
2-命令行中antlr4常用命令
(1)antlr4 -visitor -no-listener Test.g4(生成visitor而不生成listener),-o选项指定输出位置,-lib选项指定 .tokens文件输出位置。
(2)$ grun规则如下:
java org.antlr.v4.runtime.misc.TestRig GrammarName startRuleName
[-tokens] [-tree] [-gui] [-ps file.ps] [-encoding encodingname]
[-trace] [-diagnostics] [-SLL]
[input-filename(s)]
3-g4语法
(1)采用与java类似的注释方式
(2)TOKEN均采用开头字母大写,语法规则均采用开头字母小写
(3)在单引号中写字符串,使用\表示转义,大致的词法规则与正则表达式类似,很容易上手
(4)antlr的关键字有:import, fragment, lexer, parser, grammar, returns, locals, throws, catch, finally, mode, options, tokens
(5)使用#为可选规则标记名称
grammar AltLabels;
stat: 'return' e ';' # Return
| 'break' ';' # Break
;
e : e '*' e # Mult
| e '+' e # Add
| INT # Int
;
生成的listener如下:
public interface AltLabelsListener extends ParseTreeListener {
void enterMult(AltLabelsParser.MultContext ctx);
void exitMult(AltLabelsParser.MultContext ctx);
void enterBreak(AltLabelsParser.BreakContext ctx);
void exitBreak(AltLabelsParser.BreakContext ctx);
void enterReturn(AltLabelsParser.ReturnContext ctx);
void exitReturn(AltLabelsParser.ReturnContext ctx);
void enterAdd(AltLabelsParser.AddContext ctx);
void exitAdd(AltLabelsParser.AddContext ctx);
void enterInt(AltLabelsParser.IntContext ctx);
void exitInt(AltLabelsParser.IntContext ctx);
}
(6)antlr4中可以处理直接左递归的情况,但无法处理具有间接左递归的情况,因此在制定语法时应当注意。
(7)antlr在处理具有二义性的文法时,优先采用写在前面的文法,如
expr: expr '*'|'/' expr # MulDiv
| expr '+'|'-' expr # AddSub
对于1+2*3,优先匹配2*3,再匹配1+(2*3)。
一个额外的例子,可以在g4中写java语言,添加局部变量等,但我还不是很会用:
grammar Count;
@header {
package foo;
}
@members {
int count = 0; //local variable
}
list
@after {System.out.println(count+" ints");}
: INT {count++;} (',' INT {count++;} )*
;
INT : [0-9]+ ;
WS : [ \r\t\n]+ -> skip ;
4-visitor与listener
在通过#标记语法名称后,使用命令行默认生成带listener的java文件。要使得程序中能够正常的传递值和各种信息,需要继承Listener或Visitor并实现其中的部分方法。《Pragmatic.The Definitive ANTLR 4 Reference.2013》中第七章提到了三种方法,以实现语法树的遍历和值传递。
a.使用visitor,将值以函数返回值的形式层层传递
public static class EvalVisitor extends LExprBaseVisitor<Integer> {
public Integer visitMult(LExprParser.MultContext ctx) {
return visit(ctx.e(0)) * visit(ctx.e(1));
}
public Integer visitAdd(LExprParser.AddContext ctx) {
return visit(ctx.e(0)) + visit(ctx.e(1));
}
public Integer visitInt(LExprParser.IntContext ctx) {
return Integer.valueOf(ctx.INT().getText());
}
}
b.使用listener,用栈代替返回值
public static class Evaluator extends LExprBaseListener {
Stack<Integer> stack = new Stack<Integer>();
public void exitMult(LExprParser.MultContext ctx) {
int right = stack.pop();
int left = stack.pop();
stack.push( left * right );
}
public void exitAdd(LExprParser.AddContext ctx) {
int right = stack.pop();
int left = stack.pop();
stack.push(left + right);
}
public void exitInt(LExprParser.IntContext ctx) {
stack.push( Integer.valueOf(ctx.INT().getText()) );
}
}
c.使用listener和antlr中的ParseTreeProperty,将值存储在语法树的节点上
public static class EvaluatorWithProps extends LExprBaseListener {
/** maps nodes to integers with Map<ParseTree,Integer> */
ParseTreeProperty<Integer> values = new ParseTreeProperty<Integer>();
/** Need to pass e's value out of rule s : e ; */
public void exitS(LExprParser.SContext ctx) {
setValue(ctx, getValue(ctx.e())); // like: int s() { return e(); }
}
public void exitMult(LExprParser.MultContext ctx) {
int left = getValue(ctx.e(0)); // e '*' e # Mult
int right = getValue(ctx.e(1));
setValue(ctx, left * right);
}
public void exitAdd(LExprParser.AddContext ctx) {
int left = getValue(ctx.e(0)); // e '+' e # Add
int right = getValue(ctx.e(1));
setValue(ctx, left + right);
}
public void exitInt(LExprParser.IntContext ctx) {
String intText = ctx.INT().getText(); // INT # Int
setValue(ctx, Integer.valueOf(intText));
}
public void setValue(ParseTree node, int value) { values.put(node, value); }
public int getValue(ParseTree node) { return values.get(node); }
}
在实现计算器的过程中,我采用的是第三种方案,因为需要添加局部变量,所以使用listener更为方便,其次利用树节点存储值会更容易思考和实现。
5-局部变量
在《Pragmatic.The Definitive ANTLR 4 Reference.2013》的第八章和《Language Implementation Patterns》的六到八章中都分别提到了如何使用Symbol和Scope实现局部变量,总体来说,每个block(也就是大括号包起来的)就是一个Scope,每个Scope,除了GlobalScope外,都可以获取上一层的Scope,也就是enclosingScope,在每个Scope中定义各种变量函数,称作Symbol,并将这些Symbol以HashMap的形式存储起来。在enterBlock的时候,就新建一个Scope,并将原来的Scope赋值给enclosingScope,在exitBlock的时候就将Scope还原。
为了实现计算器和局部变量的结合,同时采用了ParseTreeProperty存储节点值,我在每个Scope中都加入了一个memory和一个ParseTreeProperty,memory用于存储变量和变量值,ParseTreeProperty则用于存储这个块中语法树结构以及节点值。
6-错误处理
《Pragmatic.The Definitive ANTLR 4 Reference.2013》的第九章主要讲了如何更改报错信息以及错误恢复的一些问题,修改错误信息需要继承BaseErrorListener类,并重写syntaxError方法,然后在parser中移除掉默认的ErrorListener并添加自定义的Listener
public static class VerboseListener extends BaseErrorListener {
@Override
public void syntaxError(Recognizer<?, ?> recognizer,
Object offendingSymbol,
int line, int charPositionInLine,
String msg,
RecognitionException e)
{
List<String> stack = ((Parser)recognizer).getRuleInvocationStack();
Collections.reverse(stack);
System.err.println("rule stack: "+stack);
System.err.println("line "+line+":"+charPositionInLine+" at "+
offendingSymbol+": "+msg);
}
}
parser.removeErrorListeners(); // remove ConsoleErrorListener
parser.addErrorListener(new UnderlineListener()); // add ours
为了更接近于平时用的编译器,我没有将报错信息直接输出,而是将所有的报错信息字符串存储到ErrorCollector中,而将所有打印的信息保存在另一个Collector中,保证错误信息和打印信息不会同时出现。
在设置ErrorListener之后,要想自定义报错,就不能采用以往的直接throw Exception的方式了,因为这样会直接使程序中断,而且打印的错误信息也很难看。在第九章最后提到了一些如何使用报错并且自定义错误恢复的方式,主要是通过继承DefaultErrorStrategy类,重写report和recover方法,自定义错误处理的策略,查看antlr的类DefaultErrorStrategy:
public void reportError(Parser recognizer, RecognitionException e) {
if(!this.inErrorRecoveryMode(recognizer)) {
this.beginErrorCondition(recognizer);
if(e instanceof NoViableAltException) {
this.reportNoViableAlternative(recognizer, (NoViableAltException)e);
} else if(e instanceof InputMismatchException) {
this.reportInputMismatch(recognizer, (InputMismatchException)e);
} else if(e instanceof FailedPredicateException) {
this.reportFailedPredicate(recognizer, (FailedPredicateException)e);
} else {
System.err.println("unknown recognition error type: " + e.getClass().getName());
recognizer.notifyErrorListeners(e.getOffendingToken(), e.getMessage(), e);
}
}
}
public void recover(Parser recognizer, RecognitionException e) {
if(this.lastErrorIndex == recognizer.getInputStream().index() && this.lastErrorStates != null && this.lastErrorStates.contains(recognizer.getState())) {
recognizer.consume();
}
this.lastErrorIndex = recognizer.getInputStream().index();
if(this.lastErrorStates == null) {
this.lastErrorStates = new IntervalSet(new int[0]);
}
this.lastErrorStates.add(recognizer.getState());
IntervalSet followSet = this.getErrorRecoverySet(recognizer);
this.consumeUntil(recognizer, followSet);
}
从中可以观察到,错误信息是通过notifyErrorListeners广播给ErrorListener的,因此也可以直接调用这一函数,发出报错信息,函数的几个参数分别为Token,String,RecognitionException,所以对于被除数为0的情况,我们可以直接调用函数,并执行一定的错误恢复,就可以使最后的报错信息按照我们希望的方式展示出来了。
7- 计算器实现结果
(1)测试一
(2)测试二