ANTLR4权威指南 - 第6章 尝试一些实际中的语法

第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

grammar CSV;

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

<[@0,0:6='Details',<4>,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>,1:20]

[@6,21:29='Mid Bonus',< 4>,2:0]

[@7,30:30=',',<1>,2:9]

[@8,31:34='June',<4>,2:10]

[@9,35:35=',',<1>,2:14]

[@10,36:43='"$2,000"',<5>,2:15]

[@11,44:44='\n',<2>,2:23]

[@12,45:45=',',<1>,3:0]

[@13,46:52='January',<4>,3:1]

...

结果看起来不错,标点符号,文本,字符串都像预期的那样被正确分割开了。

接下来,让我们看看应该怎样去识别输入的语法结构。使用-tree选项,测试工具就会以文本的方式打印出语法分析树(书中对其做了删减)。

$ grun CSV file -tree data.csv

<(file

    (hdr (row (field Details) , (field Month) ,(field Amount) \n))

    (row (field Mid Bonus) , (field June) , (field"$2,000") \n)

    (row field , (field January) , (field"""zippo""") \n)

    (row (field Total Bonuses) , (field"") , (field "$5,000") \n)

)

树的根节点代表了file规则匹配的所有内容,包括一个开始的标题行规则以及许多行规则作为子节点。下面是这棵语法树的可视化显示(使用-ps file.ps选项):


CSV格式非常的简单直观,但是却无法实现在一个字段中包含很多值这种需求。为此,我们需要一种支持嵌套元素的数据格式。

6.2 解析JSON

JSON是一种文本的数据格式,它包含了键值对的集合,并且,值本身也可以是一个键值对的集合,所以JSON是一种嵌套的结构。在设计JSON的时候,我们可以学习到如何从语言的参考手册中推导语法,并可以尝试更多的复杂词法结构。把问题具体化,下面是一个简单的JSON数据文件:

examples/t.json

{

   "antlr.org": {

      "owners": [],

      "live": true,

      "speed": 1e100,

      "menus": ["File","Help\nMenu"]

   }

}

我们的目标是根据JSON的参考手册以及参考一些已有语法的图表来建立一个ANTLR语法。我们将提取出手册中的关键短语,并指出如何将其表述成ANTLR规则。那么从语法结构开始吧。

JSON语法规则

JSON的语法手册是这么写的:一个JSON文件可以是一个对象,也可以是一个值的数组。从语法上说,这显然是一个选项模式,于是我们便可以向下面这样来指定规则:

examples/JSON.g4

json: object

   | array

   ;

下一步就应该将json中的引用规则继续向下推导。对于object规则,参考手册中是这么写的:

一个对象(object)就是一个键值对的无序集合。它由左大括号“{”开始,以右大括号“}”结束。每一个键的后面都跟着一个冒号“:”,并且键值对是用逗号“,”分割开的。

JSON官网中的语法图中也指明,键一定是一个字符串。

将这段文字表述转换为语法结构,我们将这段表述拆开并寻找能够符合我们所了解的模式(序列,选项,符号约束和嵌套短语)的短语。最开始的那句话“一个对象就是…”显然指出了我们需要定义一个叫做object的规则。然后,“一个键值对的无序集合”其实指的就是键值对的序列。“无序集合”是指键的语义上的意义;具体来说,就是键的顺序并没有意义。这也意味着,在解析的过程中,我们只需要匹配任何出现的键值对列表就可以了。

第二句话说object是由大括号包含起来的,这显然是在说明一个符号约束模式。最后一句话定义了我们的键值对序列是一个由逗号分隔开的序列。总结起来,我们将这些用ANTLR来表示是这个样子的:

examples/JSON.g4

object

   : '{' pair (','pair)*'}'

   | '{' '}' // empty object

   ;

pair: STRING ':' value;

为了清晰并减少代码重复,最好将键值对也定义成一个规则,不然的话,object的第一个选项就会看起来像这个样子:

object : '{' STRING':'value (','STRING ':'value)*'}'| ... ;

注意,我们将STRING作为一个符号来处理,而不是一个语法规则。我们已经非常确定,我们的程序只处理完整的字符串,而不会进一步拆成字符来处理。关于这部分,详细可以参考第5.6节。

JSON参考手册也会有一些非正式的语法规则,我们来将这些规则和ANTLR的规则做个对比。下面是从参考手册中找到的语法定义:

object

   {}

   { members }

members

   pair

   pair , members

pair

   string : value

参考手册中也将pair规则给单独提取出来了,但是参考手册中定义了member规则,我们没有定义这个。在“循环对抗尾递归”一节中会具体描述如果没有“(…)*”循环的时候,语法是怎么解析序列的。

接下来再看另一个高层结构,数组。参考手册中是这样描述数组的:

数组(array)是一个有序的值的集合。数组由左中括号“[”开始,由右中括号“]”结束。不同的数值之间用逗号“,”分割开。

就像object规则一样,array规则也是一个逗号分割的序列,并且由中括号构成符号约束。

循环对抗尾递归

JSON参考手册中的members规则看起来非常奇怪,因为它看起来并没有直接像描述的那样“由一系列的逗号分割开的pair组成”,并且,它引用到了自己。

members

   pair

   pair , members

会有这样的差别,是因为ANTLR支持扩展的BNF语法(EBNF),而JSON中的规则遵循的是直接的BNF语法。BNF并不支持像“(…)*”这样的循环结构,所以,其使用尾递归(在规则的一个选项中的最后一个元素调用自己)来实现这种循环。

为了更好地说明文字描述的规则和这种尾递归形式之间的区别,下面是members规则匹配1个,2个,3个pair的例子:

members => pair

 

members => pair , members

      => pair , pair

 

members => pair , members

      => pair , pair , members

      => pair , pair , pair

这一现象体现了我们在5.2节给出的警告,现有的语法只能作为一个参考,而不能将其作为绝对真理来使用。

 

examples/JSON.g4

array

   : '[' value (','value)*']'

   | '[' ']' // empty array

   ;

再继续往下走,我们就得到了规则value,这个规则在参考手册中北描述为一种选项模式。

value可以是一个用双引号引起来的字符串,或者是一个数字,或者是true和false,或者是null,或者是一个object,或者是一个array。这些结构可以嵌套。

其中的术语嵌套自然就是指我们的嵌套短语模式,这也就意味着我们需要使用一些递归的规则引用。在ANTLR中,value规则看起来像图4所展示的那样。

通过引用object或array规则,value规则就变成了(非直接)递归规则。不管调用value中的object规则还是array规则,最终都会再次调用到value规则。

