JavaCC官方入门指南-概述
https://www.cnblogs.com/suhaha/p/11733487.html
一、前言
在最开始使用JavaCC的时候,从网上查询了许多资料,但是网上的资料水平是参差不齐的,走了许多弯路,不得已自己查阅了英文版官网文档。令我伤心的是最后我回过头来再看那些博客资料时,发现其实他们写的都是没错的,只不过某些地方少了必要的讲解,以至于新手刚接触的时候是持续懵逼的。
不管怎样,下面内容是对官方文档的翻译,加上一些自己的理解。
官方文档连接:https://www.engr.mun.ca/~theo/JavaCC-Tutorial/javacc-tutorial.pdf
这里所翻译的部分是JavaCC的入门知识,通过由浅入深的几个小例子,可以循序渐进的一步步了解JavaCC技术。
二、 JavaCC概述
JavaCC全称为Java Compiler Compiler,它是一个生成器,用于生成词法分析器(lexical
analysers)和语法分析器(parsers)。它可以通过读取一个词法和语法描述文件(即词法和语法描述是写在同一个文件中的),来生成一个java程序,这个java程序就包括了词法分析器和语法分析器。接着就可以用生成的词法分析器和语法分析器来对我们的输入进行判断,判断输入是否符合我们所要求的语法规则。
编程语言中的编译器,其实就包含了词法分析器和语法分析器,编译器便是通过这两者来识别我们所编写的代码。除了在编译器中的应用之外,词法分析器和语法分析器在其他程序中也有着广泛的应用。
那么什么是词法分析器和语法分析器呢?其中,词法分析器可以将一个字符序列拆分为一个个的子单元,这些子单元,在JavaCC中被称之为token——也就是说,词法分析器可以将一个字符序列拆分为一个个的token。这个解释可能依然有点让人摸不着头脑,下面看一个例子。
假设我们要用词法分析器来解析下面的一段C语言编写的代码:
int main() {
return 0 ;
}
那么C语言编译器中的词法分析器会将上述代码拆分为如下的token序列:
“int”, “ ”, “main”, “(”, “)”,
“ ”, “{”, “\n”, “\t”, “return”
“ ”, “0”, “ ”, “;”, “\n”,
“}”, “\n”, “” .
可以看到所谓的拆分就是把int、空格、main、左括号等等各种字符给拆开罢了。那么在JavaCC中,会给拆分后得到的一个个token取一个逻辑上的名字,比如在本例中,我们所取的名称可以如下:
KWINT, SPACE, ID, OPAR, CPAR,
SPACE, OBRACE, SPACE, SPACE, KWRETURN,
SPACE, OCTALCONST, SPACE, SEMICOLON, SPACE,
CBRACE, SPACE, EOF .
如此一来,KWINT这个token就表示“int”, SPACE这个token就表示“ ”,
OPAR就表示左括号“(”等等。因此在JavaCC中,一般说到token的时候,一般指的是KWINT、SPACE、OPAR等这些东西。另外,EOF这个token表示“文件的末尾”。
经过词法分析而得到的tokens序列,在之后将会被传给语法分析器进行语法判断。
在C语言的编译器中,有时语法分析器在分析的时候是不需要所有的token的。比如在本例中,SPACE这个token就可以不用往下传给语法分析器了,那么此时SPACE这个 token应该如何处理掉,将在后面说到。
在本例中,语法分析器将会对拿到的tokens序列进行分析,判断这些tokens序列是否符合c语言的语法结构。
此外,词法分析器和语法分析器在分析的时候,还有可能会产生错误信息,即当你的输入并不符合你所定义的词法和语法规范时,就会抛出错误信息。
最后需要注意的是,JavaCC本身并不是词法分析器和语法分析器,它是一个生成器!JavaCC通过读取一个后缀为.jj的描述文件中的特定描述,来生成词法分析器和语法分析器,并且生成的词法分析器和语法分析器使用Java书写的。)
至此,已经简单说明了JavaCC是什么,以及词法分析器和语法分析器的作用,还引出了token的概念。下面将通过例子来一步一步进行讲解。
例1:整数加法运算
在这个例子中,我们将判断如下输入的式子是否是一个合法的加法运算:
99 + 42 + 0 + 15
并且在输入上面式子的时候,数字与加号之间的任何位置,都是可以有空格或者换行符的,也就说,即使我们输入的式子是下面这种形式,我们所编写的词法和语法分析器也应该要能判断出来它是一个合法的加法运算表示形式:
99 + 42 + 0
+ 15
(注:上面输入的式子中既有空格,也有制表符,还有换行符)
1.Option块和class声明块
语法描述文件的第一部分是:
/* adder.jj Adding up numbers */
options {
STATIC = false ;
}
PARSER_BEGIN(Adder)
class Adder {
public static void main( String[] args ) throws ParseException, TokenMgrError {
Adder parser = new Adder( System.in );
parser.Start();
}
}
PARSER_END(Adder)
上面的代码可以分为两个部分,一个是options块,另一个是PARSER_BEGIN(XXX)…… PARSER_END(XXX)块。
- 在options中,几乎所有配置项的默认值都适用于本例子,除了 STATIC选项,STATIC默认是true,这里要将其修改为false,使得生成的函数不是static 的。
- 接下来是PARSER_BEGIN(XXX)……PARSER_END(XXX)块,这里定义了一个名为
Adder的类,当然在这个块中定义的并非是Adder类的全部,JavaCC会根据.jj描述文件的其他部分的描述,来生成Adder的其他声明信息。另外注意到,在该类的main方法声明中,
抛出了两个异常类,分别为ParseException和TokenMgrError,这两个异常类会在使用javacc
命令编译当前.jj描述文件的时候生成。)
2.词法描述器
我们在后面还会再讲到main方法。这里让我们先看看词法描述器。当前例子中所需的词法描述器通过如下的四行来进行描述:
SKIP : { " "}
SKIP : { "\n" | "\r" | "\r\n" }
TOKEN : { < PLUS : "+" > }
TOKEN : { < NUMBER : (["0"-"9"])+ > }
-
第一行是SKIP,表示将会被词法分析器忽略的部分:空格。被忽略掉意味着,它们将不会被传给语法分析器。
-
第二行也是SKIP,定义了将会被忽略的部分:换行符。之所以会有几个,是因为在不同的系统中,换行符有不同的表示方式——在Unix/Linux系统中,换行符是"\n";在Windows系统中,换行符是"\r";在mac系统中,换行符则是"\r\n"。这几个换行符用一个竖杠分隔,表示“或”的意思。
-
第三行定义了一个名为PLUS的token,用它来表示加号"+"。
-
第四行定义了一个名为NUMBER的token,用它来表示([”0”-”9”])+,即所有的正整数序列。可以注意到([”0”-”9”])+是一个正则表达式。
这四行描述都可以被称为表达生产式。
事实上,词法分析器中还可以生成一种token,这种token用EOF表示,用来代表输入序列的末尾。但是没有必要在词法分析器部分显式的定义EOF这个token,因为JavaCC会自动处理文件的结束符了。
假设有如下的输入:
“123 + 456\n”
词法分析器将解析的7个token,依次是NUMBER、空格、PLUS、空格、NUMBER、换行符、EOF。在解析出来的这些token中,被标记为SKIP的token将不会被往下传递给语法分析器。词法分析器在分析完成之后,只会将以下token传递给语法分析器:NUMBER,
PLUS, NUMBER, EOF。
再假设一种不合法的输入,如下:
“123 - 456\n”
词法分析器在对上面的输入进行解析时,解析到的第一个token是NUMBER,第二个token是空格,接下来,它就遇到了一个减号字符——因为在我们上面的词法描述器中没有定义减号这个token,因此就无法对其进行解析,此时词法分析器就抛出一个异常:TokenMgrError。
再看另外一种输入情况:
“123 ++ 456\n”
这时词法分析器可以对其进行解析,并给语法分析器传递如下的token序列:NUMBER, PLUS, PLUS, NUMBER, EOF。
很明显,这不是一个合法的“加运算”的输入它连续出现了两个PLUS
token。但是,词法分析器的任务是将输入解析成一个个的token,而不是判断token的顺序是否正确。判断tokens序列的顺序是否正确是语法分析器的任务。接下来将会介绍到的语法分析器,它在分析到第二个PLUS
token的时候,将会检测到错误,一旦检测到错误,它就停止从词法分析器请求tokens了,所以,实际上真正传递给语法分析器的tokens序列只有NUMBER,
PLUS, PLUS。
3.语法分析器
语法分析器的描述由BNF生产式构成。可以看到,语法分析器的描述看起来跟java的方法定义形式有点相似。
void Start() :
{}
{
<NUMBER>
(
<PLUS>
<NUMBER>
)*
<EOF>
}
4.生成词法描述器和语法描述器
将前面部分提到的几个部分都合并起来,保存成adder.jj文件:
/* adder.jj Adding up numbers */
options {
STATIC = false ;
}
PARSER_BEGIN(Adder)
class Adder {
public static void main( String[] args ) throws ParseException, TokenMgrError {
Adder parser = new Adder( System.in );
parser.Start();
}
}
PARSER_END(Adder)
SKIP : { " "}
SKIP : { "\n" | "\r" | "\r\n" }
TOKEN : { < PLUS : "+" > }
TOKEN : { < NUMBER : (["0"-"9"])+ > }
void Start() :
{}
{
<NUMBER>
(
<PLUS>
<NUMBER>
)*
<EOF>
}
然后在上面调用javacc命令。下面是在windows系统上的演示。

