ShardingJDBC内部使用的也是Antlr4来做语法生成器的,当初在看sharding源码的时候被很多.g4结尾的文件迷惑住了,后来才知道原来这个就是Antlr4

Antlr他不单单只可以分析SQL,Druid内部使用的是Sql-parse框架来做SQL解析的,这个框架也很优秀但是局限于SQL解析

Antlr 是一款强大的语法生成器工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。基本上是当前 Java 语言中使用最为广泛的语法生成器工具。Twitter搜索使用ANTLR进行语法分析,每天处理超过20亿次查询;Hadoop生态系统中的Hive、Pig、数据仓库和分析系统所使用的语言都用到了ANTLR;Lex Machina将ANTLR用于分析法律文本;Oracle公司在SQL开发者IDE和迁移工具中使用了ANTLR;NetBeans公司的IDE使用ANTLR来解析C++;Hibernate对象-关系映射框架(ORM)使用ANTLR来处理HQL语言

简单的模式匹配

我们看下官方的一个简单的例子,这是一个赋值表达式的例子。
语法这样写:

assign : ID '=' expr ';' ;

解析器的代码类似于下面这样:

void assign() {match(ID);match('=');expr();match(';');

解析只分为两种情况:第一种情况是直接模式匹配,第二种情况是调用其它函数继续分析。

我们写个完整的赋值语句的语法吧。我们简化一下,先不做递归下降,将表达式化简成只支持数字:

grammar assign;
assign : ID '=' expr ';' ;
ID : [a-z]+ ;
expr : NUMBER ;
NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ;

ID我们简化成只支持小写字母的组合,数字我们写个比较详细的。
上面的代码存成assign.g4,用antlr4 assign.g4命令,就可以生成java解析器代码了。

我们来看看生成的parser中的片段,跟上面的像不像:

    public final AssignContext assign() throws RecognitionException {AssignContext _localctx = new AssignContext(_ctx, getState());enterRule(_localctx, 0, RULE_assign);try {enterOuterAlt(_localctx, 1);{setState(4);match(ID);setState(5);match(T__0);setState(6);expr();setState(7);match(T__1);}}catch (RecognitionException re) {_localctx.exception = re;_errHandler.reportError(this, re);_errHandler.recover(this, re);}finally {exitRule();}return _localctx;}

下面是解析expr的情况:

    public final ExprContext expr() throws RecognitionException {ExprContext _localctx = new ExprContext(_ctx, getState());enterRule(_localctx, 2, RULE_expr);try {enterOuterAlt(_localctx, 1);{setState(9);match(NUMBER);}}catch (RecognitionException re) {_localctx.exception = re;_errHandler.reportError(this, re);_errHandler.recover(this, re);}finally {exitRule();}return _localctx;}

多种分支的情况

如果有多种可能的话,在语法里用"|"符号分别列出来就是了。ANTLR会把它翻译成switch case一样的语句。

我们把我们上面的例子扩展一下,不光支持'='还支持':='赋值

grammar assign2;
assign : ID '=' expr ';' | ID ':=' expr ';' ;
ID : [a-z]+ ;
expr : NUMBER ;
NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ;

生成的Parser就变成switch case了:

    public final AssignContext assign() throws RecognitionException {AssignContext _localctx = new AssignContext(_ctx, getState());enterRule(_localctx, 0, RULE_assign);try {setState(14);_errHandler.sync(this);switch ( getInterpreter().adaptivePredict(_input,0,_ctx) ) {case 1:enterOuterAlt(_localctx, 1);{setState(4);match(ID);setState(5);match(T__0);setState(6);expr();setState(7);match(T__1);}break;case 2:enterOuterAlt(_localctx, 2);{setState(9);match(ID);setState(10);match(T__2);setState(11);expr();setState(12);match(T__1);}break;}}catch (RecognitionException re) {_localctx.exception = re;_errHandler.reportError(this, re);_errHandler.recover(this, re);}finally {exitRule();}return _localctx;}

这次我们直接看java语法的例子:

typeDeclaration:   classOrInterfaceModifier* classDeclaration|   classOrInterfaceModifier* enumDeclaration|   classOrInterfaceModifier* interfaceDeclaration|   classOrInterfaceModifier* annotationTypeDeclaration|   ';';

上面的语法在:https://github.com/antlr/grammars-v4/blob/master/java/Java.g4 中,我们把它下载下来,用antlr4 Java.g4运行一下,就生成了Lexer和Parser类。
由于是真的语法,翻出来比起纯粹的例子自然是复杂一些,不过不考虑细节,整个结构上还是很好懂的。大家只要理解这套switch case的结构就好:

...try {int _alt;setState(269);_errHandler.sync(this);switch ( getInterpreter().adaptivePredict(_input,10,_ctx) ) {case 1:enterOuterAlt(_localctx, 1);{setState(243);_errHandler.sync(this);_la = _input.LA(1);while ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << ABSTRACT) | (1L << FINAL) | (1L << PRIVATE) | (1L << PROTECTED) | (1L << PUBLIC) | (1L << STATIC) | (1L << STRICTFP))) != 0) || _la==AT) {{{setState(240);classOrInterfaceModifier();}}setState(245);_errHandler.sync(this);_la = _input.LA(1);}setState(246);classDeclaration();}break;case 2:enterOuterAlt(_localctx, 2);{setState(250);_errHandler.sync(this);_la = _input.LA(1);while ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << ABSTRACT) | (1L << FINAL) | (1L << PRIVATE) | (1L << PROTECTED) | (1L << PUBLIC) | (1L << STATIC) | (1L << STRICTFP))) != 0) || _la==AT) {{{setState(247);classOrInterfaceModifier();}}setState(252);_errHandler.sync(this);_la = _input.LA(1);}setState(253);enumDeclaration();}break;case 3:enterOuterAlt(_localctx, 3);{setState(257);_errHandler.sync(this);_la = _input.LA(1);while ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << ABSTRACT) | (1L << FINAL) | (1L << PRIVATE) | (1L << PROTECTED) | (1L << PUBLIC) | (1L << STATIC) | (1L << STRICTFP))) != 0) || _la==AT) {{{setState(254);classOrInterfaceModifier();}}setState(259);_errHandler.sync(this);_la = _input.LA(1);}setState(260);interfaceDeclaration();}break;case 4:enterOuterAlt(_localctx, 4);{setState(264);_errHandler.sync(this);_alt = getInterpreter().adaptivePredict(_input,9,_ctx);while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) {if ( _alt==1 ) {{{setState(261);classOrInterfaceModifier();}} }setState(266);_errHandler.sync(this);_alt = getInterpreter().adaptivePredict(_input,9,_ctx);}setState(267);annotationTypeDeclaration();}break;case 5:enterOuterAlt(_localctx, 5);{setState(268);match(SEMI);}break;}}
...