examples/JSON.g4

value

   :STRING

   |NUMBER

   |object // recursion

   |array // recursion

   | 'true' // keywords

   | 'false'

   | 'null'

   ;


图4 ANTLR中的value规则

value规则直接引用字符串来匹配JSON的关键字。我们同样将数字作为一个符号来处理,这是因为我们的程序同样只需要将数字作为整体来处理。

这就是所有的语法规则了。我们已经完全指定了一个JSON文件的结构了。下面是针对之前给出的例子,我们的语法分析树的样子:

当然,我们在完成词法之前是无法生成上面所示的这棵语法树的。我们需要为STRING和NUMBER这两个关键字指定规则。

JSON词法规则

在JSON的参考手册中,字符串是这么被定义的:

字符串(string)是由零个或多个Unicode字符组成的序列,被双引号括起来,可以使用反斜杠转义字符。单个字符可以被看成只有一个字符的字符串。JSON中的字符串和C语言或Java中的字符串非常相似。

看吧,就像我们在上一章讨论的那样,字符串在大部分语言中都是非常相似的。JSON中的字符串与我们之前讨论过的字符串非常相似,只是需要添加对Unicode转义字符的支持。看一下现有的JSON语法,我们能看出其描述是不完整的。语法描述如下所示:

char

   any-Unicode-character-except-"-or-\-or-control-character

   \"

   \\

   \/

   \b

   \f

   \n

   \r

   \t

   \u four-hex-digits

这个语法定义了所有的转义字符,也定义了我们需要匹配除了双引号和反斜杠之外的所有Unicode字符。这种匹配,我们可以使用“~[“\\]”来反转字符集。(“~”操作符代表“除了”。)我们的STRING规则定义如下所示:

examples/JSON.g4

