首页 > 代码库 > javacc-LOOKAHEAD MiniTutorial 翻译

javacc-LOOKAHEAD MiniTutorial 翻译

       本文翻译自\javacc-5.0\doc\lookahead.html章节。

上文:http://blog.csdn.net/chaofanwei/article/details/25541065

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个部分,至少指定一个部分,如果同时出现多个部分,则使用逗号分隔。