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   # Multint right = getValue(ctx.e(1));setValue(ctx, left * right);}public void exitAdd(LExprParser.AddContext ctx) {int left = getValue(ctx.e(0)); // e '+' e   # Addint right = getValue(ctx.e(1));setValue(ctx, left + right);}public void exitInt(LExprParser.IntContext ctx) {String intText = ctx.INT().getText(); // INT   # IntsetValue(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 {@Overridepublic 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 ConsoleErrorListenerparser.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)测试二

antlr4读书笔记相关推荐

  1. 【读书笔记】知易行难,多实践

    前言: 其实,我不喜欢看书,只是喜欢找答案,想通过专业的解答来解决我生活的困惑.所以,我听了很多书,也看了很多书,但看完书,没有很多的实践,导致我并不很深入在很多时候. 分享读书笔记: <高效1 ...

  2. 读书笔记:编写高质量代码--web前端开发修炼之道(二:5章)

    读书笔记:编写高质量代码--web前端开发修炼之道 这本书看得断断续续,不连贯,笔记也是有些马虎了,想了解这本书内容的童鞋可以借鉴我的这篇笔记,希望对大家有帮助. 笔记有点长,所以分为一,二两个部分: ...

  3. 《编程匠艺》读书笔记

    <编程匠艺>读书笔记之一 <编程匠艺>读书笔记之二 <编程匠艺>读书笔记之三 <编程匠艺>读书笔记之四 <编程匠艺>读书笔记之五 <编 ...

  4. 《Java: The Complete Reference》等书读书笔记

    春节期间读了下<Java: The Complete Reference>发现这本书写的深入浅出,我想一个问题,书中很多内容我们也知道,但是为什么我们就写不出这样一本书,这么全面,这么系统 ...

  5. oracle直查和call哪个更快,让oracle跑的更快1读书笔记二

    当前位置:我的异常网» 数据库 » <>读书笔记二 <>读书笔记二 www.myexceptions.net  网友分享于:2013-08-23  浏览:9次 <> ...

  6. 《JavaScript面向对象精要》读书笔记

    JavaScript(ES5)的面向对象精要 标签: JavaScript 面向对象 读书笔记 2016年1月16日-17日两天看完了<JavaScript面向对象精要>(参加异步社区的活 ...

  7. 《The Art of Readable Code》 读书笔记 01

    放假前在学校图书馆借了一本新书<The Art of Readable Code>,寒假回来看看,写写其中的Key Idea .summary和一些读书笔记. Preface 前言部分主要 ...

  8. 读书笔记(2) OpenLayers中的图层

    OpenLayers有多个不同的图层类,每一个都可以连接到不同的地图服务器.例如通过Layer.WMS类可以连接到WMS地图服务器,通过Layer.Google类可以连接到谷歌地图服务器.OpenLa ...

  9. 《Microsoft Sql server 2008 Internals》读书笔记--第九章Plan Caching and Recompilation(10)

    <Microsoft Sql server 2008 Internals>读书笔记订阅地址: http://www.cnblogs.com/downmoon/category/230397 ...

最新文章

  1. 笔记-中项案例题-2017年下-变更管理和配置管理
  2. java string 反序列化_如何将java.lang.String的空白JSON字符串值反序列化为null?
  3. 技术人员转行产品经理读这些书就够了
  4. linux版vmware卡顿,Manjaro下Vmware安装的MacOS10.15遇到的性能问题导致几乎无法启动问题及解决...
  5. php 通讯协议,通讯协议作用
  6. python no such file or directory_Python3 no such file or directory
  7. linux下完全删除mysql
  8. java毕业设计会员刷卡积分管理系统mybatis+源码+调试部署+系统+数据库+lw
  9. 修复 Windows 10 设置界面里面混乱的语言翻译
  10. JS——正则校验域名
  11. 求助-强化学习基础-K-摇臂老虎机Python
  12. php第三方阿里云接口
  13. 计算机辅助cad职称报考,计算机辅助设计绘图员(AUTO CAD)中级证
  14. react核心精讲视频与实战教程
  15. SpringBoot微服务项目报错:Failed to process import candidates for configuration class [springfox.boot...
  16. 第6章 引导启动程序boot
  17. caffe上手2:使用INRIA行人数据集对BVLC Caffe进行fine-tuning
  18. 样本数据去标识化技术
  19. 应届大学生如何找工作,如何选择,如何投简历,如何面试
  20. 网易推区块链产品「星球」,收集隐私还是收割韭菜?

热门文章

  1. JS逆向之x讯视频wasm的ckey分析
  2. [转贴]我们需要一个什么样的证监会
  3. ChatGPT:Sorry, You Have Been Blocked - 如何解决?
  4. R语言中哑变量的设置
  5. 工业元宇宙:智能制造的未来形态
  6. CSS绝对定位(absolute)、相对定位(relative)方法(详解)
  7. vue3项目实战之在线客服-① 创建项目
  8. 手机APP测试技术要点汇总
  9. win11卸载软件怎么恢复?5个方法总有一种适合你
  10. 强劲深度图像性能现场体验!奥比中光两款3D标品相机亮相China3DV