ANTLR中由于词法规则和文法规则的结构相似,ANTLR允许二者在同一个语法文件中同时存在。不过,由于词法分析和语法分析是语言识别过程中的两个不同阶段,我们必须告ANTLR每条规则对应的阶段。它是通过这种方式完成的:词法规则以大写字母开头,而文法规则以小写字母开头。例如:ID是一个词法规则名,而expr是一个文法规则名。
对于关键字、运算符和标点符号,我们无须声明词法规则,只需要在文法规中直接使用单引号将它们括起来即可,例如'while'、'*',以及'++'。
1.匹配标识符
ID : ('a'..'z' | 'A'..'Z')+ //匹配1个或多个大小写字母
//ANTLR 还支持正则表达式中用于表示字符集的缩写
ID : [a-zA-Z]+ //匹配一个或多个大小写字母
类似ID规则有时候会和其他词法规则或者字符串常量值产生冲突,例如'enum'
grammar KeywordTest;
enumDef : 'enum';
FOR: 'for';
ID: [a-zA-z]+; //不会匹配 'enum' 和'for'
ID 规则也能够匹配类似enum 和for 的关键字,这意味着存在不止一种规则可以匹配相同的输入字符串。ANTLR是怎么处理这种情况的呢。
首先得了解ANTLR对这种混合了词法规则和文法规则的语法文件的处理机制,ANTLR从文法规则中筛选出所有的字符串常量,并将它们和词法规则放在一起。'enum'这样的字符串常量被隐式定义为词法规则,然后放置在文法规则之后、显式定义的词法规则之前。ANTLR词法分析器解决歧义问题的方法是优先使用位置靠前的词法规则。这意味着,ID规则必须定义在所有的关键字规则之后,在上面的例子中,它在FOR规则之后。ANTLR将字符串常量隐式生成的词法规则放在显式定义的词法规则之前,所以它们总是拥有最高的优先级。
2.匹配数字
数字:
INT : '0'..'9'+; //匹配1个或多个数字
或
INT : [0-9]+; //匹配一个或多个数字
浮点数:
FLOAT: DIGIT+ '.' DIGIT*; //匹配1.39
fragment
DIGIT : [0-9];
注意:将一条规则声明为fragment可以告诉ANTLR,该规则本身不是一个词法符号,它只会被其他词法规则使用。这意味着我们不能在文法规则中引用DIGIT。
3.匹配字符串常量
匹配双引号之间的字符串常量
STRING : '"' .*? '"'; //匹配"..."间的任意文本
解释:点号通配符匹配任意的单个字符。因此,.*就是一个循环,它匹配零个或多个字符组成的任意字符序列。为解决这个问题,ANTLR通过标准正则表达式(?后缀)提供了对非贪婪匹配子规则(nongreedy subrule)的支持。
STRING规则兼容双引号
使用\开头的转义序列,例如在一个被双引号包围的字符串中使用双引号,需要使用\"
STRING: '"' (ESC|.)*? '"'
fragment
ESC : '\\"' | '\\\\'; //双字符序列 \" 和 \\
注意:ANTLR语法本身需要对转义字符\进行转义,因此我们需要\\来表示单个反斜杠字符。STRING规则中的循环既能通过ESC片段规则(fragment rule)来匹配转义字符序列,
也能通过通配符来匹配任意的单个字符。*?运算符会使(ESC|.)*?循环在看到后续子规则,即一个未转义的双引号时终止。
4.匹配注释和空白字符
当词法分析器匹配到我们刚刚定义过的那些词法符号的时候,它会将匹配到的词法符号放入词法符号流,输送给语法分析器。之后,由语法分析器来检查词法符号流的语法结构。但是,当词法分析器匹配到注释和空白字符的时候,我们通常希望将它们丢弃。这样,语法分析器就不必处理注释和空白字符了。其中,WS是代表空白字符的语法规则:
assign : ID (WS|COMMENT)? '=' (WS|COMMENT)? expr (WS|COMMENT)?;
定义需要被丢弃的词法符号的方法和定义正常的词法符号一样,我们只需要使用skip指令通知词法分析器将它们丢弃。
例如:
LINE_COMMENT : '//' .*? '\r'? '\n' -> skip; //匹配"//"任意字符序列 '\n'
COMMENT : '/*' .*? '*/' -> skip; //匹配 "/*" 任意字符序列 "*/"
注意:在LINE_COMMENT 规则中,.*? 会消费掉 //后面的一切字符,直至遇到换行符\n为止。COMMENT 规则中,.*?消费/* 和 */之间的一切字符。
处理空白字符:
ANTLR 丢弃空白字符:
WS : (' ' | '\t' | '\r' | '\n')+ -> skip; //匹配一个或多个空白字符并将它们丢弃
或者
ws : [\t\r\n]+ -> skip; //匹配一个或多个空白字符并将它们丢弃
基础语法规则
标点符号 | 处理运算符和标点符号最容易的方式就是直接在文法规则中引用它们。 call : ID '(' exprList ')' ; 一些开发者更愿意定义类似LP(左括号,left parenthesis)的词法符号标签。 call : ID LP exprList RP ; LP : '(' ; RP : ')' ; |
关键字 | 关键字是保留的标识符,我们既可以直接引用它们,也可以为它们定义词法符号类型,returnStat : 'return' expr ';' |
标识符 | 几乎每种语言中的标识符看上去都差不多,它们之间的差异通常在于第一个字符的可选值以及是否允许Unicode字符。 ID : ID_LETTER (ID_LETTER | DIGIT) *; //C 语言的语法片段 fragment ID_LETTER : 'a' .. 'z' | 'A' .. 'Z' | '_'; fragment DIGIT : '0' .. '9' ; |
数字 | 下列规则定义了整数和简单的浮点数。 INT : DIGIT+; FLOAT : DIGIT+ '.' DIGIT* | '.' DIGIT+ ; |
字符串 | 匹配双引号包围的字符串 STRING : '"' ( ESC | .) *? '"'; fragment ESC : '\\' [btnr\\]; // \b, \t, \n 等 |
注释 | 匹配并丢弃注释 LINE_COMMENT : '//' .*? '\n' -> skip; COMMENT : '/*' .*? '*/' -> skip; |
空白字符 | 在词法分析器中匹配空白字符并丢弃之 WS : [\t\n\r]+ -> skip |
5 划分词法分析器和语法分析器的界限
划分规则:
-
在词法分析器中匹配并丢弃任何语法分析器无需知晓的东西。对于编程语言来说,要识别并丢弃的就是类似注释和空白字符的东西。否则,语法分析器就需要频繁检查它们是否存在于词法符号之间。
-
由词法分析器来匹配类似标识符、关键字、字符串和数字的常见词法符号。语法分析器的层级更高,所以我们不应当让它处理将数字组合成整数这样的事情。
-
将语法分析器无须区分的词法结构归为同一个词法符号类型。例如:如果我们的程序对待整数和浮点数的方式是一致的,那就把它们都归为NUMBER类型的词法符号。没必要传给语法分析器不同的类型。
-
将任何语法分析器可以以相同方式处理的实体归为一类。例如,如果语法分析器不关心XML标签的内容,词法分析器就可以将尖括号中的所有内容归为一个名为TAG的词法符号类型。
-
另一方面,如果语法分析器需要把一种类型的文本拆开处理,那么词法分析器就应该将它的各组成部分作为独立的词法符号输送给语法分析器。例如,如果语法分析器需要处理IP地址中的元素,那么词法分析器就应该把IP地址的各组成部分(整数和点)作为独立的词法符号送入语法分析器。