1. 程式人生 > 其它 >詞法分析筆記-1

詞法分析筆記-1

 

一、詞法分析簡介

1.1.詞法分析實現

手寫詞法分析器

如果要手動地實現詞法分析器, 首先建立起每個詞法單元的詞法結構圖或其他描述會有所幫助。

優點:執行效率高

缺點:程式碼複雜

2.詞法分析生成工具

這種方法使得修改詞法分析器的工作變得更加簡單, 因為我們只需改寫那些受到影響的模式, 無 需改寫整個程式

優點:實現簡單、易維護、可擴充套件

缺點:依賴工具

正則表示式->NFA->DFA

詞法分析是編譯的第一階段。 詞法分析器的主要任務是讀入源程式的輸入字元、 將它們組成詞素, 生成並輸出一個詞法單元序列.

每個詞法單元對應於一個詞素。 這個詞法單元序列被輸出到語法分析器進行語法分析。 詞法分析器通常還要和符號表進行互動。 當詞法分析器發現了 一個識別符號的詞素時, 它要將這個詞素新增到符號表中。 在某些情況下, 詞法分析器會從符號表中讀取有關識別符號種類的資訊, 以確定向語法分析器傳送哪個詞法單元。

有時, 詞法分析器可以分成兩個級聯的處理階段: 1) 掃描階段主要負責完成一些不需要生成詞法單元的簡單處理,比如刪除註釋和將多個連續的空白字元壓縮成一個字元。 2) 詞法分析階段是較為複雜的部分, 它處理掃描階段的輸出並生成詞法單元。

1.2.詞法分析及語法分析

把編譯過程的分析部分劃分為詞法分析和語法分析階段有如下幾個原因: 1) 最重要的考慮是簡化編譯器的設計。 將詞法分析和語法分析分離通常使我們至少可以簡化其中的一項任務。 2) 提高編譯器的效率。 把詞法分析器獨立出來使我們能夠使用專用於詞法分析任務、 不進行語法分析的技術。 3) 增強編譯器的可移植性。 輸入裝置相關的特殊性可以被限制在詞法分析器中

1.3.詞法單元、 模式和詞素

詞法單元由一個詞法單元名和一個可選的屬性值組成。 詞法單元名是一個表示某種詞法單位的抽象符號, 比如一個特定的關鍵字, 或者代表一個識別符號的輸入字元序列。 詞法單元名字是由語法分析器處理的輸入符號。 在後面的內容中, 我們通常使用黑體字給出詞法單元名。 我們將使用詞法單元的名字來引用一個詞法單元。

模式描述了一個詞法單元的詞素可能具有的形式。 當詞法單元是一個關鍵字時, 它的模式就是組成這個關鍵字的字元序列。 對於識別符號和其他詞法單元, 模式是一個更加複雜的結構, 它可以和很多符號串匹配。

詞素是源程式中的一個字元序列, 它和某個詞法單元的模式匹配,並被詞法分析器識別為該詞法單元的一個例項。

在很多程式設計語言中, 下面的類別覆蓋了大部分或所有的詞法單元:

1)關鍵字: 每個關鍵字有一個詞法單元。 一個關鍵字的模式就是該關鍵字本身。

2)運算子:表示運算子的詞法單元。 它可以表示單個運算子, 也可以表示一類運算子。

3)識別符號: 一個表示所有識別符號的詞法單元

4)常量: 一個或多個表示常量的詞法單元, 比如數字和字面值字串。

5) 標點符號:每一個標點符號有一個詞法單元, 比如左右括號、 逗號和分號。

二、輸入緩衝

由於在編譯一個大型源程式時需要處理大量的字元, 處理這些字元需要很多的時間, 因此開發了一些特殊的緩衝技術來減少用於處理單個輸入字元的時間開銷。

一種重要的機制就是利用兩個交替讀入的緩衝區, 我們可以使用系統讀取命令一次將N個字元讀入到緩衝區 中, 而不是每讀入一個字元呼叫一次系統讀取命令。 如果輸入檔案中的剩餘字元不足N個, 那麼就會有一個特殊字元(用eof表示) 來標記原始檔的結束。 這個特殊字元不同於任何可能出現在源程式中的字元

程式為輸入維護了兩個指標:

1) lexemeBegin指標: 該指標指向當前詞素的開始處。 當前我們正試圖確定這個詞素的結尾。

2) forward指標: 它一直向前掃描, 直到發現某個模式被匹配為止。 一旦確定了下一個詞素, forward指標將指向該詞素結尾的字元。 詞法分析器將這個詞素作為某個返回給語法分析器的詞法單元的屬性值記錄下來。 然後使lexemeBegin指標指向剛剛找到的詞素之後的第一個字元。

三、詞法單元的規約

正則表示式是一種用來描述詞素模式的重要表示方法。 雖然正則表示式不能表達出所有可能的模式, 但是它們可以高效地描述在處理詞法單元時要用到的模式型別。

3.1 串和語言

字母表(alphabet) 是一個有限的符號集合。 符號的典型例子包括字母、 數位和標點符號。 集合{0, 1}是二進位制字母表(binary alphabet) 。 ASCII是字母表的一個重要例子