STRING : '"' (ESC |~["\\])* '"' ;

ESC规则既可以匹配一个预定义的转义字符,也可以匹配一个Unicode序列。

examples/JSON.g4

fragment ESC :'\\' (["\\/bfnrt] | UNICODE) ;

fragment UNICODE : 'u' HEX HEX HEX HEX ;

fragment HEX : [0-9a-fA-F] ;

我们将UNICODE规则中的十六进制数单独提取出来,成为一个HEX规则。(规则的前面如果加上fragment前缀的话,这条规则就只能被其它规则引用,而不会单独被匹配成符号。)

最后一个需要的符号就是NUMBER。JSON手册中是这么定义数字的:

数字(number)非常类似于C语言或Java中的数字,但是JSON中不使用八进制或十六进制的数字。

JSON的语法中有相当复杂的数字的规则,但是我们可以把这些规则总结成三个主要的选项。

examples/JSON.g4

NUMBER

    : '-'? INT '.'INTEXP? // 1.35, 1.35E-9, 0.3, -4.5

    | '-'? INT EXP // 1e10 -3e4

    | '-'? INT // -3, 45

    ;

fragment INT :'0' | [1-9] [0-9]* ; // no leading zeros

fragment EXP :[Ee] [+\-]? INT ;// \- since - means "range"inside [...]

这里再说明一次,使用片段规则INT和EXP可以减少代码重复率,并且可以提高语法的可读性。

我们从JSON的非正式语法中可以得知,INT不会匹配0开始的整数。

int

    digit

    digit1-9 digits

    - digit

    - digit1-9 digits

我们在NUMBER中已经很好地处理了“-”符号操作符,所以我们只需要好好关注开头的两个选项:digit和digit1-9 digits。第一个选项匹配任何单个数码的数字,所以可以完美匹配0。第二个选项说明数字的开始只能是1到9,而不能是0。

译者注:依照本书中所写的JSON的NUMBER规则,则像1.03这样的输入不会被正确匹配,这一点有待于证实。

不同于上一节中的CSV的例子,JSON需要考虑空白字符。

空白字符(whitespace)可以出现在任何键值对的符号之间。

这是对空白字符的非常经典的定义,所以,我们可以直接利用前面“词法新人工具包”中的语法。

examples/JSON.g4

WS : [ \t\n\r]+ -> skip ;

现在,我们有JSON的完整的语法和词法规则了,接下来让我们测试下。以样例输入“[1,”\u0049”,1.3e9]”为例,测试其符号分析结果如下:

$ antlr4 JSON.g4

$ javac JSON*.java

$ grun JSON json -tokens

[1,"\u0049",1.3e9]

EOF

<  [@0,0:0='[',<5>,1:0]

    [@1,1:1='1',<11>,1:1]

    [@2,2:2=',',<4>,1:2]

    [@3,3:10='"\u0049"',<10>,1:3]

    [@4,11:11=',',<4>,1:11]

    [@5,12:16='1.3e9',<11>,1:12]

    [@6,17:17=']',<1>,1:17]

    [@7,19:18='<EOF>',<-1>,2:0]

可以看出,词法分析器正确地将输入流切分成符号流了,接下来,再试试看语法规则的测试结果。

$ grun JSON json -tree

[1,"\u0049",1.3e9]

EOF

<(json (array [ (value 1) , (value"\u0049") , (value 1.3e9) ]))

语法成功地被解释为含有三个值的数组了,如此看来,一切工作正常。要此时一个更加复杂的语法,我们需要测试更多的输入才能保证其正确性。

到目前为止,我们已经实践了两个数据语言的语法了(CSV和JSON),下面,让我们尝试下一个叫做DOT的声明式语言,这个实践增加了语法结构的复杂性,同时引进了一种新的词法模式:大小写不敏感的关键字。

6.3 解析DOT

DOT是一种用来描述图结构的声明式语言,用它可以描述网络拓扑图,树结构或者是状态机。(之所以说DOT是一种声明式语言,是因为这种语言只描述图是怎么连接的,而不是描述怎样建立图。)这是一个非常普遍而有用的图形工具,尤其是你的程序需要生成图像的时候。例如,ANTLR的-atn选项就是使用DOT来生成可视化的状态机的。

先举个例子感受下这个语言的用途,比如我们需要将一个有四个函数的程序的调用树进行可视化。当然,我们可以用手在纸上将它画出来,但是,我们可以像下面那样用DOT将它们之间的关系指定出来(不管是手画而是自动生成,都需要从程序源文件中计算出函数之间的调用关系):

examples/t.dot

digraph G{

    rankdir=LR;

    main [shape=box];

    main -> f -> g;           // main calls f which calls g

    f -> f [style=dotted] ; // f isrecursive

    f -> h;                 // f calls h

}

下图是使用DOT的可视化工具graphviz生成的图像结果:

幸运的是,DOT的参考手册中有我们需要的语法规则,我们几乎可以将它们全部直接引用过来,翻译成ANTLR的语法就行了。不幸的是,我们需要自己指定所有的词法规则。我们不得不通读整个文档以及一些例子,从而找到准确的规则。首先,让我们先从语法规则开始。

DOT的语法规则

下面列出了用ANTLR翻译的DOT参考手册中的核心语法:

examples/DOT.g4

graph : STRICT? (GRAPH | DIGRAPH) id? '{'stmt_list '}' ;

stmt_list : ( stmt ';'? )* ;

stmt : node_stmt

    |edge_stmt

    |attr_stmt

    | id '=' id

    |subgraph

    ;

attr_stmt : (GRAPH | NODE | EDGE) attr_list ;

attr_list : ('[' a_list?']')+ ;

a_list : (id ('=' id)?','?)+ ;

edge_stmt : (node_id | subgraph) edgeRHS attr_list? ;

edgeRHS : ( edgeop (node_id | subgraph) )+ ;

edgeop : '->' | '--';

node_stmt : node_id attr_list? ;

node_id : id port? ;

port : ':' id (':'id)? ;

subgraph : (SUBGRAPH id?)? '{' stmt_list '}' ;

id : ID

    |STRING

    |HTML_STRING

    |NUMBER

    ;

其中,唯一一个和参考手册中语法有点不同的就是port规则。参考手册中是这么定义这个规则的。

port: ':' ID [ ':' compass_pt ]

    | ':' compass_pt

compass_pt

    : (n | ne | e | se| s | sw | w | nw)

如果说指南针参数是关键字而不是合法的变量名,那么这些规则这么写是没问题的。但是,手册中的这句话改变了语法的意思。

注意,指南针参数的值并不是关键字,也就是说指南针参数的那些字符串也可以当作是普通的标识符在任何地方使用…

这意味着我们必须接受像“n ->sw”这样的边语句,而这句话中的n和sw都只是标识符,而不是指南针参数。手册后面还这么说道:“…相反的,编译器需要接受任何标识符。”这句话说的并不明确,但是这句话听起来像是编译器需要将指南针参数也接受为标识符。如果真是这样的话,那么我们也不用去考虑语法中的指南针参数;我们可以直接用id来替换规则中的compass_pt就可以了。

port: ':' id (':'id)? ;

为了验证我们的假设,我们不妨用一些DOT的查看器来尝试下这个假设,比如用Graphviz网站上的一些查看器。事实上,DOT也的确接受下面这样的图的定义,所以我们的port规则是没问题的:

digraph G { n -> sw; }

现在,我们的语法规则已经就位了,假设我们的词法定义也实现了,那么我们来看看t.dot这个样例输入的语法分析树长什么样子(使用grun DOT graph -gui t.dot)。

好,让我们接下来定义词法规则。

DOT词法规则

由于手册中没有提供正式的词法规则,我们只能自己从文本描述中提取出词法规则。关键字非常简单,所以就让我们从关键字开始吧。

手册中是这么描述的:“node,edge,graph,digraph,subgraph,strict关键都是大小写不敏感的。”如果它们是大小写敏感的话,我们只需要简单地将单词列出来就可以了,比如’node’这样。但是为了接受像’nOdE’这样多种多样的输入,我们需要将词法规则中的每个字母都附上大小写。

examples/DOT.g4

STRICT   : [Ss][Tt][Rr][Ii][Cc][Tt] ;

GRAPH    : [Gg][Rr][Aa][Pp][Hh] ;

DIGRAPH  :[Dd][Ii][Gg][Rr][Aa][Pp][Hh] ;

NODE     : [Nn][Oo][Dd][Ee] ;

EDGE     : [Ee][Dd][Gg][Ee] ;

SUBGRAPH :[Ss][Uu][Bb][Gg][Rr][Aa][Pp][Hh] ;

标识符的定义和大多数编程语言中的定义一致。

标识符由任何字母([a-zA-Z\200-\377]),下划线和数字组成,且不能以数字开头。

\200-\377是八进制范围,用十六进制范围表示就是80到FF,所以,我们的ID规则看起来就应该像这样:

examples/DOT.g4

ID : LETTER (LETTER|DIGIT)*;

fragment

LETTER : [a-zA-Z\u0080-\u00FF_] ;

辅助规则DIGIT同时也是我们在匹配数字的时候需要用到的一个规则。手册中说,数字遵循下面这个正则表达式:

[-]?(.[0-9]+ | [0-9]+(.[0-9]*)? )

把其中的[0-9]替换成DIGIT,那么DOT中的数字规则就如下所示:

examples/DOT.g4

NUMBER : '-'? ('.'DIGIT+ | DIGIT+ ('.' DIGIT*)? ) ;

fragment

DIGIT : [0-9] ;

DOT的字符串非常的寻常。

双引号引起来的任何字符序列(”…”),包括转义的引号(\”),就是字符串。

我们使用点通配符来匹配双引号内部的任意字符,直到遇到结束字符串的双引号为止。当然,我们也将转义的双引号作为子规则循环中的一个选项进行匹配。

examples/DOT.g4

STRING : '"' ('\\"'|.)*?'"' ;

DOT同时也支持HTML字符串。尽可能简单地说,HTML字符串就是双引号内部的字符串还用尖括号括起来的字符串。手册中使用“<…>”符号,并这样描述:

…在HTML字符串中,尖括号必须成对匹配,并且允许非转义的换行符。另外,HTML字符串的内容必须符合XML标准,所以一些特殊的XML转义序列(”,&,<,>)可能就会非常重要,因为我们可能会需要将其嵌入到属性值或原始文本中。

这段描述告诉了我们大部分我们需要的信息,但是却没有说明我们是否可以在HTML元素内部使用尖括号。这似乎意味着我们可以在尖括号中这样包含字符序列:“<<i>hi</i>>”。从用DOT查看器来做实验的结果来看,事实确实是这样的。DOT似乎允许尖括号之间出现任何字符串,只要括号匹配就行。所以,出现在HTML元素内部的尖括号并不会像其它XML解析器那样被忽略掉。HTML字符串“<foo<!--ksjdf > -->>”就会被看成是“foo<!--ksjdf> --”。

要实现HTML字符串,我们可以使用“'<'.*? '>'”这种结构。但是这种结构不能支持尖括号的嵌套,因为这种结构会将第一个“>”与第一个“<”进行结合,而不是与它最近的“<”结合。下面的规则实现了这种嵌套:

examples/DOT.g4

/** "HTML strings, angle brackets must occur in matchedpairs, and

* unescaped newlines are allowed."

*/

HTML_STRING : '<' (TAG|~[<>])*'>' ;

fragment

TAG : '<' .*? '>';

HTML_STRING规则允许出现带有一对尖括号的TAG规则,实现了标签的单层嵌套。“~[<>]”字符集要小心匹配XML字符实体,比如&lt;。这个集合匹配除了左右尖括号以外的所有字符。我们不能在这里使用非贪婪循环的通配符。“(TAG|.)*?”会匹配像“<<foo>”这样的无效输入,因为循环内部的通配符是可以匹配上“<foo”的。在非贪婪模式下,HTML_STRING就不会调用TAG去匹配一个标签或这标签的一部分。

你可能会试着去用递归来匹配尖括号嵌套,就像这样:

HTML_STRING : '<' (HTML_STRING|~[<>])*'>' ;

但是,这样仅仅会匹配嵌套标签,而不会去平衡标签的起始和结束的位置。嵌套标签可以匹配这样的输入:“<<i<br>>>”,但是这并不是我们应该接受的输入。

DOT还有最后一种我们之前没有见过的词法结构。DOT匹配并丢弃以“#”符号开头的行,因为DOT将其认为是C语言的预处理器输出。我们可以将其作为单行的注释规则来看待。

examples/DOT.g4

PREPROC : '#' .*? '\n'-> skip ;

以上就是DOT的语法(除了一些我们已经非常熟悉的规则以外)。这是我们实现的第一个比较复杂的语法!先不管那些更复杂的语法和词法结构,这一章主要强调了我们应该查阅多方面的资源来实现一个完整的语言。语言结构越庞大,我们需要的参考资源和代表性输入就需要越多。有时候翻出一些现有的实现程序才是我们测试边缘情况的唯一方法。没有任何的语言手册会是完美无缺的。

我们也经常要面临一些选择,在语法分析过程中怎样划分才是合理的,哪些又是需要作为分割短语而稍后进行处理的。举个例子,我们在处理特殊的port的名称的时候,比如ne和sw,就是将其作为简单的标识符传递给语法分析器。同时,我们也不去翻译“<…>”内部的HTML信息。从某些方面来说,一个完整的DOT实现应该识别并处理这些HTML元素,但是语法分析器只需要将其视为一整块就可以了。

下面,是时候尝试一些编程语言了。在下一节,我们要建立一个传统的命令式编程语言的语法(比较像C语言)。然后,我们就要开始我们最大的挑战,实现一个函数式编程语言,R语言。

6.4 解析Cymbol

接下来我们主要说明如何解析从C语言衍生过来的编程语言,我们要实现一个我设计的语言,叫做Cymbol。Cymbol是一个很简单的,不支持对象的编程语言,它看起来就像是没有structs的C语言。如果你懒得去从头到尾设计一门新的语言,你可以将这个语言的语法作为其它新的编程语言的原型。在这里面,我们不会看到新的ANTLR语法,但是我们的语法将实践怎样建立简单的左递归表达式规则。

当我们设计新语言的时候,我们就没有正式语法或语言手册来参考了。相反的,我们从建立语言的样例输入开始。从这里开始,我们就要像5.1节中说的那样来衍生一个语言的语法了。(当我们要处理一些没有官方语法规范或参考手册的现有语言的时候也可以这么做。)下面是一个Cymbol代码的例子,其中包括全局变量的声名以及递归函数的声明:

examples/t.cymbol

// Cymbol test

int g = 9; // a global variable

int fact(int x) { // factorial function

   if x==0 then return 1;

   return x * fact(x-1);

}

为了直观,我们先看看程序的最终效果,从而可以对程序的任务有一个更好的把握。下面的语法树展示了我们的程序应该怎样解析输入程序(通过grun Cymbol file -gui t.cymbol命令):

从最高的层次上来考虑Cymbol程序,我们可以发现其是由一系列的全局变量和函数声明组成的。

examples/Cymbol.g4

file: (functionDecl | varDecl)+ ;

变量的声明和C语言十分类似,是由一个类型说明符后面跟上一个标识符组成的,这个标识符后面还可以选择出现初始化表达式。

examples/Cymbol.g4

varDecl

   : typeID ('=' expr)?';'

   ;

type: 'float' | 'int'| 'void' ; // user-defined types

函数声明也基本是相同的:类型说明符,后面跟一个函数名,后面跟一个括号括起来的参数列表,后面跟一个函数体。

examples/Cymbol.g4

functionDecl

    : typeID '(' formalParameters?')'block // "void f(int x) {...}"

    ;

formalParameters

    :formalParameter (',' formalParameter)*

    ;

formalParameter

    : typeID

    ;

函数体其实就是由花括号括起来的语句块。这里,我们考虑以下6种不同的语句:嵌套语句,变量声明,if语句,return语句,赋值语句以及函数调用。这6种语句用ANTLR语法来写如下所示:

examples/Cymbol.g4

block: '{' stat* '}' ; // possibly empty statement block

stat: block

   |varDecl

   | 'if' expr 'then'stat ('else' stat)?

   | 'return' expr? ';'

   | expr '=' expr ';' // assignment

   | expr ';' // func call

   ;

最后一个主要的语法就是表达式语法了。因为Cymbol只是创建其它编程语言的原型,所以我们在表达式中只考虑一些常见的运算符。我们要处理的运算符有:一元取反,布尔非,乘法,加法,减法,函数调用,数组下标,等值比较,变量,整数以及括号表达式。

examples/Cymbol.g4

expr: ID '(' exprList?')'// func call like f(), f(x), f(1,2)

   | expr '[' expr ']' // array index like a[i], a[i][j]

   | '-' expr // unary minus

   | '!' expr // boolean not

   | expr '*' expr

   | expr ('+'|'-') expr

   | expr '==' expr // equality comparison (lowest priority op)

   | ID // variable reference

   | INT

   | '(' expr ')'

   ;

exprList : expr (',' expr)* ; // arg list

在这里我们需要注意的是,我们需要根据优先级从高到低列出每一条选项。(在第14章会详细讨论ANTLR是怎样处理左递归以及符号优先级的。)

更直观一些来看优先级的处理,我们可以看看输入“-x+y;”和“-a[i];”的语法分析树(为了直观,规则从stat开始分析)。


左边的语法树显示了一元减符号会先和x结合,因为其优先级比加号要高。这是因为,在语法中,一元减符号选项在加号选项之前出现。在右边的语法树中,一元减符号的优先级要比取数组下标符号低,这是因为一元减符号选项出现在取数组下标符号选项的后面。右边的语法树清楚地表明,负号是作用在“a[i]”上的,而不是作用在标识符“a”上面的。在下一节,我们会看到一些更复杂的表达式。

我们暂时不去关注这个语言的词法规则,因为词法规则都和前面的差不多。这里,我们关注的焦点应该是命令式编程语言中的语法结构。

我们可以想象一下无结构体或无类和对象的Java语言是什么样子的,这样有助于我们建立我们的Cymbol语言。并且,如果这个语法你已经理解得十分透彻的话,你可以尝试着去创建你自己的更复杂的命令式编程语言了。

接下来,我们要去尝试另一个极端的语言。要实现一个差不多的R语言,我们不得不推导出非常精确的语言结构。我们可以通过参考多个手册,测试样例程序以及在现有的一些R编译器上进行测试的方法来推导其语言结构。

6.5 解析R

R语言是在统计问题领域内非常有表现力的一个编程语言。例如,R语言非常容易创建向量,并支持函数对向量操作以及对向量进行过滤(下面展示了一些R的交互脚本)。

x <- seq(1,10,.5)     # x = 1, 1.5, 2, 2.5, 3, 3.5, ..., 10

y <- 1:5              #y = 1, 2, 3, 4, 5

z <- c(9,6,2,10,-4)      # z = 9, 6, 2, 10, -4

y + z                 #add two vectors

<[1] 10 8 5 14 1 # result is 1-dimensionalvector

z[z<5]                #all elements in z < 5

<[1] 2 -4

mean(z)               #compute the mean of vector z

<[1] 4.6

zero <- function() { return(0) }

zero()

<[1] 0

R语言是一个中小型但是却又十分复杂的编程语言,并且大部分人都有一个障碍存在:我们都不了解R语言。这意味着我们不可能像写Cymbol那样直接凭着直觉来编写R语言的语法结构了。我们不得不根据大量的文献资料、例子,以及一个现有实现中的正式的yacc语法来获取R语言的准确语法结构。

开始之前,最好先看一些R语言的语言概述。同时,我们最好也看一些R语言的例子,从而对R语言有一个更好的把握,并从中选择一些作为成功测试的例子。Ajay Shah已经建立了一些挺不错的例子,我们可以直接使用。能够成功解析这些例子就表明我们能够处理大部分的R程序了。(在不了解一门语言的前提下要完美实现一门语言怎么想都是不可能。)在R语言的官网主页上有许多能够帮助我们建立R语言的文档,这里我们着重关注于“R-intro”和语言定义文档“R-lang”。

和前面的一样,我们从一个非常粗糙的层次开始我们的语法结构。从语言概述中可以得知,R程序就是由一系列的表达式和赋值语句组成的。就算是函数定义也是赋值语句;我们将一个函数赋值给一个变量。唯一我们不熟悉的就是R语言中有三种赋值操作符:<-,=,以及<<-。我们的目的只是建立解析器,所以我们不需要关心这三种操作符的意义。所以我们程序的第一个主要结构就应该看起来这样:

prog : (expr_or_assign '\n')* EOF ;

 

expr_or_assign

   : expr ('<-' | '=' | '<<-' ) expr_or_assign

   | expr

   ;

在读了一些例子之后,我们发现,在同一行内我们可以同时书写多个表达式,只要用分号分开它们就可以了。“R-intro”中证实了这一点。同时,虽然手册中没有写,但是R编译器允许并忽略空行。所以,我们需要根据这些规定来调整我们的起始规则。

examples/R.g4

prog: ( expr_or_assign (';'|NL)

   | NL

   )*

   EOF

   ;

 

expr_or_assign

   : expr('<-'|'='|'<<-') expr_or_assign

   | expr

   ;

为了考虑到Windows下的换行符(\r\n),我们使用NL来作为换行符,而不是直接使用’\n’符号,这个我们在之前定义过。

examples/R.g4

// Match both UNIX and Windows newlines

NL : '\r'? '\n' ;

注意,NL并不像前面那样可以直接丢弃。因为语法解析器同时需要将其作为表达式的终结符,就像Java语法中的分号一样,所以,词法分析器必须将其传递给语法分析器。

R语言的主体部分就是表达式,所以我们接下来关注的重点也在表达式上。R语言中主要有三种不同的表达式:语句表达式,操作符表达式和函数关联表达式。由于R语言的语句和命令式编程语言非常接近,所以就让我们先从语句表达式开始入手。下面是处理expr规则的语句选项(expr规则中出现在运算符选项后面的):

examples/R.g4

| '{' exprlist'}' // compound statement

| 'if' '(' expr ')' expr

| 'if' '(' expr ')' expr 'else' expr

| 'for' '(' ID 'in' expr ')' expr

| 'while' '(' expr ')' expr

| 'repeat' expr

| '?' expr // get help on expr, usually string or ID

| 'next'

| 'break'

根据“R-intro”中描述,第一个选项匹配的是表达式块。“R-intro”中是这么描述的:“基础命令可以通过花括号组合成符合表达式。”下面是exprlist的定义:

examples/R.g4

exprlist

   :expr_or_assign ((';'|NL)expr_or_assign?)*

   |

   ;

大部分的R表达式需要处理非常多的运算符。为了获得这些表达式的准确定义,我们最好的方法就是参考其yacc语法。可执行的代码通常(但不是所有的都这样)是了解语言作者意图的最好方式。要知道运算符的优先级,我们首先需要看一些运算符优先级表,优先级表列出了所有相关的运算符的优先级。例如,下面是yacc语法中对算术运算符的描述(用“%left”列在前面的表示优先级比较低):

%left '+' '-'

%left '*' '/'

“R-lang”文档中有一节叫做“中缀和前缀操作符”,在这一节中给出了运算符的优先级规则,但是,这一节中似乎没有关于“:::”运算符的描述,而其却能在yacc语法中找到。将这些信息全部合起来,我们就能得到下面这些针对二元运算符,前缀运算符以及后缀运算符的规则了:

examples/R.g4

expr: expr '[[' sublist']' ']' // '[[' follows R'syacc grammar

   | expr'[' sublist ']'

   | expr('::'|':::') expr

   | expr('$'|'@') expr

   | expr'^'<assoc=right> expr

   | ('-'|'+') expr

   | expr':' expr

   | exprUSER_OP expr // anything wrappedin %: '%' .* '%'

   | expr('*'|'/') expr

   | expr('+'|'-') expr

   | expr ('>'|'>='|'<'|'<='|'=='|'!=') expr

   | '!' expr

   | expr('&'|'&&') expr

   | expr('|'|'||') expr

   | '~' expr

   | expr'~' expr

   | expr ('->'|'->>'|':=') expr

我们只是想识别输入的话,就暂时不用管这些操作符到底是什么意思。我们只需要关心我们的语法是否能正确匹配优先级和结合顺序。

在上面的expr的规则中,有一种用法我们不常用到,那就是在第一条选项中(expr ‘[[‘ sublist ‘]’ ‘]’)使用’[[‘来代替’[‘ ’[‘。([[…]]的作用是选择一个单个元素,其中的[…]用于产生一个子列表。)我直接从R语言的yacc语法中抄过来的’[[‘这种表述,这样写法大概是要表明两个左中括号之间不能有空白字符,但是这一点在参考手册中并没有任何说明。

“^”运算符跟了一个后缀“<assoc=right>”,这是因为“R-lang”中这样指定了这个运算符:

幂运算符“^”和左赋值运算符“<-= <<-”的结合顺序是从右到左的,剩下的其它运算符都是从左到右结合的。例如,2^2^3的结果应该是2^8,而不是4^3。

语句和运算符表达式都搞定之后,我们可以开始着手我们的最后一个expr规则的构成部分了:定义以及调用函数。我们可以使用下面的两个选项:

examples/R.g4

| 'function' '(' formlist?')' expr // define function

| expr '(' sublist')' // call function

formlist和sublist分别定义了声明过程中的形式参数列表和调用过程中的实际参数列表。我将规则的名字和yacc语法中的规则名字保持一致,这样可以方便我们对比这两种语法。

在“R-lang”中,形式参数列表是这样表述的:

…由逗号分隔开来,其中的每一项可以是一个标识符,也可以是“标识符 = 默认值”这种形式,或者是一个特殊的标记“…”。默认值可以是任何一个有效的表达式。

用ANTLR语法来表述这一点和yacc语法中的formlist比较相似(见图5)。

examples/R.g4

formlist : form (',' form)* ;

 

form: ID

   | ID '=' expr

   | '...'

   ;

图5 formlist的ANTLR表述

下面,要调用一个函数,“R-lang”描述的参数列表语法如图6所示。

每一个参数都可以进行标记(标记名=表达式),或者只是一个简单的表达式。同时,参数也可以为空,或者是一些特殊的符号,比如’…’,’..2’等。

图6 调用函数时的参数语法

偷看一下yacc语法中的这一部分,我们对参数的语法就有更明确的认识;yacc语法中表明了,我们也可以使用像“”n”=0”,“n=1”以及“NULL=2”这样的写法。结合这些规范,我们就得到了下面的函数调用参数的规则:

examples/R.g4

sublist : sub (',' sub)* ;

sub : expr

   | ID '='

   | ID '=' expr

   | STRING'='

   |STRING '=' expr

   | 'NULL' '='

   | 'NULL' '=' expr

   | '...'

   |

   ;

你可能会奇怪,在sub规则中怎么去匹配像“..2”这样的输入。其实,我们并不需要精确地去匹配这些,因为我们的词法分析器会将其识别成标识符。根据“R-lang”所描述的那样:

标识符是由字母,数字,小数点(“.”)和下划线组成。标识符不能以数字或下划线打头,也不能以一个小数点后面跟数字打头。…注意,以小数点打头的标识符(比如“…”以及“..1”,“..2”等)都具有特殊意义。

为了表述上面描述的标识符,我们使用下面的标识符规则:

examples/R.g4

ID : '.' (LETTER|'_'|'.')(LETTER|DIGIT|'_'|'.')*

   |LETTER (LETTER|DIGIT|'_'|'.')*

   ;

fragment LETTER: [a-zA-Z] ;

第一个选项指定了以小数点开头的标识符,我们不得不保证第二个字符不能是数字。对于这一点,我们可以使用子规则“(LETTER|’_’|’.’)”来实现。为了确保标识符不会以数字或者下划线开头,我们在第二个选项中使用了辅助规则LETTER。要匹配“..2”这样的输入,我们使用第一个选项就可以了。其中第一个小数点匹配第一个子规则“’.’”,第二个小数点匹配第二个子规则“(LETTER|’_’|’.’)”,而最后一个子规则匹配了数字“2”。

词法规则的剩余部分和我们之前写过的那些规则大同小异,所以我们在这里就不再讨论它们了。

下面,让我们使用grun来测试下目前为止我们的所有工作吧,测试输入如下:

examples/t.R

addMe <- function(x,y) { return(x+y) }

addMe(x=1,2)

r <- 1:5

下面是针对输入t.R如何建立可视化语法树的过程(语法树见图7):

$ antlr4 R.g4

$ javac R*.java

$ grun R prog -gui t.R

只要我们将表达式写在一行里面,我们的R语法就能工作得很好。然而,这样的假设不合适,因为R语言允许函数或其他表达式拆成多行编写。尽管如此,我们将先止步于此,因为我们的目的仅仅是了解R语言的语法结构。在code/extras资源目录中,你可以找到忽略表达式中间的换行符这个小问题的解决方案(参见R.g4,RFilter.g4以及TestR.java)。这个解决方案会根据语法适当地选择保留或剔除换行符。

这一章中,我们的目的是巩固我们的ANTLR语法的知识,并学习如何从语言参考手册、样例输入和现有非ANTLR语法中派生语法。最后,我们实现了两个数据语言(CSV,JSON),一个声明式语言(DOT),一个命令式语言(Cymbol)和一个函数式语言(R)。对于建立一个中等复杂的语言,这些例子几乎覆盖了所有你需要的技能。在你开始继续学习之前,我建议最好先下载这些语法,并有针对性地做些小小的修改,从而巩固新学到的知识。例如,你可以给Cymbol语言添加更多的运算符和语句。然后,可以使用TestRig工具来查看你修改后的语法是怎样工作在样例输入上的。

图7 t.R的语法分析树

到目前为止,我们已经学习了怎样识别语言,但是,语法本身必须还能识别只符合语言本身的输入。下面,我们将学习如何在解析机制中加入特定应用程序的代码,这将在下一章具体介绍。完成这个之后,我们就可以试着建立真正的语言应用程序了。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ANTLR是一款强大的语法分析器生成工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。它被广泛应用于学术领域和工业生产实践,是众多语言、工具和框架的基石。Twitter搜索使用ANTLR进行语法分析,每天处理超过20亿次查询;Hadoop生态系统的Hive、Pig、数据仓库和分析系统所使用的语言都用到了ANTLR;Lex Machina将ANTLR用于分析法律文本;Oracle公司在SQL开发者IDE和迁移工具使用了ANTLR;NetBeans公司的IDE使用ANTLR来解析C++;Hibernate对象-关系映射框架(ORM)使用ANTLR来处理HQL语言。   除了这些鼎鼎大名的项目之外,还可以利用ANTLR构建各种各样的实用工具,如配置文件读取器、遗留代码转换器、维基文本渲染器,以及JSON解析器。我编写了一些工具,用于创建数据库的对象-关系映射、描述三维可视化以及在Java源代码插入性能监控代码。我甚至为一次演讲编写了一个简单的DNA模式匹配程序。   一门语言的正式描述称为语法(grammar),ANTLR能够为该语言生成一个语法分析器,并自动建立语法分析树——一种描述语法与输入文本匹配关系的数据结构。ANTLR也能够自动生成树的遍历器,这样你就可以访问树的节点,执行自定义的业务逻辑代码。   本书既是ANTLR 4的参考手册,也是解决语言识别问题的指南。你会学到如下知识:   识别语言样例和参考手册语法模式,从而编写自定义的语法。   循序渐进地为从简单的JSON到复杂的R语言编写语法。同时还能学会解决XML和Python棘手的识别问题。   基于语法,通过遍历自动生成的语法分析树,实现自己的语言类应用程序。   在特定的应用领域,自定义识别过程的错误处理机制和错误报告机制。   通过在语法嵌入Java动作(action),对语法分析过程进行完全的掌控。   本书并非教科书,所有的讨论都是基于实例的,旨在令你巩固所学的知识,并提供语言类应用程序的基本范例。   本书的读者对象本书尤其适用于对数据读取器、语言解释器和翻译器感兴趣的开发者。虽然本书主要利用ANTLR来完成这些工作,你仍然可以学到很多有关词法分析器和语法分析器的知识。初学者和专家都需要本书来高效地使用ANTLR 4。如果希望学习第三部分的高级特性,你需要先了解之前的ANTLR基础知识。此外,读者还需要具备一定的Java功底。   Honey Badger版本ANTLR 4的版本代号是“Honey Badger”,这个名字来源于一段著名的YouTube短片The Crazy Nastyass Honey Badger(网址为:http://www.youtube.com/watch?v=4r7wHMg5Yjg)的勇敢无畏的主角——一只蜜獾。它敢吃你给它的任何东西,根本不在乎那是什么!   ANTLR 4有哪些神奇之处ANTLR 4引入了一些新功能,降低了入门门槛,使得语法和语言类应用程序的开发更加容易。最重要的新特性在于,ANTLR 4几乎能够处理任何语法(除了间接左递归,稍后会提到)。在ANTLR将你的语法转换成可执行的、人类可读的语法分析代码的过程语法冲突或者歧义性警告不会再出现。   无论多复杂的语法,只要你提供给ANTLR自动生成的语法分析器的输入是合法的,该语法分析器就能够自动识别之。当然,你需要自行保证该语法能够准确地描述目标语言。   ANTLR语法分析器使用了一种名为自适应LL(*)或者ALL(*)(读作“all star”)的新技术,它是由我和Sam Harwell一起开发的。ALL(*)是ANTLR 3的LL(*)的扩展,在实际生成的语法分析器执行前,它能够在运行时以动态方式对语法执行分析,而非先前的静态方式。由于ALL(*)语法分析器能够访问实际的输入文本,通过反复分析语法的方式,它最终能够决定如何识别输入文本。相比之下,静态分析必须考虑所有可行的(无限长的)输入序列。   在实践,拥有ALL(*)意味着你无须像在其他语法分析器生成工具(包括ANTLR 3)那样,扭曲语法以适应底层的语法分析策略。如果你曾经为ANTLR 3的歧义性警告和yacc的归约/归约冲突(reduce/reduce conflict)而抓狂,ANTLR 4就是你的不二之选!   另外一个强大的新功能是ANTLR 4极大地简化了匹配某些句法结构(如编程语言的算术表达式)所需的语法规则。长久以来,处理表达式都是ANTLR语法(以及手工编写的递归下降语法分析器)的难题。识别表达式最自然的语法对于传统的自顶向下的语法分析器生成器(如ANTLR 3)是无效的。现在,利用ANTLR 4,你可以通过如下规则匹配表达式:   类似expr的自引用规则是递归的,更准确地说,是左递归(left recursive)的,因为它的至少一个备选分支直接引用了它自己。   ANTLR 4自动将类似expr的左递归规则重写成了等价的非左递归形式。唯一的约束是左递归必须是直接的,也就是说规则直接引用自身。一条规则不能引用另外一条规则,如果后者的备选分支之一在左侧直接引用了前者(而没有匹配一个词法符号)。详见5.4节。   除了上述两项与语法相关的改进,ANTLR 4还使得编写语言类应用程序更加容易。ANTLR生成的语法分析器能够自动建立名为语法分析树(parse tree)的视图,其他程序可以遍历此树,并在所需处理的结构处触发回调函数。在先前的ANTLR 3,用户需要补充语法来创建树。除了自动建立树结构之外,ANTLR 4还能自动生成语法分析树遍历器的实现:监听器(listener)或者访问器(visitor)。监听器与在XML文档的解析过程响应SAX事件的处理器相似。   由于拥有以下几点ANTLR 3所不具备的新特性,ANTLR 4显得非常容易上手:   最大的改变是ANTLR 4降低了语法内嵌动作(代码)的重要性,取而代之的是监听器和访问器。新机制将语法和应用的逻辑代码解耦,使得应用程序本身被封装起来,而非散落在语法的各处。在没有内嵌动作的情况下,你可以在多个程序复用同一份语法,甚至都无须重新编译生成的语法分析器。虽然ANTLR仍然允许内嵌动作的存在,但是在ANTLR 4,它们更像是一种进阶用法。这样的行为能够最大程度地掌控语法分析过程,但其代价是语法复用性的丧失。   由于ANTLR能够自动生成语法分析树和树的遍历器,在ANTLR 4,你无须再编写树语法。取而代之的是一些广为人知的设计模式,如访问者模式。这意味着,在学会了ANTLR语法之后,你就可以重回自己熟悉的Java领域来实现真正的语言类应用程序。   ANTLR 3的LL(*)语法分析策略不如ANTLR 4的ALL(*)强大,所以ANTLR 3为了能够正确识别输入的文本,有时候不得不进行回溯。回溯的存在使得语法的调试格外困难,因为生成的语法分析器会对同样的输入进行(递归的)多趟语法分析。回溯也为语法分析器在面对非法输入时给出错误消息设置了重重障碍。   ANTLR 4是25年前我读研究生时所走的一小段弯路的成果。我想,我也许会稍微改变我曾经的座右铭。   为什么不花5天时间编程,来使你25年的生活自动化呢?ANTLR 4正是我所期望的语法分析器生成器,现在,我终于能够回头去研究我原先在20世纪80年代试图解决的问题——假如我还记得它的话。   本书的主要内容本书是你所能找到的有关ANTLR 4的信息源最好、最完整的。免费的在线文档提供了足够多有关基础语法的句法和语义的资料,不过没有详细解释ANTLR的相关概念。在本书,识别语言的语法模式和将其表述为ANTLR语法的内容是独一无二的。贯穿全书的示例能够在构建语言类应用程序方面助你一臂之力。本书可帮助你融会贯通,成为ANTLR专家。   本书由四部分组成。   第一部分介绍了ANTLR,提供了一些与语言相关的背景知识,并展示了ANTLR的一些简单应用。在这一部分,你会了解ANTLR的句法以及主要用途。   第二部分是一部有关设计语法和使用语法来构建语言类应用程序的“百科全书”。   第三部分展示了自定义ANTLR生成的语法分析器的错误处理机制的方法。随后,你会学到在语法嵌入动作的方法——在某些场景下,这样做比建立树并遍历之更简单,也更有效率。此外,你还将学会使用语义判定(semantic predicate)来修改语法分析器的行为,以便解决一些充满挑战的识别难题。   本部分的最后一解决了一些充满挑战的识别难题,例如识别XML和Python的上下文相关的换行符。   第四部分是参考节,详细列出了ANTLR语法元语言的所有规则和ANTLR运行库的用法。   完全不了解语法和语言识别工具的读者请务必从头开始阅读。具备ANTLR 3使用经验的用户可从第4开始阅读以学习ANTLR 4的新功能。   有关ANTLR的更多在线学习资料在http://www.antlr.org上,你可以找到ANTLR、ANTLRWorks2图形界面开发环境、文档、预制的语法、示例、文,以及文件共享区。技术支持邮件组是一个对初学者十分友好的公开讨论组。   Terence Parr2012年11月于旧金山大学致  谢Acknowledgements大约25年前,我开始致力于ANTLR的相关工作。那时,在许多人的帮助下,ANTLR工具的句法和功能逐渐成形,在此,我向他们致以由衷的感谢。要特别感谢的是Sam Harwell,他是ANTLR 4的另一位开发者。他不仅帮助我完成了此软件,而且在ALL(*)语法分析算法上做出了突出的贡献。Sam也是ANTLRWorks2语法IDE的开发者。   感谢以下人员对本书进行了技术审阅:Oliver Ziegermann、Sam Rose、Kyle Ferrio、Maik Schmidt、Colin Yates、Ian Dees、Tim Ottinger、Kevin Gisi、Charley Stran、Jerry Kuch、Aaron Kalair、Michael Bevilacqua-Linn、Javier Collado、Stephen Wolff以及Bernard Kaiflin。同时,我还要感谢那些在本书和ANTLR 4软件处于beta版本时报告问题的热心读者。尤其要感谢的是Kim Shrier和Graham Wideman,他们二位的审阅格外认真。Graham的审阅报告之仔细、翔实和广博,令我不知是该紧握他的手予以感谢,还是该为自己的疏漏羞愧难当。   最后,我还要感谢编辑Susannah Davidson Pfalzer,她一如既往地支持我完成了三本书的创作。她提出的宝贵建议和对本书内容的精雕细琢使本书更加完美。   The Translator's Words译 者 序四年前,我在读研究生时曾经参考龙书编写过一个简单的编译器前端。经过一个星期的实践后,我意识到,从头实现一个编译器前端的难度远远超出了一般开发者的能力。编写编译器前端所需要的理论基础、技术功底和精力都远非普通软件可比。   幸运的是,ANTLR的出现使这个过程变得易如反掌。ANTLR能够根据用户定义的语法文件自动生成词法分析器和语法分析器,并将输入文本处理为(可视化的)语法分析树。这一切都是自动进行的,所需的仅仅是一份描述该语言的语法文件。   一年前,我在为淘宝的一个内部数据分析系统设计DSL时,第一次接触到了ANTLR。使用ANTLR之后,我在一天之内就完成了整个编译器前端的开发工作,从而能够迅速开始处理真正的业务逻辑。从那时起,我就被它强大的功能所深深吸引。简而言之,ANTLR能够解决别的工具无法解决的问题。   软件改变了世界。数十年来,信息化的浪潮在全球颠覆着一个又一个的行业。然而,整个世界的信息化程度还远未达到合理的高度,还有大量传统行业的生产力可以被信息化所解放。在这种看似矛盾的情形背后存在着一条鸿沟:大量从事传统行业的人员拥有在本行业无与伦比的业务知识和经验,却苦于跟不上现代软件发展的脚步。解决这个问题的根本方法就是DSL(Domain Specific Language),让传统行业的人员能够用严谨的方式与计算机对话。其实,本质上任何编程语言都是一种DSL,殊途同归。   而实现DSL的主要困难就在编译器前端。编译器被称为软件工程皇冠上的明珠。一直以来,对于普通的开发者而言,编译器的设计与实现都如同诗描述的那样:“白云在青天,可望不可即。”   ANTLR改变了这一切。ANTLR自动生成的编译器前端高效、准确,能够将开发者从繁杂的编译理论解放出来,集精力处理自己的业务逻辑。ANTLR 4引入的自动语法分析树创建与遍历机制,极大地提高了语言识别程序的开发效率。   时至今日,ANTLR仍然是Java世界实现编译器的不二之选,同时,它对其他编程语言也提供了不同程度的支持。在开始学习ANTLR时,我发现国内有关ANTLR的资料较为贫乏,这催生了我翻译本书的念头。我期望通过本书的翻译,让更多的开发者能够更加自如地解决职业生涯碰到的难题。   本书没有冗长的理论,而是从一些具体的需求出发,由浅入深地介绍了语言的背景知识、ANTLR语法的设计方法以及基于ANTLR 4实现语言识别程序的详细步骤。它尤其适用于对语言识别程序的开发感兴趣的开发者。不过,假如你现在没有这样的需求,我仍然建议你阅读本书,因为它能够开拓你的眼界,让你深入实现层面加深对编程语言的理解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值