1.概述
语法分析器
通过输入的词法符号流来识别特定的语言结构
词法分析器
通过输入的字符流来识别特定的语言结构。
词法规则
以大写字母开头
文法规则
以小写字母开头。
例如,ID是一个词法规则名,而expr是一 个文法规则名。
2.配置标识符
在语法的伪代码中,一个基本的标识符就是一个由大小写字母组成的字符序列
。
我们知道,可以使用刚刚掌握的方法(...) +
来表达序列模式。因为序列中的元素既可以是大写字母也可以是小写字母.
我们还知道,应当在子规则中使用选择运算符:
ID : ('a'..'z'|'A'..'Z')+ ; //匹配1个或多个大小写字母
上面的ANTLR标记中,唯一让我们感到新鲜的是范围运算符: a'..'z'
它的意思是从a到z的所有字符。这实际上是从97到122的ASCII码。如果我们需要使用Unicode字符(Unicode code point),就必须写作’\uXXXX’,其中XXXX是相应的Unicode字符以十六进制表示
的码点值。此外,ANTLR还支持正则表达式中用于表示字符集的缩
写:
ID : [a-zA-Z]+ ; //匹配1个或多个大小写字母
类似ID的规则有时候会和其他词法规则或者字符串常量值产生冲
突,例如’enum’。
grammar KeywordTest;
enumDef : 'enum' '{' ... '}' ;
...
FOR:'for';
...
ID : [a-zA-Z]+ ; //不会匹配'enum'和'for'
ID规则也能够匹配类似enum和for的关键字,这意味着存在不止-种规则可以匹配相同的输入字符串。
要弄清此事,我们需要了解ANTLR对这种混合了词法规则和文法规则的语法文件的处理机制
。
首先,ANTLR从文法规则中筛选出所有的字符串常量,并将它们和词法规则放在一起。'enum这样的字符串常量被隐式定义为词法规则
然后放置在文法规则之后、显式定义的词法规则之前。ANTLR词法分析器解决歧义问题的方法是优先使用位置靠前的词法规则。这意味着,ID规则必须定义在所有的关键字规则之后
,
在上面的例子中,它在FOR规则之后。ANTLR将为字符串常量隐式生成的词法规则放在显式定义的词法规则之前,所以它们总是拥有最高的优先级。因此,在本例中,'enum’被 自动赋予了比ID更高的优先级。因为ANTLR自动将词法规则放置在文法规则之后,下面的
KeywordTest语法的变体会生成相同的语法分析器和词法分析器:
grammar KeywordTes tReordered;
FOR : 'for' ;
ID : [a-zA-Z]+ ; //不会匹配'enum'和'for'
...
enumDef : ' enum' '{' ... '}' ;
...
3.匹配数字
描述10这样的数字非常容易,它不过是-列数字而已。
INT : '0'..'9'+ ; //匹配1个或多个数字
或者:
INT : [0-9]+ ; //匹配1个或多个数字
不幸的是,浮点数要复杂得多,不过,我们可以先完成一-个简化
的版本,忽略掉指数形式。 一个浮点数以- 列数字为开头,后面跟着一个点,然后是可选的小数部分;
浮点数的另外一种格式是,以点为开头,后面是一-列数字。一个
单独的点不是一个合法的浮点数定义。基于上述格式,我们的浮点数
规则使用了选择模式和序列模式。
FLOAT: DIGIT+ '.' DIGIT* // 匹配1. 39. 3.14159 等...
| '.' DIGIT+ //匹配.1 .14159
;
fragment
DIGIT : [0-9] ;
在这里,我们使用了一条辅助规则DIGIT,这样就不用重复书写[0-9]
了。将一条规则声明为fragment可以告诉ANTLR,该规则本身不是一个词法符号
,它只会被其他的词法规则使用。这意味着我们不能在文法规则中引用DIGIT
。
4.匹配字符串常量
另外一种计算机语言共有的词法符号是类似"Hello"
的字符串常
量。大多数语言中的字符串常量使用双引号
,部分语言使用单引号或者同时使用单引号和双引号(Python) 。
不论哪种分界符,我们都使用同一种规则来匹配字符串常量:识别分界符之间的全部内容
。
用语法伪代码表示,一个字符串就是两个双引号之间的任意字符
序列。
STRING : '"' .*? '"' ; //匹配"... "间的任意文本
其中,点号通配符匹配任意的单个字符
。因此,.*就是一 个循环
,它匹配零个或多个字符组成的任意字符序列。
显然,它可以一直匹配到文件结束,但这没有任何意义。为解决这个问题,ANTLR通过标准正则表达式的标记(?后缀)提供了对非贪婪匹配子规则(nongreedy subrule)的支持。
非贪婪匹配的基本含义是:“获取一些字符,直到发现匹配后续子规则的字符为止”
。更准确的描述是,在保证整个父规则完成匹配的前提下,非贪婪的子规则匹配数量最少的字符
。
如果.*?
令你感到迷惑不解,不要担心,只需要记住它是一种匹配双引号或者其他分界符之间的东西的模式即可。
我们的STRING规则还不够完善,因为它不允许其中出现双引号。为了解决这个问题,很多语言都定义了以开头的转义序列。在这些语言中,如果希望在一个被双引号包围的字符串中使用双引号,我们就需要使用\"
。下列规则能够支持常见的转义字符:
STRING: '"' (ESC|.)*? "'
f ragment
ESC : '\\"'| '\\\\' ; //双字符序列\"和\\
其中,ANTLR语法本身需要对转义字符\
进行转义,因此我们需要\
来表示单个反斜杠字符。
现在,STRING规 则中的循环既能通过ESC片段规则(fragment
rule)来匹配转义字符序列,也能通过通配符来匹配任意的单个字符。*?
运算符会使(ESC|.) *?
循环在看到后续子规则,即一个未转义
的双引号时终止。
5.匹配注释和空白字符
当词法分析器匹配
到我们刚刚定义过的那些词法符号的时候,它
会将匹配到的词法符号放入词法符号流
,输送给语法分析器
。之后,由语法分析器来检查
词法符号流的语法结构
。
但是,当词法分析器匹配到注释和空白字符
的时候,我们通常希望将它们丢弃。这样,语法分析器就不必处理注释和空白字符了。否则,下列文法规则就变成了一团乱麻,且十分容易出错,其中,WS是代表空白字符的词法规则
:
assign : ID (WS |COMMENT)? '=' (WS ICOMMENT)? expr (WS COMMENT)? ;
定义需要被丢弃的词法符号的方法和定义正常的词法符号的方法
-样。我们只需要使用skip指令通知词法分析器将它们丢弃即可。例如,下面是匹配类C语言中的单行和多行注释的方法:
LINE_ COMMENT : '//' .*? '\r'? '\n' -> skip ; //匹配"//" 任意字符序列'\n'
COMMENT : '/*' .*? '*/' -> skip ; //匹配"/*"任意字符序列"*/"
在LINE_ COMMENT规则中,.*?
会消费掉//
后面的一切字符,直至遇到换行符\n
为止。(可以将本条规则放在空白字符串之前来匹配
Windows风格的换行符\r\n
)。在COMMENT规则中,.*? 消费/*和*/
之间的一-切字符。词法分析器可以接受许多种位于->
操作符之后的指令,skip只是其中之一。例如,我们能够使用channel指令将某些词法符号放入一个“隐藏的通道”并输送给语法分析器。
现在,让我们来处理最后一种词法符号一一空白字符。 大多数
编程语言将空白字符看作词法符号间的分隔符,并将它们忽略
(Python是一个例外,它使用空白字符来达到某些语法上的目的:换
行符代表一条命令的终止,特定数量的缩进指明嵌套的层级)。下列
规则告诉ANTLR丢弃空白字符:
WS : (' '|'\t'|'\r'|'\n')+ -> skip ; // 匹配一个或多个空白字符并将它们丢弃
或者:
WS : [ \t\r\n]+ -> skip ; //匹配- -个或多个空白字符并将它们丢弃
当换行符既是可以忽略的空白字符,又是命令终止符的时候,我
们的麻烦就来了。换行符变成了上下文相关(context- sensitive)
的。在某种语法上下文中,我们应该丢弃换行符,在另外一些上下文中,我们需要将它输送给语法分析器,从而让语法分析器得知命令被
终止了。
现在,我们知道了如何匹配最常见的词法结构标识符、数字、字符串、注释以及空白字符的基础版本。信不信由你,即使是一
门大型编程语言的词法分析器,也需要这些词法结构作为基础。
6.一些基础的语法规则
分支 | 匹配 |
---|---|
词类名 | 词类中的词 |
'字符序列' | 字面上的字符序列,除了转义序列\n (换行)、\r (回车)、\t (制表符)、\b (退格)、\f (换页)、\uXXXX (Unicode四位十六进制代码点)或\u{XXXXXX}’ (Unicode十六进制代码点) |
[字符集] | 字符集中的一个字符,其中字符集由单字符(包括上述转义序列、\\ 、\] 、\- )、形如单字符-单字符 的字符区间、形如\p{属性名} 或\p{枚举属性=值} 的Unicode子集、以及它们形如\P{属性名} 或\P{枚举属性=值} 的补集组成 |
'字符'..'字符' | 字符区间中的字符(包括这两个字符) |
. | 任何一个字符 |
词法规则 | 匹配指定词法规则(包括fragment 规则)的字符串,可以递归但不能左递归(需要手动改成右递归) |
{动作代码} | 空,用于在读取到这位置时执行指定代码,当代码中花括号不配对时额外的花括号要用\{ 或\} 转义 |
{谓词代码}? | 空,布尔表达式的值为假时放弃继续尝试当前规则 |
~子规则 | 一个不匹配指定子规则的字符 |
子规则 子规则 | 由分别匹配子规则的字符串接起来的 |
子规则* | 由零个或多个匹配子规则的字符串串接起来,匹配尽可能长 |
子规则+ | 由一个或多个匹配子规则的字符串串接起来,匹配尽可能长 |
子规则? | 由零个或一个匹配子规则的字符串串接起来,匹配尽可能长 |
子规则*? | 由零个或多个匹配子规则的字符串串接起来,匹配尽可能短 |
子规则+? | 由一个或多个匹配子规则的字符串串接起来,匹配尽可能短 |
子规则?? | 由零个或一个匹配子规则的字符串串接起来,匹配尽可能短 |
在动作代码或谓词代码中可以通过$规则名引用匹配子规则的词(当有多个同名子规则时可在规则前加上名称=来指定别名),进而可通过.引用其字段或方法,例如以下只读属性:
属性 | 方法 | 类型 | 值 |
---|---|---|---|
text | getText | String | 匹配的文本 |
type | getType | int | 词类代号 |
line | getLine | int | 词开始的行号(从1开始) |
pos | getCharPositionInLine | int | 词在行的偏移(从0开始) |
index | getTokenIndex | int | 当前词的序号(从0开始 |
channel | getChannel | int | 通道代码,默认为0 (Token.DEFAULT_CHANNEL ),隐藏通道为Token.HIDDEN_CHANNEL |
int | int | 匹配文本表示的整数 |
在一个分支的最后可以加上->命令,…,其中可用的命令有:
- skip用于放弃当前词
- mode(模式)修改栈顶模式(栈可用于实现仅用正则表达式无法描述的模式,如某些语言容许的嵌套注释)
- pushMode(模式)推入栈顶模式
- popMode弹出栈顶模式
- more要求继续匹配以延长当前词
- type(词类)用于修改当前词所属的词类
- channel(通道)用于把当前词送到指定通道
如果一条词法规则纯粹为了共用代码或提高可读性,而不是要实际生成词,可在规则前加上fragment