某個字母表上的一個串(string) 是該字母表中符號的一個有窮序列。 在語言理論中, 術語“句子”和“字”常常被當作“串”的同義詞。

串s的長度, 通常記作|s|, 是指s中符號出現的次數。 例如, banana是一個長度為6的串。 空串(empty string) 是長度為0的串, 用∈ 表示。

語言(language) 是某個給定字母表上一個任意的可數的串集合。這個定義非常寬泛。 根據這個定義, 像空集 和僅包含空串的集合{∈ }都是語言。

3.1.1.與串相關的常用術語:

1) 串s的字首(prefix) 是從s的尾部刪除0個或多個符號後得到的串。 例如, ban、 banana和∈ 是banana的字首。

2) 串s的字尾(suffix) 是從s的開始處刪除0個或多個符號後得到的串。 例如, nana、 banana和∈ 是banana的字尾。

3) 串s的子串( substring) 是刪除s的某個字首和某個字尾之後得到的串。 例如, banana、 nan和∈ 是banana的子串。

4) 串s的真( true) 字首、 真字尾、 真子串分別是s的既不等於∈ , 也不等於s本身的字首、 字尾和子串。

5) 串s的子序列(subsequence) 是從s中刪除0個或多個符號後得到的串, 這些被刪除的符號可能不相鄰。 例如, baan是banana的一個子序列。

3.2 語言上的運算

在詞法分析中, 最重要的語言上的運算是並、 連線和閉包運算。

令L表示字母的集合[A-Za-z], 令D表示數位的集合[0-9]。

1) L∪ D是字母和數位的集合——嚴格地講, 這個語言包含62個長度為1的串, 每個串是一個字母或一個數位。

2) LD是包含520個長度為2的串的集合, 每個串都是一個字母跟一個數位。

3) L4是所有由四個字母構成的串的集合。

4) L*是所有由字母構成的串的集合, 包括空串∈

5) L(L∪ D) *是所有以字母開頭的, 由字母和數位組成的串的集合。

6) D+是由一個或多個數位構成的串的集合。

3.3 正則表示式

正則表示式可以描述所有通過對某個字母表上的符號應用這些運算子而得到的語言。 在這種表示法中, 如果使用letter來表示任一字母或下劃線,用digit來表示數位, 那麼可以使用如下的正則表示式來描述對應於C語言識別符號的語言: letter(letter| digit)*

上式中的豎線表示並運算, 括號用於把子表示式組合在一起, 星號表示“零個或多個”括號中表達式的連線, 將letter和表示式的其餘部分並列表示連線運算。 正則表示式可以由較小的正則表示式按照如下規則遞迴地構建。

每個正則表示式r表示一個語言L(r) , 這個語言也是根據r的子表示式所表示的語言遞迴地定義的。 下面的規則定義了某個字母表Σ上的正則表示式以及這些表示式所表示的語言

歸納步驟: 由小的正則表示式構造較大的正則表示式的步驟有四個部分。 假定r和s都是正則表示式, 分別表示語言L(r) 和L(s) , 那麼:

1) (r) |(s) 是一個正則表示式, 表示語言L(r) ∪ L(s) 。

2) (r) (s) 是一個正則表示式, 表示語言L(r) L(s) 。

3) ( r) *是一個正則表示式, 表示語言( L( r) ) *。

4) ( r) 是一個正則表示式, 表示語言L( r) 。 最後這個規則是說在表示式的兩邊加上括號並不影響表示式所表示的語言。

按照上面的定義, 正則表示式經常會包含一些不必要的括號。 如果我們採用如下的約定, 就可以丟掉一些括號: 1) 一元運算子*具有最高的優先順序, 並且是左結合的。 2) 連線具有次高的優先順序, 它也是左結合的。 3) |的優先順序最低, 並且也是左結合的。

令Σ={a, b}。 1) 正則表示式a|b表示語言{a, b}。 2) 正則表示式( a|b) ( a|b) 表示語言{aa, ab, ba, bb}, 即在字母表Σ上長度為2的所有串的集合。 可表示同樣語言的另一個正則表示式是aa|ab|ba|bb。 3) 正則表示式a表示所有由零個或多個a組成的串的集合, 即{∈ , a, aa, aaa, …}。 4) 正則表示式( a|b) 表示由零個或多個a或b的例項構成的串的集合, 即由a和b構成的所有串的集合{∈ , a, b, aa, ab, ba, bb, aaa,…}。 另一個表示相同語言的正則表示式是( ab*) 5) 正則表示式a|ab表示語言{a, b, ab, aab, aaab, …}, 也就是串a和以b結尾的零個或多個a組成的串的集合。 可以用一個正則表示式定義的語言叫做正則集合( regular set) 。 如果兩個正則表示式r和s表示同樣的語言, 則稱r和s等價( equivalent) ,

3.4 正則定義

為方便表示, 我們可能希望給某些正則表示式命名, 並在之後的正則表示式中像使用符號一樣使用這些名字。 如果Σ是基本符號的集合,

