写点什么

ANTLR 入门(二)

用户头像
zane
关注
发布于: 2020 年 04 月 30 日
ANTLR 入门(二)

ANTLR基本语法

前面已经简单介绍了ANTLR以及怎么安装和测试。

同学们应该大概清楚ANTLR的使用场景,但是对于关键步骤,怎么编写一个语法文件并没有详细介绍,这篇笔记主要详细讲解一下ANTLR的语法。

在过去的几十年内人类发明了很多种编程语言,现在还在持续增加。而ANTLR的语法就是要把任意的编程语言的语法规则通过自身的语法描述文件来定义。好消息是,这么多的编程语言,相对而言,基本的语言模式并不多。之所以这样,其实原因也很简单。因为我们在设计编程语言时,倾向于将语言设计的和脑海中的自然语言相类似。我们期望看到有序的词法符号,也期望同词法符号的依赖关系。比如不会有任何语言出现{(})这种语法,大家会用数学符号,标识符,字符串。

总结下来,所有编程语言的语言模式可以抽象成四类:

  • 序列:一列元素,类似数组里面的值

  • 选择:在多种可选方案中做选择,eg:if else

  • 词法符号依赖:符号是成对出现,eg:左右括号

  • 嵌套结构: 自相似的语言结构,eg:编程语言中的一个语法嵌套另一个语法。

ANTLR实际就是基于以上的原则进行语法的设计,达到定义一种语法的作用。下面我们一个个详细说明一下这四种语言模式

序列

序列模式是最常见的一种模式。简单来说把一连串的词法按顺序排列就是一种序列,所有的指令也是一个序列。

然后ANTLR结合正则表达式,就可以快速的描述出多个元素的序列模式,

INT+表示一个或多个整数,INT\*表示零或多个整数,INT?表示零个或一个整数。

CSV这个文件为例,CSV的语法用ANTLR描述出来就是

file : (row '\n')*; //一个‘\n’作为终止符的序列,表示文件由多行组成
row : field (',' field)*; //一个‘,’作为分隔符的序列,文件每行由多个字段组成, 这里其实也用到了嵌套模式
field : STRING; // 假设字段都是字符串



选择

如果一个编程语言只有一种语句,就太无聊了,也做不了什么。选择模式就是表示一个地方可能支持多种有效的语句。

在ANTLR中使用|来表达选择模式,选择模式在语法中随处可见。

上面的例子,CSV的字段肯定不一定都是字符串,那么我们应该改成

field : STRING | INT;

注意的是在ANTLR中是按顺序来匹配解析规则,所以多个|分割的规则顺序是有意义的。

词法符号依赖

词法符号依赖最常见的用法还是定义括号的限制,规定括号必须成对出现。

还是那上面的字段举例子,比如我们要限制CSV的字段必须用“包括。那可以改成

field : ‘“’ STRING | INT ‘”’;



嵌套结构

嵌套词组是一种自相似的语言结构,即它的子词组也遵循相同的规则。表达式是一种典型的自相似语言结构,它包含多个嵌套的,以运算符分割的子表达式。类似于我们程序中的递归。

我们看一下简单wihle循环怎么定义

stat : 'while' '(' expr ')' stat //匹配wihle语句,必须开头是while
| '{' stat* '}' //匹配while里面的多条语句,这里的关键是stat里面的子语句也是stat词法,就形成了一个嵌套。


其实上面这种直接递归是比较难理解,一般的写法会写成间接递归

stat : 'while' '(' expr ')' stat
| '{' block* '}'
block : '{' stat* '}'



常用语法

前面已经介绍了常用的四种语法模式,下面列一下ANTLR常用的语法标记,后面可以当作写语法文件的字典。





JSON的语法

下面通过分析一下大家常用的json的语法来加强理解。

json
: value
;
obj
: '{' pair (',' pair)* '}'
| '{' '}'
;
pair
: STRING ':' value
;
arr
: '[' value (',' value)* ']'
| '[' ']'
;
value
: STRING
| NUMBER
| obj
| arr
| 'true'
| 'false'
| 'null'
;

上面是json格式最核心的语法定义,回顾一下,这里其实用到了前面说的全部四种模式,下面一一讲解。

json
: value
;

这里其实是定义了一个json的基础,json基础规则就由一个value规则组成。



value
: STRING
| NUMBER
| obj
| arr
| 'true'
| 'false'
| 'null'
;

然后这里定义了value的规则, 可以看到这里用到了选择模式。 json的value可以是字符串,数字,true,false,null, 这四个其实是传统定义json格式“值”部分能够使用的基本类型。然后除了基本类型,value还可以是obj和arr。



arr
: '[' value (',' value)* ']'
| '[' ']'
;

