在这个例子中,咱们将判断以下输入的式子是不是一个合法的加法运算:java
99 + 42 + 0 + 15
而且在输入上面式子的时候,数字与加号之间的任何位置,都是能够有空格或者换行符的,也就说,即便咱们输入的式子是下面这种形式,咱们所编写的词法和语法分析器也应该要能判断出来它是一个合法的加法运算表示形式:正则表达式
99 + 42 + 0 + 15
(注:上面输入的式子中既有空格,也有制表符,还有换行符)windows
语法描述文件的第一部分是:函数
/* 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)块。编码
咱们在后面还会再讲到main方法。这里让咱们先看看词法描述器。当前例子中所需的词法描述器经过以下的四行来进行描述:3d
SKIP : { " "} SKIP : { "\n" | "\r" | "\r\n" } TOKEN : { < PLUS : "+" > } TOKEN : { < NUMBER : (["0"-"9"])+ > }
“123 + 456\n”
词法分析器将解析的7个token,依次是NUMBER、空格、PLUS、空格、NUMBER、换行符、EOF。在解析出来的这些token中,被标记为SKIP的token将不会被往下传递给语法分析器。词法分析器在分析完成以后,只会将如下token传递给语法分析器:NUMBER, PLUS, NUMBER, EOF。
再假设一种不合法的输入,以下:code
“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。blog
语法分析器的描述由BNF生产式构成。能够看到,语法分析器的描述看起来跟java的方法定义形式有点类似。token
void Start() : {} { <NUMBER> ( <PLUS> <NUMBER> )* <EOF> }
上面的BNF生产式指定了合法的token序列的规则:必须以NUMBER token开头,以EOF token结尾,而在NUMBER和EOF中间,能够是0至多个PLUS和NUMBER的token,并且必须是PLUS后跟着NUMBER。
根据上面的语法描述器语法,解析器将只检测输入序列是否无错误,它不会把数字加起来。咱们接下来将很快修改解析器描述文件以更正此问题,可是首先,让咱们生成Java组件并运行它们。
将前面部分提到的几个部分都合并起来,保存成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文件。以下所示:
其中:
接下来咱们对这些java文件进行编译:
编译完成以后,可获得对应的class文件:
如今,让咱们来看看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中状况之一:
想要知道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。