第6章
尝试一些实际中的语法
在前一章,我们学习了通用词法结构和语法结构,并学习了如何用ANTLR的语法来表述这些结构。现在,是时候把我们学到的这些用来构建一些现实世界中的语法了。我们的主要目标是,怎样通过筛选引用手册,样例输入文件和现有的非ANTLR语法来构建一个完整语法。这一章,我们要实现五种语言,难度依次递增。现在,你不需要将它们全部都实现了,挑一个你最喜欢的实现,当你在实践过程中遇到问题了再回过头来看看就好了。当然,也可以看看上一章学习到的模式和ANTLR代码片段。
我们要实现的第一个语言就是逗点分割值格式(CSV),这种格式经常会在Excel这样的电子表格以及数据库中用到。从CSV开始是一个很好的选择,因为它不仅简单,而且应用非常广泛。第二种要实现的语言也是一种数据格式,叫做JSON,它包含嵌套数据元素,实现它可以让我们掌握实际语言中递归规则的用法。
下一个,我们要实现一个说明性语言,叫做DOT,用来描述图形(网络上的)。在这个说明性语言中,我们只感受下其中的逻辑结构而不指定控制流。DOT语言能够让我们实践更加复杂的词法结构,比如不区分大小写的关键字。
我们要实现的第四个语言是一种简单的非面向对象的编程语言,叫做Cymbol(在《语言实现模式》这本书的第6章也会讨论到这个语言)。这种语法可以作为一种典型的语法,我们可以将其作为参考,或者是实现其它编程语言的入手点(那些由函数,变量,语句和表达式组成的编程语言)。
最后,我们要实现一个函数式编程语言,R语言。(函数式编程语言通过计算表达式进行计算。)R语言是一种用在统计上的语言,现在多用于数据分析。我选择R语言作为例子,是因为其语法主要由巨型表达式规则组成。这对于我们加深对算符优先的理解,并结合到实际语言中,有着极大的好处。
当我们对建立语法比较熟练之后,我们就可以撇下语法识别,而去研究当程序看到自己感兴趣的输入时,应该怎样采取行动。在下一章,我们会创建分析监听器来创建数据结构,并通过符号表来追踪变量和函数定义,并实现语言的翻译。
那么,就让我们先从CSV文件开始吧。
6.1 解析逗点分割值
虽然,我们在第5章的序列模式中曾经介绍过一个简单的CSV语法,现在,让我们对其添加点规则:首行作为标题行,并且允许某一格的值为空。下面是一个具有代表性的输入文件的例子:
examples/data.csv
Details,Month,Amount
Mid Bonus,June,"$2,000"
,January,"""zippo"""
Total Bonuses,"","$5,000"
标题行和数据行基本上没什么差别,我们只是简单地将标题行里面的字段作为标题使用。但是,我们需要将其单独分离出来,而不是简单地使用row+这样的ANTLR片段去匹配。这是因为,当我们在这个语法上建立实际应用的时候,我们往往都需要区别对待标题行。这样,我们就可以很好地对第一行进行特殊处理了。下面是这个语法的一部分:
examples/CSV.csv
grammarCSV;
file : hdr row+ ;
hdr : row ;
注意到我们在上面引入了一个特殊的规则hdr来表示首行。但是这个规则在语法上就是一个row规则。我们通过将其分离出来使其作用更加清晰。你可以仔细对比下这种写法与直接在规则file右边写一个“row+”或者“row*”之间的差别。
row规则和前面介绍的一样:是一系列由逗号分隔开的字段,由换行符结束。
examples/CSV.csv
row : field (','field)*'\r'?'\n';
为了让我们的字段比前面介绍的更具有通用性,我们允许这个字段出现任意文本,字符串甚至什么都不出现(两个逗号之间什么也没有,也就是空字段)。
examples/CSV.csv
field
: TEXT
| STRING
| ;
符号的定义不算太坏。TEXT符号就是一个字符的序列,这个字符的序列在遇到下一个逗号或者换行符之前结束。STRING符号就是用双引号引起来的字符序列。下面是这两个符号的定义:
examples/CSV.csv
TEXT : ~[,\n\r"]+ ;
STRING : '"'('""'|~'"')* '"' ; // quote-quote is an escaped quote
如果要在双引号引起来的字符串中间出现双引号,CSV格式采用的是使用两个双引号表示,这就是STRING规则中“(‘””’|~’”’)”子规则所代表的意义。注意,我们在这里不能使用像“(‘””’|.)*?”这样的非贪婪循环的通配符,因为这种情况下,通配符的匹配会在遇到第一个“””的时候而结束。像”x””y”这样的输入,将会被匹配为两个字符串,而不会被匹配为一个字符串中出现一个“”””。记住,非贪婪子规则就算是匹配了内部规则的时候也会尽可能地匹配最少的字符。
在测试我们的语法规则之前,我们最好先看下解析得到的符号流,以确保我们的词法分析器能够正确地分割字符流。利用重命名为grun的TestRig工具,加上-tokens选项,我们能够得到下面的结果:
➾$ antlr4 CSV.g4
➾$ javac CSV*.java
➾$ grun CSV file -tokens data.csv
,1:0]
[@1,7:7=',',<1>,1:7]
[@2,8:12='Month',<4>,1:8]
[@3,13:13=',',<1>,1:13]
[@4,14:19='Amount',<4>,1:14]
[@5,20:20='\n',<2>,