antlr

https://github.com/antlr/antlr4
https://www.cntofu.com/book/115/index.html



安装ANTLR
ANTLR是由Java写成的,所以在安装ANTLR前必须保证已经安装有Java 1.6或以上版本。你可以到 http://www.antlr.org/download.html 下载ANTLR的最新版本,或者也可使用命令行工具下载:



curl -O http://www.antlr.org/download/antlr-4.5.1-complete.jar
antlr-4.5.1-complete.jar包含运行ANTLR工具的所有必要依赖,以及编译和执行由ANTLR生成的识别器所需的运行库。ANTLR工具将由语法文件描述的语法转换成识别程序,识别程序利用ANTLR运行库中的某些支持类识别输入的语句。该jar包还包含两个支持库:TreeLayout(一个复杂的树布局库)和StringTemplate(一个用于生成代码和其它结构化文本的模板引擎)。



现在来测试下ANTLR工具是否工作正常:



java -jar antlr-4.5.1-complete.jar # 启动org.antlr.v4.Tool
如果正常的话会看到以下帮助信息:



ANTLR Parser Generator Version 4.5.1
-o __ specify output directory where all output is generated
-lib __
specify location of grammars, tokens files

每次运行ANTLR工具都要输入这么长的命令是否有些痛苦?写个脚本来解放我们的手指吧!



#!/bin/sh
java -cp .:./antlr-4.5.1-complete.jar:$CLASSPATH org.antlr.v4.Tool $*
把它保存为antlr.sh,以后就可以使用下列命令来运行ANTLR工具了:



antlr



mac 下安装
1.安装



brew install antlr



2.设置环境变量



在.bash_profile中设置



export CLASSPATH=”.:/usr/local/Cellar/antlr/4.7.2/antlr-4.7.2-complete.jar:$CLASSPATH”



修改之后文件生效source .bash_profile



在IDEA中安装使用Antlr
在Settings-Plugins中安装ANTLR v4 grammar plugin
新建一个Maven项目,在pom.xml文件中添加ANTLR4插件和运行库的依赖。注意一定要用最新版的,依赖,不知道最新版本号的可以自己google一下maven antlr4。



antlr是指可以根据输入自动生成语法树并可视化的显示出来的开源语法分析器。ANTLR—Another Tool for Language Recognition,其前身是PCCTS,它为包括Java,C++,C#在内的语言提供了一个通过语法描述来自动构造自定义语言的识别器(recognizer),编译器(parser)和解释器(translator)的框架。



ANTLR—Another Tool for Language Recognition,其前身是PCCTS,它为包括Java,C++,C#在内的语言提供了一个通过语法描述来自动构造自定义语言的识别器(recognizer),编译器(parser)和解释器(translator)的框架。ANTLR可以通过断言(Predicate)解决识别冲突;支持动作(Action)和返回值(Return Value)来;更棒的是,它可以根据输入自动生成语法树并可视化的显示出来(这一点我将在下面的例子中演示)。由此,计算机语言的翻译变成了一项普通的任务—在这之前YACC/LEX显得过于学院派,而以LL(k)为基础的ANTLR虽然在效率上还略有不足,但是经过近些年来的升级修改,使得ANTLR足以应付现存的绝大多数应用。感谢Terence Parr博士和他的同事们十几年来的出色工作,他们为编译理论的基础和语言工具的构造做了大量基础性工作,也直接导致了ANTLR的产生。
1.1 词法分析器(Lexer)
词法分析器又称为Scanner,Lexical analyser和Tokenizer。程序设计语言通常由关键字和严格定义的语法结构组成。编译的最终目的是将程序设计语言的高层指令翻译成物理机器或虚拟机可以执行的指令。词法分析器的工作是分析量化那些本来毫无意义的字符流,将他们翻译成离散的字符组(也就是一个一个的Token),包括关键字,标识符,符号(symbols)和操作符供语法分析器使用。
1.2 语法分析器(Parser)
编译器又称为Syntactical analyser。在分析字符流的时候,Lexer不关心所生成的单个Token的语法意义及其与上下文之间的关系,而这就是Parser的工作。语法分析器将收到的Tokens组织起来,并转换成为目标语言语法定义所允许的序列。
无论是Lexer还是Parser都是一种识别器,Lexer是字符序列识别器而Parser是Token序列识别器。他们在本质上是类似的东西,而只是在分工上有所不同而已。如下图所示:
字符输入流、tokens和AST之间的关系
字符输入流、tokens和AST之间的关系
1.3 树分析器 (tree parser)
树分析器可以用于对语法分析生成的抽象语法树进行遍历,并能执行一些相关的操作。
1.4 ANTLR
ANTLR将上述结合起来,它允许我们定义识别字符流的词法规则和用于解释Token流的语法分析规则。然后,ANTLR将根据用户提供的语法文件自动生成相应的词法/语法分析器。用户可以利用他们将输入的文本进行编译,并转换成其他形式(如AST—Abstract Syntax Tree,抽象的语法树)。