执行完之后,会生成7个java文件。如下所示:

其中:
- TokenMgrError 是一个简单的定义错误的类,它是Throwable类的子类,用于定义在词法分析阶段检测到的错误。
- ParseException是另一个定义错误的类。它是Exception 和Throwable的子类,用于定义在语法分析阶段检测到的错误。
- Token类是一个用于表示token的类。我们在.jj文件中定义的每一个token(PLUS, NUMBER, or
EOF),在Token类中都有对应的一个整数属性来表示,此外每一个token都有名为image的string类型的属性,用来表示token所代表的从输入中获取到的真实值。 - SimpleCharStream是一个转接器类,用于把字符传递给语法分析器。
- AdderConstants是一个接口,里面定义了一些词法分析器和语法分析器中都会用到的常量。
- AdderTokenManager 是词法分析器。
- Adder 是语法分析器。
接下来我们对这些java文件进行编译:

编译完成之后,可得到对应的class文件:

5.执行程序
现在,让我们来看看Adder类中的main方法:
Pubic static void main( String[] args ) throws ParseException, TokenMgrError {
Adder parser = new Adder( System.in );
parser.Start();
}
首先注意到main方法有可能抛出两个异常错误类:ParseException和TokenMgrError,它们都是Throwable类的子类。这并不是一种好的编码习惯,理论上我们应该对着两异常进行try-catch捕获,但是在本例中我们暂且将其抛出,以使得代码简洁易懂。
main方法的第一行代码new了一个parser对象,使用的是Adder类的默认构造器,它接收一个InputStream类型对象作为输入。此外Adder类还有一个构造器,这个构造器接收的入参是一个Reader对象。构造函数依次创建一个SimpleCharacterStream类实例和一个AdderTokenManager类的实例(即词法分析器对象)。因此,最后的效果是,词法分析器通过SimpleCharacterStream实例对象从System.in中读取字符,而语法分析器则是从语法分析器中读取tokens。
第二行代码则是调用了语法分析器的一个名为Start()的方法。对于在.jj文件中的每一个BNF生产式,javacc在parser类中都会生成相应的方法。此方法负责尝试在其输入流中找到与输入描述匹配的项。比如,在本例中,调用Start()方法将会使得语法分析器尝试从输入中符合下面描述的tokens序列:
<NUMBER> (<PLUS> <NUMBER>)* <EOF>
我们可以事先准备一个input.txt文件,里面的内容下面会说到。在准备了输入文件之后,接下来就可以用下面的命令来执行程序了。