二义性文法

选择太多了也未必见得是好事儿,有一种副作用就是选择不是唯一的,这叫做『二义性文法』。
最简单的二义性文法就是把同一条规则写两遍,比如上面例子的":="我们就改成"=",让"|"之前和之后两条都一样。

grammar assign2;
assign : ID '=' expr ';' | ID '=' expr ';' ;
ID : [a-z]+ ;
expr : NUMBER ;
NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ;

但是ANTLR4是兼容这种情况的,不报错。在实际应用的时候,它选择第一条符合条件的规则,请看生成的代码

        try {setState(14);_errHandler.sync(this);switch ( getInterpreter().adaptivePredict(_input,0,_ctx) ) {case 1:enterOuterAlt(_localctx, 1);{setState(4);match(ID);setState(5);match(T__0);setState(6);expr();setState(7);match(T__1);}break;case 2:enterOuterAlt(_localctx, 2);{setState(9);match(ID);setState(10);match(T__0);setState(11);expr();setState(12);match(T__1);}break;}}

最著名的二义性的例子就是关键字。在常见的编程语言中,关键字都是和标识符冲突的.
比如我们定义一个if关键字:

IF : 'if' ;
ID : [a-z]+ ;

明显,IF和ID两个规则都可以解析'if'这个串,那到底是按IF算,还是按ID算呢?在ANTLR里,规则很简单,按照可以匹配的第一条处理。

但是,光靠第一条优先,也还是解决不了所有的问题。
我们看两类新的问题

第一类:1 + 2 * 3。这个如何处理,是先算+还是先算*?
前人想出了三种办法来解决:

  • 从左到右:管人是如何理解乘除加减的,我就从左到右算。Smalltalk就是这样做的
  • 中缀转前缀:带来问题的是中缀表达式,我们给换个形式不就OK了吗,比如改成这样(+ 1 (* 2 3)),lisp就是这么做的
  • 运算符优先级:最常用的一种作法,后面我们详情分析。基本上大部分常见的语言都有一个运算符优先级的表。

第二类,是一些语言的设计所导致的,给词法分析阶段带来困难。
比如"*"运算符,在大部分语言中都只表示乘法,但是在C语言中表示指针,当i*j时,表示乘法,但是当int *j;时,就变成表示指针。
所以像Go语言在设计时,就把类型定义移到了后面。我们入门阶段暂时也不打算解析这么复杂的,将来用到了再说。

下一步做啥

经过前面学习的写grammar的过程,我们可以把字符流CharStream,转换成一棵ParseTree。
CharStream是字符流,经过词法分析会变成Token流。
Token流再最终组装成一棵ParseTree,叶子节点是TerminalNode,非叶子节点是RuleNode.

