antlr4入门篇

2020-08-18 16:04:11 浏览数 (1)

环境准备

ANTLR实际上有两件事:一种将您的语法转换为Java(或其他目标语言)的解析器/词法分析器的工具,以及生成的解析器/词法分析器所需的运行时。即使您使用ANTLR Intellij插件或ANTLRWorks来运行ANTLR工具,生成的代码仍将需要运行时库。

您应该做的第一件事可能是下载并安装开发工具插件。即使仅使用此类工具进行编辑,它们也很棒。然后,按照以下说明操作,以使系统可以使用运行时环境来运行生成的解析器/词法分析器。在接下来的内容中,我将讨论antlr-4.7.1-complete.jar,该文件具有工具,运行时以及其他任何支持库(例如,ANTLR v4是用v3编写的)。

如果要使用mvn,ant或将ANTLR集成到您的IDE(例如eclipse或intellij)中,将ANTLR集成到现有的构建系统中,请参阅将ANTLR集成到开发系统中。

在IntelliJ IDEA里面安装ANTLR v4 grammar plugin,离线下载地址:https://github.com/antlr/intellij-plugin-v4/blob/master/README.md。

示例

定义hello.g4文件:

代码语言:javascript复制
grammar Hello;
r  : 'hello' ID ;         // match keyword hello followed by an identifier
ID : [a-z]  ;             // match lower-case identifiers
WS : [ trn]  -> skip ; // skip spaces, tabs, newlines

紧接着在idea上的操作步骤为:

然后在工程的目录下会自动生成对应的java文件,当然更多的配置可以选择configure antlr来配置。文件目录为:

在Hello.g4文件的r上右键:

这时在idea下方会出现一个输入框,我们输入hello world后,右侧对应的parse tree如下图所示:

语法词典

ANTLR遵循C及其派生词的语法,并带有一些语法描述扩展。

注释

有单行,多行和Javadoc样式的注释。

身份标识

令牌名称始终以大写字母开头,而Java Character.isUpperCase方法定义的词法分析器规则也是如此。解析器规则名称始终以小写字母(失败的字母)开头Character.isUpperCase。初始字符后可以跟大写和小写字母,数字和下划线。以下是一些示例名称:

代码语言:javascript复制
ID, LPAREN, RIGHT_CURLY // token names/rules
expr, simpleDeclarator, d2, header_file // rule names

为了支持Unicode解析器和词法分析器规则名称,ANTLR使用以下规则:

代码语言:javascript复制
ID : a=NameStartChar NameChar*
     {  
     if ( Character.isUpperCase(getText().charAt(0)) ) setType(TOKEN_REF);
     else setType(RULE_REF);
     }  
   ;

规则NameChar标识有效的标识符字符:

代码语言:javascript复制
fragment
NameChar
   : NameStartChar
   | '0'..'9'
   | '_'
   | 'u00B7'
   | 'u0300'..'u036F'
   | 'u203F'..'u2040'
   ;
fragment
NameStartChar
   : 'A'..'Z' | 'a'..'z'
   | 'u00C0'..'u00D6'
   | 'u00D8'..'u00F6'
   | 'u00F8'..'u02FF'
   | 'u0370'..'u037D'
   | 'u037F'..'u1FFF'
   | 'u200C'..'u200D'
   | 'u2070'..'u218F'
   | 'u2C00'..'u2FEF'
   | 'u3001'..'uD7FF'
   | 'uF900'..'uFDCF'
   | 'uFDF0'..'uFFFD'
   ;

规则NameStartChar是可以启动的标识符(规则,标记或标签名称)的字符列表:这些或多或少对应于isJavaIdentifierPartisJavaIdentifierStartJava的字符类。-encoding如果语法文件不是UTF-8格式,请确保使用ANTLR工具上的选项,以便ANTLR正确读取字符。

字符处理

ANTLR不能像大多数语言一样区分字符和字符串文字。所有文字串的一个或多个字符的长度被包围在单引号如’;’’if’’>=’,和’’(是指含有单引号字符的一个字符的字符串)。文字绝不包含正则表达式。

文字可以包含以下形式的Unicode转义序列’uXXXX’:(对于Unicode代码点直至’U FFFF’)或’u{XXXXXX}’(对于所有Unicode代码点),其中’XXXX’是十六进制Unicode代码点值。