执行的结果有可能为以下3中情况之一:
-
程序报一个词法错误。比如,词法错误只有在词法分析器无法解析出输入的字符时才会抛出。假设input.txt文件中的内容是123 – 456,在这种情况下,程序就会抛出一个TokenMgrError的错误,报错的信息是:。即:词法分析器不认识“-”减号,因为在.jj文件中,我们并没有对“-”定义相应的token。
-
程序报一个语法错误。当语法分析器接收到的tokens序列不匹配Start()方法中的规范时,就会抛出该错误。比如,若input.txt文件中的内容是123
++ 456或123 456又或者什么都不写,此时程序就会抛出一个ParseException.异常,对于123 ++
456来说,抛出的异常就是:。 -
若输入中的tokens序列跟Start()方法中的规范匹配时,将不抛出任何错误异常,程序正常结束。
但是从我们的代码可以看出,当我们的输入是合法的时候,语法分析器什么也不干,它仅仅用于检查我们的输入是否符合相应的表达式规范而已。在下一小节中,我们将对.jj文件进行一些修改,使得生成的语法分析器有更多的用处。
6.生成的代码解析
想要知道JavaCC生成的语法分析器是如何工作的,我们需要看一些它生成的代码。
final public void Start() throws ParseException {
jj_consume_token(NUMBER);
label_1:
while (true) {
jj_consume_token(PLUS);
jj_consume_token(NUMBER);
switch ((jj_ntk == -1) ? jj_ntk() : jj_ntk) {
case PLUS:
;
break;
default:
jj_la1[0] = jj_gen;
break label_1;
}
}
jj_consume_token(0);
}
jj_consume_token方法以token类型作为入参,并试图从语法分析器中获取指定类型的token,如果下一个获取到的token跟语法分析器中定义的不同,此时就会抛出一个异常。看下面的表达式:
(jj_ntk == -1) ? jj_ntk() : jj_ntk
该表达式计算下一个未读的token。
程序的最后一行是要获取一个为0的token。JavaCC总是使用0来表示EOF的token。