3.5 正則表示式的擴充套件

四、詞法單元的識別

4.1.狀態轉換圖

作為構造詞法分析器的一箇中間步驟, 我們首先將模式轉換成具有特定風格的流圖, 稱為“狀態轉換圖”。 在本節中, 我們用手工方式將正則表示式表示的模式轉化為狀態轉換圖。

狀態轉換圖(transition diagram) 有一組被稱為“狀態”(state) 的結點或圓圈。 詞法分析器在掃描輸入串的過程中尋找和某個模式匹配的詞素, 而轉換圖中的每個狀態代表一個可能在這個過程中出現的情況。

我們可以將一個狀態看作是對我們已經看到的位於lexemeBegin指標和forward指標之間的字元的總結, 它包含了我們在進行詞法分析時需要的全部資訊。

狀態圖中的邊(edge) 從圖的一個狀態指向另一個狀態。 每條邊的標號包含了一個或多個符號。 如果我們處於某個狀態s, 並且下一個輸入符號是a, 我們就會尋找一條從s離開且標號為a的邊(該邊的標號中可能還包括其他符號) 。

如果我們找到了這樣的一條邊, 就將forward指標前移, 並進入狀態轉換圖中該邊所指的狀態。 我們假設所有狀態轉換圖都是確定的, 這意味著對於任何一個給定的狀態和任何一個給定的符號, 最多隻有一條從該狀態離開的邊的標號包含該符號。 從3.5節開始, 我們將放鬆對確定性的要求, 令詞法分析器的設計者更加容易完成任務, 但同時提高了對實現者的技巧要求。

一些關於狀態轉換圖的重要約定如下:

1) 某些狀態稱為接受狀態或最終狀態。 這些狀態表明已經找到了一個詞素, 雖然實際的詞素可能並不包括lexemeBegin指標和forward指標之間的所有字元。 我們用雙層的圈來表示一個接受狀態, 並且如果該狀態要執行一個動作的話——通常是向語法分析器返回一個詞法單元和相關屬性值——我們將把這個動作附加到該接受狀態上。 2) 另外, 如果需要將forward回退一個位置(即相應的詞素並不包含那個在最後一步使我們到達接受狀態的符號) , 那麼我們將在該接受狀態的附近加上一個。 我們的例子都不需要將forward指標回退多個位置, 但萬一出現這種情況, 我們將為接受狀態附加相應數目的 3) 有一個狀態被指定為開始狀態, 也稱初始狀態, 該狀態由一條沒有出發結點的、 標號為“start”的邊指明。 在讀入任何輸入符號之前,狀態轉換圖總是位於它的開始狀態

4.2 保留字和識別符號的識別

識別關鍵字及識別符號時有一個問題要解決。 通常, 像if或then這樣的關鍵字是被保留的, 因此雖然它們看起來很像識別符號, 但它們不是識別符號。 因此, 儘管我們通常使用如圖3-14所示的狀態轉換圖來尋找識別符號的詞素, 但這個圖也可以識別出連續使用的例子中的關鍵字if、 then及else。

我們可以使用兩種方法來處理那些看起來很像識別符號的保留字: 1) 初始化時就將各個保留字填入符號表中。 符號表條目的某個欄位會指明這些串並不是普通的識別符號, 並指出它們所代表的詞法單元。 2) 為每個關鍵字建立單獨的狀態轉換圖。

4.4 基於狀態轉換圖的詞法分析器的體系結構

有幾種方法可以根據一組狀態轉換圖構造出一個詞法分析器。 不管整體的策略是什麼, 每個狀態總是對應於一段程式碼。 我們可以想象有一個變數state儲存了一個狀態轉換圖的當前狀態的編號。 有一個switch語句根據state的值將我們轉到對應於各個可能狀態的相應程式碼段, 我們可以在那裡找到該狀態需要執行的動作。 一個狀態的程式碼本身常常也是一條switch語句或多路分支語句。 這個語句讀入並檢查下一個輸入字元, 由此確定下一個狀態

如果將所有的狀態轉換圖合併為一個圖。 我們允許合併後的狀態轉換圖儘量讀取輸入, 直到不存在下一個狀態為止

Aho-Corasick演算法

該演算法可以在文字串中識別一組關鍵字, 所需時間和文字長度以及所有關鍵字的總長度成正比。 該演算法使用了一種稱為“trie”的特殊形式的狀態轉換圖。 trie是一個樹型結構的狀態轉換圖, 從一個結點到它的各個子結點的邊上有不同的標號。 Trie的葉子結點表示識別到的關鍵字。

KMP演算法

Knuth、 Morris和Pratt提出了一種在文字串中識別單個關鍵字b1b2…bn的演算法。 這裡的trie是一個包含了從0~n共n+1個狀態的狀態轉換圖。 狀態0是初始狀態, 狀態n表示接受, 也就是發現關鍵字的情形。 從0到n-1之間的任意一個狀態s出發, 存在一個標號為bs+1的到達狀態s+1的轉換。 例如, 關鍵字ababaa的trie樹為