为了节省空间,Token流之上都没有复制字符流的内容,都是通过指向字符流区缓冲区来获取内容。空白字符在Token流以上就不存在了。

既然有了ParseTree,后面的事情就好办了。我们只要遍历这棵ParseTree,就可以访问所有的节点,然后继续做代码生成之类的后端的工作。

为了方便使用,ANTLR将这些节点,封装成RuleNode的子类,前面代码中我们看到的xxxContext类,就是这些子类。比如AssignContext,ExprContext等。

具体的接口,请看图:

我们看个AssignContext是如何被实现的:

    public static class AssignContext extends ParserRuleContext {public TerminalNode ID() { return getToken(assign2Parser.ID, 0); }public ExprContext expr() {return getRuleContext(ExprContext.class,0);}public TerminalNode IF() { return getToken(assign2Parser.IF, 0); }public AssignContext(ParserRuleContext parent, int invokingState) {super(parent, invokingState);}@Override public int getRuleIndex() { return RULE_assign; }@Overridepublic void enterRule(ParseTreeListener listener) {if ( listener instanceof assign2Listener ) ((assign2Listener)listener).enterAssign(this);}@Overridepublic void exitRule(ParseTreeListener listener) {if ( listener instanceof assign2Listener ) ((assign2Listener)listener).exitAssign(this);}}

两种访问ParserTree的方法

ANTLR提供了两种方法来访问ParseTree:

  • 一种是通过Parse-Tree Listener的方法
  • 另一种是通过Parse-Tree Visitor的方法

Listener方法有点类似于解析XML的SAX方法。
废话不多说了,这篇文章已经有点长了,我们直接上代码:

// Generated from assign2.g4 by ANTLR 4.6
import org.antlr.v4.runtime.tree.ParseTreeListener;/*** This interface defines a complete listener for a parse tree produced by* {@link assign2Parser}.*/
public interface assign2Listener extends ParseTreeListener {/*** Enter a parse tree produced by {@link assign2Parser#assign}.* @param ctx the parse tree*/void enterAssign(assign2Parser.AssignContext ctx);/*** Exit a parse tree produced by {@link assign2Parser#assign}.* @param ctx the parse tree*/void exitAssign(assign2Parser.AssignContext ctx);/*** Enter a parse tree produced by {@link assign2Parser#expr}.* @param ctx the parse tree*/void enterExpr(assign2Parser.ExprContext ctx);/*** Exit a parse tree produced by {@link assign2Parser#expr}.* @param ctx the parse tree*/void exitExpr(assign2Parser.ExprContext ctx);
}

开始解析Assign的时候,会回调etnterAssign方法,结束时回调exitAssign方法。

另一种是采用visitor模式的方法,我们调用antlr4的时候要增加-visitor参数来生成。

Visitor仍然非常简单,我们直接看代码:

// Generated from assign2.g4 by ANTLR 4.6
import org.antlr.v4.runtime.tree.ParseTreeVisitor;/*** This interface defines a complete generic visitor for a parse tree produced* by {@link assign2Parser}.** @param <T> The return type of the visit operation. Use {@link Void} for* operations with no return type.*/
public interface assign2Visitor<T> extends ParseTreeVisitor<T> {/*** Visit a parse tree produced by {@link assign2Parser#assign}.* @param ctx the parse tree* @return the visitor result*/T visitAssign(assign2Parser.AssignContext ctx);/*** Visit a parse tree produced by {@link assign2Parser#expr}.* @param ctx the parse tree* @return the visitor result*/T visitExpr(assign2Parser.ExprContext ctx);
}

好的,基本概念已经准备好了,下一篇教程我们就正式利用这些组件来实现了一个解析器。

结束之前,我们搞个能运行的调用前面语法解析器的例子,最终生成一棵ParseTree.

语法文件再列一遍,省得大家向上翻了:

grammar Assign;
assign : ID '=' expr ';' | ID ':=' expr ';' ;
ID : [a-z]+ ;
expr : NUMBER ;
NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ;
WS : [ \t\r\n]+ -> skip ;

调用antlr4 Assign.g4,然后写个调用的main方法吧:

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;public class TestAssign {public static void main(String[] args) throws Exception {ANTLRInputStream input = new ANTLRInputStream(System.in);AssignLexer lexer = new AssignLexer(input);CommonTokenStream tokens = new CommonTokenStream(lexer);AssignParser parser = new AssignParser(tokens);ParseTree tree = parser.assign();System.out.println(tree.toStringTree(parser));}
}

试试灵不灵吧:

java TestAssign
a = 1;

输出如下:

(assign a = (expr 1) ;)

再试一个用:=赋值的:

java TestAssign
b := 0;

输出如下:

(assign b := (expr 0) ;)

虽然例子很简单,但是我们已经完成了从写语法规则到使用ParseTree的全过程。