arr这个规则其实就是一个json数组,它由多个value通过,分割的数组,或者是一个空数组。 这里用到了间接嵌套模式。 统通过这个规则,json的某一个值可以是另一组json格式



obj
: '{' pair (',' pair)* '}'
| '{' '}'
;
pair
: STRING ':' value
;

obj是由一个json对象,它由多个pair通过,分割。或者可以是一个空的{}。然后其中的pair则是一个最基础的key-val的格式,这也是json最基础的语法。可以看出一点json定义中key必须是字符串。

这里其实就是json最核心的一些定义,大家可以回想一下json格式的规则是不是就是这样的。然后再加上一下针对不同格式的正则要求,就完成了json的ANTLR语法定义。下面附带了完整的文件,有兴趣可以自己结合之前的分享,读一下这文件。

[JSON的ANTLR语法文件](https://github.com/antlr/grammars-v4/blob/master/json/JSON.g4)



语言类程序

在之前的学习中,我们已经知道了如何使用ANTLR来定义一种语言,现在进行一些深入的研究。通常单独的语法并没实际作用,应该有一个语法分析器才能帮助我们实现一些具体功能,才能开发一个语言类的程序。

语法分析器除了能够解析语法外,应该还能在遇到特定的语句,词组,或者语法符号时触发一些特定的行为。这样的语法和特殊行为的集合就构成了语言类程序。

ANTLR中为了实现这种功能,引入了访问器和监听器。

  • 访问器可以通过显式触发的方式访问特定的节点。

  • 监听器能够对特定规则的进入和退出事件作出响应。

下面详细介绍一些访问器和监听器能做什么,还是以之前学习的简单赋值语法做例子

grammar Hello; //定义一个名为 Hello 的语法
statement: ID '=' NUM; //匹配类似 a=1 age=100 这样的语句
ID: [a-z]+; // 定义了一个词法 ID,由小写字母组成
NUM:[0-9]+; // 定义了一个词法 NUM,由数字组成
WS: [ \t\r\n]+ -> skip; //在进行解析的过程中,忽略掉空格,换行



ParseTreeVisitor(访问器)

根据语法生成的访问器接口

public interface HelloVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link HelloParser#statement}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitStatement(HelloParser.StatementContext ctx);
}

从这个接口可以看到

  1. ANTLR的访问器的顶级接口是ParseTreeVisitor

  2. 访问器中会生成每个规则的visit方法,Hello里面只有statement这个规则,所以只有一个方法

接下来我们可以实现一个访问器的实现类,进行相关逻辑处理



public class HelloBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements HelloVisitor<T> {
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitStatement(HelloParser.StatementContext ctx) {
System.out.println(ctx.ID().getText());
System.out.println(ctx.NUM().getText());
return visitChildren(ctx);
}
}
  1. visit方法的参数是一个\*\*Context的类,每个规则会有一个\*\*Context的类,这个类里面包含了规则里面的词组。

  2. 可以提前规则里面的词组进行处理,这里就有了实现逻辑代码的空间。

ParseTreeListener(监听器)

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);
}
  1. ANTLR的监听器的顶级接口是ParseTreeListener

  2. 访问器中会每个规则的一组方法。enterStatement和exitStatement,分别在分析树在进入规则和退出规则时触发。



public class HelloBaseListener implements HelloListener {
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
@Override public void enterStatement(HelloParser.StatementContext ctx) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
@Override public void exitStatement(HelloParser.StatementContext ctx) { }
}

和访问器类似,我们也可以通过实现接口来添加代码逻辑。

这里有一点需要注意,在enterStatement方法时, 因为时刚进入该规则触发,所以这时还获取不到Statement里面的ID和NUM。如果需要用到这两个,只能在exitStatement方法类处理。

使用方式

在写好了访问器和监听器后,如何在ANTLR解析中使用?

这两个的使用方法有一定区别。

public static void main(String[] args) {
HelloLexer lexer = new HelloLexer(CharStreams.fromString("a = 1"));
CommonTokenStream tokenStream = new CommonTokenStream(lexer);
HelloParser parser = new HelloParser(tokenStream);
parser.addParseListener(new HelloBaseListener());//监听器
StatementContext context = parser.statement();
context.accept(new HelloBaseVisitor<>());//访问器
}

监听器是在解析前通过addParseListener方法添加到Parser里面。

访问器是通过Context调用accept方法来指定访问规则。

思考

介绍了访问器和监听器使用后,大家可以思考一下分别使用这个两个来做一个简单的计算器。



发布于: 2020 年 04 月 30 日阅读数: 99
用户头像

zane

关注

还未添加个人签名 2019.12.09 加入

还未添加个人简介

评论

发布
暂无评论
ANTLR 入门(二)