解析器生成器之 JavaCC(2):如何基于 JavaCC 描述扫描器
Author: stormQ
Created: Saturday, 06. March 2021 07:51PM
Last Modified: Sunday, 07. March 2021 04:49PM
本文介绍了如何基于 JavaCC 描述扫描器,并通过测试程序来验证一些较复杂的词法规则的正确性。
JavaCC 采用正则表达式来描述扫描器(又称词法分析器),支持如下四种描述扫描器的命令:TOKEN
、SKIP
、MORE
和SPECIAL_TOKEN
。
TOKEN
命令用来描述要生成token
的单词的词法分析规则(或者称匹配规则)。
SKIP
和SPECIAL_TOKEN
命令用来描述不生成token
的单词的匹配规则。两者的唯一区别在于是否保存跳过的token
。SKIP
命令不会保存跳过的token
。因此,无法访问被SKIP
命令跳过的单词。而SPECIAL_TOKEN
命令可以。
MORE
命令可以将一个token
分割为由多个词法分析的规则来描述。
在 JavaCC 中,每个词法规则都属于一个扫描器状态。仅当词法规则所属于的状态与扫描器的当前状态一致时,该词法规则才会生效。如果不显式指定,词法规则的默认状态为DEFAULT
。默认情况下,扫描器的初始状态也是DEFAULT
。
1) 固定字符串
要识别 C 语言中的保留字int
时,可以采用固定字符串
进行识别。对应的正则表达式如下:
"int"
注:对于固定字符串
,需要用英文双引号""
括起来。
对应的TOKEN
如下:
TOKEN :
{
< INT: "int" >
}
注:上面的INT
为自定义的token
名称。
2) 连接
要识别 C 语言中的十六进制整数时,可以采用连接
进行识别。
如果十六进制整数需要满足如下规则:“必须以 0x 或 0X 开头,后续字符可以是数字 0~9、大写字母 A~F、小写字母 a~f 中的任意一个,后续字符的个数至少为 1”,那么对应的正则表达式如下:
("0x" | "0X") (["0"-"9","A"-"F","a"-"f"])+
注:对于连接
,被连接的各部分之间用空格隔开。
上面的连接
由两部分组成:("0x" | "0X")
和(["0"-"9","A"-"F","a"-"f"])+
。至于两者表示的含义,下文会详细讲解。
对应的TOKEN
如下:
TOKEN :
{
< HEXADECIMALINT : ("0x" | "0X") (["0"-"9","A"-"F","a"-"f"])+ >
}
由于 JavaCC 支持忽略大小写
的特性。因此,上述TOKEN
可以简化为:
TOKEN [IGNORE_CASE] :
{
< HEXADECIMALINT : "0x" (["0"-"9","a"-"f"])+ >
}
这样,诸如:0x0、0X0、0x12fF 等十六进制整数都会被识别为HEXADECIMALINT
token。
3) 字符组
要识别特定字符中的任意一个
时,可以采用字符组
进行识别。
如果要识别“1、0、2、4 四个数字中的任意一个”,那么对应的正则表达式如下:
["1","0","2","4"]
注:对于字符组
,需要用方括号[]
括起来,并且各部分之间用英文逗号,
隔开。
对应的TOKEN
如下:
TOKEN :
{
< DAY: ["1","0","2","4"] >
}
4) 限定范围的字符组
要识别指定范围内的任意一个
时,可以采用限定范围的字符组
进行识别。
如果要识别“a~z 26 个小写字母中的任意一个”,那么对应的正则表达式如下:
["a"-"z"]
注:对于限定范围的字符组
,需要用方括号[]
括起来,并且在范围的上界和下界部分之间加上一个中划线-
。
对应的TOKEN
如下:
TOKEN :
{
< LOWERCASE: ["a"-"z"] >
}
5) 排除型字符组
要识别任意一个非数字的字符
时,可以采用排除型字符组
进行识别。对应的正则表达式如下:
~["0"-"9"]
注:对于排除型字符组
,只需要在要排除的字符组前面加上一个英文波浪号~
。
对应的TOKEN
如下:
TOKEN :
{
< NONDIGITAL: ~["0"-"9"] >
}
6) 任意一个字符
要识别任意一个字符
时,可以采用排除型字符组
进行识别。对应的正则表达式如下:
~[]
对应的TOKEN
如下:
TOKEN :
{
< ANY: ~[] >
}
7) 重复 0 次或多次
要识别一个模式可以重复 0 次或多次
时,可以采用*
进行识别。
如果要识别“0 到 9 的数字可以重复 0 次或多次”,那么对应的正则表达式如下:
(["0"-"9"])*
注:对于可以重复 0 次或多次
的模式,需要将模式用英文小括号()
括起来,并且在)
后面加一个*
。
注意: 在 JavaCC 中,重复 0 次或多次
等重复性模式中的英文小括号()
不能省略。
对应的TOKEN
如下:
TOKEN :
{
< A: (["0"-"9"])* >
}
8) 重复 1 次或多次
要识别一个模式可以重复 1 次或多次
时,可以采用+
进行识别。
如果要识别“0 到 9 的数字可以重复 1 次或多次”,那么对应的正则表达式如下:
(["0"-"9"])+
注:对于可以重复 1 次或多次
的模式,需要将模式用英文小括号()
括起来,并且在)
后面加一个+
。
对应的TOKEN
如下:
TOKEN :
{
< B: (["0"-"9"])+ >
}
9) 重复 n 次到 m 次
要识别一个模式可以重复 n 次到 m 次
时,可以采用{n,m}
进行识别。
如果要识别“0 到 9 的数字可以重复 3 次到 5 次",那么对应的正则表达式如下:
(["0"-"9"]){3,5}
注:对于可以重复 n 次到 m 次
的模式,需要将模式用英文小括号()
括起来,并且在)
后面加上{n,m}
。其中,n
表示可以重复的最小次数,m
表示可以重复的最大次数。
对应的TOKEN
如下:
TOKEN :
{
< C: (["0"-"9"]){3,5} >
}
10) 正好重复 n 次
要识别一个模式正好重复 n 次
时,可以采用{n}
进行识别。
如果要识别“0 到 9 的数字正好重复 3 次”,那么对应的正则表达式如下:
(["0"-"9"]){3}
注:对于可以正好重复 n 次
的模式,需要将模式用英文小括号()
括起来,并且在)
后面加上{n}
。其中,n
表示可以正好重复的次数。
对应的TOKEN
如下:
TOKEN :
{
< C: (["0"-"9"]){3} >
}
11) 可以省略
要识别一个模式可以省略
时,可以采用?
进行识别。
如果要识别“字母 u 可以省略”,那么对应的正则表达式如下:
("u")?
注:对于可以省略
的模式,需要将模式用英文小括号()
括起来,并且在)
后面加上英文问号?
。
对应的TOKEN
如下:
TOKEN :
{
< D: ("u")? >
}
12) 选择
要识别从多个模式中选择其中一个
时,可以采用选择
进行识别。
如果从"1"、"2"
两者中选择其中一个,那么对应的正则表达式如下:
"1" | "2"
注:对于选择
,被选择的各部分之间用|
隔开。
如果从"1"、"2"、"3"
三者中选择其中一个,那么对应的正则表达式如下:
"1" | "2" | "3"
需要注意:
"1" "2" | "3" "4"
,表示选择"1" "2"
、"3" "4"
两者中的一个。可以匹配的模式有两个:12
、34
。
"1" ("2" | "3") "4"
,表示选择"2"
、"3"
两者中的一个,然后再进行连接
。可以匹配的模式有两个:124
、134
。
JavaCC 的匹配规则用于解决这样一个问题:当一个单词同时满足多个token
的匹配规则时,应该选择哪一个作为该单词的token
。
JavaCC 的匹配规则为:1)优先采用最长匹配原则,即选择匹配长度最长的作为该单词的token
;2)匹配长度相同时,选择在语法描述文件中先定义的那个token
。
因此,在编写 JavaCC 语法描述文件时,必须将标识符的词法规则放在保留字的词法规则的后面。
本节我们介绍如何基于 JavaCC 描述诸如:保留字、标识符等无结构的单词的方法。
1) 编写描述保留字的匹配规则
如果要描述 C 语言中的诸如int
、long
、for
等保留字,那么TOKEN
命令可以如下书写:
TOKEN :
{
< INT: "int" >
| < LONG: "long" >
| < FOR: "for" >
}
最好将所有的保留字在同一个TOKEN
命令中描述。从而,便于理解和维护。
2) 编写描述标识符的匹配规则
如果要描述的标识符需要满足:首字符是字母或者下划线,第二个及后面的字符可以是字母或下划线或数字,标识符的长度可以无限长
,那么TOKEN
命令可以如下书写:
TOKEN :
{
< IDENTIFIER: ["a"-"z","A"-"Z", "_"] (["a"-"z","A"-"Z","0"-"9","_"])* >
}
由于 JavaCC 支持忽略大小写
的特性。因此,上述TOKEN
可以简化为:
TOKEN [IGNORE_CASE] :
{
< IDENTIFIER: ["a"-"z", "_"] (["a"-"z","0"-"9","_"])* >
}
3) 编写描述数值的匹配规则
如果要描述二进制、八进制、十进制、十六进制的有符号和无符号整数,具体匹配规则如下表:
整数分类 | 必须以什么开头 | 后续字符的可取值 | 终结符 | 后续字符的个数 |
---|---|---|---|---|
二进制有符号整数 | 0b 或 0B | 0 或 1 | 无 | 大于等于 1 |
二进制无符号整数 | 0b 或 0B | 0 或 1 | u 或 U | 大于等于 2 |
八进制有符号整数 | 0 | 0~7 | 无 | 大于等于 0 |
八进制无符号整数 | 0 | 0~7 | u 或 U | 大于等于 1 |
十进制有符号整数 | 1~9 | 0~9 | 无 | 大于等于 0 |
十进制无符号整数 | 1~9 | 0~9 | u 或 U | 大于等于 1 |
十六进制有符号整数 | 0x 或 0X | 0~9 或 a~f 或 A~F | 无 | 大于等于 1 |
十六进制无符号整数 | 0x 或 0X | 0~9 或 a~f 或 A~F | u 或 U | 大于等于 2 |
注意: 这里没有区分整型和长整型。
那么TOKEN
命令可以如下书写:
TOKEN [IGNORE_CASE] :
{
< BINARYINT: "0b" (["0"-"1"])+ > // 二进制有符号整数
| < UNSIGNED_BINARYINT: <BINARYINT> "u" > // 二进制无符号整数
| < OCTALINT: "0" (["0"-"7"])* > // 八进制有符号整数
| < UNSIGNED_OCTALINT: <OCTALINT> "u" > // 八进制无符号整数
| < DECIMALINT: ["1"-"9"] (["0"-"9"])* > // 十进制有符号整数
| < UNSIGNED_DECIMALINT: <DECIMALINT> "u" > // 十进制无符号整数
| < HEXADECIMALINT: "0x" (["0"-"9","a"-"f"])+ > // 十六进制有符号整数
| < UNSIGNED_HEXADECIMALINT: <HEXADECIMALINT> "u" > > // 十六进制无符号整数
}
本节我们介绍如何基于 JavaCC 描述诸如:空白符、行注释等不需要生成token
的单词的方法。
1) 编写描述跳过空白符的匹配规则
如果要描述跳过空格、制表符\t
、换行符\n
,并且不保存这些token
,那么匹配规则可以如下书写:
SKIP:
{
<[" ","\t","\n"]>
}
注:SKIP
命令中可以省略token
名。
2) 编写描述跳过行注释的匹配规则
如果要描述跳过行注释,并且保存该token
,那么匹配规则可以如下书写:
SPECIAL_TOKEN :
{
< LINE_COMMENT: "//" (~["\r","\n"])* ("\r" | "\n" | "\r\n")? >
}
注:
上述模式可以分为三部分,各部分之间采用连接
。第一部分为"//"
,表示固定字符串"//"
。第二部分为(~["\r","\n"])*
,表示非换行符重复 0 次或多次。第三部分为("\r" | "\n" | "\r\n")?
,表示允许以换行符结尾,也不可以末尾没有换行符。
MAC、UNIX、WINDOWS 平台上的换行符分别为\r
、\n
、\r\n
。
思考题: 上述行注释的匹配规则如何匹配如下的输入?是将该输入当作一行注释,还是其他情况?
// "\n" '\n' \n
验证过程见下文的 行注释之测试程序。
本节我们介绍如何基于 JavaCC 描述具有起始符和终结符的单词的方法。
诸如:块注释、字符串字面量、字符字面量等都是带结构的单词。
对于带结构的单词,通常使用基于状态迁移
的方法进行描述。
1) 编写描述跳过块注释的匹配规则
如果要描述跳过块注释/*......*/
,并且不保存该token
,那么匹配规则可以如下书写:
MORE :
{
"/*" : IN_COMMENT
}
<IN_COMMENT> MORE :
{
<~[]>
}
<IN_COMMENT> SKIP :
{
"*/" : DEFAULT
}
注:
MORE
命令,表示仅匹配该规则后该token
的扫描还未结束。
可以在模式后面添加扫描状态,表示匹配模式后迁移到对应的状态。比如:"/*" : IN_COMMENT
,表示匹配/*
后将扫描状态迁移到IN_COMMENT
状态。IN_COMMENT
状态是自定义的扫描状态,扫描的默认状态是DEFAULT
。
可以指定词法规则在哪个扫描状态下才生效。比如:< IN_COMMENT > MORE
,表示该词法规则仅当扫描状态为IN_COMMENT
时生效。
因此,上述匹配规则表示:
匹配/*
后,该token
的扫描尚未结束,将扫描状态迁移到IN_COMMENT
状态;
进入IN_COMMENT
状态后,仅模式~[]
和"*/"
生效;
模式~[]
,表示匹配任意一个字符。模式"*/"
,表示匹配*/
这两个字符。由于 JavaCC 采用最长匹配原则。所以,当第一次遇到*/
时,匹配的模式会选择后者。匹配后,将扫描状态迁移到DEFAULT
状态。
进入DEFAULT
状态后,仅模式"/*"
生效。
2) 编写描述字符串字面量的匹配规则
a) 第一次尝试
MORE :
{
"\"" : IN_STRING
}
<IN_STRING> MORE :
{
<~["\r","\n","\""]>
}
<IN_STRING> TOKEN :
{
<STRING: "\""> : DEFAULT
}
上述匹配规则存在两个限制:
限制 1:字符串字面量中不可以有\"
。比如,输入为"Hello, world!\"1234\""
时,上述匹配规则认为"Hello, world!\"
是一个字符串。但实际上,如果字符"
被转义字符\
修饰,那么不应该将其视为字符串字面量的终结符。因此,需要修改上述匹配规则。
限制 2:字符串字面量不可以跨行书写。该限制不便于书写较长的字符串字面量。
b) 第二次尝试
修改上述匹配规则,使其允许字符串字面量中有\"
。
MORE :
{
"\"" : IN_STRING // 规则 1
}
<IN_STRING> MORE :
{
<~["\r","\n","\""]> // 规则 2
| <"\\" ~[]> // 规则 3
}
<IN_STRING> TOKEN :
{
<STRING: "\""> : DEFAULT // 规则 4
}
上述匹配规则表示的含义:
如果匹配了字符"
,那么将扫描器状态迁移到IN_STRING
。
扫描器状态为IN_STRING
时,仅规则 2、3、4 有效。
规则 1、2、3 匹配时,扫描尚未结束。规则 4 匹配时,将扫描器状态迁移到DEFAULT
,该token
的扫描结束。
当字符串字面量中出现字符\
时,如果该字符后面没有字符了,那么规则 2 和 规则 3 都满足。因为规则 2 先定义,所以实际匹配的是规则 2。如果该字符后面还有字符,那么根据最长匹配原则,规则 2 会被匹配,并且只匹配该字符后面的一个字符。匹配后,再根据后面的内容,从规则 2、3、4 中选择进行匹配。
需要注意的是, 上述规则支持字符串字面量跨行书写。但必须满足:如果下一行还要继续写字符串的内容,那么当前行的最后一个字符必须是\
(即该字符后面不能有任何字符,包括空格字符)。
c) 字符串字面量的匹配规则也有如下书写的
MORE :
{
"\"" : IN_STRING
}
<IN_STRING> MORE :
{
<(~["\r","\n","\"","\\"])+>
| <"\\" (["0"-"7"]){3}>
| <"\\" ~[]>
}
<IN_STRING> TOKEN :
{
<STRING: "\""> : DEFAULT
}
3) 编写描述字符字面量的匹配规则
a) 第一次尝试
MORE :
{
"\'" : IN_CHARACTER
}
<IN_CHARACTER> MORE :
{
<~["\r","\n","\'"]> : CHARACTER_TERM
| <"\\" (["0"-"9"]){1,3}> : CHARACTER_TERM
| <"\\" ~[]> : CHARACTER_TERM
}
<CHARACTER_TERM> TOKEN :
{
<CHARACTER: "\'"> : DEFAULT
}
上述匹配规则存在这样一个限制:字符字面量不可以跨行书写。
也就是说,上述匹配规则无法识别如下的输入:
'\
A'
b) 字符字面量的匹配规则也有如下书写的
MORE :
{
"\'" : IN_CHARACTER
}
<IN_CHARACTER> MORE :
{
<~["\r","\n","\'","\\"]> : CHARACTER_TERM
| <"\\" (["0"-"7"]){3}> : CHARACTER_TERM
| <"\\" ~[]> : CHARACTER_TERM
}
<CHARACTER_TERM> TOKEN :
{
<CHARACTER: "\'"> : DEFAULT
}
需要注意的是, 上述匹配规则也存在限制:字符字面量不可以跨行书写。除此之外,还无法识别诸如\12
这样的字符。
本节我们编写一个简单的测试程序,用于测试行注释的匹配规则是否正确。测试程序从指定的文件中读取输入,如果成功匹配了,那么打印所匹配的行注释。
研究过程:
step 1: 准备
1) 准备语法描述文件
parser.jj:
options {
STATIC = false;
}
PARSER_BEGIN(Parser)
import java.io.*;
public class Parser {
static public void main(String[] args) {
for (String arg : args) {
try {
evaluate(arg);
}
catch (ParseException ex) {
System.err.println(ex.getMessage());
}
}
}
static public void evaluate(String file) throws ParseException {
try {
InputStream input = new FileInputStream(file);
Parser parser = new Parser(input);
parser.expr();
}
catch (FileNotFoundException ex) {
System.err.println(ex.getMessage());
}
}
}
PARSER_END(Parser)
TOKEN :
{
< LINE_COMMENT: "//" (~["\r","\n"])* ("\r" | "\n" | "\r\n")? >
}
void expr():
{
Token x;
}
{
x=<LINE_COMMENT>
{
System.out.println(x.toString());
}
}
注意: 为了便于打印,这里用TOKEN
命令生成行注释的token
。
2) 准备测试用例文件
ts_line_comment:
// "\n" '\n' \n
step 2: 生成并运行解析器
1) 生成解析器
$ javacc -DEBUG_TOKEN_MANAGER=true parser.jj
$ javac Parser.java
注意: 这里指定选项DEBUG_TOKEN_MANAGER
的值为true
,从而便于分析扫描过程。
2) 运行解析器,并将输出保存到指定的文件中
$ java Parser ts_line_comment > scan_tlc_output
step 3: 扫描过程分析
1) scan_tlc_output 文件中 1~2 行的内容如下
1 Current character : / (47) at line 1 column 1
2 Starting NFA to match one of : { <LINE_COMMENT> }
上面的输出表示,匹配了字符/
并开始LINE_COMMENT
的匹配过程。
2) 3~4 行的内容如下
3 Current character : / (47) at line 1 column 1
4 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符/
(ASCII 码值为 47)并继续扫描。
3) 5~7 行的内容如下
5 Current character : / (47) at line 1 column 2
6 Currently matched the first 2 characters as a <LINE_COMMENT> token.
7 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符/
,已匹配字符的数量为 2,并继续扫描。
4) 8~9 行的内容如下
8 Current character : (32) at line 1 column 3
9 Currently matched the first 3 characters as a <LINE_COMMENT> token.
10 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符空格
(ASCII 码值为 32),已匹配字符的数量为 3,并继续扫描。
5) 11~13 行的内容如下
11 Current character : \" (34) at line 1 column 4
12 Currently matched the first 4 characters as a <LINE_COMMENT> token.
13 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符"
(ASCII 码值为 34),已匹配字符的数量为 4,并继续扫描。
6) 14~16 行的内容如下
14 Current character : \\ (92) at line 1 column 5
15 Currently matched the first 5 characters as a <LINE_COMMENT> token.
16 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符\
(ASCII 码值为 92),已匹配字符的数量为 5,并继续扫描。
7) 17~19 行的内容如下
17 Current character : n (110) at line 1 column 6
18 Currently matched the first 6 characters as a <LINE_COMMENT> token.
19 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符n
(ASCII 码值为 110),已匹配字符的数量为 6,并继续扫描。
需要注意的是, 在扫描器看来,"\n"
是 4 个字符。而不是一个换行符(ASCII 码值为 10)。
8) 20~22 行的内容如下
20 Current character : \" (34) at line 1 column 7
21 Currently matched the first 7 characters as a <LINE_COMMENT> token.
22 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符"
,已匹配字符的数量为 7,并继续扫描。
9) 23~25 行的内容如下
23 Current character : (32) at line 1 column 8
24 Currently matched the first 8 characters as a <LINE_COMMENT> token.
25 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符空格
,已匹配字符的数量为 8,并继续扫描。
10) 26~28 行的内容如下
26 Current character : \' (39) at line 1 column 9
27 Currently matched the first 9 characters as a <LINE_COMMENT> token.
28 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符'
(ASCII 码值为 39),已匹配字符的数量为 9,并继续扫描。
11) 29~31 行的内容如下
29 Current character : \\ (92) at line 1 column 10
30 Currently matched the first 10 characters as a <LINE_COMMENT> token.
31 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符\
,已匹配字符的数量为 10,并继续扫描。
12) 32~34 行的内容如下
32 Current character : n (110) at line 1 column 11
33 Currently matched the first 11 characters as a <LINE_COMMENT> token.
34 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符n
,已匹配字符的数量为 11,并继续扫描。
13) 35~37 行的内容如下
35 Current character : \' (39) at line 1 column 12
36 Currently matched the first 12 characters as a <LINE_COMMENT> token.
37 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符'
,已匹配字符的数量为 12,并继续扫描。
需要注意的是, 在扫描器看来,'\n'
是 4 个字符。而不是一个换行符。
14) 38~40 行的内容如下
38 Current character : (32) at line 1 column 13
39 Currently matched the first 13 characters as a <LINE_COMMENT> token.
40 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符空格
,已匹配字符的数量为 13,并继续扫描。
15) 41~43 行的内容如下
41 Current character : \\ (92) at line 1 column 14
42 Currently matched the first 14 characters as a <LINE_COMMENT> token.
43 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符\
,已匹配字符的数量为 14,并继续扫描。
16) 44~46 行的内容如下
44 Current character : n (110) at line 1 column 15
45 Currently matched the first 15 characters as a <LINE_COMMENT> token.
46 Possible kinds of longer matches : { <LINE_COMMENT> }
上面的输出表示,匹配了字符n
,已匹配字符的数量为 15,并继续扫描。
17) 47~49 行的内容如下
47 Current character : \n (10) at line 1 column 16
48 Currently matched the first 16 characters as a <LINE_COMMENT> token.
49 ****** FOUND A <LINE_COMMENT> MATCH (// \"\\n\" \'\\n\' \\n\n) ******
上面的输出表示,匹配了换字符\n
(ASCII 码值为 10),已匹配字符的数量为 16,LINE_COMMENT
token 的扫描结束。
18) 50~52 行的内容如下
50
51 // "\n" '\n' \n
52
上面的输出就是我们打印的行注释token
的值。也就是说,// "\n" '\n' \n
被识别为一个行注释token
。
研究结论:
上文中的行注释匹配规则,会将输入// "\n" '\n' \n
识别为一个行注释token
。
我们自己写在输入文件中的"\n"
、'\n'
、\n
,在扫描器看来,这些都不是换行符。
本节我们修改上文中的测试程序,使之能够测试块注释的匹配规则是否正确。测试程序从指定的文件中读取输入,如果成功匹配了,那么打印所匹配的块注释。
step 1: 准备
1) 修改语法描述文件
将块注释的匹配规则添加到语法描述文件 parser.jj 中:
MORE :
{
"/*" : IN_COMMENT
}
<IN_COMMENT> MORE :
{
<~[]>
}
<IN_COMMENT> TOKEN :
{
< BLOCK_COMMENT: "*/" > : DEFAULT
}
将语法描述文件 parser.jj 中的expr
修改为:
void expr():
{
Token x;
}
{
(x=<LINE_COMMENT> | x=<BLOCK_COMMENT>)
{
System.out.println(x.toString());
}
}
(x=<LINE_COMMENT> | x=<BLOCK_COMMENT>)
表示如果行注释匹配成功了,那么 Token 对象x
表示行注释;否则,匹配块注释。如果块注释匹配成功了,那么 Token 对象x
表示块注释。
2) 准备测试用例文件
ts_block_comment:
/* "\n" '\n' \n
123 4 */
56a bc
de */
step 2: 生成并运行解析器
1) 生成解析器
$ javacc -DEBUG_TOKEN_MANAGER=true parser.jj
$ javac Parser.java
2) 运行解析器
$ java Parser ts_block_comment
输出结果为:
<DEFAULT>Current character : / (47) at line 1 column 1
Possible string literal matches : { "/*" }
<DEFAULT>Current character : * (42) at line 1 column 2
No more string literal token matches are possible.
Currently matched the first 2 characters as a "/*" token.
****** FOUND A "/*" MATCH (/*) ******
省略 ...
<IN_COMMENT>Current character : * (42) at line 2 column 7
<IN_COMMENT>Current character : * (42) at line 2 column 7
Possible string literal matches : { "*/" }
<IN_COMMENT>Current character : / (47) at line 2 column 8
No more string literal token matches are possible.
Currently matched the first 2 characters as a "*/" token.
****** FOUND A "*/" MATCH (*/) ******
/* "\n" '\n' \n
123 4 */
从上面的输出结果可以看出,测试用例文件 ts_block_comment 中的块注释被正常地识别了(不存在过度匹配的问题)。
本节我们继续修改上文中的测试程序,使之能够测试字符串字面量的匹配规则是否正确。测试程序从指定的文件中读取输入,如果成功匹配了,那么打印所匹配的字符串字面量。
step 1: 准备
1) 修改语法描述文件
将字符串字面量的匹配规则添加到语法描述文件 parser.jj 中:
MORE :
{
"\"" : IN_STRING
}
<IN_STRING> MORE :
{
<~["\r","\n","\""]>
| <"\\" ~[]>
}
<IN_STRING> TOKEN :
{
<STRING: "\""> : DEFAULT
}
将语法描述文件 parser.jj 中的expr
修改为:
void expr():
{
Token x;
}
{
(x=<LINE_COMMENT> | x=<BLOCK_COMMENT> | x=<STRING>)
{
System.out.println(x.toString());
}
}
2) 准备测试用例文件
ts_string:
"Hello, world!\"1234\\\"\123456\
\"next line"
step 2: 生成并运行解析器
1) 生成解析器
$ javacc -DEBUG_TOKEN_MANAGER=true parser.jj
$ javac Parser.java
2) 运行解析器
$ java Parser ts_string
输出结果为:
<DEFAULT>Current character : \" (34) at line 1 column 1
No more string literal token matches are possible.
Currently matched the first 1 characters as a "\"" token.
****** FOUND A "\"" MATCH (\") ******
省略 ...
<IN_STRING>Current character : \" (34) at line 2 column 12
<IN_STRING>Current character : \" (34) at line 2 column 12
No more string literal token matches are possible.
Currently matched the first 1 characters as a "\"" token.
****** FOUND A "\"" MATCH (\") ******
"Hello, world!\"1234\\\"\123456\
\"next line"
从上面的输出结果可以看出,测试用例文件 ts_string 中的字符串字面量被正常地识别了。
本节我们继续修改上文中的测试程序,使之能够测试字符字面量的匹配规则是否正确。测试程序从指定的文件中读取输入,如果成功匹配了,那么打印所匹配的字符字面量。
step 1: 准备
1) 修改语法描述文件
将字符字面量的匹配规则添加到语法描述文件 parser.jj 中:
MORE :
{
"\'" : IN_CHARACTER
}
<IN_CHARACTER> MORE :
{
<~["\r","\n","\'"]> : CHARACTER_TERM
| <"\\" (["0"-"9"]){1,3}> : CHARACTER_TERM
| <"\\" ~[]> : CHARACTER_TERM
}
<CHARACTER_TERM> TOKEN :
{
<CHARACTER: "\'"> : DEFAULT
}
将语法描述文件 parser.jj 中的expr
修改为:
void expr():
{
Token x;
}
{
(x=<LINE_COMMENT> | x=<BLOCK_COMMENT> | x=<STRING> | x=<CHARACTER>)
{
System.out.println(x.toString());
}
}
2) 准备测试用例文件
ts_char:
'\18'
step 2: 生成并运行解析器
1) 生成解析器
$ javacc -DEBUG_TOKEN_MANAGER=true parser.jj
$ javac Parser.java
2) 运行解析器
$ java Parser ts_char
输出结果为:
<DEFAULT>Current character : \' (39) at line 1 column 1
No more string literal token matches are possible.
Currently matched the first 1 characters as a "\'" token.
****** FOUND A "\'" MATCH (\') ******
省略 ...
<CHARACTER_TERM>Current character : \' (39) at line 1 column 5
<CHARACTER_TERM>Current character : \' (39) at line 1 column 5
No more string literal token matches are possible.
Currently matched the first 1 characters as a "\'" token.
****** FOUND A "\'" MATCH (\') ******
'\18'
从上面的输出结果可以看出,测试用例文件 ts_char 中的字符字面量被正常地识别了。
下一篇:解析器生成器之 JavaCC(3):如何基于 JavaCC 描述解析器