Antlr4 的两种AST遍历方式:Visitor方式Listener方式

Antlr4规则文法:

  • 注释:和Java的注释完全一致,也可参考C的注释,只是增加了JavaDoc类型的注释;
  • 标志符:参考Java或者C的标志符命名规范,针对Lexer 部分的 Token 名的定义,采用全大写字母的形式,对于parser rule命名,推荐首字母小写的驼峰命名;
  • 不区分字符和字符串,都是用单引号引起来的,同时,虽然Antlr g4支持 Unicode编码(即支持中文编码),但是建议大家尽量还有英文;
  • Action,行为,主要有@header 和@members,用来定义一些需要生成到目标代码中的行为,例如,可以通过@header设置生成的代码的package信息,@members可以定义额外的一些变量到Antlr4语法文件中;
  • Antlr4语法中,支持的关键字有:import, fragment, lexer, parser, grammar, returns, locals, throws, catch, finally, mode, options, tokens

基于IDEA调试Antlr4语法规则(文法可视化)

基于IDEA调试Antlr4语法一般步骤:

1) 创建一个调试工程,并创建一个g4文件

这里,我自己测试用Java开发,所以创建的是一个Maven工程,g4文件放在了src/main/resources 目录下,取名 Test.g4

2)写一个简单的语法结构

这里我们参考写一个加减乘除操作的表达式,然后在赋值操作对应的Rule上右键,可选择测试:

grammar Test;

@header {
package com.chaplinthink.antlr;
} stmt : expr; expr : expr NUL expr # Mul
| expr ADD expr # Add
| expr DIV expr # Div
| expr MIN expr # Min
| INT # Int
; NUL : '*';
ADD : '+';
DIV : '/';
MIN : '-'; INT : Digit+;
Digit : [0-9]; WS : [ \t\u000C\r\n]+ -> skip; SHEBANG : '#' '!' ~('\n'|'\r')* -> channel(HIDDEN);

看我们 3/ 4 是可以识别出来的 语法中 channel(HIDDEN) (代表隐藏通道) 中的 Token,不会被语法解析阶段处理,但是可以通过Token遍历获取到。

Antlr4生成并遍历AST

1. 通过命令行如上篇文章

java -jar antlr-4.7.2--complete.jar -Dlanguage=Python3 -visitor Test.g4

这样就可以生成Python3 target的源码,如果不希望生成Listener,可以添加参数 -no-listener

2. Maven Antlr4插件自动生成(针对Java工程,也可以用于Gradle)

此处使用第一种方式

访问者模式遍历Antlr4语法树

java -jar  /usr/local/lib/antlr-4.7.2-complete.jar  -visitor -no-listener  Test.g4

生成源码文件:

通过代码展示访问者模式在Antlr4中使用:

public class App {

    public static void main(String[] args) {
CharStream input = CharStreams.fromString("12*2+12");
TestLexer lexer = new TestLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
TestParser parser = new TestParser(tokens);
TestParser.ExprContext tree = parser.expr();
TestVisitor tv = new TestVisitor();
tv.visit(tree);
} static class TestVisitor extends TestBaseVisitor<Void> {
@Override
public Void visitAdd(TestParser.AddContext ctx) {
System.out.println("========= test add");
System.out.println("first arg: " + ctx.expr(0).getText());
System.out.println("second arg: " + ctx.expr(1).getText());
return super.visitAdd(ctx);
}
}
}

一般来说,面向程序静态分析时,都是使用访问者模式的,很少使用监听器模式(无法主动控制遍历AST的顺序,不方便在不同节点遍历之间传递数据)

Antlr4词法解析和语法解析

如前面的语法定义,分为Lexer和Parser,实际上表示了两个不同的阶段:

  • 词法分析阶段:对应于Lexer定义的词法规则,解析结果为一个一个的Token;
  • 解析阶段:根据词法,构造出来一棵解析树或者语法树。

如下图所示:

Spark & Antlr4

Spark SQL /DataFrame 执行过程是这样子的:

我们看下在 Spark SQL 中是如何使用Antlr4的.

当你调用spark.sql的时候, 会调用下面的方法:

  def sql(sqlText: String): DataFrame = {
Dataset.ofRows(self, sessionState.sqlParser.parsePlan(sqlText))
}

parse sql阶段主要是parsePlan(sqlText)这一部分。而这里又会辗转去org.apache.spark.sql.catalyst.parser.AbstractSqlParser调用parse方法:

