javacc-LOOKAHEAD MiniTutorial 翻譯

chaofanwei發表於2014-05-11

       本文翻譯自\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個部分,至少指定一個部分,如果同時出現多個部分,則使用逗號分隔。

 

 

 

 






 

 

 

相關文章