例如,’u00E8’是带有重音符号的法语字母:’è’’u{1F4A9}’是著名的表情符号:’?’

ANTLR还理解通常的特殊转义序列:(’n’换行符),’r’(回车),’t’(制表符),’b’(退格)和’f’(换页)。您可以直接在文字中使用Unicode代码点,也可以使用Unicode转义序列:

代码语言:javascript复制
grammar Foreign;
a : '外' ;

ANTLR生成的识别器假定包含所有Unicode字符的字符词汇表。运行时库假定的输入文件编码取决于目标语言。对于Java目标,运行时库假定文件位于UTF-8中。使用中的工厂方法CharStreams,您可以指定其他编码。

代码编写

用花括号括起来的任意文本。如果它在字符串或注释中,则不需要转义结束的卷曲字符:"}"/*}*/。如果花括号是平衡的,你也不必逃避} {...}。否则,请使用反斜杠转出多余的小卷:{}。操作文本应符合语言选项所指定的目标语言。嵌入式代码可以出现在:@header以及@members命名的动作,解析器和词法分析器规则,异常捕获规范,解析器规则的属性部分(返回值,参数和局部变量)以及某些规则元素选项(当前谓词)。ANTLR在动作内部所做的唯一解释与语法属性有关。

关键字

这是ANTLR语法中保留字的列表:

代码语言:javascript复制
import, fragment, lexer, parser, grammar, returns,
locals, throws, catch, finally, mode, options, tokens

另外,尽管它不是关键字,但不要将单词rule用作规则名称。此外,请勿将目标语言的任何关键字用作标记,标签或规则名称。例如,rule if将产生一个名为的函数if。那显然不会编译。

语法结构

语法本质上是一个语法声明,后面是规则列表,但具有以下一般形式:

代码语言:javascript复制
/** Optional javadoc style comment */
grammar Name; ①
options {...}
import ... ;

tokens {...}
channels {...} // lexer only
@actionName {...}

rule1 // parser and lexer rules, possibly intermingled
...
ruleN

包含语法的文件名X必须称为X.g4。您可以按任何顺序指定选项,导入,令牌规范和操作。选项,导入和令牌规范中最多可以有一个。所有这些元素都是可选的,但标题①和至少一个规则除外。规则采用基本形式:

代码语言:javascript复制
ruleName : alternative1 | ... | alternativeN ;

解析器规则名称必须以小写字母开头,而词法分析器规则必须以大写字母开头。

grammar标头上没有前缀定义的语法是可以同时包含词法和解析器规则的组合语法。要制作仅允许解析器规则的解析器语法,请使用以下标头。

代码语言:javascript复制
parser grammar Name;
...

而且,自然地,纯词法语法看起来像这样:

代码语言:javascript复制
lexer grammar Name;
...

只有词法分析器语法可以包含mode规范。

只有词法分析器语法可以包含自定义渠道规范

代码语言:javascript复制
channels {
  WHITESPACE_CHANNEL,
  COMMENTS_CHANNEL
}

这些通道然后可以像词法分析器规则中的枚举一样使用:

代码语言:javascript复制
WS : [ rtn]  -> channel(WHITESPACE_CHANNEL) ;

语法导入

语法imports使您可以将语法分解为逻辑和可重用的块,如我们在导入语法中[1]所看到的。ANTLR对待导入的语法非常类似于面向对象的编程语言对待超类。语法从导入的语法继承所有规则,标记规范和命名操作。“主语法”中的规则会覆盖导入语法中的规则以实现继承。

认为import它更像是一个聪明的include语句(其中不包括已定义的规则)。所有导入的结果是一个单一的组合语法;ANTLR代码生成器看到了完整的语法,并且不知道是否存在导入的语法。

要处理主语法,ANTLR工具会将所有导入的语法加载到从属语法对象中。然后,它将规则,标记类型和命名操作从导入的语法合并到主语法中。在下图中,右侧的语法说明了语法MyELang导入语法的效果ELang

MyELang继承规则statWSID,但是重写规则expr,并增加了INT。这是一个示例构建和测试运行,显示MyELang可以识别整数表达式,而原始表达式则ELang不能。第三个错误的输入语句触发一条错误消息,该错误消息还表明解析器正在寻找MyELang'expr not ELang'。

代码语言:javascript复制
$ antlr4 MyELang.g4
$ javac MyELang*.java
$ grun MyELang stat
=>     34;
=>     a;
=>     ;
=>     EOF
<=     line 3:0 extraneous input ';' expecting {INT, ID}