强大的语法生成器:Antlr 使用相关推荐

  1. Jenkins 流水线语法 02 片段生成器和声明式语法生成器

    Pipeline 开发工具 选择任意pipeline类型的作业,点击"流水线语法"即可进入pipeline开发工具页面. 片段生成器 有些pipeline代码不是自己去写的,是要工 ...

  2. 开源语法分析器--ANTLR

    开源语法分析器--ANTLR 序言 有的时候,我还真是怀疑过上本科时候学的那些原理课究竟是不是在浪费时间.比方学完操作系统原理之后我们并不能自己动手实现一个操作系统:学完数据库原理我们也不能弄出个像样 ...

  3. 解析器生成器 ANTLR的详细介绍

    什么是ANTLR ANTLR(Another Tool for Language Recognition)是一个强大的解析器生成器,用于读取.处理.执行和翻译结构化文本或二进制文件.它被广泛应用于构建 ...

  4. 第4.2节 神秘而强大的Python生成器精讲

    一. 生成器(generator)概念 生成器是一个特殊的迭代器,它保存的是算法,每次调用next()或send()就计算出下一个元素的值,直到计算出最后一个元素,没有更多的元素时,抛出StopIte ...

  5. Python强大的语法支持

    学习任何一种编程语言,包括但不限于C.C++.Java.Python,我们都需要先深入了解其基本数据类型:可以概括为整型.浮点型和字符串--因为所有程序代码都会用到这些. 1 Python便捷的数学运 ...

  6. c语言语法生成器,C中的生成器

    我得到了一些我无法理解的代码. 在将pow2s的g替换为地图的gen结构后,我陷入了困境.从那里,我无法看到它如何继续跟踪价值以及如何存储价值. 代码编译并运行. 有人可以帮我理解这段代码吗?谢谢! ...

  7. 【Spark】一条 SQL 在 Apache Spark 之旅(上)

    1.概述 转载学习加深印象:一条 SQL 在 Apache Spark 之旅(上) Spark SQL 是 Spark 众多组件中技术最复杂的组件之一,它同时支持 SQL 查询和 DataFrame ...

  8. 微服务链路追踪SkyWalking第十一课 OAL详解实战

    第31讲:OAL 语言,原来定义创造一门新语言如此轻松(上) 在前文介绍 Metrics 实现以及对应的 DIspatcher 实现的时候,会发现有一部分实现类位于 generated-analysi ...

  9. 时序数据库-2-[IoTDB]的原理解析

    清华自研时间序列数据库Apache IoTDB原理解析 时序数据库 Apache-IoTDB 源码解析之前言(一) 时序数据库 Apache-IoTDB 源码解析之系统架构(二) 时序数据库 Apac ...

最新文章

  1. 计算机 维修 pdf,简单计算机维修..pdf
  2. gcc生成dll linux,gcc编译dll和调用dll
  3. Spring Cloud 微服务实战系列-Eureka注册中心(一)
  4. sql脚本比较大,sqlserver 无法导入,就用cmd命令执行
  5. 明星AI芯片公司Graphcore获红杉5000万美元投资
  6. 基于RGB图像的草莓叶片白粉病检测深度学习方法
  7. jQuery入门笔记
  8. 软件测试自学指南---从入门到精通
  9. Anylogic问题-----模型运行中外部输入控制模型
  10. tablet2+android,z2tablet不开机了 怎么处理,索尼Sony Xperia 安卓平板论坛
  11. Javascript构建Bingo卡片游戏
  12. BDBR和BD-PSNR
  13. android源码模块编译错误,【转】Android 源码编译make的错误处理--不错
  14. JAVA毕设项目喜枫日料店自助点餐系统(java+VUE+Mybatis+Maven+Mysql)
  15. 单文件、多文件上传 - Tomcat
  16. blocked Queue
  17. MFRC522应用详解
  18. Linux网络技术管理及进程管理(week2_day4)--技术流ken...
  19. AI-Info-Micron-Insight:案例分析:美光使用数据和人工智能来发现、倾听和感觉
  20. mysql 下一年_mysql时间增加一年

热门文章

  1. Telerik 2023 R1 Progress Developer -Retail
  2. 利用RecordRTC.js实现H5录音功能
  3. java-Tomcat中的多个上下文路径可以服务一个appBase吗?
  4. 2013年云端现状调查:规避隐形云成本
  5. mysql中phpmyadmin安装教程_安装phpMyAdmin图文教程
  6. 如何使用Vuex来管理应用程序的状态?
  7. 机器学习笔记 - 什么是条件随机场?
  8. STM32F429I-Discovery学习笔记--(1)简单上手和官方例程的下载与使用
  9. 哔哩哔哩赴港上市:6年累计亏损超70亿,游戏业务贡献四成营收
  10. LeetCode:978. Longest Turbulent Subarray - Python