ANTLR4 Vistor Listener示例与对比

  • Visitor Calculator
    • 预期效果
    • 语法文件
    • 访问器模式生成语法分析器
    • 重写Visitor
    • 运行结果
  • Listener Java
    • 预期效果
    • 语法文件
    • 访问器模式生成语法分析器
    • 重写Listener
    • 运行结果
  • Visitor 与 Listener

Visitor Calculator

我们将以访问者模式做一个计算器。

预期效果

输入:

193
a=5
b=6
a+b*2
(1+2)*3

输出

193
17
9

PS:每次操作都需要换行,输入’!'可以重置标识符对应的数值。

语法文件

Visitor模式:通过在语法规则的每条分支后加上 # Identifier (注意不能和规则名冲突)这样类似标签的形式。
使得对于每种输入我们都有不同的处理方法,后续会介绍如何定义这些处理方法。

此外分享一下在设计clear也就是清零语法时的一些心得:

  1. 文中采用的是,输入时,自动释放标识符与数值的所有对应,并且输出clear并且换行。但一开始考虑的是输入字符串clear,清零后自动换行。放弃这种方法的理由是:clear字符串本身会先被expr规则中的ID识别出来。如果要在ID的visit处理函数中识别clear,又会因为返回值必须是Integar类型而矛盾,最后放弃了输入clear这个方法。
  2. 要注意CLEAR后必须要跟NEWLINE,因为每一行都需要以回车或者换行结束操作。
//LabeledExpr.g4
grammar LabeledExpr; prog:   stat+ ;stat:   expr NEWLINE                # printExpr|   ID '=' expr NEWLINE         # assign|   CLEAR NEWLINE               # clearMemory|   NEWLINE                     # blank;expr:   expr op=('*'|'/') expr      # MulDiv|   expr op=('+'|'-') expr      # AddSub|   INT                         # int|   ID                          # id|   '(' expr ')'                # parens;CLEAR :   '!' ;
MUL :   '*' ;
DIV :   '/' ;
ADD :   '+' ;
SUB :   '-' ;
ID  :   [a-zA-Z]+ ;      // match identifiers
INT :   [0-9]+ ;         // match integers
NEWLINE:'\r'? '\n' ;     // return newlines to parser (is end-statement signal)
WS  :   [ \t]+ -> skip ; // toss out whitespace

访问器模式生成语法分析器

输入:

antlr4 -no-listener -visitor LabeledExpr.g4

除了常见的几个文件以外,我们会生成以下两个文件:

LabeledExprVisitor、LabeledExprBaseVisitor

前者是分支处理函数的定义,后者是分支处理函数的实现

我们可以重写LabeledExprBaseVistor,来自定义每个分支标签在输入情况匹配时的动作。

重写Visitor

观察以下代码我们可以发现,Visitor生成的处理函数命名为 visitor+标签名

理论上,我们需要重写每个分支处理函数来应对不同的输入情况。

有一句格外的显眼:visit(ctx.expr())。

我们不管括号内的expr(),实际上是左循环的token,为了获取子分支的情况,我们需要显式地调用visit()。

visitPrintExpr为例,这句语法的目的是打印出表达式的结果。而表达式本身有多种情况,可以是INT、标识符、嵌套表达式,但是我们

不需要在这个分支中去操心这些,上文说过我们需要重写每个子分支的处理函数,因此在这里只需要通过visit得到expr的值就可以了。

而在下面的visitInt、visitId等分支处理函数中,最后返回了处理过后的值

因此我们以这样一种由上而下的方式,满足了各种输入的情况。