protected def parse[T](command: String)(toResult: SqlBaseParser => T): T = {
logDebug(s"Parsing command: $command") val lexer = new SqlBaseLexer(new UpperCaseCharStream(CharStreams.fromString(command)))
lexer.removeErrorListeners()
lexer.addErrorListener(ParseErrorListener) val tokenStream = new CommonTokenStream(lexer)
val parser = new SqlBaseParser(tokenStream)
parser.addParseListener(PostProcessor)
parser.removeErrorListeners()
parser.addErrorListener(ParseErrorListener) try {
try {
// first, try parsing with potentially faster SLL mode
parser.getInterpreter.setPredictionMode(PredictionMode.SLL)
toResult(parser)
}
catch {
case e: ParseCancellationException =>
// if we fail, parse with LL mode
tokenStream.seek(0) // rewind input stream
parser.reset() // Try Again.
parser.getInterpreter.setPredictionMode(PredictionMode.LL)
toResult(parser)
}
}
catch {
case e: ParseException if e.command.isDefined =>
throw e
case e: ParseException =>
throw e.withCommand(command)
case e: AnalysisException =>
val position = Origin(e.line, e.startPosition)
throw new ParseException(Option(command), e.message, position, position)
}
}

这里SqlBaseLexer 、SqlBaseParser都是Antlr4的东西,包括最后的toResult(parser)也是调用访问者模式的类去遍历语法树来生成Logical Plan

spark提供了一个.g4文件,编译的时候会使用Antlr根据这个.g4生成对应的词法分析类和语法分析类,同时还使用了访问者模式,用以构建Logical Plan(语法树)。

访问者模式简单说就是会去遍历生成的语法树(针对语法树中每个节点生成一个visit方法),以及返回相应的值。我们接下来看看一条简单的select语句生成的树是什么样子:

这个sqlBase.g4文件我们也可以直接复制出来,用antlr相关工具就可以生成一个生成一个解析SQL的图

将SELECT A.B FROM A,转换成一棵语法树。我们可以看到这颗语法树非常复杂,这是因为SQL解析中,要适配这种SELECT语句之外,还有很多其他类型的语句,比如INSERT,ALERT等等。Spark SQL这个模块的最终目标,就是将这样的一棵语法树转换成一个可执行的Dataframe(RDD)

Spark使用Antlr4的访问者模式,生成Logical Plan. 我们继承SqlBaseBaseVisitor,里面提供了默认的访问各个节点的触发方法。我们可以通过继承这个类,重写对应节点的visit方法,实现自己的访问逻辑,Spark SQL中这个继承的类就是org.apache.spark.sql.catalyst.parser.AstBuilder

通过观察这棵树,我们可以发现针对我们的SELECT语句,比较重要的一个节点,是querySpecification节点,实际上,在AstBuilder类中,visitQuerySpecification也是比较重要的一个方法(访问对应节点时触发),正是在这个方法中生成主要的Logical Plan的。

以下是querySpecification在Spark SQL 中实现的 代码:

  /**
* Create a logical plan using a query specification.
*/
override def visitQuerySpecification(
ctx: QuerySpecificationContext): LogicalPlan = withOrigin(ctx) {
val from = OneRowRelation().optional(ctx.fromClause) {
visitFromClause(ctx.fromClause)
}
withQuerySpecification(ctx, from)
}

先判断是否有FROM子语句,有的话会去生成对应的Logical Plan,再调用withQuerySpecification()方法,

withQuerySpecification是逻辑计划核心方法, 根据不同的子语句生成不同的Logical Plan.

参考:

[1] Spark SQL: Relational Data Processing in Spark: https://amplab.cs.berkeley.edu/wp-content/uploads/2015/03/SparkSQLSigmod2015.pdf

[2] Antlr4简明使用教程: https://bbs.huaweicloud.com/blogs/226877