如果主语法或任何导入的语法中存在模式,则导入过程将导入这些模式并在不覆盖它们的情况下合并其规则。如果任何模式变为空,因为其所有规则都已被该模式之外的规则覆盖,则该模式将被丢弃。

如果有任何tokens说明,则主要语法将合并标记集。如果有任何channel规范,则主要语法将合并通道集。任何已命名的动作,例如@members都会被合并。通常,应避免在导入语法中的命名动作和规则内的动作,因为那样会限制它们的重用。ANTLR还忽略导入语法中的任何选项。

导入的语法也可以导入其他语法。ANTLR以深度优先的方式学习所有导入的语法。如果两个或多个导入的语法定义了规则r,则ANTLR会选择r它找到的第一个版本。在下面的图中,ANTLR检查以下面的顺序的语法NestedG1G3G2

Nested包含r来自的规则,G3因为它可以看到rin 之前的版本G2

并非每种语法都可以导入其他所有语法:

•词法分析器语法可以导入词法分析器,包括包含模式的词法分析器。•解析器可以导入解析器。•组合语法可以导入没有模式的解析器或词法分析器。

ANTLR在主词法语法中将导入的规则添加到规则列表的末尾。这意味着主语法中的词法分析器规则优先于导入的规则。例如,如果主语法定义了规则,IF : ’if’ ;而导入语法定义了规则ID : [a-z] ;(也可以识别if),则导入ID将不会隐藏主语法的IF标记定义。

tokens部分

tokens节的目的是定义没有关联词汇规则的语法所需的标记类型。基本语法为:

代码语言:javascript复制
tokens { Token1, ..., TokenN }

大多数时候,令牌部分用于定义语法中的动作所需的令牌类型。

代码语言:javascript复制
// explicitly define keyword token types to avoid implicit definition warnings
tokens { BEGIN, END, IF, THEN, WHILE }

@lexer::members { // keywords map used in lexer to assign token types
Map<String,Integer> keywords = new HashMap<String,Integer>() {{
    put("begin", KeywordsParser.BEGIN);
    put("end", KeywordsParser.END);
    ...
}};
}

tokens部分实际上只是定义了一组标记,以添加到整个集合中。

代码语言:javascript复制
$ cat Tok.g4
grammar Tok;
tokens { A, B, C }
a : X ;
$ antlr4 Tok.g4
warning(125): Tok.g4:3:4: implicit definition of token X in parser
$ cat Tok.tokens
A=1
B=2
C=3
X=4

语法级别的动作

当前,在语法规则之外仅使用了两个已定义的命名操作(用于Java目标):headermembers。前者将代码注入到识别器类定义之前的生成的识别器类文件中,后者将代码作为字段和方法注入到识别器类定义中。

对于组合语法,ANTLR将动作同时注入解析器和词法分析器。要将操作限制为生成的解析器或词法分析器,请使用@parser::name@lexer::name

这是语法为生成的代码指定包的示例:

代码语言:javascript复制
grammar Count;

@header {
package foo;
}

@members {
int count = 0;
}

list
@after {System.out.println(count " ints");}
: INT {count  ;} (',' INT {count  ;} )*
;

INT : [0-9]  ;
WS : [ rtn]  -> skip ;

然后,语法本身应位于目录中,foo以便ANTLR在同一foo目录中生成代码(至少在不使用-oANTLR工具选项时):

代码语言:javascript复制
$ cd foo
$ antlr4 Count.g4 # generates code in the current directory (foo)
$ ls
Count.g4        CountLexer.java    CountParser.java
Count.tokens    CountLexer.tokens
CountBaseListener.java CountListener.java
$ javac *.java
$ cd ..
$ grun foo.Count list
=>     9, 10, 11
=>     EOF
<=     3 ints

Java编译器期望package中的类foo位于directory中foo

# 附录

•https://github.com/antlr/antlr4/blob/master/doc/index.md•https://pragprog.com/titles/tpantlr2/the-definitive-antlr-4-reference/

本文关于antlr4的语法部分整理自antlr4的官网,文档地址:https://github.com/antlr/antlr4/blob/master/doc/index.md

References

[1] 导入语法中: http://pragprog.com/book/tpantlr2/the-definitive-antlr-4-reference

0 人点赞