1 目的
語法分析是根據源語言的語法規則從源程序記號序列(詞法分析階段的輸出)中識別出各種語法成分,同時進行語法檢查,為語義分析和代碼生成做准備。
2 方法
對記號序列自左向右掃描,每次讀一個記號。文法推導是一棵分析樹,如果匹配成功,終結符是葉子結點連起來的輸入串。
2.1 自頂向下分析:面向目標分析法 就是用文法硬湊與輸入符號匹配的句子
2.1.1 遞歸下降預測分析 (一種不確定的,帶回溯的預測分析方法)
一種沒什么卵用的高代價窮舉試探方法,然鵝卻和我們的腦回路最接近
下面,試圖分析輸入串abbcde是否是以下文法的句子。
概括一下就是從起始符S開始,面對非終結符要展開的時候,盡可能地湊當前指針所指的輸入串字符。這種只顧眼前利益不顧子孫后代幸福的方法,后面一定會付出慘痛代價。顯然,很大可能后面會回溯,代價太大。
2.1.2 遞歸調用預測分析(確定的無回溯分析)
1 文法要求
當然想確定每次展式,文法必須無左遞歸且提取左公因子后無二義性。這些要求說白了就是想讓非終結符為了最左邊(此時的指針位置)與輸入串匹配成功,選擇的推導式是唯一的。嚴謹一點說,就是文法中的任意非終結符A,定義A的某候選式的開頭終結符號集如下:
為了不產生提取左公因子后的二義性,要求對於A任意的候選式和
,都有:
注意:若候選式可以經過有限次推導推出ε,則其FIRST集含ε。
注:對形式語言不太熟悉的小伙伴,這里回顧一下消左遞歸的方法(想得美,我才懶得寫呢 只寫核心的一個變化步驟:
2 狀態轉換圖
1)構造
文法的每個非終結符產生式對應一個自動機,由初始狀態S出發,尋找狀態轉移過程,分以下三種情況:
第一種容易理解,第二種本質上是中間一段的序列被A終結符推導匹配,然后回到T。第三種表示如果S無法匹配當前a且有空轉移,則到T狀態進行匹配。
2)化簡
將輸入自身到達的狀態可以和初始狀態進行合並,不同狀態在輸入相同到達狀態相同時,也可進行合並,還有空轉移也需注意。下面舉個栗子:
3 利用狀態轉移圖寫出分析程序
這種與其啰里八嗦寫一堆,當然不如看個栗子自己頓悟。先上化簡完的狀態圖 :
則E的過程:
void procE(void){ procT(); if(char == '+'){ forward pointer; procE(); } }
T的過程:
void procT(void){ procF(); if(char == '*'){ forward pointer; procT(); } }
是不是很簡單呀,這里自行嘗試寫F的程序吧。沒有答案(我是內種寫完兩個簡單的就跑路的小編么?咳咳,這是F,自行對照:
void procF(void){ if(char == '('){ forward pointer; procE(); if(char == ')') forward pointer; else error(); } else if(char == 'id') forward pointer; else error(); }
請自行思考一下,為什么只有F的代碼里有錯誤處理error().別看了,這回真是讓自行思考,hhh
2.1.3 非遞歸預測分析(確定,無遞歸,無回溯)
1 非遞歸模型
還記得算法里面,把一個遞歸程序改成非遞歸程序,思維上是件不太容易的事情,非遞歸程序在問題理解上也沒有遞歸程序清晰明了,但是非遞歸程序開銷更小,效率更高。探索非遞歸來代替遞歸是一件可以,且有必要的事。話不多說,先上非遞歸模型:
em,一個看起來像是圖靈機,又不知所謂的圖
反正看起來核心的預測程序,要從緩存區去輸入字符,查找分析表M之后,借助棧進行操作最后得到輸出。(這不廢話么 緩沖區沒什么好說的,分析棧棧底是$,里面還可以放文法的任意終結符和非終結符。M是二維表[A,a],一個坐標是非終結符,另一坐標是終結符或者$,表項內容是一個動作。輸出其實是剛剛用到的推導式。假設當前棧頂為X,輸入為a,則有以下四種情況以及相應處理過程。
(我是真懶,懶得打,hhh)下面更懶 再貼個栗子:有如下預測分析表
分析id + id * id的過程:
灰常長,自己跳着隨便看看,看跟自個兒想的一不一樣就行,畢竟過程還是非常easy的嘛。所以現在的關鍵問題就變成了,這張表是怎么變出來的。
2 構造M分析表
1)構造FIRST集合
還記得FIRST么,這里沿用定義,只不過把FIRST的作用范圍從A的候選式擴展為任意文法符號串。那怎么寫出FIRST的全集呢?
(貼圖警告
好長一段,反正就是除了終結符自個兒也是自個兒first,以及定義中的基本情況,推導式終結符前綴是first以外,如果推導式前綴是個非終結,再去找那個非終結的first,考量的當前前綴有ε的話,在找下一個非終結的first。如果全有ε,再把ε加進自個兒的first。其實挺容易想明白的。還在糾結的話,看栗子。(我真是個花里胡哨的沙雕
文法: FIRST表
2)構造FOLLOW集合 (什么?!first后面居然不是second?)
FOllOW呢,就是說該文法中所有句型中(划重點),緊跟在A之后出現的終結符號或者$組成的集合。
怎么構造呢?(貼圖沒差
這次的主要思路是在所有產生式的右邊找自個兒,找到之后看自己后邊都有啥。后面是終結符就加進去,不是就想辦法蹭非終結符的first。如果后面有ε,還能碰瓷產生式左邊那個非終結符的follow。所以顯然follow里面沒有ε。讓我們看一下first栗子里的那個文法的follow表:
已經懶到中間內行first都懶得刪了
3)構造分析表M
栗子大法好,實踐出真知。我們把剛剛first和follow的栗子在這里再用一下,得到M:
細心的朋友們這里會發現,咦,納尼?怎么[S', e]有倆推導式呢,那程序看到這個不是就懵逼了么?對呀,這誰的鍋呢,既然是標答,那肯定不是我們造表的鍋啊,對,沒錯,是文法的鍋(題給的文法是二義性文法)。一個好的文法造出來的表不應該讓預測分析程序懵逼,我們給這種好的文法(無二義性文法)起名叫做LL(1)文法。(這名字是不是起得及其詭異?L(從左到右掃描輸入串)L(生成輸入符號串的一個最左推導)1(程序每步動作,向前看一個符號)就這么來的唄
那這玩意兒有沒有啥實際點兒的判斷方法呢?首先不能左遞歸,然后:
2.2 自底向上分析(太長了太長了,下一節專門講)
3 語法錯誤恢復策略(出現錯誤時,為了使分析繼續進行)
3.1 緊急恢復:分析程序每次拋棄一個輸入記號,直到向前指針所指向的記號屬於某個指定的同步記號集合(適當選取,一般包括結束符分號,end等)
3.2 短語級恢復:出錯后對剩余輸入做局部糾正,替換剩余輸入串的前綴。例如分號代替逗號,刪除多余分號,插入遺漏的分號等。(分號大法好)
3.3 出錯產生式:增加產生錯誤結構的產生式,擴充源語言的文法。
3.4 全局糾正:做盡可能少的修改,使得輸入串語法正確。處於理論階段。就說真要有這么完美的,為啥前面還啰嗦一堆
目前為止,是不是3.1簡單一點,靠譜一點?我們以3.1為例,描述一種非遞歸預測分析的錯誤處理方案。3.1的關鍵在於選取(構造)同步記號。還記得非遞歸預測分析什么情況可以判斷語法錯誤么?
針對(1):將棧頂的終結符號彈出即可
針對(2):向前移指針,移到此時的棧頂非終結符和剩余輸入符號串可以重新協同工作(就是M表里他倆不為空了)。實現方法是改造M,如果非終結符A的follow里有b,然后M里的[A,b]還是空,就在這個位置添加同步記號synch。然后如果遇到情形(2),就前移指針,若[A,a]是synch,就把A從棧頂彈出。是不是還有些細節懵懵的?栗子栗子栗子!
湊活看吧,我累了,嗚嗚嗚嗚
注:本文章所有知識內容和樣例均來源於編譯技術與原理 (李文生) 第2版