Antlr4 语法解析器(下)的更多相关文章

  1. Anrlr4 生成C++版本的语法解析器

    一. 写在前面 我最早是在2005年,首次在实际开发中实现语法解析器,当时调研了Yacc&Lex,觉得风格不是太好,关键当时yacc对多线程也支持的不太好,接着就又学习了Bison&F ...

  2. 在.NET Core中使用Irony实现自己的查询语言语法解析器

    在之前<在ASP.NET Core中使用Apworks快速开发数据服务>一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语 ...

  3. 用java实现编译器-算术表达式及其语法解析器的实现

    大家在参考本节时,请先阅读以下博文,进行预热: http://blog.csdn.net/tyler_download/article/details/50708807 本节代码下载地址: http: ...

  4. Boost学习之语法解析器--Spirit

    Boost.Spirit能使我们轻松地编写出一个简单脚本的语法解析器,它巧妙利用了元编程并重载了大量的C++操作符使得我们能够在C++里直接使用类似EBNF的语法构造出一个完整的语法解析器(同时也把C ...

  5. 使用 java 实现一个简单的 markdown 语法解析器

    1. 什么是 markdown Markdown 是一种轻量级的「标记语言」,它的优点很多,目前也被越来越多的写作爱好者,撰稿者广泛使用.看到这里请不要被「标记」.「语言」所迷惑,Markdown 的 ...

  6. 语法解析器续:case..when..语法解析计算

    之前写过一篇博客,是关于如何解析类似sql之类的解析器实现参考:https://www.cnblogs.com/yougewe/p/13774289.html 之前的解析器,更多的是是做语言的翻译转换 ...

  7. 手写token解析器、语法解析器、LLVM IR生成器(GO语言)

    最近开始尝试用go写点东西,正好在看LLVM的资料,就写了点相关的内容 - 前端解析器+中间代码生成(本地代码的汇编.执行则靠LLVM工具链完成) https://github.com/daibinh ...

  8. 【读书笔记】-【编程语言的实现模式】-【LL(1)递归下降的语法解析器】

    形如:[a,b,c] [a,[b,cd],f] 为 嵌套列表 其ANTLR文法表示: list :'[' elements ']'; // 匹配方括号 elements : elements (',' ...

  9. 使用golang+antlr4构建一个自己的语言解析器(一)

    Antlr4 简介 ANTLR(全名:ANother Tool for Language Recognition)是基于LL(*)算法实现的语法解析器生成器(parser generator),用Ja ...

  10. 自己动手实现一个简单的JSON解析器

    1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...

随机推荐

  1. RabbitMq高级特性之消费端限流 通俗易懂 超详细 【内含案例】

    RabbitMq高级特性之消费端限流 介绍 消息队列中囤积了大量的消息, 或者某些时刻生产的消息远远大于消费者处理能力的时候, 这个时候如果消费者一次取出大量的消息, 但是客户端又无法处理, 就会出现 ...

  2. disconf分布式配置管理(二) 与spring集成

    上一章介绍了disconf的安装预配置,这章主要介绍下disconf与spring集成 1.添加依赖 <dependency> <groupId>com.baidu.disco ...

  3. Atcoder ABC299 E-G

    Atcoder ABC299 E-G E - Nearest Black Vertex 链接: E - Nearest Black Vertex (atcoder.jp) 简要题意: 问题陈述 给你一 ...

  4. 通过 ob-operator 部署 OceanBase 数据库

    本文介绍如何通过 ob-operator 来部署 OceanBase 数据库. 背景信息 ob-operator 与其他 operator 一样,旨在让 OceanBase 以容器的方式,无缝运行在 ...

  5. 兼容ios11的正则匹配

    ios11不支持正则零宽断言,以字符串 $哈哈哈(sh039488)$ 为例: 不兼容写法:/\$(?<=\$).*?(?=\)\$)\)\$/g 兼容写法:/\$(.*?)\)\$/g

  6. React的prop-types下载安装教程

    最近刚入门react,所有react的资源都是从本地导入的,这就难免要去网上找要用的包,react包和reactdom,还有babel的包都挺好找的,官网就有现成的可以用,但是prop-types包貌 ...

  7. Openharmony 跑 CV 应用

    最近有个项目,老同学让帮忙验证一个在ARM 板上跑 OpenHarmony,然后再集成一个CV算法上去,写这个文章主要是整理一下思路.如果有思路不对的地方,也烦请指出. 1. 个人做纯软件比较多,所以 ...

  8. 传染病模型 SI

    参考了这篇写的很好的[1],讲了各种模型 因为是各种模型都是用微分方程写的,所以又去学习了一下微分方程 ,真的忘了有没有学过这个,反正一点印象也没有了. 好在[2] 这个文章又把我带回去了. SI 的 ...

  9. canvas图片旋转扩展出原生JS实现移动端横竖屏手写签名示例

    前提知识 canvas是提供了各种各样的接口去控制画布,比如旋转rotate方法. 这里的旋转并不是真的把这个画布旋转了,例如ctx.rotate(90 * Math.PI / 180)顺时针旋转90 ...

  10. AtCoder Regular Contest 182(A B C)

    原来第二题比第一题简单吗 A.Chmax Rush! \(\texttt{Diff 1110}\) 给定三个序列 \(S,P,V\),其中 \(S\) 的长度为 \(N\),\(P,V\) 的长度为 ...