环境准备
环境:java8+maven+idea
插件:安装idea-antlr4的插件(file–>setting–>plugins–>install plugin from disk



antlr前端
一些概念
前端:定义语法规则,antlr通过g4文件来定义
lexer:词法解规则,就是将一个句子多个字符进行组装分成多个单词的规则
parser:语法解析,对分词后的整个句子进行解析,可以对每个分词单元做出自定义的处理,从而来实现自己的语法解析功能。
g4文件
g4文件是antlr生成词法解析规则和语法解析规则的基础。该文件是我们自定义的,文件名后缀需要是.g4。g4文件的结构大致为:



grammar
comment(同java //)
options
import
tokens
@actionName
rule
我们需要关注的主要是grammar与rule
grammar
grammar是规则文件的头,需要与文件名保持一致。当antlr生成词法语法解析的规则代码时,类名就是根据grammar的名字来的。



rule
rule是antlr生成词法语法解析的基础。包括了lexer与parser,每条规则都是key:value的形式,以分号结尾。lexer首字母大写,lexer小写。



g4文件的编写与解释
grammar Dsl; //定义规则文件grammar
@header { //一种action,定义生成的词法语法解析文件的头,当使用java的时候,生成的类需要包名,可以在这里统一定义
package antlr;
}



//parsers
sta:(sql ender); //定义sta规则,里面包含了(0个以上)个 sql ender组合规则
ender:’;’; //定义ender规则,是一个分号
sql //定义sql规则,sql规则有两条分支:select/load
: SELECT ~(‘;’)* as tableName //select语法规则,以lexer SELECT开头, 以as tableName 结尾,其中as 和tableName分别是两个parser
| LOAD format ‘.’ path as tableName //load语法规则,大致就是 load json.’path’ as table1,load语法里面含有format,path, as,tableName四种规则
; //sql规则结束符
as: AS; //定义as规则,其内容指向AS这个lexer
tableName: identifier; //tableName 规则,指向identifier规则
format: identifier; //format规则,也指向identifier规则
path: quotedIdentifier; //path,指向quotedIdentifier
identifier: IDENTIFIER | quotedIdentifier; //identifier,指向lexer IDENTIFIER 或者parser quotedIdentifier
quotedIdentifier: BACKQUOTED_IDENTIFIER; //quotedIdentifier,指向lexer BACKQUOTED_IDENTIFIER



//lexers antlr将某个句子进行分词的时候,分词单元就是如下的lexer
//keywords 定义一些关键字的lexer,忽略大小写
AS: [Aa][Ss];
LOAD: [Ll][Oo][Aa][Dd];
SELECT: [Ss][Ee][Ll][Ee][Cc][Tt];



//base 定义一些基础的lexer,
fragment DIGIT:[0-9]; //匹配数字
fragment LETTER:[a-zA-Z]; //匹配字母
STRING //匹配带引号的文本
: ‘'’ ( ~(‘'’|’\’) | (‘\’ .) )* ‘'’
| ‘”’ ( ~(‘”’|’\’) | (‘\’ .) )* ‘”’
;
IDENTIFIER //匹配只含有数字字母和下划线的文本
: (LETTER | DIGIT | ‘_’)+
;
BACKQUOTED_IDENTIFIER //匹配被包裹的文本
: '`' ( ~'`' | '
’ )* ‘`’
;



//–hiden 定义需要隐藏的文本,指向channel(HIDDEN)就会隐藏。这里的channel可以自定义,到时在后台获取不同的channel的数据进行不同的处理
SIMPLE_COMMENT: ‘–’ ~[\r\n]* ‘\r’? ‘\n’? -> channel(HIDDEN); //忽略行注释
BRACKETED_EMPTY_COMMENT: ‘/*/’ -> channel(HIDDEN); //忽略多行注释
BRACKETED_COMMENT : ‘/
’ ~[+] .? ‘/’ -> channel(HIDDEN) ; //忽略多行注释
WS: [ \r\n\t]+ -> channel(HIDDEN); //忽略空白符



// 匹配其他的不能使用上面的lexer进行分词的文本
UNRECOGNIZED: .;



插件配置生成代码
创建一个maven项目
将Dsl.g4文件放入项目中
配置antlr插件的config

生成代码解释
DslLexer 词法解析类
DslParser 语法解析类,在类中有各种Context,每个parser都赌对应了一个xxxContext的内部类,在Context中记录了与其他Context的包含关系,还提供了获取parser中的lexer的方法,以及进出这个rule的回调函数
DslListener 语法解析监听器。antlr有listener和visitor两种遍历方式,前面配置的时候选择的是listener,因此只生成了listener。 在Listener中提供了进入和退出每一种规则的回调方法。我们可以通过实现Listtener类,按需覆写回调方法,以此来实现我们的业务。
antlr后端
简单使用
添加依赖



org.antlr
antlr4-runtime
4.7.1

打印解析树
public static void main(String[] args) throws IOException {
String sql= “Select ‘abc’ as a, hahah as c From a aS table;”;
ANTLRInputStream input = new ANTLRInputStream(sql); //将输入转成antlr的input流
DslLexer lexer = new DslLexer(input); //词法分析
CommonTokenStream tokens = new CommonTokenStream(lexer); //转成token流
DslParser parser = new DslParser(tokens); // 语法分析
DslParser.StaContext tree = parser.sta(); //获取某一个规则树,这里获取的是最外层的规则,也可以通过sql()获取sql规则树……
System.out.println(tree.toStringTree(parser)); //打印规则数
}
load语法实现
功能解说
load的语法: load json.’F:\tmp\user’ as temp; 通过类似的语法,实现spark加载文件夹的数据,然后将数据注册成一张表。这里的json可以替换为spark支持的文件格式。



实现思路
如load json.’F:\tmp\user’ as temp这样一个sql,对应了我们自定义规则的sql规则里面的load分支。 load–>LOAD,json–>format,’F:\tmp\user’ –>path, as–>as,temp–> tableName。
我们可以通过覆写Listener的enterSql()方法,来获取到sql规则里面,与之相关联的其他元素,获取到各个元素的内容,通过spark来根据不同的内容加载不同的数据。



实现代码
public class ParseListener extends DslBaseListener {
@Override
public void enterSql(DslParser.SqlContext ctx) {
String keyword = ctx.children.get(0).getText(); //获取sql规则的第一个元素,为select或者load
if(“select”.equalsIgnoreCase(keyword)){
execSelect(ctx); //第一个元素为selece的时候执行select
}else if(“load”.equalsIgnoreCase(keyword)){
execLoad(ctx); //第一个元素为load的时候执行load
}



}
public void execLoad(DslParser.SqlContext ctx){
List<ParseTree> children = ctx.children; //获取该规则树的所有子节点
String format = "";
String path = "";
String tableName = "";
for (ParseTree c :children) {
if(c instanceof DslParser.FormatContext){
format = c.getText();
}else if(c instanceof DslParser.PathContext){
path = c.getText().substring(1,c.getText().length()-1);
}else if(c instanceof DslParser.TableNameContext){
tableName = c.getText();
}
}
System.out.println(format);
System.out.println(path);
System.out.println(tableName);
// spark load实现,省略
}

public void execSelect(DslParser.SqlContext ctx){

}

public static void main(String[] args) throws IOException {
String len = "load json.`F:\\tmp\\user` as temp;";
ANTLRInputStream input = new ANTLRInputStream(len);
DslLexer lexer = new DslLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
DslParser parser = new DslParser(tokens);
DslParser.SqlContext tree = parser.sql();
ParseListener listener = new ParseListener();
ParseTreeWalker.DEFAULT.walk(listener,tree); //规则树遍历
} } https://www.jianshu.com/p/21f2afca65e8 ANTLR其实很简单 ANTLR是通过递归下降的方式来解析一个语法的。 所谓的递归下降,其实很简单,不过就是一些模式匹配而己。


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



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语言中表示指针,当ij时,表示乘法,但是当int *j;时,就变成表示指针。
所以像Go语言在设计时,就把类型定义移到了后面。我们入门阶段暂时也不打算解析这么复杂的,将来用到了再说。



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



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



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



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



具体的接口,请看图:



ParserRuleContext
我们看个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; }
@Override
public void enterRule(ParseTreeListener listener) {
if ( listener instanceof assign2Listener ) ((assign2Listener)listener).enterAssign(this);
}
@Override
public 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 The return type of the visit operation. Use {@link Void} for

  • operations with no return type.
    */
    public interface assign2Visitor extends ParseTreeVisitor {
    /**

    • 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的全过程。
https://www.jianshu.com/p/1f5e72156075
什么是 Antlr?
ANTLR™ (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files. It’s widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build and walk parse trees.



为什么要有 Antlr?
简易性
可以通过断言(Predicate)解决识别冲突
 支持动作(Action)和返回值(Return Value)
 它可以根据输入自动生成语法树,并可视化的展示出来



模块化
复杂情况下,需要基于语法树遍历(walking the tree)生成目标代码
 Embeded action 将处理代码跟语法描述混合起来,语法复杂时使语法文件臃肿
 语法可能经常需要修改,而语法的主要表达式却不会变动,因此,Antlr 将语法识别与转换、生成(目标代码)的处理分离



Antlr 工作机制
词法分析器(Lexer)
分析量化字符流,翻译成离散的字符组(也就是一堆 Token), 包括关键字,标识符,符号(symbols)和操作符,以供语法分析器使用



语法分析器(Parser)
将 Tokens 组织起来,并转换成为目标语言语法(默认是 Java)定义所允许的序列



树分析器(Tree Parser)
用于对语法分析生成的抽象语法树进行遍历,并在先序经过每个树节点的时候,进行一些定制操作



Antlr 运作流程
安装


Download


$ cd /usr/local/lib
$ curl -O https://www.antlr.org/download/antlr-4.7.2-complete.jar



Add antlr-4.7.2-complete.jar to your CLASSPATH


Create aliases for the ANTLR Tool, and TestRig


$ vim ~/.bashrc
export CLASSPATH=”.:/usr/local/lib/antlr-4.7.2-complete.jar:$CLASSPATH”
alias antlr4=’java -Xmx500M -cp “/usr/local/lib/antlr-4.7.2-complete.jar:$CLASSPATH” org.antlr.v4.Tool’
alias grun=’java -Xmx500M -cp “/usr/local/lib/antlr-4.7.2-complete.jar:$CLASSPATH” org.antlr.v4.gui.TestRig’



$ source ~/.bashrc
编写源文件
$ vim Benedict.g4
// Define a grammar called Benedict
grammar Benedict;
r : ‘hello’ Name ; // match keyword hello followed by an identifier
Name : [a-z]+ ; // match lower-case identifiers
WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines
词法语法分析
$ antlr4 Benedict.g4
$ ll
total 36K
-rw-r–r– 1 benedictjin wheel 93 1 15 14:47 Benedict.g4
-rw-r–r– 1 benedictjin wheel 321 1 15 14:47 Benedict.interp
-rw-r–r– 1 benedictjin wheel 38 1 15 14:47 Benedict.tokens
-rw-r–r– 1 benedictjin wheel 1.3K 1 15 14:47 BenedictBaseListener.java
-rw-r–r– 1 benedictjin wheel 1.3K 1 15 14:47 BenedictLexer.interp
-rw-r–r– 1 benedictjin wheel 3.7K 1 15 14:47 BenedictLexer.java
-rw-r–r– 1 benedictjin wheel 38 1 15 14:47 BenedictLexer.tokens
-rw-r–r– 1 benedictjin wheel 569 1 15 14:47 BenedictListener.java
-rw-r–r– 1 benedictjin wheel 3.9K 1 15 14:47 BenedictParser.java
编译
$ javac Benedict*.java
$ ll
total 60K
-rw-r–r– 1 benedictjin wheel 93 1 15 14:47 Benedict.g4
-rw-r–r– 1 benedictjin wheel 321 1 15 14:47 Benedict.interp
-rw-r–r– 1 benedictjin wheel 38 1 15 14:47 Benedict.tokens
-rw-r–r– 1 benedictjin wheel 822 1 15 14:48 BenedictBaseListener.class
-rw-r–r– 1 benedictjin wheel 1.3K 1 15 14:47 BenedictBaseListener.java
-rw-r–r– 1 benedictjin wheel 3.8K 1 15 14:48 BenedictLexer.class
-rw-r–r– 1 benedictjin wheel 1.3K 1 15 14:47 BenedictLexer.interp
-rw-r–r– 1 benedictjin wheel 3.7K 1 15 14:47 BenedictLexer.java
-rw-r–r– 1 benedictjin wheel 38 1 15 14:47 BenedictLexer.tokens
-rw-r–r– 1 benedictjin wheel 329 1 15 14:48 BenedictListener.class
-rw-r–r– 1 benedictjin wheel 569 1 15 14:47 BenedictListener.java
-rw-r–r– 1 benedictjin wheel 896 1 15 14:48 ‘BenedictParser$SayContext.class’
-rw-r–r– 1 benedictjin wheel 4.4K 1 15 14:48 BenedictParser.class
-rw-r–r– 1 benedictjin wheel 3.9K 1 15 14:47 BenedictParser.java



$ grun Benedict r -tree
hello benedict
(r hello benedict)
可视化
$ grun Benedict r -gui
hello benedict
^D
踩到的坑
给每次 antlr 的结果文件添加 package
在 .g4 中使用 @header{ … } 添加



@header{
package com.yuzhouwan.antlr;
}
确定节点与子节点之间的关系
使用 if-else 在 exit/enter() 中判别,节点与子节点之间的关系,确定是否是 operator



say
: ‘say’ Colon Name #Colon
;



benedictListener 将会得到一个定位粒度为 Colon 的访问节点



/**



  • Enter a parse tree produced by the {@code Colon}

  • labeled alternative in {@link benedictParser#say}.

  • @param ctx the parse tree
    */
    void enterColon(benedictParser.ColonContext ctx);
    /**

  • Exit a parse tree produced by the {@code Colon}

  • labeled alternative in {@link benedictParser#say}.

  • @param ctx the parse tree
    */
    void exitColon(benedictParser.ColonContext ctx);
    https://www.jianshu.com/p/c9ca3e55bd2e
    antlr作为一门解析语言,被用在很多场景。查询引擎presto就是用的antlr做底层sql语法解析。今天写了一个小demo,作为过去一年认识了antlr的总结。
    ANTLR, 语言识别的另一个工具(ANother Tool for Language Recognition ),(前身是
    PCCTS)是一种语言工具,它提供了一个框架,可以通过包含 Java,C++,或 C#动作(action)
    的语法描述来构造语言识别器,编译器和解析器。



计算机语言的解析已经变成了一种非常普遍的工作。传统的计算机语言的编译器和工具
(如 C 或 Java)仍旧需要被构造,它们的数量与需要开发的那些成千上万的小语言的识别
工具和解析工具相比是相形见拙。程序员为了解析数据格式,图形文件(如,
PostScript,AutoCAD),文本文件(如,HTML,SGML 等)而需要构造解析器。ANTLR 被设计出来处理所有这些转换工作。



antlr 插件 antlr works download:【记录】尝试使用ANTLRWorks 1.5开发



antlr4.1源码 download:antlr/antlr4



网上能找到2.7的中文版文档,推荐看那个,写的很详细,其实3.几和我在用的4.几我也没有去了解他们的差别。如果英文好还是看英文吧,我是太懒了,中文看着方便。



下面贴上写的小demo



解决一个java的入门程序Helloworld的语法解析。
其中有很多不足之处,类似于符号不完全等问题,dont mind这些细节。。。



首先确定好我们要做的事,开始写语法文件:
grammar SomeLanguage; // 声明语法头
/*
*========================



  • 一些 options 配置
    =======================
    */
    options {
    language = Java; //设定生成代码的语言
    }
    /**
    =========================
    rule
    =========================
    */
    classDeclaration : ‘class’ className ‘{‘ (method)
    ‘}’;
    className : (ID|’ ‘)+;
    method : methodName ‘(‘ (parameter)* ‘)’(‘ ‘)’{‘ (instruction)+’}’ ;
    parameter:instruction|’ ‘|’[’|’]’;
    methodName : (ID|’ ‘)+ ;
    instruction : ID|’.’|’(‘|’)’|’;’;
    str:STR;
    ID : [a-zA-Z0-9|’_’]+ ;
    STR:’'’ (‘''’ | ~(‘'’))
    ‘'’;
    WS: [ \t\n\r]+ -> skip ;
    这不是一下子写出来的,是一步步调出来的,可以利用IDEA的插件,自己写一段demo放进去,然后调语法保存即可看到效果,直到整棵树解析没问题我们就可以开始下一步了。



这里我这么写有点丑陋,叶子节点太多了,横向特别长,主要是一行代码的字符太多了,要解析出来就只能每一个字符都作为instruction了。。



搞定语法文件后,可以动手搞树的遍历了,Antlr提供了两种遍历手段,一种是Listener模式,一种是Visitor模式。两者各有优劣,Listener模式适合全局查找,默认是深度优先遍历,而Visitor模式适合指定某个节点作遍历。这种遍历方式默认是先根,然后遍历左右子树。



ANTLR插件有自动生成代码的功能,很好的为我们新手学习ANTLR快速上手ANTLR做了铺垫,我们继承它提供的BaseListener和BaseVisitor即可。只需实现我们关注的节点即可,非常完美。



贴上代码
解析接口,将语法树解析成JAVABean用于序列化
public interface ILangParser {
SomeClass parse(ParseTree parseTree);
}
Listener解析类
public class LangeListenerParser implements ILangParser {
public SomeClass parse(ParseTree parseTree) {
ClassListener classListener = new ClassListener();
new ParseTreeWalker().walk(classListener, parseTree);
SomeClass someClass = classListener.getSomeClass();
return someClass;
}
class ClassListener extends SomeLanguageBaseListener {
SomeClass someClass;
@Override
public void enterClassDeclaration(SomeLanguageParser.ClassDeclarationContext ctx) {
SomeLanguageParser.ClassNameContext classNameContext = ctx.className();
final MethodListener methodListener = new MethodListener();
ctx.method().forEach(method -> {
method.enterRule(methodListener);
}
);
someClass = new SomeClass(classNameContext.getText(), methodListener.getMethod());
}
public SomeClass getSomeClass() {
return someClass;
}
}
class MethodListener extends SomeLanguageBaseListener {
Collection method;
public MethodListener() {
method = new ArrayList<>();
}
@Override
public void enterMethod(SomeLanguageParser.MethodContext ctx) {
String methodName = ctx.methodName().getText();
InstructionListener instructionListener = new InstructionListener();
ctx.instruction().forEach(instructionContext -> {
instructionContext.enterRule(instructionListener);
}
);
StringBuilder parameter = new StringBuilder("");
ctx.parameter().forEach(e -> parameter.append(e.getText()));
method.add(new Method(methodName, parameter.toString().split(","), instructionListener.getInstruction()));
}
public Collection getMethod() {
return method;
}
}
class InstructionListener extends SomeLanguageBaseListener {
Collection instruction;
StringBuilder instructionDesc = new StringBuilder("");
public InstructionListener() {
instruction = new ArrayList<>();
}
@Override
public void enterInstruction(SomeLanguageParser.InstructionContext ctx) {
String instructionName = ctx.getText();
instructionDesc.append(instructionName);
if (instructionName.equals(";")) {
instruction.add(new Instruction(instructionDesc.toString()));
instructionDesc.delete(0, instructionDesc.length());
}
}
public Collection getInstruction() {
return instruction;
}
}
}
Visitor解析类
public class SomeLangVisitorParser implements ILangParser {
@Override
public SomeClass parse(ParseTree parseTree) {
ClassVisitor classVistor = new ClassVisitor();
return classVistor.visit(parseTree);
}
class ClassVisitor extends SomeLanguageBaseVisitor {
@Override
public SomeClass visitClassDeclaration(SomeLanguageParser.ClassDeclarationContext ctx) {
MethodVisitor methodVisitor = new MethodVisitor();
Collection methods = ctx.method()
.stream()
.map(methodContext -> methodContext.accept(methodVisitor))
.collect(Collectors.toList());
return new SomeClass(ctx.className().getText(), methods);
}
}
class MethodVisitor extends SomeLanguageBaseVisitor {
@Override
public Method visitMethod(SomeLanguageParser.MethodContext ctx) {
InstructionVisitor instructionVisitor = new InstructionVisitor();
Collection instructions = ctx.instruction()
.stream()
.map(instructionContext -> instructionContext.accept(instructionVisitor))
.collect(Collectors.toList());
StringBuilder sb = new StringBuilder("");
ctx.parameter().forEach(e -> sb.append(e.getText()));
return new Method(ctx.methodName().getText(), sb.toString().split(","), instructions);
}
}
class InstructionVisitor extends SomeLanguageBaseVisitor {
@Override
public Instruction visitInstruction(SomeLanguageParser.InstructionContext ctx) {
return new Instruction(ctx.getText());
}
}
}
JavaBean
public class SomeClass {
private String className;
private Collection methods;
public SomeClass(String className, Collection methods) {
this.className = className;
this.methods = methods;
}
}
public class Method {
private String methodName;
private String[] parameters;
private Collection instructions;
public Method(String methodName, String[] parameters, Collection instructions) {
this.methodName = methodName;
this.parameters = parameters;
this.instructions = instructions;
}
}
public class Instruction {
private String instructionName;
public Instruction(String name) {
this.instructionName = name;
}
}
测试类
public class TestSomeLang {
public static void main(String[] args) {
// listener 方式遍历
TestSomeLang testSomeLang = new TestSomeLang();
String code = testSomeLang.readContent();
SomeLanguageParser parser = getParseTree(code);
SomeClass someClass = new LangeListenerParser().parse(parser.classDeclaration());
Gson gson = new Gson();
System.out.println(gson.toJson(someClass));
System.out.println("======================================\n visitor方式");
// visitor方式遍历
SomeLanguageParser parser2 = getParseTree(code);
SomeClass someClass1 = new SomeLangVisitorParser().parse(parser2.classDeclaration());
System.out.println(gson.toJson(someClass1));
}
public String readContent() {
File dir = null;
try {
dir = new File(getClass().getResource("/someLang").toURI());
for (File file : dir.listFiles()) {
if (!file.getName().equalsIgnoreCase("lang1")) {
continue;
}
String code = FileUtils.readFileToString(file, "utf-8");
return code;
}
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static SomeLanguageParser getParseTree(String code) {
ANTLRInputStream antlrInputStream = new ANTLRInputStream(code);
SomeLanguageLexer lexer = new SomeLanguageLexer(antlrInputStream);
CommonTokenStream commonStream = new CommonTokenStream(lexer);
SomeLanguageParser parseTree = new SomeLanguageParser(commonStream);
return parseTree;
}
}



https://zhuanlan.zhihu.com/p/25028169
https://www.cnblogs.com/clonen/p/9083359.html
https://blog.csdn.net/yangguosb/article/details/85332340
安装ANTLR
作者的电脑是MAC的操作系统macOS Catalina 10.15.2。



安装步骤后linux操作的系统的一样, Windows系统大致步骤一样,但是环境变量等配置有差别,作者很久没使用过win系统,只能基于MAC的系统介绍了。



环境准备
ANLTR是用JAVA编写的,需要先安装好JAVA,需要的JAVA版本是1.6以上。相信看这篇文章的各位同学电脑上应该都有安装JAVA。😊



下载ANTLR
使用ANTLR的功能其实很简单, 下载一个ANTLR的jar包即可。
官方下载地址



配置
最后一步配置环境变量,方便后面在命令行操作ANTLR
打开环境变量文件,作者安装的是zsh,所以编辑~/.zshrc文件,各位同学应该也清楚环境变量的配置,在文件加入几行:



export CLASSPATH=”.:/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH” #设置antlr的jar包到环境变量
alias antlr4=’java -Xmx500M -cp “/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH” org.antlr.v4.Tool’ #快速运行ANTLR的解释器
alias grun=’java -Xmx500M -cp “/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH” org.antlr.v4.gui.TestRig’ #快速运行ANTLR测试工具



完成上面的准备工作就可以开始体验ANTLR的强大功能了



第一个语法解释器
下面用一个简单的例子展示ANTLR的使用,编译一个最简单的赋值语句的语法。



编写g4文件
新建一个Hello.g4的文件,这个文件是针对语法的描述,相当于告诉ANTLR我的语法规范



Hello.g4内容如下:



grammar Hello; //定义一个名为 Hello 的语法
statement: ID ‘=’ NUM; //匹配类似 a=1 age=100 这样的语句
ID: [a-z]+; // 定义了一个词法 ID,由小写字母组成
NUM:[0-9]+; // 定义了一个词法 NUM,由数字组成
WS: [ \t\r\n]+ -> skip; //在进行解析的过程中,忽略掉空格,换行
这样其实就定义好了一个简单语法。
对于不同的程序语言来说,语法结构越是复杂,想对应的g4文件也会越复杂
这个项目里面有针对目前流行的语言的ANTLR语法定义文件
各位同学可以找找接触过哪些,看看你用的语言复杂不复杂。反正JAVA文件是很复杂💔



生成解释器
生成ANTLR的解释器很简单,一条命令搞定。



antlr4 Hello.g4
运行完这条命令会生成如下几个文件:



Hello.interp
Hello.tokens
HelloBaseListener.java
HelloLexer.interp
HelloLexer.java
HelloLexer.tokens
HelloListener.java
HelloParser.java
HelloParser.java



该文件包含一个语法解释器类的定义,负责识别我们定义的语法



public class HelloParser extends Parser { … }
HelloLexer.java



该文件包含一个词法解释器类的定义,负责自动识别我们定义的语法中的文法规则和词法规则。



public class HelloLexer extends Lexer { … }
HelloListener.java和HelloBaseListener.java



这两个类都是事件监听类,是留给开发者自己来定义相应的事件。因为ANTLR在进行遍历解析时,遍历器会触发一系列的事件。 比如进入某某标签,读到一个数字等。ANTLR开放了这些接口,开发者通过实现这些事件可以做到除了解释语法以外更复杂的功能。这里就不详细解释,后面会再介绍。先挖个坑😊



public interface HelloListener extends ParseTreeListener {
/**
* Enter a parse tree produced by {@link HelloParser#statement}.
* @param ctx the parse tree
*/
void enterStatement(HelloParser.StatementContext ctx);
/**
* Exit a parse tree produced by {@link HelloParser#statement}.
* @param ctx the parse tree
*/
void exitStatement(HelloParser.StatementContext ctx);
}
其他非java文件



ANTLR会给每个定义的词法符号指定一个数字形式的类型,然后将对应关系存储在这些文件中。当不同的语法有相同的词时这个文件就有大作用了。



测试解释器
最后到了验收结果的时候,测试一下ANTLR的语法解释器做了什么。下面会展示两种测试方式。



JAVA代码
将前面生成的4个JAVA文件添加到一个测试工程内,该工程需要引入ANTLR依赖。



org.antlr
antlr4
4.7.1

编写相关测试代码
public static void main(String[] args) {
HelloLexer lexer = new HelloLexer(CharStreams.fromString(“a = 1”));
CommonTokenStream tokenStream = new CommonTokenStream(lexer);
HelloParser parser = new HelloParser(tokenStream);
System.out.println(parser.statement().toStringTree(parser));
}
运行后会有如下输入:
(statement a = 1)
这个代码ANTLR成功识别到了我们的赋值语句。
命令行
ANTLR也提供了自动的测试工具,可以直接在命令行测试。详细用法如下



编译java代码,就跟一般的java代码一样我们需要同javac把java文件编译成class文件。
javac *.java
使用ANTLR测试工具,输入如下命令
grun Hello statement -gui
Hello 对应我们定义的语法 grammar Hello
statement 对应我们定义的词法 statement: ID ‘=’ NUM;
-gui 表示输出图形界面



进入测试工具后,输入a = 1。 MAC电脑结束输入符号 control+D。
$ grun Hello statement -gui

a = 1
^D
ANTLR会输出图形界面



可以看到ANTLR最终解释出来的语法树。



总结
这篇文章带大家完成了第一个ANTLR的语法文件,并展示了测试过程。中间有一些细节没有在这里仔细讲解,在后面的笔记会陆续更新。最后附上一个稍微复杂一点的语法树的图。
https://www.cnblogs.com/zanezhou/p/12624712.html



Antlr 是一个基于 Java 开发的功能强大的语言识别工具,Antlr 以其简介的语法和高速的运行效率在这类工具中出类拔萃。当你需要开发一种领域语言时,语言可能像 Excel 中的公式一样复杂,也可能像本文中的例子一样简单(只有算术运算),这时你可以考虑使用 Antlr 来处理你的语言。



Antlr 简介
ANTLR 语言识别的一个工具 (ANother Tool for Language Recognition ) 是一种语言工具,它提供了一个框架,可以通过包含 Java, C++, 或 C# 动作(action)的语法描述来构造语言识别器,编译器和解释器。 计算机语言的解析已经变成了一种非常普遍的工作,在这方面的理论和工具经过近 40 年的发展已经相当成熟,使用 Antlr 等识别工具来识别,解析,构造编译器比手工编程更加容易,同时开发的程序也更易于维护。
语言识别的工具有很多种,比如大名鼎鼎的 Lex 和 YACC,Linux 中有他们的开源版本,分别是 Flex 和 Bison。在 Java 社区里,除了 Antlr 外,语言识别工具还有 JavaCC 和 SableCC 等。
和大多数语言识别工具一样,Antlr 使用上下文无关文法描述语言。最新的 Antlr 是一个基于 LL(*) 的语言识别器。在 Antlr 中通过解析用户自定义的上下文无关文法,自动生成词法分析器 (Lexer)、语法分析器 (Parser) 和树分析器 (Tree Parser)。
Antlr 能做什么
编程语言处理
识别和处理编程语言是 Antlr 的首要任务,编程语言的处理是一项繁重复杂的任务,为了简化处理,一般的编译技术都将语言处理工作分为前端和后端两个部分。其中前端包括词法分析、语法分析、语义分析、中间代码生成等若干步骤,后端包括目标代码生成和代码优化等步骤。



Antlr 致力于解决编译前端的所有工作。使用 Anltr 的语法可以定义目标语言的词法记号和语法规则,Antlr 自动生成目标语言的词法分析器和语法分析器;此外,如果在语法规则中指定抽象语法树的规则,在生成语法分析器的同时,Antlr 还能够生成抽象语法树;最终使用树分析器遍历抽象语法树,完成语义分析和中间代码生成。整个工作在 Anltr 强大的支持下,将变得非常轻松和愉快。
文本处理



文本处理
当需要文本处理时,首先想到的是正则表达式,使用 Anltr 的词法分析器生成器,可以很容易的完成正则表达式能够完成的所有工作;除此之外使用 Anltr 还可以完成一些正则表达式难以完成的工作,比如识别左括号和右括号的成对匹配等。



在IDEA中安装使用Antlr
在Settings-Plugins中安装ANTLR v4 grammar plugin
新建一个Maven项目,在pom.xml文件中添加ANTLR4插件和运行库的依赖。注意一定要用最新版的,依赖,不知道最新版本号的可以自己google一下maven antlr4。





org.antlr
antlr4-runtime
4.5.3


<build>
<plugins>
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
<version>4.3</version>
<executions>
<execution>
<id>antlr</id>
<goals>
<goal>antlr4</goal>
</goals>
<phase>none</phase>
</execution>
</executions>
<configuration>
<outputDirectory>src/test/java</outputDirectory>
<listener>true</listener>
<treatWarningsAsErrors>true</treatWarningsAsErrors>
</configuration>
</plugin>
</plugins>
</build> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 antlr4-maven-plugin用于生产Java代码,antlr4-runtime则是运行时所需的依赖库。把antlr4-maven-plugin的phase设置成none,这样在Maven 的lifecycle种就不会调用ANTLR4。如果你希望每次构建生成文法可以将这个配置去掉。


我们定义一个最简单的领域语言,从一个简单的完成算术运算的例子出发,详细说明 Antlr 的使用。首先我们需要在src\main\java中新建一个 Antlr 的文法文件, 一般以 .g4 为文件名后缀,命名为 Demo.g4 。
表达式定义
文法定义
在这个文法文件 Demo.g4 中根据 Antlr 的语法规则来定义算术表达式的文法,文件的头部是 grammar 关键字,定义文法的名字,必须与文法文件文件的名字相同:



grammar Demo;
1
为了简单起见,假设我们的自定义语言只能输入一个算术表达式。从而整个程序有一个语句构成,语句有表达式或者换行符构成。如清单 1 所示:



清单1.程序和语句



prog: stat
;
stat: expr
|NEWLINE
;
1
2
3
4
5
在 Anltr 中,算法的优先级需要通过文法规则的嵌套定义来体现,加减法的优先级低于乘除法,表达式 expr 的定义由乘除法表达式 multExpr 和加减法算符 (‘+’|’-‘) 构成;同理,括号的优先级高于乘除法,乘除法表达式 multExpr 通过原子操作数 atom 和乘除法算符 (‘*’|’/’) 构成。整个表达的定义如清单 2 所示:



清单2.表达式



expr : multExpr ((‘+’|’-‘) multExpr)*
;
multExpr : atom ((‘’|’/’) atom)
;
atom: ‘(‘ expr ‘)’
| INT

| ID

;
1
2
3
4
5
6
7
8
最后需要考虑的词法的定义,在 Antlr 中语法定义和词法定义通过规则的第一个字符来区别, 规定语法定义符号的第一个字母小写,而词法定义符号的第一个字母大写。算术表达式中用到了 4 类记号 ( 在 Antlr 中被称为 Token),分别是标识符 ID,表示一个变量;常量 INT,表示一个常数;换行符 NEWLINE 和空格 WS,空格字符在语言处理时将被跳过,skip() 是词法分析器类的一个方法。如清单 3 所示:



清单 3. 记号定义



ID:(‘a’..’z’|’A’..’Z’)+;
INT:’0’..’9’+;
NEWLINE:’\r’?’\n’;
WS:(‘ ‘|’\t’|’\n’|’\r’)+{skip();};
1
2
3
4
Antlr 支持多种目标语言,可以把生成的分析器生成为 Java,C#,C,Python,JavaScript 等多种语言,默认目标语言为 Java,通过 options {language=?;} 来改变目标语言。我们的例子中目标语言为 Java。



整个Demo.g4文件内容如下:



grammar Demo;



//parser
prog:stat
;
stat:expr|NEWLINE
;



expr:multExpr((‘+’|’-‘)multExpr)*
;
multExpr:atom((‘’|’/’)atom)
;
atom:’(‘expr’)’
|INT
|ID
;



//lexer
ID:(‘a’..’z’|’A’..’Z’)+;
INT:’0’..’9’+;
NEWLINE:’\r’?’\n’;
WS:(‘ ‘|’\t’|’\n’|’\r’)+{skip();};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
运行ANTLR
右键Demo.g4,选择Configure ANTLR,配置output路径。



右键Demo.g4,选择Generate ANTLR Recognizer。可以看到生成结果结果。
其中Demo.tokens为文法中用到的各种符号做了数字化编号,我们可以不关注这个文件。DemoLexer是Antlr生成的词法分析器,DemoParser是Antlr 生成的语法分析器。



调用分析器。新建一个Main.java。
public static void run(String expr) throws Exception{



    //对每一个输入的字符串,构造一个 ANTLRStringStream 流 in
ANTLRInputStream in = new ANTLRInputStream(expr);

//用 in 构造词法分析器 lexer,词法分析的作用是产生记号
DemoLexer lexer = new DemoLexer(in);

//用词法分析器 lexer 构造一个记号流 tokens
CommonTokenStream tokens = new CommonTokenStream(lexer);

//再使用 tokens 构造语法分析器 parser,至此已经完成词法分析和语法分析的准备工作
DemoParser parser = new DemoParser(tokens);

//最终调用语法分析器的规则 prog,完成对表达式的验证
parser.prog();
} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 完整Main.java代码:


import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ANTLRInputStream;



public class Main {



public static void run(String expr) throws Exception{

//对每一个输入的字符串,构造一个 ANTLRStringStream 流 in
ANTLRInputStream in = new ANTLRInputStream(expr);

//用 in 构造词法分析器 lexer,词法分析的作用是产生记号
DemoLexer lexer = new DemoLexer(in);

//用词法分析器 lexer 构造一个记号流 tokens
CommonTokenStream tokens = new CommonTokenStream(lexer);

//再使用 tokens 构造语法分析器 parser,至此已经完成词法分析和语法分析的准备工作
DemoParser parser = new DemoParser(tokens);

//最终调用语法分析器的规则 prog,完成对表达式的验证
parser.prog();
}

public static void main(String[] args) throws Exception{

String[] testStr={
"2",
"a+b+3",
"(a-b)+3",
"a+(b*3"
};

for (String s:testStr){
System.out.println("Input expr:"+s);
run(s);
}
} } 运行Main.java 当输入合法的的表达式时,分析器没有任何输出,表示语言被分析器接受;当输入的表达式违反文法规则时,比如“a + (b * 3”,分析器输出 line 0:-1 mismatched input ‘’ expecting ‘)’;提示期待一个右括号却遇到了结束符号。


文法可视化
打开Antlr Preview。
在Demo.g4中选中一个语法定义符号,如expr。右键选中的符合,选择Text Rule expr。



在ANTLR Preview中选择input,输入表达式,如a+b*c+4/2。则能显示出可视化的文法。
https://blog.csdn.net/sherrywong1220/article/details/53697737?utm_source=blogxgwz4



https://blog.csdn.net/qq_36616602/article/details/85858133



https://plugins.jetbrains.com/plugin/7358-antlr-v4-grammar-plugin%20
http://www.antlr.org/tools.html
Download:
https://plugins.jetbrains.com/plugin/7358-antlr-v4-grammar-plugin
或者从这里http://www.antlr.org/tools.html进入下载。



前提
安装好



  • jdk

  • IntelliJ



配置好ANTLR的java运行时环境(库)。具体见这里



插件安装



Hello.g4示例
在IntelliJ中新建java项目(即一个Module),并创建grammar文件Hello.g4 。代码如下:



//Define a grammar called Hello
grammar Hello;
r : ‘hello’ ID; // match keyword hello followed by an identifier
ID : [a-z]+; // match lower-case identifiers
WS : [ \t\r\n]+ -> skip; // skip spaces, tabs, newlines



IntelliJ 中的Project相当于eclipse中的workspace;
而IntelliJ中的Module相当于eclipse中的project。
因此,我们创建项目时只需在当前Project中创建一个新的Module项目(File -> New -> Module)。



编辑好该.g4文件后即可测试该grammar规则。在rule “r”处选中“r”并右击鼠标后,点击“Test Rule r”,如下图。然后在左下方便可以输入“hello name”,在右下方框中便会显示对应的Parse tree。



Hello.g4的ANTLR代码生成配置
注意:IntelliJ中的ANTLR插件指定了antlr的运行时库的版本为最新版本(此处即为ANTLR4 4.7版本)



右击项目中刚刚创建的Hello.g4文件,点击“Configure ANTLR…”,然后便会弹出如下图所示窗口。



在该窗口中便可以对从grammar自动生成对应的ANTLR API的java代码进行配置。
其中,“Output directory where all output is generated”表示指定随后生成的java代码所存放的路径。
“generate parse tree vistor”表示生成ANTLR中用于遍历parse tree的visitor类相关API。(我这里勾选上了,万一要”实现”了呢^_^)



Hello.g4的ANTLR代码生成
如下图所示,在项目中选中Hello.g4文件并右击选择“Generate ANTLR Recognizer”。随后便会在app目录下看到生成的各种.java 文件等。(上一步配置中我们设置了代码生成的目标路径为app目录)



生成了这些API后,我们就可以通过调用API实现相应的功能了。
https://www.cnblogs.com/wynjauu/articles/9873231.html
https://www.pw88.com/teach/python/495.html
https://stackoverflow.com/questions/23568467/how-to-configure-antlr4-plugin-for-intellij-idea
https://www.jianshu.com/p/21f2afca65e8



Category lang