解析器生成器之 JavaCC(2):如何基于 JavaCC 描述扫描器

Author: stormQ

Created: Saturday, 06. March 2021 07:51PM

Last Modified: Sunday, 07. March 2021 04:49PM



摘要

本文介绍了如何基于 JavaCC 描述扫描器,并通过测试程序来验证一些较复杂的词法规则的正确性。


基于 JavaCC 描述扫描器之概述

JavaCC 采用正则表达式来描述扫描器(又称词法分析器),支持如下四种描述扫描器的命令:TOKENSKIPMORESPECIAL_TOKEN

在 JavaCC 中,每个词法规则都属于一个扫描器状态。仅当词法规则所属于的状态与扫描器的当前状态一致时,该词法规则才会生效。如果不显式指定,词法规则的默认状态为DEFAULT。默认情况下,扫描器的初始状态也是DEFAULT


JavaCC 的正则表达式

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 等十六进制整数都会被识别为HEXADECIMALINTtoken。

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"

需要注意:


JavaCC 的匹配规则

JavaCC 的匹配规则用于解决这样一个问题:当一个单词同时满足多个token的匹配规则时,应该选择哪一个作为该单词的token

JavaCC 的匹配规则为:1)优先采用最长匹配原则,即选择匹配长度最长的作为该单词的token;2)匹配长度相同时,选择在语法描述文件中先定义的那个token

因此,在编写 JavaCC 语法描述文件时,必须将标识符的词法规则放在保留字的词法规则的后面。


JavaCC 如何描述无结构的单词

本节我们介绍如何基于 JavaCC 描述诸如:保留字、标识符等无结构的单词的方法。

1) 编写描述保留字的匹配规则

如果要描述 C 语言中的诸如intlongfor等保留字,那么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 的单词

本节我们介绍如何基于 JavaCC 描述诸如:空白符、行注释等不需要生成token的单词的方法。

1) 编写描述跳过空白符的匹配规则

如果要描述跳过空格、制表符\t、换行符\n,并且不保存这些token,那么匹配规则可以如下书写:

SKIP:
{
  <[" ","\t","\n"]>
}

注:SKIP命令中可以省略token名。

2) 编写描述跳过行注释的匹配规则

如果要描述跳过行注释,并且保存该token,那么匹配规则可以如下书写:

SPECIAL_TOKEN :
{
  < LINE_COMMENT: "//" (~["\r","\n"])* ("\r" | "\n" | "\r\n")? >
}

注:

思考题: 上述行注释的匹配规则如何匹配如下的输入?是将该输入当作一行注释,还是其他情况?

// "\n" '\n' \n

验证过程见下文的 行注释之测试程序


JavaCC 如何描述带结构的单词

本节我们介绍如何基于 JavaCC 描述具有起始符和终结符的单词的方法。

诸如:块注释、字符串字面量、字符字面量等都是带结构的单词。

对于带结构的单词,通常使用基于状态迁移的方法进行描述。

1) 编写描述跳过块注释的匹配规则

如果要描述跳过块注释/*......*/,并且不保存该token,那么匹配规则可以如下书写:

MORE :
{
  "/*" : IN_COMMENT
}

<IN_COMMENT> MORE :
{
  <~[]>
}

<IN_COMMENT> SKIP :
{
  "*/" : DEFAULT
}

注:

因此,上述匹配规则表示:

2) 编写描述字符串字面量的匹配规则

a) 第一次尝试

MORE :
{
  "\"" : IN_STRING
}

<IN_STRING> MORE :
{
  <~["\r","\n","\""]>
}

<IN_STRING> TOKEN :
{
  <STRING: "\""> : DEFAULT
}

上述匹配规则存在两个限制:

b) 第二次尝试

修改上述匹配规则,使其允许字符串字面量中有\"

MORE :
{
  "\"" : IN_STRING            // 规则 1
}

<IN_STRING> MORE :
{
  <~["\r","\n","\""]>         // 规则 2
| <"\\" ~[]>                  // 规则 3
}

<IN_STRING> TOKEN :
{
  <STRING: "\""> : DEFAULT    // 规则 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_COMMENTtoken 的扫描结束。

18) 50~52 行的内容如下

 50 
 51 // "\n" '\n' \n
 52 

上面的输出就是我们打印的行注释token的值。也就是说,// "\n" '\n' \n被识别为一个行注释token

研究结论:


块注释之测试程序

本节我们修改上文中的测试程序,使之能够测试块注释的匹配规则是否正确。测试程序从指定的文件中读取输入,如果成功匹配了,那么打印所匹配的块注释。

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 中的字符字面量被正常地识别了。


References


下一篇:解析器生成器之 JavaCC(3):如何基于 JavaCC 描述解析器

上一篇:解析器生成器之 JavaCC(1): JavaCC 准备篇

首页