//EvalVistor.java
import java.util.HashMap;
import java.util.Map;public class EvalVisitor extends LabeledExprBaseVisitor<Integer> {/** "memory" for our calculator; variable/value pairs go here */Map<String, Integer> memory = new HashMap<String, Integer>();/** ID '=' expr NEWLINE */@Overridepublic Integer visitAssign(LabeledExprParser.AssignContext ctx) {String id = ctx.ID().getText();  // id is left-hand side of '='int value = visit(ctx.expr());   // compute value of expression on rightmemory.put(id, value);           // store it in our memoryreturn value;}/** expr NEWLINE */@Overridepublic Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {Integer value = visit(ctx.expr()); // evaluate the expr childSystem.out.println(value);         // print the resultreturn 0;                          // return dummy value}/** CLEAR MEMORY */@Overridepublic Integer visitClearMemory(LabeledExprParser.ClearMemoryContext ctx) {memory.clear();System.out.println("clear");return 0;}/** INT */@Overridepublic Integer visitInt(LabeledExprParser.IntContext ctx) {return Integer.valueOf(ctx.INT().getText());}/** ID */@Overridepublic Integer visitId(LabeledExprParser.IdContext ctx) {String id = ctx.ID().getText();if ( memory.containsKey(id) ) return memory.get(id);return 0;}/** expr op=('*'|'/') expr */@Overridepublic Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) {int left = visit(ctx.expr(0));  // get value of left subexpressionint right = visit(ctx.expr(1)); // get value of right subexpressionif ( ctx.op.getType() == LabeledExprParser.MUL ) return left * right;return left / right; // must be DIV}/** expr op=('+'|'-') expr */@Overridepublic Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {int left = visit(ctx.expr(0));  // get value of left subexpressionint right = visit(ctx.expr(1)); // get value of right subexpressionif ( ctx.op.getType() == LabeledExprParser.ADD ) return left + right;return left - right; // must be SUB}/** '(' expr ')' */@Overridepublic Integer visitParens(LabeledExprParser.ParensContext ctx) {return visit(ctx.expr()); // return child expr's value}
}

运行结果

编写一个主函数去整合语法分析器以及自定义Vistor:

//Calc.java
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.ParseTree;import java.io.FileInputStream;
import java.io.InputStream;public class Calc {public static void main(String[] args) throws Exception {String inputFile = null;if ( args.length>0 ) inputFile = args[0];InputStream is = System.in;if ( inputFile!=null ) is = new FileInputStream(inputFile);ANTLRInputStream input = new ANTLRInputStream(is);LabeledExprLexer lexer = new LabeledExprLexer(input);CommonTokenStream tokens = new CommonTokenStream(lexer);LabeledExprParser parser = new LabeledExprParser(tokens);ParseTree tree = parser.prog(); // parseEvalVisitor eval = new EvalVisitor();eval.visit(tree);}
}

待用的输入:

//t.expr
aa = 5
bb = 6
aa+bb*2
!
aa
bb

编译后,输入java Calc t.expr:

Listener Java

我们将以监听者模式抽取Java类中的方法,并整合成一个接口。

预期效果

输入:

import java.util.List;
import java.util.Map;
public class Demo {void f(int x, String y) { }int[ ] g(/*no args*/) { return null; }List<Map<String, Integer>>[] h() { return null; }
}

输出

语法文件

整个Java.g4文件比较大,这里我们只列举出关于class、method、import的语法识别部分。

Listener模式:与Visitor模式不同的是,Listener无需手动地在各个分支后打上标签,当我们以监听者模式生成语法分析器时,语法分析器

每遍历一个规则都会有两次响应事件,分别是enter以及exit

例如:classDeclaration,会生成enterclassDeclaration和exitclassDeclaration,我们可以在这两个时刻被监听到时响应它们。

//Java.g4
classDeclaration:   'class' Identifier typeParameters? ('extends' type)?('implements' typeList)?classBody;
importDeclaration:   'import' 'static'? qualifiedName ('.' '*')? ';';
methodDeclaration:   type Identifier formalParameters ('[' ']')* methodDeclarationRest|   'void' Identifier formalParameters methodDeclarationRest;

访问器模式生成语法分析器

输入:

antlr4 Java.g4(缺省是listener模式)

除了常见的几个文件以外,我们会生成以下两个文件:

JavaListener.java JavaBaseListener.java

前者是遍历整个语法分析树的全部响应事件定义,后者是它的实现

我们可以重写 JavaBaseListener,选择性地编写那些响应事件。

重写Listener

实际上我们只需要重写ImportDeclaration的enter、ClassDeclaration的enter和exit、MethodDeclaration的enter即可。

因为我们预想的类定义需要开始和结尾的{和},因此需要在exit时响应。而import和method对结尾并无要求。

以下代码中需要注意的是,有时需要用到parser.getTokenStream去获取token,有时候直接可以通过ctx.就可以获取。

比如:qualifiedName需要通过parser.getTokenStream获取,而Identifier通过ctx.就可以获取。

观察可以发现前者是语法规则的命名,而后者是词法规则的命名,可见前者这样的语法规则定义的词法符号往往需要通过语法分析器遍历获取。

//ExtractInterfaceListener.java
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.misc.Interval;public class ExtractInterfaceListener extends JavaBaseListener {JavaParser parser;public ExtractInterfaceListener(JavaParser parser) {this.parser = parser;}@Override public void enterImportDeclaration(JavaParser.ImportDeclarationContext ctx) {TokenStream tokens = parser.getTokenStream();String temp=tokens.getText(ctx.qualifiedName());System.out.println("import "+temp+';');}/** Listen to matches of classDeclaration */@Overridepublic void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx){System.out.println("interface I"+ctx.Identifier()+" {");}@Overridepublic void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) {System.out.println("}");}/** Listen to matches of methodDeclaration */@Overridepublic void enterMethodDeclaration(JavaParser.MethodDeclarationContext ctx){// need parser to get tokensTokenStream tokens = parser.getTokenStream();String type = "void";if ( ctx.type()!=null ) {type = tokens.getText(ctx.type());}String args = tokens.getText(ctx.formalParameters());System.out.println("\t"+type+" "+ctx.Identifier()+args+";");}
}

运行结果

编写一个主函数去整合语法分析器以及自定义Listener:

//ExtractInterfaceTool.java
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.*;import java.io.FileInputStream;
import java.io.InputStream;public class ExtractInterfaceTool {public static void main(String[] args) throws Exception {String inputFile = null;if ( args.length>0 ) inputFile = args[0];InputStream is = System.in;if ( inputFile!=null ) {is = new FileInputStream(inputFile);}ANTLRInputStream input = new ANTLRInputStream(is);JavaLexer lexer = new JavaLexer(input);CommonTokenStream tokens = new CommonTokenStream(lexer);JavaParser parser = new JavaParser(tokens);ParseTree tree = parser.compilationUnit(); // parseParseTreeWalker walker = new ParseTreeWalker(); // create standard walkerExtractInterfaceListener extractor = new ExtractInterfaceListener(parser);walker.walk(extractor, tree); // initiate walk of tree with listener}
}

待用的输入:

//Demo.java
import java.util.List;
import java.util.Map;
public class Demo {void f(int x, String y) { }int[ ] g(/*no args*/) { return null; }List<Map<String, Integer>>[] h() { return null; }
}

编译后,输入java ExtractInterfaceTool Demo.java:

Visitor 与 Listener

我们可以从主函数的角度去对比:

//VisitorEvalVisitor eval = new EvalVisitor();eval.visit(tree);
//ListenerParseTreeWalker walker = new ParseTreeWalker(); // create standard walkerExtractInterfaceListener extractor = new ExtractInterfaceListener(parser);walker.walk(extractor, tree); // initiate walk of tree with listener

在产生Visitor的语法分析器之前,我们需要对子分支打上标签。产生语法分析器后,需要重写Visitor(每个子分支的visit函数)。最后在主函数中,我们用重写的Visitor实例来访问语法分析树。

在产生Listener的语法分析器之前,我们并不需要外的操作。产生语法分析器后,需要重写Listener(可选的每个子分支的enter及exit函数)。最后在主函数中,我们新建一个语法树唤醒器,再新建一个重写的Listener实例,把语法分析树和Listener实例放到唤醒器中。

对比以下可以发现:

  1. visitor对于子分支的处理是主动的(打标签),而listener是自动的。
  2. visitor会将输入与子分支进行比对再触发响应,而listener会在enter、exit两个节点触发响应。
  3. 在对某个分支的处理时,如果需要调用其子分支,visitor需要主动地visit。而listener在遍历时会自动处理,这让我们无需关注子分支的细节。

ANTLR4(二) Vistor Listener相关推荐

  1. Java Web基础入门第八十二讲 Listener(监听器)——监听器在开发中的应用(一)

    监听器在JavaWeb开发中用得比较多,下面说一下监听器(Listener)在开发中的常见应用. 统计当前在线人数 在JavaWeb应用开发中,有时候我们需要统计当前在线的用户数,此时就可以使用监听器 ...

  2. java web 全局_JavaWeb - 【Listener】初始化全局资源

    JavaWeb - [Listener]初始化全局资源 JavaWeb - [Listener]初始化全局资源 Listener使用步骤 Listener功能 初始化全局资源(读取配置文件) Serv ...

  3. 从定义到AST及其遍历方式,一文带你搞懂Antlr4

    摘要:本文将首先介绍Antlr4 grammer的定义方式,如何通过Antlr4 grammer生成对应的AST,以及Antlr4 的两种AST遍历方式:Visitor方式和Listener方式. 1 ...

  4. Filter 过滤器和 Listener 监听器,java面试必问底层

    一.Filter 过滤器 1.概述 2.开发步骤 3.过滤器执行流程 4.过滤器生命周期 5.过滤器配置问题 6.过滤器链(配置多个过滤器) [二.Listener 监听器]( <一线大厂Jav ...

  5. Listener(监听器)的简单介绍

    Listener(监听器)的作用和内部机制 作用:监听某个事件的发生,状态的改变 内部机制:接口回调 八个web监听器 实现监听: 创建类实现监听器接口 web.xml文件中配置(注册)监听器< ...

  6. oracle tns和sid,oracle tns listener配置 (附TNS介绍)

    一.tnsnames.ora 用途:(用于客户端)告诉oracle client应该从哪连.连到哪.TEST = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)( ...

  7. Oracle RAC万能集群测试大全 支持11g/12c/18c/19c版本

    为了方便阅读,请横屏观看代码部分 一.Oracle RAC集群测试背景 某中大型制造业公司,由于要新上项目,建设了一套业务系统-ERP系统,这套系统的数据库环境是Oracle RAC(RHEL Lin ...

  8. 09_Filter过滤器(访问所有资源前,首先执行自定义过滤器类的doFilter方法)_Listener监听器(监听域对象的改变)

    Filter 什么是Web过滤器? 如何使用Filter过滤器? 自定义Filter类的生命周期 过滤器可以拦截哪些资源路径? 拦截方式配置:资源被访问的方式 过滤器链的默认执行顺序是什么? 如何配置 ...

  9. FilterListener(超详细)

    1.Filter ?什么是过滤器 Filter 过滤器它是 JavaWeb 的三大组件之一. 三大组件分别是:Servlet 程序.Listener 监听器.Filter 过滤器 Filter 过滤器 ...

最新文章

  1. 整数转换为罗马数字 Integer to Roman
  2. Red Hat Linux 挂载外部资源
  3. (转)各种纹理贴图技术
  4. YUV / RGB 格式及快速转换算法总结(转载)
  5. c语言两种加法,两个超长正整数的加法
  6. UIImagePickerController和UIAlertController结合使用
  7. docker centos 环境 安装 python
  8. 遵义大数据中心项目工程概况_市委书记张新文到曹州云都大数据中心等项目现场调研建设情况...
  9. JS代码优化工具Prepack
  10. npm 端口设置成80_13 个 NPM 快速开发技巧
  11. ACM弱校ACMer A HDU1045Fire Net有感
  12. 【转】各种字符串算法大总结
  13. [转载] python 列表List中index函数的坑
  14. 50个提高会话技巧的方法 (转IT经理人)
  15. hex2bin和bin2hex互转的小程序源代码
  16. caffe 安装教程(一)
  17. 平均股价的时间序列图形_统计学-时间序列分析ppt
  18. 家用wifi的配置和重置
  19. 主板后置音频接口图解_图解主板前置音频线接法(一)
  20. Android 国内阿里云镜像

热门文章

  1. upc 7834 送礼物
  2. 准备进入ReRAM速度!Crossbar发布SMIC芯片样品
  3. 怎样一键比较2个CAD图纸文件的不同呢?
  4. CSS使用小操作(隐藏滚动,实现三角行等...)
  5. 淘宝打标API,旺旺打标签接口文档
  6. java对中文首字母拼音排序
  7. RN实现仿余额宝余额数字翻滚动画特效
  8. 微信小程序开发者工具使用vant组件
  9. 加息靴子落地铁矿石继续反弹,甲醇认购大涨,苹果10-01大跳水2022.5.5
  10. ‘parent.relativePath‘ points at com.xxx instead of org.springframework.boot:spring-boot-starter的快速解决