这篇博客主要是记录一下Javacc的一些知识点,和我在编写中遇到的问题
建议1:使用之前请下载官网实例学习下,感觉对于javacc的编写,其实没有全面的指导。。so sad..
建议2:不要用javacc写c++的语法,用yacc和lex可能会更好?
文章主要分为以下三个部分以及附录:
-------------------------------------------------------------------------------
附:参考链接和学习资料
1. 安装和使用
2. 词法文件编写说明
3. 语法文件编写说明
-------------------------------------------------------------------------------
附录
Javacc下载源码:https://javacc.java.net/
javacc的安装:http://blog.csdn.net/pngfiwang/article/details/49182433
javacc建议安装(译文):http://www.oschina.net/question/5189_8814
编译原理之Javacc使用:http://www.tuicool.com/articles/rYvAVvr
javacc研究与应用(学习的时候看到过):http://www.cnblogs.com/Gavin_Liu/archive/2009/03/07/1405029.html
javacc jjtree 写法 以及 jj写法 基本语法 以及应用(很详细):http://blog.csdn.net/zyb243380456/article/details/7240225
javacc-LOOKAHEAD MiniTutorial 翻译:http://www.tuicool.com/articles/QRzmee
javacc入门(一个小知识):http://zhoujinhuang.iteye.com/blog/169252
抽象语法树(这篇不是说javacc的,而是单纯说语法树的,可参考): http://blog.csdn.net/zhouhuozhi/article/details/4250258
正则表达式30分钟入门(正则在词法书写中尤其重要):http://www.jb51.net/tools/zhengze.html
github c的javacc:https://github.com/jeffrycopps/compiler-C-javaCC
javacc for C++ : http://jmvanel.free.fr/parse/javacc-cpp.html
c++ parser (和前面的很像,但是都不全): http://star.fbk.eu/Internal/CodeAnalysisPlatform/CppParser/
官网上发布的一些jj和jjt文件:https://java.net/projects/javacc/downloads/directory/contrib/grammars
安装和使用
1.在JavaCC首页http://javacc.java.net/上下载最新版的JavaCC。配置环境变量,在dos命令行里就可以使用了,这个方法似乎对我的电脑失效了。。而且这种方法我觉得不是很方便
2.这里我们使用第二种,安装eclipse插件的方法;刚好我使用的是eclipse,所以就很棒啦
Step1:下载
Step2:eclipse安装新的插件
Step3:
Step4: 选择后,就会在上图中间框中显示“JavaCC Eclipse Plug-in“;如果不能FQ,可能会卡在下一步。。我也不是很确定了,但是可以吧那个Contact那个选项取消勾选,然后直接next就行。
Step5:到这里就可以用了,但是想要编辑框
以及这种高亮表示
就要先建立一个新的jjt文件,然后就自动会出现了
事先说明下文件结构
一个jj/jjt文件,先有option部分,定义一些设置,然后就是
PARSE_BEGIN(你要生成的java文件名,这里以JavaParser为例子)
package ..
import …
基本上就是你怎么写java怎么写这个部分,在生成文件的时候,这部分会原样生成到JavaParser.java文件里;
在这里我们可以写一写可以操作TOKEN的函数
PARSE_END(JavaParser)
TOKEN定义
语法定义
词法文件编写说明
这个我其实真的的不会,但是好办的是,基本上大部分语言的词法分析都有人替我做完了 ,哈哈哈哈哈哈。不过你要能看懂,这样你就好改。。我在做c++的语法树的时候,遇到不少问题,不得不弄懂啊。。
在开源中国社区上,有一个讲解,讲的就是jj文件的编写。是一篇翻译的文章,我觉得写的很好哦!小白入门级,转载如下(参考和转载的文章都在附录,这个转载我会稍加修改或者注释)
JavaCC是一个解析器生成器和词法分析生成器。解析器和词法分析器用于处理输入的字符串。编译器和解释器被用来和解析器/词法分析器一起处理文件中的程序。但是解析器/词法分析器在实际中有更加广泛的应用,正如我在本文中希望介绍的一样。 那么,什么是解析器/词法分析器?词法分析器可以吧一个字符串分离成若干叫做“Token”的子字串,并同时对这些Token进行分类。考虑下面的程序:
int main(){
return 0;
}
一个C语言的词法分析器将会把这段代码分离成下列子串:
int \s main ( ) \s { \n \t
return \s 0 \s ; \n } \n \s
同时,它会对这些子串分类,在本例中分类结果是:
KWINT SPACE ID OPAR CPAR SPACE OBRACE SPACE SPACE
KWRETURN SPACE OCTALCONST SPACE SEMCOLON SPACE CBRACE SPACE EOF
EOF表示文件(输入)结束,这些Token串将会被送到解析器
如果输入不符合目标词法和词法时,词法分析器和解析器同时也负责生产错误信息。 JavaCC本身并不是一个词法分析器或者解析器而是一个代码生成器,这意味着它可以根据输入的语言定义输出一个词法分析器和解析器。JavaCC输出的代码是合法的可编译Java代码。 解析器和词法分析器本身就是一个冗长而复杂的组件,手工编写一个这样的程序需要仔细考虑各条件的相互作用,例如分析C语言时,处理整数的代码不可能和处理浮点的代码相互独立,因为整数和浮点数的开头都是数字。而使用像JavaCC这样的分析器生成器时处理整数的规则和处理浮点数的规则是分开书写的,而两者之间的共享代码在代码生成是被自动添加了。这就增强了程序的模块性,同时也意味着定义文件较手工编写的Java代码来说更加易于编写,阅读和更改。通过 JavaCC这样的解析器生成器,程序员们能节省更多时间,同时也能增加编写的软件的质量。
第一个例子——我们来做加法吧!
作为第一个例子,我们将计算一连串整数的加法,请看下面的例子:
99+42+0+15
我们忽略所有数字和符号间的空格和换行符,除此之外,我们不接受除了10个数字和加号之外的其他字符。 这一节的剩下的部分中的代码都是文件adder.jj的一部分。这个文件包含了符合JavaCC词法的解析器/词法分析器的定义,并且将作为JavaCC 程序的输入文件。
选项和类定义
这个文件的第一部分是:
/*adder.jj 吧一堆数字相加*/
options{//option是选项,这是你对整个jj文件的配置参数设置的地方,关于这个static,有必要提一下,就是一旦static就不能更改了嘛,这可能会导致你在程序中,无法调用两次jj生成的文件来解析不同的文件。所以一般要多次解析都要设置成FALSE
STATIC = false;
}
PARSER_BEGIN(Adder) //这里面的“Adder”就是后续你生成的文件的前缀
public class Adder{
public static void main(String[] args) throws ParseException, TokenMgrError{//作者这里没有加public,这会在某些情况下产生错误(译注)
Adder parser = new Adder(System.in);
parser.Start();//方法名竟然是大写开头的,真不地道(翻译吐槽)
}
}
PARSER_END(Adder)
开头部分的options节说明了除了我们明确指定的STATIC选项,所有其他的JavaCC选项为都默认值。关于 JavaCC选项的详细信息,请参考JavaCC文档。接下来我们定义了一个名为Adder的Java类,但是我们并没有写出这个类的全部代码,JavaCC会在处理时自动生成其他的代码。main方法声明抛出的ParserException和TokenMgrError有可能在执行这些代码时被抛出。
指定一个词法解析器吧!
我们待会儿在看那个main函数,现在我们首先来定义一个词法分析器。在这个简单的例子中,词法分析器的定义只有下面4行:
//说白了,token的生成就是一个替换过程,把你的程序替换成我们规定的一些固定的字符表示
SKIP:{“ “}
SKIP:{“\n”|”\r”|”\r\n”} // SKIP远比这个要有用,后面会提到
TOKEN:{< PLUS : “+”>} // + 号替换成 PLUS
TOKEN:{< NUMBER : ([“0”-“9”])+ >} //感觉这个TOKEN的写法比较像正则表达式
* 第一行说明了空格是一个token,但是会被忽略。于是乎解析器并不会收到任何单独的空格。
* 第二行也说了差不多的事情,只不过被忽略的是换行符,我们用一个小竖线分割了不同的匹配模式。
* 第三行告诉JavaCC一个单独的加号是一个token,并且给这个Token一个名字:PLUS。
* 最后一行叫JavaCC吧连续的数字当作一个token,命名为NUMBER,如果你使用过Perl或者 Java的正则表达式库,就应该能明白定义的含义。让我们仔细看一下这个表达式([“0”-“9”])+。圆括号中间的部分[“0”-“9”]是一个匹配所有数字字符的正则表达式(不过正则表达式好像不用引号 ——译者吐槽),这表明所有unicode中的0-9之间的支付都能被匹配。其他的部分:(x)+可以匹配一连串符合模式x的字符。所以表达式 ([“0”-“9”])+就可以匹配一个或者多个连续的数字。这四行中的每一行都被称作一个“正则表达式结果(regular expression production)”
还有另一种可以被词法分析器生成的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序列是否有意义,这通常是解析器的工作。我们接下来要定义的解析器会找到这个有两个加号的错误,然后完美的罢工。所以解析器实际上处理的只有:
NUMBER,PLUS,PLUS
同时,跳过(skip)一个token并不代表忽略(ignore)。考虑下列输入:
“123 456\n”
词法分析器会发现3个token:两个NUMBER和一个空格。然后解析器又会优美的罢工了……
出现吧,我的解析器!
解析器的定义使用了一种叫BNF范式的东西,这看起来有点像Java的方法定义:
void Start(): //马勒戈壁,BS不按标准写代码的(神秘的译者再吐槽)
{
//这里随便写什么都可以,就是一些预定义需要在这里定义,比如要用到的变量啊什么的,这个部分对应到生成的Java文件里,就是Start函数的开头的语句,而且是原样复制,所以写法和Java文件木有区别
}
{
//这里写了什么,token序列就要消耗掉一个token,所以当文件被解析成token序列之后,就会去匹配这里的token标记,那么这里要输出或者写java语句,就要用{}括起来,如下
{
//这里也可以写Java语句,也是原样复制,生成java文件之后你看下它生成的位置就理解了。里面不能乱写东西,我一般是写一下输出语句,检查下语法的分支
}
<NUMBER>
(
<PLUS>
<NUMBER>
)*
<EOF>
}
这个BNF范式声明了一个有效的token序列的模式,从而避免了错误的语法。我们研究一下它的意思:一个以 NUMBER开头的序列,以EOF结束,中间存在若干以一个PLUS和一个NUMBER组成的子序列。 正如所见,一个解析器仅仅决定了一个输入序列是否合法,而没有吧数字们实际上加起来。待会儿我们会来调教这个解析器好让他能够好好的干活,但是首先我们先让我们目前的成果跑一下吧!
开始炼成解析器和词法分析器咯!
我们已经有一个叫adder.jj的文件了,接下来我们用JavaCC进行提炼。原作者罗嗦了一堆OS 相关的玩意儿我们掠过不表,直接看就行了:
E:\javacc-book>javacc adder.jj
Java Compiler Compiler Version 4.2 (Parser Generator)
(type "javacc" with no arguments for help)
Reading from file adder.jj . . .
File "TokenMgrError.java" does not exist. Will create one.
File "ParseException.java" does not exist. Will create one.
File "Token.java" does not exist. Will create one.
File "SimpleCharStream.java" does not exist. Will create one.
Parser generated successfully.
这个操作生成了7个Java类,每个都在独立的java文件中:
* TokenMgrError是一个简单的错误类;用于表示词法分析器参数的错误,父类是java.lang.Throwable
* ParserException 是另一个代表错误的异常类;表示解析器罢工的情况,父类是java.lang.Excpetion
* Token是表示token的类,每个Token对象都有一个int类型的字段:kind,表示它的类型(PLUS,NUMBER或者EOF)(其实应该用enum的,不过JDK1.5以前是没有的,所以将就了吧——翻译的技术性吐槽)和一个String类型的字段:image,存储了token所代表的内容,这里的内容也可修改哦。
* SimpleCharStream 是一个辅助类,用于吧输入的字符串传给词法分析器,一般就没改过
* AdderConstants 是一个包含了常量的辅助性接口,里面可以查看到自己定义的所有token
* AdderTokenManager 就是传说中的词法分析器咯
* Adder就是可爱的解析器,这个文件超级重要,而且和jj文件联系最大,对照两个文件,不理解的都理解了;而且这个文件其实可编辑,只不过要小心。
现在我们可以把它们编译一下咯~
E:\javacc-book>javac *.java
注意:Adder.java 使用了未经检查或不安全的操作。
注意:要了解详细信息,请使用 -Xlint:unchecked 重新编译。
终于要运行咯!
现在我们来回头看看 Adder这个类吧。
static void main(String[] args) throws ParseException,TokenMgrError{
Adder parser = new Adder(System.in);
parser.Start();//方法名竟然是大写开头的,真不地道(翻译吐槽)
}
首先注意,这里的方法声明直接抛出了ParseException和TokenMgrError,事实上这不是一个好习惯,我们应该捕捉这些异常并进行一定的处理(哪怕是简单的报错),但是为了节约篇幅,这些玩意儿统统不要了。
* 第一个语句创建了一个解析器实例,构建函数使用了自动生成的接受一个java.io.InputStream的重载。其实还有一个(更好的)接受Reader实例的重载(java建议在处理字符串时尽量使用 Reader(Writer)而不是InputStream(OutputStream),这样能更好的避免字符编码带来的问题——翻译如是说)。这个构建函数创建了一个SimpleCharStream对象和我们的词法分析器AdderTokenManager的实例。于是乎,词法分析器通过 SimpleCharStream顺利的获取到了我们的输入。
* 第二句调用了一个由 JavaCC生成的方法Start(),对于每个BNF范式来说JavaCC都会生成一个对应的方法。这个方法负责尝试在输入序列中寻找符合模式的输入,例如,调用Start时会使解析器试图寻找一个匹配下面模式的输入序列:
<NUMBER>(<PLUS><NUMBER>)*<EOF>
让我们来准备一个合适的输入然后运行这个程序吧!
E:\javacc-book>java Adder
我们运行程序,输入表达式以后,会出现下面3中不同情况:
1. 出现词法错误:本例中,词法错误只出现在遇到未知字符时,我们可以通过下面的输入引发一个词法错误:
“123-456\n”
这种情况下,程序会完美的罢工并扔个 TokenMrgError出来以示抗议,这个异常的message是:
Lexical error at line 1, column 4. Encountered: "-" (45), after : ""
2. 出现一个解析错误:这发生在输入序列不符合Start的BNF范式时,例如
“123++456\n”
或者
“123 456\n”
或者
“\n”
这时,程序会扔一个ParseException出来,像这样:
Exception in thread "main" ParseException: Encountered " <NUMBER> "1 "" at line 2, column 1.
Was expecting one of:
<EOF>
"+" ...
3. 输入的串完美的符合了Start的定义,这时,程序什么都不做(囧——翻译又忍不住吐槽了)
由于解析器除了挑错什么都不做,所有现在这个程序除了检查输入合法性以外什么都做不了,在下一节,我们将调教这个程序让他能为我们做我们爱做的事情,敬请期待,次回もお楽しみに。
看一下我们的劳动成果吧!
要了解JavaCC生成的代码是如何工作的,最好的办法是看看他生成的代码是什么。
final public void Start() throws ParseException {
jj_consume_token(NUMBER); //jj_consume_token超级重要的!基本上就是,每个token的消耗就是利用这个函数,里面填写的是你要消耗的TOKEN类型,就在AdderConstants里面定义的,事实上,NUMBER代表的就是这个token的整数值
label_1:
while (true) {
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
case PLUS:
;
break;
default:
jj_la1[0] = jj_gen;
break label_1;
}
jj_consume_token(PLUS);
jj_consume_token(NUMBER);
}
jj_consume_token(0); //这个表示消耗EOF,EOF默认为0
}
方法jj_consume_token将试图从输入中读取一个指定类型的token,如果得到的token与期望的类型不符,则抛出一个异常。表达式(jj_ntk==-1)?jj_ntk():jj_ntk计算下一个未读token的类型。而最后一行则要求匹配EOF的token。
让我们调教解析器吧!
向上文中提到的start方法一样的,由JavaCC根据BNF文法生成的方法,在默认情况下仅仅是检查了输入是否符合规则,但是我们可以教BNF做更多事情,我们可以在BNF中间夹杂Java代码,JavaCC为我们提供了骨骼,而我们要为他提供肌肉。 下面我们来给adder.jj中的BNF做些许改动,添加的代码用黑体表示:(讲真的,范式我真的懂得不多,所以这里就不讲解了,感觉正常读就好了)
int start() throws NumberFormatException:
{
Token t;
int i;
int value;
}
{
t=<NUMBER>
{i=Integer.parseInt(t.image);} //看见了吧,要做什么动作,都要用{}包裹起来
{value=i;}
(
<PLUS>
t=<NUMBER>
{i=Integer.parseInt(t.image);}
{value+=I;}
)*
<EOF>
{return value;}
}
首先,我们定义了BNF结果的返回类型,然后还声明了NumberFormatException可能在处理时抛出。然后我们定义了一个叫t的Token变量,我们想要获取BNF匹配结果时可以这样用:
t=<NUMBER>
在BNF中的大括号里,我们可以在里面写任何Java语句,这些语句会原封不动的copy到生产的代码里面。
由于更改了start的返回类型,我们有必要更改一下我们的 main函数:
public static void main(String[] args) throws ParseException,TokenMgrError{
Adder parser = new Adder(System.in);
System.out.println(parser.Start());//方法名竟然是大写开头的,真不地道(翻译吐槽)
}
在结束这个例子前,我们再做一点小小的改进,下面的代码在start中出现了两次:
{i=Integer.parseInt(t.image);}
{value=i;}
为了避免代码重复,最好把它们独立出来,我们把提取出来的范式称作Primary,那么我们应该如此这般的更改我们的代码:
int start() throws NumberFormatException:
{
Token t;
int i;
int value;
}
{
value=Primary()
(
<PLUS>
i= Primary()
{value+=I;}
)*
<EOF>
{return value;}
}
int Primary() throws NumberFormatException:
{
Token t;
}
{
t=<NUMBER>
{return Integer.parseInt(t.image);}
}
这时我们再来看看JavaCC所生成的代码:
final public int start() throws ParseException, NumberFormatException {
Token t;
int i;
int value;
value = Primary();
label_1:
while (true) {
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
case PLUS:
;
break;
default:
jj_la1[0] = jj_gen;
break label_1;
}
jj_consume_token(PLUS);
i = Primary();
value+=I;
}
jj_consume_token(0);
{if (true) return value;}
throw new Error("Missing return statement in function");
}
final public int Primary() throws ParseException, NumberFormatException {
Token t;
t = jj_consume_token(NUMBER);
{if (true) return Integer.parseInt(t.image);}
throw new Error("Missing return statement in function");
}
待会儿我们还能看到如何向BNF传递参数。
我觉得到这里,基本的东西都已经涉及了,考虑到篇幅,我就不往下粘贴复制了,到时候看链接就好了,后面还有更重要的呢!
关于学习资料,就是下载的javacc-6.0的里面的examples里面,可以看到很多例子:
我们来分析下java1.5.jj的词法部分
SKIP部分:
/* WHITE SPACE */
SKIP :
SKIP: { " " | "\t" | "\n" | "\r" | "\f" | "\b" } /* COMMENTS */ /* 在一个token后面打:然后接一个带有标记的词法组,就表示遇到这个这标志然后转到这个词法组;比如遇到"/*"然后一直到遇到IN_MULTI_LINE_COMMENT为止,IN_MULTI_LINE_COMMENT就是结束注释的“*/”*/ MORE : { <"/**" ~["/"]> { input_stream.backup(1); } : IN_FORMAL_COMMENT | "/*" : IN_MULTI_LINE_COMMENT } SPECIAL_TOKEN : { <SINGLE_LINE_COMMENT: "//" (~["\n", "\r"])* ("\n" | "\r" | "\r\n")?> } <IN_FORMAL_COMMENT> SPECIAL_TOKEN : { <FORMAL_COMMENT: "*/" > : DEFAULT } /*关于DEFAULT我也不是很能摸清,但是不是很影响;这个也不需要你来定义*/ <IN_MULTI_LINE_COMMENT> SPECIAL_TOKEN : { <MULTI_LINE_COMMENT: "*/" > : DEFAULT } <IN_FORMAL_COMMENT,IN_MULTI_LINE_COMMENT> MORE : { < ~[] > } /* RESERVED WORDS AND LITERALS */
TOKEN定义法:
TOKEN : { < ABSTRACT: "abstract" > | < ASSERT: "assert" > | < BOOLEAN: "boolean" > | < BREAK: "break" > | < BYTE: "byte" > | < CASE: "case" > | < CATCH: "catch" > | < CHAR: "char" > | < CLASS: "class" > | < CONST: "const" > | < CONTINUE: "continue" > | < _DEFAULT: "default" > | < DO: "do" > | < DOUBLE: "double" > | < ELSE: "else" > | < ENUM: "enum" > } TOKEN : { < CHARACTER : ("L")? "‘" ( (~["‘","\\","\n","\r"]) | ("\\" ( ["n","t","v","b","r","f","a","\\","?","‘","\""] | "0" (["0"-"7"])* | ["1"-"9"] (["0"-"9"])* | ("0x" | "0X") (["0"-"9","a"-"f","A"-"F"])+ ) ) )* "‘" > | < STRING : ("L")? "\"" ( ( ~["\"","\\","\n","\r"]) | ("\\" ( ["n","t","v","b","r","f","a","\\","?","‘","\""] | "0" (["0"-"7"])* | ["1"-"9"] (["0"-"9"])* | ("0x" | "0X") (["0"-"9","a"-"f","A"-"F"])+ ) ) )* "\"" > } ...
到这里,词法文件的种种我们就说得差不多了,还有更细致的东西,我自己也不是很懂啦,多看看我给的链接吧。最后来说下怎么应用;就是怎么写token生成入口:
我们最后编写完成之后,就会生成很多文件了:
生成的文件如下,其中JavaCharStream.java|JavaParserTokeeManager.java|ParseException.java|TokenFactory.java|TokenMgrError.java基本都不用管,主要来分析下JavaParser.java和Token.java以及来看下Constants.java
JavaParserConstants.java,全都是我们定义的TOKEN的,在JavaParser.java文件里,jj_consume_token常常会用到这些定义的常量
Token.java:
在new JavaParser(需要token分析的字符串,或者说程序源码),就会自动生成这个文件或者字符串的token序列了,我们利用token.next和getNextToken()【getNextToken()是JavaParserTokenManager里面的】
小写的token应该是java文件自己定义的,表示当前的token,我们可以利用token.next来得到第一个token
然后也可以用getNextToken()
语法文件编写说明
在熟悉了词法文件和jj文件的结构之后,讲实话,语法树文件基本就没特别多好讲的了,一般语法分析我们用jjt文件来写,但是内容和jj差不多,为啥呢,因为jjt会生成jj..
我们举一个最简单的例子,在javacc-6.0里面被提供
option{ 略 } PARSER_BEGIN(Eg1) /** An Arithmetic Grammar. 首先new Eg1 () 生成了token序列,然后调用Start()进入语法分析入口,重点看Start()函数*/ public class Eg1 { /** Main entry point. */ public static void main(String args[]) { System.out.println("Reading from standard input..."); Eg1 t = new Eg1(System.in); try { SimpleNode n = t.Start(); n.dump(""); System.out.println("Thank you."); } catch (Exception e) { System.out.println("Oops."); System.out.println(e.getMessage()); e.printStackTrace(); } } } PARSER_END(Eg1) /*词法分析就直接跳过了*/ SKIP : { " " | "\t" | "\n" | "\r" | <"//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")> | <"/*" (~["*"])* "*" (~["/"] (~["*"])* "*")* "/"> } TOKEN : /* LITERALS */ { < INTEGER_LITERAL: <DECIMAL_LITERAL> (["l","L"])? | <HEX_LITERAL> (["l","L"])? | <OCTAL_LITERAL> (["l","L"])? > | < #DECIMAL_LITERAL: ["1"-"9"] (["0"-"9"])* > | < #HEX_LITERAL: "0" ["x","X"] (["0"-"9","a"-"f","A"-"F"])+ > | < #OCTAL_LITERAL: "0" (["0"-"7"])* > } TOKEN : /* IDENTIFIERS */ { < IDENTIFIER: <LETTER> (<LETTER>|<DIGIT>)* > | < #LETTER: ["_","a"-"z","A"-"Z"] > | < #DIGIT: ["0"-"9"] > } /** Main production. 从这里开始就算是语法部分了,Start是总节点,必须要返回jjtThis这种奇怪的东西,哈哈,然后调用了Expression()感觉语法书的写法就是层级式调用*/ SimpleNode Start() : {} { Expression() ";" { return jjtThis; } } /** An Expression. */ void Expression() : {} { AdditiveExpression() } /** An Additive Expression. */ void AdditiveExpression() : {} { MultiplicativeExpression() ( ( "+" | "-" ) MultiplicativeExpression() )* } /** A Multiplicative Expression. */ void MultiplicativeExpression() : {} { UnaryExpression() ( ( "*" | "/" | "%" ) UnaryExpression() )* } /** A Unary Expression. */ void UnaryExpression() : {} { "(" Expression() ")" | Identifier() | Integer() } /** An Identifier. 在这个函数里面,我们就是消耗一个<IDENTIFIER>的token然后这个函数就结束了*/ void Identifier() : {} { <IDENTIFIER> } /** An Integer. */ void Integer() : {} { <INTEGER_LITERAL> }
有几点要说明:
1. 以其中一个函数为例说明下
void UnaryExpression() #AAAAAAA: //这个#AAAAAA啥意思呢,就是给这个函数节点命名的,在jjt里面,如果这个函数被调用了,会自动生成同名字的节点,然后存储到树里;如果我们使用#给它重命名,这个节点名字就是AAAAAA了;如果是#void,那么默认不生成这个节点到语法树,仅仅是调用而已。
{//这里面是可以写东西的,是定义变量的地方,前面讲词法的时候提到过的}
{//这里面是写规则的地方,这里面重点不少,前面也讲过,如果要输出java语句,还要在里面用{}括起来
"(" Expression() ")" | Identifier() | Integer()
}
2. LOOKAHEAD
LOOKAHEAD博大精深,我已经死在里面了。。。
这个大家多看链接吧;我说下我浅薄的理解;LOOKAHEAD呢,就像试婚一样;在匹配的时候,有可能出现两个分支它们可能前段相同后段不同或者说有冲突;比如匹配 (“A|B”)+”B”和 (“A|C”)+”A|B” 那你说 AAB到底算哪个呢?或者说我不确定这个分支我的token序列能否正确完全匹配,也可以用LOOKAHEAD提前看看这个函数能否正确执行;要是不,大家一拍两散不过了呗,换个分支
下面转载部分翻译的文章片段
1、LOOKAHEAD是什么
lookahead就是当语法分析器从词法分析器里取token时,需要取多少个才能让分析器正确的走下去。
例一
void Input() : {} { "a" BC() "c" } void BC() : {} { "b" [ "c" ] }
在这个语法中,只能匹配“abc”和“abcc”。
假设我们现在把 “abc”当成输入字符,来看javacc是如何工作的。
1、第一个字符是‘a’,这里没什么疑问,匹配“a”
2、读取下一个字符‘b’,毫无疑问进入BC(),刚好匹配‘b’
3、读取下一个自称‘c’,在这一步,我们看到了一个选择点,即是匹配[‘c‘]呢,还是跳出 BC(),匹配最后一个‘c’。这里假定我们选择了[...],那么继续往下走。
4、因为现在我们已经跳出了BC(),但是Input说现在还需要一个‘c’,但我们已经没有字符了,因此宣告失败。
5、在遇到这种问题时,就说明我们在前面的选择点的地方可能选择了一个错误的决定,因此需要回溯到[...]
6、这个时候我们就应该选择Input里面的‘c’,这时候才能正确执行。
2、javacc里面的选择点
可以将javacc中choice point归结为以下几类:
l . 由选择算子 | 引入的冲突,
2. 由可省略算子 [] 或 ?引入的冲突
3. 由重复算子 * 引入的冲突
4. 由重复算子 + 引入的冲突
3、默认的选择点算法
看语法:
TOKEN [IGNORE_CASE] : { <ID: (["a"-"z"])+> } void basic_expr() : {} { <ID> "(" expr() ")" // Choice 1 | "(" expr() ")" // Choice 2 | "new" <ID> // Choice 3 }
if (next token is "(" ) {
choose Choice 2
} else if (next token is " new ") {
choose Choice 3
} else if (next token is <ID> ) {
choose Choice 1
} else {
produce an error message
}
上面语法是没有什么冲突的,假如改成如下语法:
void basic_expr() : {} { <ID> "(" expr() ")" // Choice 1 | "(" expr() ")" // Choice 2 | "new" <ID> // Choice 3 | <ID> "." <ID> // Choice 4 }
就会报如下冲突信息:
arning: Choice conflict involving two expansions at line 25, column 3 and line 31, column 3 respectively. A common prefix is: <ID> Consider using a lookahead of 2 for earlier expansion.
意思就是说在默认的选择算法在这种情况下不能正确执行,因为1和4都是以ID开头的,这就是我们上面说的 左因子 。
4、选择点冲突解决算法
1.修改语法,使之成为LL(1)语法。
2.只要将LOOKAHEAD=k写到Options块中即可,javacc产生的分析器就可以分析LL(K)语法生成的语言
采用第一种方法的好处是效率非常高,易于维护。采用第二种方法的好处是语法更加直观,但是却不易维护。有时候采用第一种方法是无法解决冲突的,第二种方法是唯一的选择。
5、设置全局LOOKAHEAD
全局的lookahead是在jj文件中的options中指定的,可以指定为非负整数,javacc自动生成LL(K)算法。这种方法是不提倡的,因为这会在每个选择点都进行 LL(K)算法,即多向前看k个token,但大部分选择点都是一个(默认)就可以了。
假定这时把lookahead设置成2,那么在上面的3中的第二个文法就会变成:
当下来的两个token是<ID> 和"("时,那么选择点1,
如果下来的两个token是<ID> and "."时,那么就选择点4。
这样就能让上面的语法正常执行。
6、设置局部LOOKAHEAD
可以通过设置局部的lookahea方法,使语法分析器只在需要的时候向前看K个字符,别的情况下只用看一个就可以了,这种情况下,效率自然比通过全局设置好。
可以把上面语法改下:
void basic_expr() : {} { LOOKAHEAD(2) <ID> "(" expr() ")" // Choice 1 | "(" expr() ")" // Choice 2 | "new" <ID> // Choice 3 | <ID> "." <ID> // Choice 4 }
通过以上设置,只使第一个选择点使用LOOKAHEAD(2)。这种情况下工作逻辑如下:
if (next 2 tokens are <ID> and "(" ) { choose Choice 1 } else if (next token is "(") { choose Choice 2 } else if (next token is "new") { choose Choice 3 } else if (next token is <ID>) { choose Choice 4 } else { produce an error message }
7、语法上的LOOKAHEAD
看语法:
void TypeDeclaration() : {} { ClassDeclaration() | InterfaceDeclaration() }
这里假定ClassDeclaration定义为在class的前面可以出现无数多次的public,final,而InterfaceDeclaration的定义也是前面可以出现出现无数多次的public,final。那么问题就出现了,因为当分析器在工作时,并不知道到底有多少个public或者fianl,也就不知道到底需要向前看多不个token,才能确定到底是选择ClassDeclaration还是InterfaceDeclaration。
显然简单的方法就是向前看无数多个,如下:
void TypeDeclaration() : {} { LOOKAHEAD(2147483647) ClassDeclaration() | InterfaceDeclaration() }
但这样显示是不合理的,合理的做法应该是下面的方法:
void TypeDeclaration() : {} { LOOKAHEAD(ClassDeclaration()) ClassDeclaration() | InterfaceDeclaration() }
意思就是说,还是一直向前看,如果ClassDeclaration()能够匹配成功,则就用ClassDeclaration(),否则的话进入InterfaceDeclaration()。即:
if (the tokens from the input stream match ClassDeclaration) { choose ClassDeclaration() } else if (next token matches InterfaceDeclaration) { choose InterfaceDeclaration() } else { produce an error message }
当然还有一种 优化的方法 ,见下:
void TypeDeclaration() : {} { LOOKAHEAD( ( "abstract" | "final" | "public" )* "class" ) ClassDeclaration() | InterfaceDeclaration() }
if (the nest set of tokens from the input stream are a sequence of "abstract"s, "final"s, and "public"s followed by a "class") { choose ClassDeclaration() } else if (next token matches InterfaceDeclaration) { choose InterfaceDeclaration() } else { produce an error message }
即如果下面的一些列token匹配( "abstract" | "final" | "public" )* "class",那么就选择ClassDeclaration(),否则选择InterfaceDeclaration()。
当然还有一种 更加优化的方法 ,见下:
void TypeDeclaration() : {} { LOOKAHEAD(10, ( "abstract" | "final" | "public" )* "class" ) ClassDeclaration() | InterfaceDeclaration() }
在这种情况下,具体的工作过程就是,这时lookahead最多向前看10个token,如果超过10token后,还匹配( "abstract" | "final" | "public" )* "class"的话,那么就进入选择点ClassDeclaration()。
其实当不设置10的时候,默认的值就是最大值,即2147483647。
8、语义上的LOOKAHEAD
看语法:
void Input() : {} { "a" BC() "c" } void BC() : {} { "b" [ "c" ] }
为了解决上面说到的回溯问题,我们可以使用如下的语法:
void BC() : {} { "b" [ LOOKAHEAD( { getToken(1).kind == C && getToken(2).kind != C } ) <C:"c"> ] }
意思就是:
if (next token is "c" and following token is not "c") { choose the nested expansion (i.e., go into the [...] construct) } else { go beyond the [...] construct without entering it. }
首先把‘c’封装成一个label C,便于我们在lookahead里面引用它,即如果下来的第一个token是C和第二个不是C,那么选择[..],否则跳出BC。
其实上面的改写还可以使用下面的形式:
void BC() : {} { "b" [ LOOKAHEAD( "c", { getToken(2).kind != C } ) <C:"c"> ] }
即同时使用语法和语义上的lookahead。第一个c为语法上的lookahead,第二个为语义上面的lookahead。
9、LOOKAHEAD结构总结
通用的格式为:
LOOKAHEAD( amount, expansion, { boolean_expression } )
amount:即设置向前看的token个数;
expansion:即指定的语法上的lookahead,
boolean_expression:即指定的语义上的lookahead。
格式中的3个部分,至少指定一个部分,如果同时出现多个部分,则使用逗号分隔。
这也基本上是我理解的LOOKAHEAD,别看说的好像很明了,我自己写的时候,遇到了很多问题,为此想出了很多奇葩的方法。。
LOOKAHEAD({ getToken(1).kind == STD }) //这个居然也可以,我也是服! getToken(1)表示当前token
LOOKAHEAD({ getToken(0).kind == TYPEDEF && getToken(1).kind == ID }) 【= LOOKAHEAD(<TYPEDEF>|<ID>)但是我这样当时出了点问题,没办法。。唉,不知道哪里错了】
有一点要注意,如果你的函数写得不是很符合javacc的语法规范,那么这个函数在LOOKAHEAD(函数)的时候,可能会造成莫名其妙的错误,总之最好还是符合规范吧。。
最后就是要注意冲突,查问题的时候可以看看在对应的java(我们的例子比如JavaParser.java)生成的函数,然后就可以理解了。
终于写完了,呼~~