這是關於編譯原理的第三篇筆記。
編譯有五大步驟,本篇筆記將會講解編譯的第一步:詞法分析。
詞法分析的任務是:從左往右逐個字符地掃描源程序,產生一個個的單詞符號。也就是說,它會對輸入的字符流進行處理,再輸出單詞流。執行詞法分析的程序即詞法分析器,或者說掃描器。閉包
詞法分析的成果就是由一系列單詞符號構成的單詞流。單詞符號其實就是 token,通常有如下五大類:函數
while
,if
,int
等100
,'text'
,TRUE
等+
,*
,/
等具體來講,一個單詞符號在形式上是這樣的一個二元式:(單詞種別,單詞符號的屬性值)
。編碼
單詞種別:spa
單詞種別一般用整數編碼。一個語言的單詞符號如何分種,分紅幾種,怎樣編碼是一個技術問題。它取決於處理上的方便。設計
a
和 b
,可能咱們都只用 1
做爲它們的單詞種別。2
表示,布爾值可能用 3
表示。單詞符號的屬性值3d
由上面的單詞種別能夠知道,關鍵字、運算符、界符基本都是一字(或者一符)對應一個種別,因此只依靠單詞種別便可確切地判斷出具體是哪種單詞符號了。可是標識符和常數卻不是這樣,一個種別可能對應好幾個單詞符號。因此咱們須要藉助單詞符號的屬性值作進一步的區分。指針
對於標識符類型的單詞符號,它的屬性值一般是一個指針,這個指針指向符號表的某個表項,這個表項包含了該單詞符號的相關信息;對於常數類型的單詞符號,它的屬性值也是一個指針,這個指針指向常數表的某個表項,這個表項包含了該單詞符號的相關信息。code
以 while(i>=j)i++
爲例,它的單詞符號流大概以下:blog
<while,-> <(,-> <id,pointer1> <>=,-> <id,pointer2> <),-> <id,pointer3> <++,-> <;,->
注意:實際上,對於關鍵字、界符這些,應該用整數表示單詞種別,不過這裏爲了便於區分,直接用對應的單詞符號表示了。對於標識符,因爲 id 這個單詞種別可能對應多個標識符,因此能夠看到咱們用不一樣的指針進行了標識。其它不須要標識的,則統一用短橫線代替。遞歸
按照咱們常規的想法,應該是詞法分析器掃描整個源程序,產生單詞流,以後再由語法分析器分析生成的單詞。若是是這樣,那麼就說詞法分析器獨立負責了一趟的掃描。但其實,更多的時候咱們認爲詞法分析器並不負責獨立的一趟,而是做爲語法分析器的子程序被調用。也就是說,一上來就準備對源程序進行語法分析,可是語法分析沒法處理字符流,因此它又回過頭調用了詞法分析器,將字符流轉化成單詞流,再去分析它的語法。以此類推,後面每次遇到字符串流,都是這樣的一個過程。
字符流輸入後首先到達輸入緩衝區,在詞法分析器正式對它進行掃描以前,還得先作一些預處理的工做。預處理子程序會對必定長度的字符流進行處理,包括去除註釋、合併多個空白符、處理回車符和換行符等。處理完以後再把這部分字符流送到掃描緩衝區。此時,詞法分析器才正式開始拆分字符流的工做。
詞法分析器對掃描緩衝區進行掃描時通常使用兩個指示器:起點指示器指向當前正在識別單詞的開始位置,搜索指示器用於向前搜索以尋找單詞的終點。問題在於,就算緩衝區再大,也難保不會出現突破緩衝區長度的單詞符號。也就是說,輸入緩衝區把處理好的一段字符流送到掃描緩衝區時,掃描緩衝區可能裝不下這段字符流,在這種狀況下,若是依然只用一個緩衝區存放字符流,可能會致使某個過長的單詞符號沒法被正確讀取。所以,掃描緩衝區最好使用以下一分爲二的區域:
這樣,在搜索指示器向前搜索到 A 半區邊緣時,若是發現尚未找到單詞符號的終點,那麼就會調用預處理程序把剩下的部分送到 B 半區,搜索指示器再來到 B 半區掃描。這樣就能夠避免截斷,從而將這個過長的單詞符號順利銜接起來。若是單詞符號實在太長,兩個半區都沒法解決,那就沒轍了。因此應該對單詞符號的長度加以限制。
像 FORTRAN 這樣的語言,關鍵字不加保護(只要不引發矛盾,用戶能夠用它們做爲普通標識符),關鍵字和用戶自定義的標識符或標號之間沒有特殊的界符做間隔。這使得關鍵字的識別變得很麻煩。好比 DO99K=1,10
和 DO99K=1.10
。前者的意思是,K 從 1 變到 10 以後,跳轉到第 99 行執行;後者的意思是,爲變量 DO99K 賦值 1.10。問題在於,咱們並不能在掃描到 DO
的時候就確定這是一個關鍵字,事實上,它既有多是關鍵字,也有可能做爲標識符的一部分。而具體是哪種,只有在咱們掃描到 =1
後面才能肯定 —— 若是後面是逗號,則這是關鍵字,若是是點號,則是標識符的一部分。
也就是說,咱們須要超前掃描到達第一個界符 =
,可是 =
還不能肯定,再繼續超前掃描到達第二個界符(逗號或者點號),這時候才能徹底肯定。
狀態轉換圖是設計詞法分析程序的一種模型,咱們能夠藉助這個模型體會識別某個特定字符串的過程。它是一張有限方向圖,結點表示狀態,結點之間的箭弧上有字符,表示遇到該字符就將其讀進來,而且轉換到另外一個狀態。如下面這張圖爲例,在狀態 0 下若是輸入的是字母,則將字母讀進來,並進入狀態 1 ;在狀態 1 下若是輸入的是字母或者數字,則將其讀進來並從新進入狀態 1 。不斷重複,直到輸入的不是字母和數字,這時候也將其讀進來,並進入狀態 2。狀態 2 是終態,有一個 *
做爲標記,標記着多讀進來一個不屬於目標的符號,應該把它退還給原輸入串。這張圖實際表示的是標識符類型的輸入串。
狀態轉換圖的結點(狀態)個數是有限的,其中有一個初態,以及至少一個終態(同心圓表示)。
左圖是 FORTRAN 語言的一些單詞符號,右圖是對應的狀態轉換圖:
狀態轉換圖的實現:
好比上面的狀態轉換圖,它的詞法分析器大概以下:
狀態轉換圖是製造詞法分析器的模型,不過這個模型過於具體,咱們應該想個辦法,用一種更接近數學的、更爲形式化的方法來表示狀態轉換圖。而這種狀態轉化圖的形式化表達,就是有限自動機。因爲有限自動機涉及到了正規式、正規集等其它概念,因此咱們這裏先普及一下這些概念。
推導
正規式和正規集都是相對於字母表來講的概念,一般說「xx 字母表的正規式是......,字母表的正規集是......」。對於正規式和正規集,咱們採用遞歸的方式進行定義。即,對於某個給定的字母表 ∑
,規定:
ε
和 Ø
都是該字母表的正規式,這兩個正規式分別表示了 {ε}
和 Ø
這兩個正規集a
都是字母表的正規式,它表示了 {a}
這個正規集a
和 b
都是字母表上的正規式,且分別表示了 L(a)
和 L(b)
這兩個正規集。那麼,(a|b)
,(ab)
,(a)*
也都是正規式,它們分別表明了 L(a)∪L(b)
,L(a)L(b)
和 (L(a))*
這幾個正規集。(笛卡爾積和閉包)根據上面這四條規則,咱們能夠遞歸列舉出某個字母表的正規式和對應的正規集
例如對於給定的字母表 ∑ = {a,b}
,咱們能夠像下面這樣推導出它的正規式和對應的正規集:
ba*
:a
是正規式,因此 a*
也是正規式(規則二),因此 ba*
也是正規式(規則二)。a
表示 {a}
這個集合,加上星號則表示該集合的閉包,b
表示 {b}
這個集合,因此並排放在一塊兒表示兩個集合做笛卡爾積運算
等價的正規式
若是兩個正規式 U
和 V
表示的正規集相同,則認爲這兩個正規式等價,記做 U = V
。例如,b(ab)*
和 (ba)*b
就是等價的兩個正規式。它們表示的集合形如 {ba,bb,bab,babab,babababab,......}
。能夠看出這個集合的元素特色是,以 b
開頭,後面跟着 a 和 b 自由組合的符號串。在沒有引入正規式的概念以前,要表示這樣的集合是比較麻煩的,但如今則方便不少。
正規式運算規則
對於正規式 U
,V
,W
,它們知足下面的運算規律:
一、交換律:U|V=V|U
二、結合律:U|(V|W)=(U|V)|W
三、結合律:(UV)W=U(VW)
四、分配律:U(V|W) = UV|UW
五、分配律: (V|W)U = VU|WU
六、零一概:εU=U
七、零一概:Uε=U
最後再來看一道題:
令 ∑={d,. ,e,+,-},其中d爲 0~9 中的數字,則 ∑ 上的正規式
d*(.dd*|ε)(e(+|-|ε)dd*|ε)
表示的是?
先來劃分結構,以 d*
開頭,說明第一個部分是一個整數,第二個部分是 (.dd*|ε)
,能夠取空,第三個部分是 (e(+|-|ε)dd*|ε)
,一樣能夠取空。若是後面兩個部分都取空,則確定表明一個整數;若是第二個部分不取空,則會出現小數點,代表這時候會是一個小數;若是第三個部分不取空,則會出現 e
,代表這是一個用科學計數法表示的數字。綜上,這個正規式表示的是全部無符號數構成的集合。
有個須要注意的地方是,d*
已經能夠表示全部整數了,爲何小數點後使用的是 dd*
而不是 d*
呢?這裏實際上是起到一個佔位的做用,由於單純用 d*
的話,其實也包括了空符號串,可是既然出現了小數點,後面至少要跟一位數,不能爲空。因此這裏用 dd*
。對於 e
後面也是同理,既然出現了 e
,後面就不能爲空了。
1. 肯定有限自動機的結構
咱們先來回顧一下這副狀態轉換圖:
考慮到要用形式化的方法來表示它,咱們得先考慮轉換圖的一些重要組成因素。
那麼,咱們能夠構造一個有限的狀態集合 S ,用以保存該轉換圖的全部狀態;構造一個有限的字母集合 ∑,用以保存每個輸入的字符;構造包括多個單值映射對 的 δ,每一對都表示從「當前狀態和輸入字符」到「跳轉狀態」的映射關係。具體地說,用 δ(s,a) = a'
表示,當前狀態爲 s
且輸入字符爲 a
時,跳轉到狀態 a'
;此外,須要用來自於狀態集合 S 的 s0 做爲惟一的初態;最後,構造一個終態集合 F,它是 S 的子集,可取空。
這樣,咱們就有了 S,∑,δ,s0,F。這五個元素在一塊兒就構成了咱們要講的是肯定有限自動機。即,肯定有限自動機 DFA 可用以下的五元式表示:
M = {S,∑,δ,s0,F}
2. 肯定有限自動機的其它表示
正如咱們所說的,有限自動機是抽象層面上的形式化表達,而它在具體層面上的表達就是以前所講的狀態轉換圖。另外,肯定有限自動機還能夠用一個矩陣來表示,這樣的矩陣即 狀態轉換矩陣。它的行表示當前狀態,列表示輸入字符,而矩陣元素則表示跳轉狀態,也就是 δ(s,a)
的值。
以 DFA M = ({0,1,2,3,4},{a,b},δ,0,{3})
爲例,若是它的映射以下:
δ(0,a) = 1 δ(0,b) = 2 δ(1,a) = 3 δ(1,b) = 2 δ(2,a) = 1 δ(2,b) = 3 δ(3,a) = 3 δ(3,b) = 3
那麼它的狀態轉換矩陣以下所示:
當前狀態 | a | b |
---|---|---|
0 | 1 | 2 |
1 | 3 | 2 |
2 | 1 | 3 |
3 | 3 | 3 |
3. 肯定有限自動機的做用
肯定有限自動機是狀態轉換圖的形式化表達,它能夠用於識別(或者說讀出、接受)正規集。
對於 ∑* 中的任何一個字 a,若存在一條從初態結點到某一終態結點的通路,且這條通路上全部箭弧的標記符鏈接成的字等於 a,則稱 a 爲 DFA M 所識別(讀出或接受)。
若是 M 的初態結點同時也是終態結點,那麼就說空符號串能夠被 M 所識別。
DFA M 能夠識別的字的全體記爲 L(M)。
看下面的例子:
這是某個肯定有限自動機對應的狀態轉換圖,那麼這個 DFA M 能夠識別什麼樣的正規集呢?咱們能夠先走幾條路線看看(假定在遇到狀態 3 就中止),不難發現它能夠識別出諸如 aa
,bb
,abb
,baa
這樣的符號串。這樣的符號串的特色是,中間要麼是 aa
,要麼是 bb
,因此首先肯定中間是 (aa|bb)
。因爲 aa
和 bb
均可以獨立存在,說明 (aa|bb)
的前面和後面必須能夠是空符號串,說到空符號串,咱們會想到閉包,因此它的前面後面一定會分別出現一個閉包。考慮前面,能夠出現 a
或者 b
,因此前面應該是 (a|b)*
;考慮後面,咱們在遇到狀態 3 的時候就中止了,但實際上,在這以後遇到 a
或者 b
,狀態變化會循環往復,也就是說,無論遇到什麼樣的 ab
組合符號串,都可以被識別並循環轉換到狀態 3,這裏說明後面的狀態是任意的,因此肯定後面是 (a|b)*
。
結合起來,這個有限自動機能夠識別的正規集能夠用正規式 (a|b)*(aa|bb)(a|b)*
表示。
1. 「肯定」和「不肯定」指的是什麼?
「肯定」指的是,五元式中的映射是一個單值函數,也就是說,在當前狀態下,面對某個輸入字符,其跳轉狀態是惟一肯定的,即只會跳轉到某一個值。可是,有的時候映射是多值函數,也就是說,在某個輸入字符下有多個跳轉狀態可供選擇。具備這樣特色的有限自動機,就叫作非肯定有限自動機。
2. 非肯定有限自動機的結構
非肯定有限自動機能夠用以下的五元式表示:
M = {S,∑,δ,s0,F}
3. 非肯定有限自動機的做用
非肯定有限自動機一樣能夠用於識別(或者說讀出、接受)正規集。
對於 ∑* 中的任何一個字 a,若存在一條從初態結點到某一終態結點的通路,且這條通路上全部箭弧的標記符鏈接成的字等於 a,則稱 a 爲 NFA M 所識別(讀出或接受)。
若是 M 的初態結點同時也是終態結點,或者存在一條從某個初態結點到某個終態結點的 ε 通路,那麼就說空符號串 ε 能夠被 M 所識別。(由於輸入符號來自於集合閉包,因此輸入符號接受空符號串 ε)
看下面的例子:
假設有非肯定有限自動機 NFA M=({0,1,2,3,4},{a,b},δ,{0},{2,4})
,其中,
δ(0,a)={0,3} δ(2,b)={2} δ(0,b)={0,1} δ(3,a)={4} δ(1,b)={2} δ(4,a)={4} δ(2,a)={2} δ(4,b)={4}
能夠看到,有很多 δ 是被映射到 S 的一個子集,而不是像肯定 DFA 那樣映射到一個輸入字符。這個 NFA 對應的狀態轉換圖以下:
這裏會發現,這個 NFA 所能識別的正規集和以前的 DFA 是同樣的,都是含有相繼兩個 a 或者相繼兩個 b 的符號串。事實上,儘管 DFA 是 NFA 的特例,可是對於每一個 NFA M,都會有一個 DFA M‘ 與之對應,使得 L(M) = L(M')
。這時候,咱們就說 NFA M 等價於 DFA M’。
非肯定有限自動機的肯定化,指的就是將非肯定有限自動機轉換爲一個與之等價的肯定有限自動機。總的來講分爲兩步,第一步是利用等價轉換規則細化 NFA 狀態轉換的過程;第二步是利用子集法對第一步轉化獲得的 NFA 進行肯定化。因爲第二步又涉及到了一些概念,因此這裏咱們先來對這些概念進行解釋。
(1)空閉包集合
若 I
是一個狀態集合的子集,那麼 I
會有一個空閉包集合,記做 ε-closure(I)
。這個空閉包集合一樣是一個狀態集合,它的元素符合如下幾點:
I
的全部元素都是空閉包集合的元素I
中的每個元素,從該元素出發通過任意條 ε 弧可以到達的狀態,都是空閉包集合的元素如下面這張圖爲例:
ε-closure({5,3,4})
會等於多少呢?這裏的 I
是 {5,3,4}
,因此空閉包集合必定包含了5,3,4。從 5 出發,通過一條 ε 弧到達 6,兩條 ε 弧到達 2,因此 6 和 2 也是閉包集合的元素;從 3 出發,通過一條 ε 弧到達 8,因此 8 也是;從 4 出發,通過一條 ε 弧 7,因此 7 也是。綜上,ε-closure({5,3,4}) = {5,3,4,6,2,8,7}
。
(2)Ia
若 I
是一個狀態集合的子集,那麼它相對於狀態 a
的 Ia
等於 ε-closure(J)
。其中,J
表示的是,從 I
中每一個狀態出發,通過標記爲 a 的單條弧而到達的狀態的集合。也就是說,Ia
表示的是從 I
中每一個狀態出發,通過標記爲 a 的弧而到達的狀態,再加上從這些狀態出發,通過任意條 ε 弧可以到達的狀態。
仍是以這幅圖爲例:
當 I
是 {1,2}
的時候,Ia
等於多少呢?
Ia
。從 5,4 出發,通過 ε 弧可以到達 6,2,7,因此 6,2,7 屬於 Ia
Ia
。從 3 出發,通過 ε 弧可以到達 8,2,7,因此 8 屬於 Ia
綜上,Ia = {5,4,6,2,7,3,8}
下面,介紹具體的肯定化過程。
第一條和第二條都好理解,重點在第三條規則。爲何右邊的圖能夠等價於左邊的圖呢?A*
其實表示的是相似 {ε,A,AA,AAA,AAAA,......}
這樣的集合,由於 A
自由組合造成的符號串是能夠用一個 A
的自循環來表示的,因此中間有一個自循環,而 ε 則能夠用 εε 來表示,因此考慮在先後各加一個 ε,對於 A
的符號串不影響。
子集法的核心是,針對上面規則轉換後獲得的 NFA,畫出它的狀態轉換矩陣,這個矩陣的矩陣元素是映射的子集,不是單值,而咱們要作的事情就是把這個子集用一個單值來表示。也就是說,對於 NFA 的每一組映射狀態集,都用一個來自 DFA 的映射單值與之對應,從而求出等價的 DFA。
假設通過第一步,咱們已經獲得下面的 NFA:
選取 NFA 的初態集合的空閉包做爲初始集合 I
,這個集合 I
將是 ε-closure({i}) = {i,1,2}
。同時因爲輸入符號只有 a 和 b,因此第二列爲 Ia
,第三列爲 Ib
。獲得以下這個表:
Ia |
Ib |
|
---|---|---|
{i,1,2} |
根據前面的說法求解 Ia
和 Ib
。從 i 出發沒有 a 弧,無視之;從 1 出發通過 a 弧 到達 1,從 2 出發通過 a 弧到達 3;從 1 出發通過 ε 弧到達 2,從 1 出發沒有 ε 弧。因此,Ia = {1,2,3}
。從 i 出發沒有 b 弧,無視之;從 1 出發通過 b 弧到達 1,從 2 出發通過 b 弧到達 4。從 1 出發通過 ε 弧到達 2,從 4 出發沒有 ε 弧,因此 Ib = {1,2,4}
。記新獲得的兩個
集合爲 A 和 B,獲得下面的表:
Ia |
Ib |
|
---|---|---|
{i,1,2} |
A:{1,2,3} |
B:{1,2,4} |
將新獲得的集合 A 和 B 做爲第一列的元素,獲得下面的表:
Ia |
Ib |
|
---|---|---|
{i,1,2} |
A:{1,2,3} |
B:{1,2,4} |
A:{1,2,3} |
||
B:{1,2,4} |
分別對 A 集合和 B 集合求解對應的 Ia
和 Ib
,獲得下表(對於一樣形式的集合仍採起以前命名,僅對新出現集合給定新的命名):
Ia |
Ib |
|
---|---|---|
{i,1,2} |
A:{1,2,3} |
B:{1,2,4} |
A:{1,2,3} |
C:{1,2,3,5,6,f} |
B:{1,2,4} |
B:{1,2,4} |
A:{1,2,3} |
D:{1,2,4,5,6,f} |
將新獲得的 C、D 集合做爲第一列的元素,一樣求解 Ia
和 Ib
,獲得下面的表:
Ia |
Ib |
|
---|---|---|
{i,1,2} |
A:{1,2,3} |
B:{1,2,4} |
A:{1,2,3} |
C:{1,2,3,5,6,f} |
B:{1,2,4} |
B:{1,2,4} |
A:{1,2,3} |
D:{1,2,4,5,6,f} |
C:{1,2,3,5,6,f} |
C:{1,2,3,5,6,f} |
E:{1,2,4,6,f} |
D:{1,2,4,5,6,f} |
F:{1,2,3,6,f} |
D:{1,2,4,5,6,f} |
同理,繼續推導,直到再也沒有新集合出現:
Ia |
Ib |
|
---|---|---|
{i,1,2} |
A:{1,2,3} |
B:{1,2,4} |
A:{1,2,3} |
C:{1,2,3,5,6,f} |
B:{1,2,4} |
B:{1,2,4} |
A:{1,2,3} |
D:{1,2,4,5,6,f} |
C:{1,2,3,5,6,f} |
C:{1,2,3,5,6,f} |
E:{1,2,4,6,f} |
D:{1,2,4,5,6,f} |
F:{1,2,3,6,f} |
D:{1,2,4,5,6,f} |
E:{1,2,4,6,f} |
F:{1,2,3,6,f} |
D:{1,2,4,5,6,f} |
F:{1,2,3,6,f} |
C:{1,2,3,5,6,f} |
E:{1,2,4,6,f} |
如今,用字母命名代替全部的集合(初始集合給定名字 S
),獲得下面的矩陣:
Ia |
Ib |
|
---|---|---|
S | A | B |
A | C | B |
B | A | D |
C | C | E |
D | F | D |
E | F | D |
F | C | E |
這個矩陣實際上已是一個 DFA 矩陣。咱們再以初始集合 S
將做爲初態,包含原始 NFA 終態的集合(即 C、D、E、F)做爲終態,畫出它對應的狀態轉換圖,以下:
那麼,這個轉換圖實際上就是與最初 NFA 等價的 DFA 所對應的轉換圖了,到這裏,咱們就完成了對非肯定有限自動機進行肯定化的工做了。
最後咱們再對這篇筆記涉及的知識點作一下回顧。首先咱們解釋了詞法分析的結果,也就是單詞符號,以後講解了一些詞法分析過程當中的要點(預處理、超前掃描),最後則是本篇筆記的重點,詞法分析的模型,包括狀態轉換圖以及它的形式化表達 —— 有限自動機。
到這裏,詞法分析的內容尚未結束。剩下的內容咱們將在下一篇筆記中繼續講解。