([”0”-”9”])+>}
第一行说明了空格是一个token,但是会被忽略。
所以解析器并不会收到任何单独的空格。
第二行也说了差不多的事情,只不过被忽略的是换行符,换行符会因操作系统而不同。
Unix/Linux采用LF(linefeed)字符;DOS和Windows则用CR+LF(carriage+linefeed),在老的Macintoshes机子上,就用一个回车表示。
我们要告之JavaCC所有的可能,就如上面用一个小竖线”|”把不同的匹配模式隔开。
第三行告诉JavaCC一个单独的加号是一个token,而且给这个Token取了一个名字:
PLUS。
最后一行告诉JavaCC数字的语法并为它们取名为NUMBER。
如果你熟悉Perl或者Java的正则表达式包,就不难明白这些式子的含义。
让我们仔细看一下这个表达式([“0”-“9”])+。
圆括号中的[“0”-“9”]是一个匹配任意数字的正则表达式,这表明unicode编码中的0-9之间的字符都能被匹配。
一个形如(x)+的正则式可以匹配任意重复的x串。
所以表达式([“0”-“9”])+就可以匹配任意连续数字串。
这四行每一行都是一个正则表达式实例(regularexpressionproduction)。
还有一种由词法分析器生成的token,它的名字是EOF,正如其名,它代表了输入的终止。
不能,也不需要任何对EOF的匹配,JavaCC会自动生成它们。
考虑一个包含如下字符串的输入文件:
“123+456\n”
我们定义的词法分析器将会找到7个token:
NUMBER,空格,PLUS,又一个空格,另一个数字,一个换行,然后是EOF。
当然,标记了SKIP的token不会被传到解析器。
所以,解析器只会看到这些东西:
NUMBER,PLUS,NUMBER,EOF
设想一个包含未定义字符的输入文件,例如:
“123–456\n”
在处理完第一个空格之后,我们的可爱的词法分析器将遇到一个不认识的字符:
减号。
由于没有任何token的定义是以减号打头,词法分析器会扔出一个TokenMgrError异常。
现在我们看看另一种情况:
“123++456\n”
我们的词法分析器会提交一个这样的串:
NUMBER,PLUS,PLUS,NUMBER,EOF
词法分析器还没有智能到判断一个token序列是否有意义,这通常是语法分析器的工作。
我们接下来要讨论的解析器会在词法分析器提交第二个PLUS之后发觉这个错误,然后拒绝处理之后的任何token。
所以解析器实际上处理的只有:
NUMBER,PLUS,PLUS
同时,跳过(skip)一个token并不代表忽略(ignore)它。
考虑下列输入:
“123456\n”
词法分析器会识别出3个token:
两个NUMBER和夹在它们中间的空格;然后报错。
2.3. 详述语法分析器语法分析器的定义使用了一种叫BNF范式的东西,这看起来有点像Java的方法定义:
voidStart():
{}
{
(
)*
}
这个BNF范式声明了一个正确的输入序列的模式。
我们解释一下它的意思:
它以NUMBER开头的序列,以EOF结束,中间存在零个或多个由一个PLUS后面跟一个NUMBER组成的子序列。
正如所见,语法分析器只会检查一个输入序列是否合法,而并没有真的把数字加起来。
待会儿我们还会修改这个语法分析器,但现在我们先让它生成Java组件,然后run起来。
2.4. 生成一个解析器和一个词法分析器我们现在用JavaCC根据我们写好的adder.jj文件生成分析器。
具体怎么做依赖于操作系统。
下面是在WindowsNT,2000和XP上完成的。
首先使用“命令提示符”程序(CMD.EXE)运行JavaCC:
2010-12-1319:
23
回复
叶流征
41位粉丝
铁杆会员
8
4楼
D:
\home\JavaCC-Book\adder>javaccadder.jj
JavaCompilerCompilerVersion2.1(ParserGenerator)
Copyright(c)1996-2001SunMicrosystems,Inc.
Copyright(c)1997-2001WebGain,Inc.
(type"javacc"withnoargumentsforhelp)
Readingfromfileadder.jj...
File"TokenMgrError.java"doesnotexist.Willcreateone.
File"ParseException.java"doesnotexist.Willcreateone.
File"Token.java"doesnotexist.Willcreateone.
File"SimpleCharStream.java"doesnotexist.Willcreateone.
Parsergeneratedsuccessfully.
这个操作生成了七个Java类,每一个在独立的文件中:
l TokenMgrError是一个简单的错误类;词法分析器用它来侦测错误,父类是Throwable.
l ParserException是另一个错误类;解析器用它侦测错误,父类是Exception,因此也是Throwable的子类。
l Token是一个表示token的类。
每个Token对象都有一个整数域kind表示token的类型(PLUS,NUMBER,或者EOF),和一个String域image,存储token所代表的内容。
l SimpleCharStream是一个把字符串提交给词法分析器的接口转换类。
l AdderConstants是一个接口,定义了一组在词法分析器和解析器中都要用到的类。
l AdderTokenManager就是词法分析器。
l Adder是解析器。
现在我们可以用一个Java编译器编译这些类了:
D:
\home\JavaCC-Book\adder>javac*.java
2.5. 让它跑起来现在我们换个角度来看Adder类的main方法。
staticvoidmain(String[]args)
throwsParseException,TokenMgrError{
Adderparser=newAdder(System.in);
parser.Start();
}
最先注意到main可能会抛出继承自Throwable的两个子类(译注:
TokenMgrError和ParserException)中的任意一个。
这风格不是很好,我们应该捕捉这些异常。
但是为了保持第一个例子简洁(译注:
为了让读者能迅速把握要点,而不是陷入无穷的细节之中),我们忽略了这些东西。
第一个语句创建了一个解析器实例,构建函数使用了自动生成的接受一个java.io.InputStream的重载。
其实还有一个(更好的)接受Reader实例的重载(java建议在处理字符串时尽量使用Reader(Writer)而不是InputStream(OutputStream),这样能更好的避免字符编码带来的问题——译者如是说)。
这个构建函数创建了一个SimpleCharStream对象和一个词法分析器AdderTokenManager的实例。
这样,词法分析器通过SimpleCharStream顺利地获取到了我们的输入。
第二句调用了一个由JavaCC生成的方法Start()。
对语法规范中的每个BNF产生式,JavaCC都会生成一个对应的方法。
这个方法负责尝试在输入序列中寻找符合模式的输入。
例如,调用Start时会使解析器试图寻找一个匹配下面模式的输入序列:
()*
我们可以准备一个合适的输入然后运行这条命令
D:
\home\JavaCC-Book\adder>javaAdder我们运行程序,输入表达式以后,会出现以下三种不同的情况:
1. 出现词法错误。
本例中,词法错误只出现在遇到未知字符时。
我们可以通过下面的输入引发一个词法错误:
“123-456\n”
这种情况下,程序会抛出一个TokenMrgError异常。
这个异常的message域是:
Exceptioninthread“main”TokenMgrError:
Lexicalerroratline1,column5.Encountered:
“-“(45),after:
“”
2. 出现一个解析错误。
这发生在输入序列不符合Start的BNF范式时。
例如
2010-12-1319:
23
回复
叶流征
41位粉丝
铁杆会员
8
5楼
“123++456\n”
或者
“123456\n”
或者
“\n”
这时,程序会扔出一个ParseException异常。
这种异常的第一条信息分别是:
Exceptioninthread“main”ParseException:
Encountered”+”atline1,column6.
Wasexpecting:
...
3. 输入串符合Start的定义。
这时,程序不抛出任何异常,只会默默的停止。
由于解析器除了挑错什么都不做,所有现在这个程序除了检查输入合法性以外什么都做不了。
在下一节,我们将会做一些改变让它更有用。
2.6. 生成的代码为了了解JavaCC生成的代码是如何工作的,最好的办法是看看它生成的代码。
finalpublicvoidStart()throwsParseException{
jjconsumetoken(NUMBER);
label1:
while(true){
jjconsumetoken(PLUS);
jjconsumetoken(NUMBER);
switch((jjntk==-1)?
jjntk():
jjntk){
casePLUS:
;
break;
default:
jjla1[0]=jjgen;
breaklabel1;
}
}
jjconsumetoken(0);
}
方法jj_consume_token将试图从输入中读取一个指定类型的token,如果得到的token与期望的类型不符,则抛出一个异常。
表达式
(jj_ntk==-1)?
jj_ntk():
jj_ntk
计算下一个未读token的类型。
而最后一行则要求匹配一个类型0的token;JavaCC总是用0来编码EOF类型。
2.7. 增强解析器像上文中提到的start方法一样的,由JavaCC根据BNF文法生成的方法,在默认情况下仅仅是检查了输入是否符合规则。
但是我们可以在BNF中间夹杂Java代码,这些代码将来会被包含在生成的方法中。
JavaCC为我们提供了一个骨架,而我们要让它有血有肉。
下面我们改变adder.jj中的BNF规范,为Start添加一些声明和Java代码。
新的文件叫做adder1.jj。
添加或改变的部分用黑体标出:
intstart()throwsNumberFormatException:
{
Tokent;
inti;
intvalue;
}
{
t=
{i=Integer.parseInt(t.image);}
{value=i;}
(
t=
{i=Integer.parseInt(t.image);}
{value+=i;}
)*
{returnvalue;}
}
首先,我们定义了BNF产生式的返回类型,这样生成的方法就从void变为int。
然后还声明了NumberFormatException可能会在运行时抛出。
我们定义了三个变量。
变量t是一个Token,Token是一个生成的类用来表示token;Token类的image域记录了匹配的字符串。
当一个token匹配上了一个BNF产生式,我们就能通过赋上一个引用来记下这个Token对象。
像这样
t=
我们可以在BNF产生式的大括号里添加任意的Java语句,这些语句会原封不动的copy到生产的代码里面。
由于更改了Start的返回类型,我们有必要更改一下我们的main函数:
staticvoidmain(String[]args)
throwsParseException,TokenMgrError,NumberFormatException{
Adderparser=newAdder(System.in);
intval=parser.Start();
System.out.println(val);
}
在结束这个例子前,我们再做一点小小的改进。
下面的代码在start中出现了两次:
t=
{i=Integer.parseInt(t.image);}
虽然在这个例子中不会引起太大的差异,仅仅涉及两行代码,但这种重复会导致维护的问题。
所以我们把这两行提出来作为另一个BNF产生式,叫做Primary。
最新的修改依旧用黑体标出。
intstart()throwsNumberFormatException:
{
inti;
intvalue;
}
{
value=Primary()
(
2010-12-1319:
23
回复
叶流征
41位粉丝
铁杆会员
8
6楼
i=Primary()
{value+=i;}
)*
{returnvalue;}
}
intPrimary()throwsNumberFormatException:
{
Tokent;
}
{
t=
{returnInteger.parseInt(t.image);}
}
这时我们再来看看JavaCC所生成的代码:
finalpublicintStart()throwsParseException,NumberFormatException{
inti;
intvalue;
value=Primary();
label1:
while(true){
switch((jjntk==-1)?
jjntk():
jjntk){
casePLUS:
;
break;
default:
jjla1[0]=jjgen;
breaklabel1;
}
jjconsumetoken(PLUS);
i=Primary();
value+=i;
}
jjconsumetoken(0);
{if(true)returnvalue;}
thrownewError(”Missingreturnstatementinfunction”);
}
finalpublicintPrimary()throwsParseException,NumberFormatException{
Tokent;
t=jjconsumetoken(NUMBER);
{if(true)returnInteger.parseInt(t.image);}
thrownewError(”Missingreturnstatementinfunction”);
}
待会儿我们还能看到如何向BNF产生式传递参数。
3. 第二个例子:
运算器接下来,我们继续改进我们的adder,使它成为一个简易的四则运算计算器。
第一步,我们让它能够和我们进行交互,把每行作为一个单独的表达式,并计算输出。
稍后,我们会考虑加法之外的其他操作,减法,乘法和除法。
3.1. 选项和类定义calculator0.jj的开头如下:
/*calculator0.jjAninteractivecalculator.*/
options{
STATIC=false;
}
PARSERBEGIN(Calculator)
importjava.io.PrintStream;
classCalculator{
staticvoidmain(String[]args)
throwsParseException,TokenMgrError,NumberFormatException{
Calculatorparser=newCalculator(System.in);
parser.Start(System.out);
}
doublepreviousValue=0.0;
}
PARSEREND(Calculator)
类Calculator的previousValue域用于保存前一行的计算结果,我们的下一版本将允许在表达式中使用美元符号($)表示这个值。
import语句可以写在PARSER_BEGIN和PARSER_END之间,他们将被复制到生成的类文件中,包定义同样也在这时声明。
3.2. 词法定义词法定义的改变不大,