• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            歲月流轉,往昔空明

            C++博客 首頁 新隨筆 聯系 聚合 管理
              118 Posts :: 3 Stories :: 413 Comments :: 0 Trackbacks

            先來段前言。

            今天跟某vczh在群里面聊天的時候,他突然很詭秘的說要我看看他的空間連接。
            然后翻開一看,我靠,一連串1-7的標題。
            從尾到頭倒讀一通,才發現寫的挺清楚的,比一般的教科書都要到位。
            不愧是要去google/msra實習的編譯器狂人。(此人成天琢磨編譯器)
            遂轉發,希望對有志了解編譯器工作原理的人們有所幫助。
            雖然這小子說不能修改,但是有的玩意貼的時候還是稍微動一下好發一點。
            不跟他打招呼了,嘿嘿。


            構造可配置詞法分析

            陳梓瀚
            華南理工大學計算機軟件學院軟件工程05級本科
            vczh _at_ 163 _dot_ com
            2007-11-8

             

                本文詳細描述了通過正則表達式構造通用詞法分析器的整個算法流程。如果有需要的話請在評論留下自己的Email,或通過QQ向本人索取文章及附件。任何人(除了作者)不得修改本文。自由轉載,注明出處。

                文檔使用doc格式。附件帶有文章所有圖片的jpg及vsd格式。

            構造可配置詞法分析器

             

            陳梓瀚

            華南理工大學計算機軟件學院軟件工程05級本科

            vczh@163.com

            2007-11-8

             

            一、問題概述

             

            隨著計算機語言的結構越來越復雜,為了開發優秀的編譯器,人們已經漸漸感到將詞法分析獨立出來做研究的重要性。不過詞法分析器的作用卻不限于此。回想一下我們的老師剛剛開始向我們講述程序設計的時候,總是會出一道題目:給出一個填入了四則運算式子的字符串,寫程序計算該式子的結果。除此之外,我們有時候建立了比較復雜的配置文件,譬如XML的時候,分析器首先也要對該文件進行詞法分析,把整個字符串斷成了一個一個比較短小的記號(指的是具有某種屬性的字符串),之后才進行結構上的分析。再者,在實現某種控制臺應用程序的時候,程序需要分析用戶打進屏幕的命令。如果該命令足夠復雜的話,我們也首先要對這個命令進行詞法分析,之后得到的結果會大大方便進行接下去的工作。

             

            當然,這些問題大部分已經得到了解決,而且歷史上也有人做出了各種各樣專門的或者通用的工具(Lex、正則表達式引擎等)來解決這一類問題。我們在使用這種工具的時候,為了更加高效地書寫配置,或者我們在某種特殊情況下需要自己制作類似的工具,就需要了解詞法分析背后的原理。本文將給出一個構造通用詞法分析工具所需要的原理。由于實現的代碼過長,本文將不附帶實現。

             

            究竟什么是“把一個字符串斷成一些記號”呢?我們先從四則運算式子入手。一個四則運算式子是一個字符數列,可是我們關心的對象實際上是操作符、括號和數字。于是此法分析的作用就是把一個字符串斷開成我們關心的帶有屬性的記號。舉個例子:(11+22)*(33+44)是一個合法的四則運算式子,如果輸入是(左括號,”(“) (數字,”11”) (一級操作符,”+”) (數字,”22”) (右括號,”)”) (二級操作符,”*”) (左括號,”(“) (數字,”33”) (一級操作符,”+”) (數字,”44”) (右括號,”)”)的話,我們在檢查結構的時候只需要關心這個記號的屬性(也就是左括號、右括號、數字、操作符等)就行了,具體計算的時候才需要關心這個記號實際上的內容。如果式子里邊有空格的話,我們也僅僅需要把空格當成是一種記號類型,在詞法分析得出結果之后,將具有空格屬性的記號丟棄掉就可以了,接下去的步驟不需變化。

             

            但需要注意的是,詞法分析得到的結果是沒有層次結構的,所有的記號都是等價的對象。我們在計算表達式的時候把+*看成了不同層次的操作符,類似的結構是具有嵌套的層次的。詞法分析不能得出嵌套層次結構的信息,最多只能得到關于重復結構的信息。

             

            二、正則表達式

             

            我們現在需要尋找一種可以描述記號類型的工具,在此之前我們首先研究一下常見的記號的結構。為了表示出具有某種共性的字符串的集合,我們需要書寫出一些能代表字符串集合的規則。這個集合中的所有成員都將被認為是一種特定類型的記號。

             

            首先,規則可以把一個特定的字符或者是空字符串認為是一種類型的記號的全部。上文所說到的四則運算式子的例子,“左括號”這種類型的記號就僅僅對應著字符”(“,其他的字符或者字符串都不可能是“左括號”這個類型的記號。

             

            其次,規則可以進行串聯。串聯的意思是這樣的,我們可以讓一個字符串的前綴符合某一個指定的規則,剩下的部分的前綴符合第二個規則,剩下的部分的前綴符合第三個規則等等,一直到最后一個部分的全部要符合最后一個規則。如果我們把”function”這個字符串作為一個記號類型來處理的話,我們可以把”function”這個字符串替換成8個串聯的規則:”f”,”u”,”n”,”c”,”t”,”i”,”o”,”n”。首先,字符串”function”的前綴”f”符合規則”f”,剩下的部分”unction”的前綴”u”符合規則”u”,等等,一直到最后一個部分”n”的全部符合規則”n”

             

            第三,規則可以進行并聯。并聯的意思就是,如果一個字符串符合一系列規則中的其中一個的話,我們就說這個字符串符合這一些規則的并聯。于是這些規則的并聯就構成了一個新的規則。一個典型的例子就是判斷一個字符串是否關鍵字。關鍵字可以是”if”,可以是”else”,可以是”while”等等。當然,一個關鍵字是不可能同時符合這些規則的,不過只要一個字符串符合這些規則的其中一個的話,我們就說這個字符串是關鍵字。于是,關鍵字這個規則就是”if””else””while”等規則的并聯。

             

            第四,一個規則可以是可選的。可選的規則實際上是屬于并聯的一種特殊形式。加入我們需要規則”abc””abcde”并聯,我們會發現這兩個規則有著相同的前綴”abc”,而且這個前綴恰好就是其中的一個規則。于是我們可以把規則改寫成”abc””””de”的并聯的串聯。但是規則””指定的規則是空串,因此這個規則與”de”的并聯就可以看成是一個可選的規則”de”

             

            第五,規則可以被重復。有限次的重復可以使用串聯表示,但是如果我們不想限制重復的次數的話,串聯就沒法表示這個規則了,于是我們引入了“重復”。一個典型的例子就是程序設計語言的標識符。標識符可以是一個變量的名字或者是其他東西。一門語言通常沒有規定變量名的最大長度。因此為了表示這個規則,就需要將52個字母進行并聯,然后對這個規則進行重復。

             

            上述的5種構造規則的方法中,后面的4個方法被用于把規則組合成為更大的規則。為了給出這種規則的形式化表示,我們引入了一種范式。這種范式有以下語法:

            1:字符用雙引號包圍起來,空串使用ε代替。

            2:兩個規則頭尾連接代表規則的串聯。

            3:兩個規則使用 | 隔開代表規則的并聯。

            4:規則使用[]包圍代表該規則是可選的,規則使用{}包圍代表該規則是重復的。

            5:規則使用()包圍代表該規則是一個整體,通常用于改變操作符 | 的優先級。

             

            舉個例子,一個實數的規則書寫如下:

            {“0”|”1”|”2”|”3”|”4”|”5”|”6”|”7”|”8”|”9”}”.”[{“0”|”1”|”2”|”3”|”4”|”5”|”6”|”7”|”8”|”9”}]

             

            但是,我們如何表示“不是數字的其他字符呢”?字符的數量是有限的,因此我們可以使用規則的并聯來表示。但是所有的字符實在是太多(ASCII字符集有127個字符,UTF-16字符集有65535個字符),因此后來人們想出了各種各樣的簡化規則書寫的辦法。比較著名的有BNF范式。BNF范式經常被用于理論研究,但是更加實用的是正則表達式。

             

            正則表達式的字符不需要用雙引號括起來,但是如果需要表示一些被定義了的字符( “|” )的話,就使用轉義字符的方法表示( “\|”)。其次,X?代表[X]X+代表{X}X*代表[{X}]。字符集合可以用區間來表示,[0-9]可以表示“0”|”1”|”2”|”3”|”4”|”5”|”6”|”7”|”8”|”9”[^0-9]則表示“除了數字以外的其他字符”。正則表達式還有各種各樣的其他規則來簡化我們的書寫,不過由于本文并不是“精通正則表達式”,因此我們只保留若干比較本質的操作來進行詞法分析原理的描述。

             

            正則表達式的表達能力極強,小數的規則可以使用[0-9]+.[0-9]來表示,C語言的注釋可以表示為/\*([^\*]|\*+[\*/])*\*+/來表示。

             

            三、有窮狀態自動機

             

            人閱讀正則表達式會比較簡單,但是機器閱讀正則表達式就是一件非常困難的事情了。而且,直接使用正則表達式進行匹配配的話,不僅工作量大,而且速度緩慢。因此我們還需要另外一種專門為機器設計的表達方式。本文在以后的章節中會給出一種算法把正則表達式轉換為機器可以閱讀的形式,就是這一章節所描述的有窮狀態自動機。

             

            有窮狀態自動機這個名字聽起來比較可怕,不過實際上這種自動機并沒有想象中的那么復雜。狀態機的這種概念被廣泛的應用在各種各樣的領域中。軟件工程的統一建模語言(UML)有狀態圖,數字邏輯中也有狀態轉移圖。不過這些各種各樣的圖在本質上都跟狀態機沒有什么區別。我將會通過一個例子來講述狀態的實際意義。

             

            假設我們現在需要檢查一個字符串中a的數量和b的數量是否都是偶數。當然我們可以用一個正則表達式來描述它。不過對于這個問題來說,用正則表達式來描述遠遠不如構造狀態機方便。我們可以設計出一個狀態的集合,然后指定集合中的某一個元素為“起始狀態”。其實狀態就是在工作還沒開始的時候,分析器所處的狀態。分析器在每一次進行一項新的工作的時候,都要把狀態重置為起始狀態。分析器每讀入一個字符就修改一次狀態,修改的方法我們也可以指定。分析器在讀完所有的字符以后,必然停留在一個確定的狀態中。如果這個狀態跟我們所期望的狀態一致的話,我們就說這個分析器接受了這個字符串,否則我們就說這個分析器拒絕了這個字符串。

             

            如何通過設計狀態及其轉移方法來實現一個分析器呢?當然,如果一個字符串僅僅包含a或者b的話,那么分析器的狀態只有四種:“奇數a奇數b”、“奇數a偶數b”、“偶數a奇數b”、“偶數a偶數 b”。我們把這些狀態依次命名為aaaBAbAB。大寫代表偶數,小寫代表奇數。當工作還沒開始的時候,分析器已經讀入的字符串是空串,那么理所當然的起始狀態應當是AB。當分析器讀完所有字符的時候,我們期望讀入的字符串的ab的數量都是偶數,那么結束的狀態也應該是AB。于是我們給出這樣的一個狀態圖:

            3.1

            檢查一個字符串是否由偶數個a和偶數個b組成的狀態圖

             

            在這個狀態圖里,有一個短小的箭頭指向了AB,代表AB這個狀態是初始狀態。AB狀態有粗的邊緣,代表AB這個狀態是結束的可接受狀態。一個狀態圖的結束狀態可以是一個或者多個。在這個例子里,起始狀態和結束狀態剛好是同一個狀態。標有字符”a”的箭頭從AB指向aB,代表如果分析器處于狀態AB并且讀入的字符是a的話,就轉移到狀態aB上。

             

            我們把這個狀態圖應用在兩個字符串上,分別是”abaabbba””aababbaba”。其中,第一個字符串是可以接受的,第二個字符串是不可接受的(因為有5a4b)

             

            分析第一個字符串的時候,狀態機所經過的狀態是:

            AB[a]aB[b]ab[a]Ab[a]ab[b]aB[b]ab[b]aB[a]AB

            分析第二個字符串的時候,狀態機所經過的狀態是:

            AB[a]aB[a]AB[b]Ab[a]ab[b]aB[b]ab[a]Ab[b]AB[a]aB

             

            第一個字符串”abaabbba”讓狀態機在狀態AB上停了下來,于是這個字符串是可以接受的。第二個字符串”aababbaba”讓狀態機在狀態aB上停了下來,于是這個字符串是不可以接受的。

             

            在機器內部表示這個狀態圖的話,我們可以使用一種比較簡單的方法。這種方法僅僅把狀態與狀態之間的箭頭、起始狀態和結束狀態集合記錄下來。對應于這個狀態圖的話,我們就可以把這個狀態圖表示成以下形式:

             

            起始狀態:AB

            結束狀態集合:AB

            (AB,a,aB)

            (AB,b,Ab)

            (aB,a,AB)

            (aB,b,ab)

            (Ab,a,ab)

            (Ab,b,AB)

            (ab,a,Ab)

            (ab,b,aB)

             

            用一個狀態圖來表示狀態機的時候有時候會遇到確定性與非確定性的問題。所謂的確定性就是指對于任何一個狀態,輸入一個字符都可以跳轉到另一個確定的狀態中去。確定性和非確定性的區別有一個直觀的描述:狀態圖的任何一個狀態都可以有不定數量的邊指向另一個狀態,如果在這些邊里面,存在兩條邊,它們所承載的字符如果相同,那么這個狀態輸入這個就字符可以跳轉到另外兩個狀態中去,于是該狀態機就是不確定的。如圖所示:

            3.2

            正則表達式ba*b的一個確定的狀態機表示

            3.3

            正則表達式ba*b的一個非確定的狀態機表示

             

            3.3中的狀態機的起始狀態讀入字符b后可以跳轉到中間的兩個狀態里,因此這個狀態機是非確定的。相反,圖3.2中的狀態機,雖然功能跟圖3.3的狀態機一致,但卻是確定的。我們還可以使用一種特殊的邊來進行狀態的轉換。我們用ε邊來表示一個狀態可以不讀入字符就跳轉到另一個狀態上。下圖給出了一個跟圖3.3功能一致的包含ε邊的非確定的狀態機:

            3.4

            正則表達式ba*b的一個帶有ε邊的非確定的狀態機

             

            在教科書中,通常把確定的有窮狀態自動機(有窮狀態自動機也就是本文討論的這種狀態機)稱為DFA,把非確定的有窮狀態自動機稱為NFA,把帶有ε邊的非確定的狀態機稱為ε-NFA。下文中也將采用這幾個術語來指示各種類型的有窮狀態自動機。

             

            在剛剛接觸到ε邊的時候,一個通常的疑問就是這種邊存在的理由。事實上如果是人直接畫狀態機的話,有時候也可以直接畫出一個確定的狀態機,復雜一點的話也可以畫出一個非確定的狀態機,在有些極端的情況下我們需要使用ε邊來更加簡潔的表示我們的意圖。不過ε邊存在的最大的理由就是:我們可以通過使用ε邊來給出一個簡潔的算法把一個正則表達式轉換成ε-NFA

             

            四、從正則表達式到ε-NFA

             

            通過第二節所描述的內容,我們知道一個正則表達式的基本元素就是字符集。通過對規則的串聯、并聯、重復、可選等操作,我們可以構造除更復雜的正則表達式。如果從正則表達式構造狀態機的時候也可以用這幾種操作對狀態圖進行組合的話,那么方法將會變得很簡單。接下來我們將一一對這5個構造正則表達式的方法進行討論。使用下文描述的算法構造出來的所有ε-NFA有且只有一個結束狀態

             

            1:字符集

             

            字符集是正則表達式最基本的元素,因此反映到狀態圖上,字符集也會是構成狀態圖的基本元素。對于字符集C,如果有一個規則只接受C的話,這個規則對應的狀態圖將會被構造成以下形式:

             

            4.1

             

            這個狀態圖的初始狀態是Start,結束狀態是EndStart狀態讀入字符集C跳轉到End狀態,不接受其他字符集。

             

            2:串聯

             

            如果我們使用AB表示規則A和規則B的串聯,我們可以很容易的知道串聯這個操作具有結合性,也就是說(AB)C=A(BC)。因此對于n個規則的串聯,我們只需要先將前n-1個規則進行串連,然后把得到的規則看成一個整體,跟最后一個規則進行串聯,那么就得到了所有規則的串聯。如果我們知道如何將兩個規則串聯起來的話,也就等于知道了如何把n個規則進行串聯。

             

            為了將兩個串聯的規則轉換成一個狀態圖,我們只需要先將這兩個規則轉換成狀態圖,然后讓第一個狀態的結束狀態跳轉到第二個狀態圖的起始狀態。這種跳轉必須是不讀入字符的跳轉,也就是令這兩個狀態等價。因此,第一個狀態圖跳轉到了結束狀態的時候,就可以當成第二個狀態圖的起始狀態,繼續第二個規則的檢查。因此我們使用了ε邊連接兩個狀態圖:


            4.2

             

            3:并聯

             

            并聯的方法跟串聯類似。為了可以在起始狀態讀入一個字符的時候就知道這個字符可能走的是并聯的哪一些分支并進行跳轉,我們需要先把所有分支的狀態圖構造出來,然后把起始狀態連接到所有分支的起始狀態上。而且,在某個分支成功接受了一段字符串之后,為了讓那個狀態圖的結束狀態反映在整個狀態圖的結束狀態上,我們也把所有分支的結束狀態都連接到大規則的結束狀態上。如下所示:


            4.3

             

            4:重復

             

            對于一個重復,我們可以設立兩個狀態。第一個狀態是起始狀態,第二個狀態是結束狀態。當狀態走到結束狀態的時候,如果遇到一個可以讓規則接受的字符串,則再次回到結束狀態。這樣的話就可以用一個狀態圖來表示重復了。于是對于重復,我們可以構造狀態圖如下所示:


            4.4

             

            5:可選

             

            為可選操作建立狀態圖比較簡單。為了完成可選操作,我們需要在接受一個字符的時候,如果字符串的前綴被當前規則接受則走當前規則的狀態圖,如果可選規則的后續規則接受了字符串則走后續規則的狀態圖,如果都接受的話就兩個圖都要走。為了達到這個目的,我們把規則的狀態圖的起始狀態和結束狀態連接起來,得到了如下狀態圖:

             

            4.5

             

            如果重復使用的是0次以上重復,也就是原來的重復加上可選的結果,那么可以簡單地把圖4.4Start狀態去掉,讓End狀態同時擁有起始狀態和結束狀態兩個角色,[Start][End]則保持原狀。

             

            至此,我們已經將5種構造狀態圖的辦法都對應到了5種構造規則的辦法上了。對于任意的一個正則表達式,我們僅需要把這個表達式還原成那5種構造的嵌套,然后把每一步構造都對應到一個狀態圖的構造上,就可以將一個正則表達式轉換成一個ε-NFA了。舉個例子,我們使用正則表達式來表達“一個字符串僅包含偶數個a和偶數個b”,然后把它轉換成ε-NFA

             

            我們先對這個問題進行分析。如果一個字符串僅包含偶數個a和偶數個b的話,那么這個字符串一定是偶數長度的。于是我們可以把這個字符串分割成兩個兩個的字符段。而且這些字符段只有四種:aabbabba。對于aabb來說,無論出現多少次都不會影響字符串中ab的數量的奇偶性(理由:在一個模2加法系統里,0是不變項,也就是說對于任何屬于模2加法的數XX+0 = 0+X = X)。對于abba的話,如果一個字符串的開始和結束是ab或者ba,中間的部分是aa或者bb的任意組合,這個字符串也是具有偶數個a和偶數個b的。我們現在得到了兩種構造偶數個a和偶數個b的字符串的方法。把串聯、并聯、可選、重復等操作應用在這些字符串上,仍然會得到具有偶數個a和偶數個b的字符串。于是我們可以把正則表達式書寫成以下形式:

            ((aa|bb)|((ab|ba)(aa|bb)*(ab|ba)))*

            根據上文提到的方法,我們可以把這個正則表達式轉換成以下狀態機:

             4.6 


            至此,我們已經得到了把一個正則表達式轉換為ε-NFA的方法了。但是只得到ε-NFA還是不行的,因為ε-NFA的不確定性太大了,直接根據ε-NFA跑的話,每一次都會得到大量的臨時狀態集合,會極大地降低效率。因此,我們還需要一個辦法消除一個狀態機的非確定性。

             

            五、消除非確定性

             

            消除ε邊算法

             

            我們見到的有窮狀態自動機一共有三種:ε-NFANFADFA。現在我們需要將ε-NFA轉換為DFA。一個DFA中不可能出現ε邊,所以我們首先要消除ε邊。消除ε邊算法基于一個很簡單的想法:如果狀態A通過ε邊到達狀態B的話,那么狀態A無需讀入字符就可以直達狀態B。如果狀態B需要讀入字符x才可以到達狀態C的話,那么狀態A讀入x也可以到達狀態C。因為從AC的路徑是A B C,其中AB不需要讀入字符。

             

            于是我們會得到一個很自然的想法:消除從狀態A出發的ε邊,只需要尋找所有從A開始僅通過ε邊就可以到達的狀態,并把從這些狀態觸出發的非ε邊復制到A上即可。剩下的工作就是刪除所有的ε邊和一些因為消除ε邊而變得不可到達的狀態。為了更加形象地描述消除ε邊算法,我們從正則表達式(ab|cd)*構造一個ε-NFA,并在此狀態機上應用消除ε邊算法。

             

            正則表達式(ab|cd)*的狀態圖如下所示:

            5.1

            1:找到所有有效狀態。

             

            有效狀態就是在完成了消除ε邊算法之后仍然存在的狀態。我們可以在開始整個算法之前就預先計算出所有有效狀態。有效狀態的特點是存在非ε邊的輸入。同時,起始狀態也是一個有效狀態。結束狀態不一定是有效狀態,但是如果存在一個有效狀態可以僅通過ε邊到達結束狀態的話,那么這個狀態應該被標記為結束狀態。因此對一個ε-NFA應用消除ε邊算法產生的NFA可能出現多個結束狀態。不過起始狀態仍然只有一個。

             

            我們可以把“存在非ε邊的輸入或者起始狀態”這個判斷方法應用在圖5.1每一個狀態上,計算出圖5.1中所有的有效狀態。結果如下圖所示。


            5.2

            所有非有效狀態的標簽都被刪除

             

            如果一個狀態同時具有ε邊和非ε邊輸入的話,那么這個狀態仍然是有效狀態。因為所有的有效狀態在下一步的操作中,都會得到新的輸出和新的輸入。

             

            2:添加所有必要的邊

             

            接下來我們要對所有的有效狀態都應用一個算法。這個算法分成兩步。第一步是尋找一個狀態的ε閉包,第二步是把這個狀態的ε閉包看成一個整體,把所有從這個閉包中輸出的邊全部復制到當前狀態上。從標記有效狀態的結果我們得到了圖5.1狀態圖的有效狀態集合是{S/E 3 5 7 9}。我們依次對這些狀態應用上述算法。第一步,計算S/E狀態的ε閉包。所謂一個狀態的ε閉包就是從這個狀態出發,僅通過ε邊就可以到達的所有狀態的集合。下圖中標記出了狀態S/E的ε閉包:

             

            5.3

            現在,我們把狀態S/E從狀態S/E的ε閉包中排除出去。因為從狀態A輸出的非ε邊都屬于從狀態A的ε閉包中輸出的非ε邊,復制這些邊是沒有任何價值的。接下來就是找到從狀態S/E的ε閉包中輸出的非ε邊。在圖5.3我們可以很容易地發現,從狀態1和狀態6(見圖5.1的狀態標簽)分別輸出到狀態3和狀態7的標記了a或者b的邊,就是我們所要尋找的邊。接下來我們把這些邊復制到狀態S/E上,邊的目標狀態仍然保持不變,可以得到下圖:

             

            5.4

            至此,這個算法在S/E上的應用就結束了,接下來我們分別對剩下的有效狀態{3 5 7 9}分別應用此算法,可以得到下圖:

             
            5.5

            紅色的邊為新增加的邊

             

            3:刪除所有ε邊和無效狀態

             

            這一步操作是消除ε邊算法的最后步驟。我們只需要刪除所有的ε邊和無效狀態就完成了整個消除ε邊算法。現在我們對圖5.5的狀態機應用第三步,得到如下狀態圖:

             
            5.6

             

            不過并不是只有新增的邊才不被刪除。根據定義,所有從有效狀態出發的非ε邊都是不能刪除的邊。

             

            我們通過把消除ε邊算法應用在圖5.1的狀態機上,得到了圖5.6這個DFA。但是并不是所有的消除ε邊算法都可以直接從ε-NFA直接得到DFA,這個其實跟正則表達式本身有關。至于什么正則表達式可以達到這個效果這里就不深究了。但是因為有可能產生NFA,所以我們還需要一個算法把NFA轉換成DFA

             

            NFA到DFA

             

            NFA是非確定性的狀態機,DFA是確定性的狀態機。確定性和非確定性的最大區別就是:從一個狀態讀入一個字符,確定性的狀態機得到一個狀態,而非確定性的狀態機得到一個狀態的集合。如果我們把NFA的起始狀態S看成一個集合{S}的話,對于一個狀態集合S’,給定一個輸入,就可以用NFA計算出對應的狀態集合T’。因此我們在構造DFA的時候,只需要把起始狀態對應到S’,并且找到所有可能在NFA同時出現的狀態集合,把這些集合都轉換成DFA的一個狀態,那么任務就完成了。因為NFA的狀態是有限的,所以NFA所有狀態的集合的冪集的元素個數也是有限的,因此使用這個方法構造DFA是完全可能的。

             

            為了形象地表達出這個算法的過程,我們將構造一個正則表達式,然后給出該正則表達式轉換成NFA的結果,并把構造DFA的算法應用在NFA上。假設一個字符串只有abc三種字符,判斷一個字符串是不是以abc開始并且以cba結束正則表達式如下:

            abc(a|b|c)*cba

            通過上文的算法,可以得出如下圖所示的NFA


            5.7

             

            現在我們開始構造DFA,具體算法如下:

             

            1:把{S}放進隊列L和集合D中。其中SNFA的起始狀態。隊列L放置的是未被處理的已經創建了的DFA狀態,集合D放置的是已經存在的DFA狀態。根據上文的討論,DFA的每一個狀態都對應著NFA的一些狀態。

             

            2:從隊列L中取出一個狀態,計算從這個狀態輸出的所有邊所接受的字符集的并集,然后對該集合中的每一個字符尋找接受這個字符的邊,把這些邊的目標狀態的并集T計算出來。如果TD則代表當前字符指向了一個已知的DFA狀態。否則則代表當前字符指向了一個未創建的DFA狀態,這個時候就把T放進LD中。在這個步驟里有兩層循環:第一層是遍歷所有接受的字符的并集,第二層是對每一個可以接受的字符遍歷所有輸出的邊計算目標DFA狀態所包含的NFA狀態的集合。

             

            3:如果L非空則跳到2

             

            現在我們開始對圖5.7的狀態機應用DFA構造算法。

             

            首先執行第一步,我們得到:


            5.8

             

            從上到下分別是隊列L、集合DDFA的當前狀態。就這樣一直執行該算法直到狀態3進入集合D,我們得到:

             
            5.9

             

            現在從隊列L中取出{3},經過分析得到狀態集合{3}接受的字符集合為{a b c}{3}讀入a到達{4},讀入b到達{5},讀入c到達{6 7}。因為{4}{5}{6 7}都不屬于D,所以把它們都放入隊列L和集合D


            5.10

             

            從隊列中取出4進行計算,我們得到:

             
            5.11

             

            顯然,對于狀態{4}的處理并沒有發現新的DFA狀態。于是處理{5}{6 7},我們可以得到:

             
            5.12

             

            在處理狀態{5}的時候沒有發現新的DFA狀態,處理{6 7}在輸入{a c}之后的跳轉也沒有發現新的DFA狀態,但是我們發現了{6 7}輸入b卻得到了新的DFA狀態{5 8}。把算法執行完,我們得到一個DFA
            5.13

             

            至此,對圖5.7的狀態機應用DFA構造算法的流程就結束了,我們得到了圖5.13DFA,其功能與圖5.7NFA等價。在DFA中,起始狀態是0,結束狀態是4E。凡是包含了NFA的結束狀態的DFA狀態都必須是結束狀態。

             

            六、使用正則表達式構造詞法分析器

             

            判斷一個字符串是否屬于某規則的算法介紹到這里就結束了。回到我們一開始的問題上,我們需要使用一些規則來吧一個長的字符串斷開成記號,然后計算出每一個記號對應的規則。在解決這個問題之前,我們先考察一下能夠成功地被詞法分析器接受的字符串應該是什么樣子的。

             

            假設我們現在有規則ABCD,分別對應于四種記號類型,那么被詞法分析器接受的字符串就是ABCD的任意組合,也就是(A|B|C|D)*。如果規定了輸入的字符串不能為空的話,那么被詞法分析器接受的字符串就是(A|B|C|D)+。但是單純地使用(A|B|C|D)+作為一個規則去應用在輸入的字符串的話,我們只能夠判斷字符串是否是詞法分析器能夠接受的字符串。因此我們需要對這個方法進行修改。

             

            首先按照上文的方法,把每一個記號類型對應的規則的正則表達式轉換成DFA,然后使用并聯的方法將他們組合起來,但是并不使用“重復”。但是這次我們要做一點修改,我們要把新的DFA的每一個狀態對應的規則的DFA狀態集合記住

             

            這里給出一個例子,我們假設需要一個簡單語言的詞法分析器,規則如下:

             

            I[a-zA-Z_][a-zA-Z_0-9]*

            N[0-9]+

            R[0-9]+.[0-9]+

            O[=>+-*/|&]

             

            按照規則構造出四個DFA并將它們組合起來:

             
            6.1

            我們構造出了I|N|R|ODFA,并且標識出了哪些狀態包含了原DFA的結束狀態。這樣做的一個好處是,當我們把一個字符串放進這樣的一個DFA之后,我們就一直等待整個字符串被接受,或者失敗。如果字符串被接受的話,我們就把當前的結束狀態記下來。如果失敗的話,我們就把這個狀態機在分析字符串的時候經過的最后一個結束狀態記下來。這個時候,結束狀態所代表的原DFA結束狀態的相應記號類型就是我們所需要的信息了。如果得不到任何結束狀態的話,輸入的字符串就是不背詞法分析其接受的。

             

            舉個例子,使用上述狀態機分析”123.ABC”

             

            首先從狀態0開始,依次經過狀態N N N 2,然后宣告失敗。最后一個N(結束狀態)以及當時被接受的字符串”123”被識別,結果為(N”123”)。然后從”.ABC”開始,輸入第一個記號就失敗了,于是”.”被識別為不可接受字串。最后輸入”ABC”,依次經過狀態0 1 I I,然后字符串結束并且被接受,于是輸出(I”ABC”)

             

            為什么我們在構造狀態機的時候不使用“重復”呢?因為在每一個記號被識別出的時候,我們都要做一些額外的工作。如果我們使用“重復”來構造詞法分析器的狀態機,我們將無從知道一個記號被識別出來的確切時間。

             

            算法到這里基本上就結束了,不過還存在一些小問題需要在細節上解決。一般來說我們給出的一些構成詞法分析器的規則很少有沖突,不過偶爾會出現兩個規則所代表的字符串集合存在交集的情況。有了DFA這個工具,我們可以很輕易地識別出規則沖突。

             

            假如我們的詞法分析器有AB兩個狀態,那么我們構造詞法分析器A|B的時候,將會得到一些包含DFA(A)DFA(B)的結束狀態的新狀態。我們只需要檢查這些狀態是否具有以下特征,就可以判斷AB的關系。我們假設DFA(A)為規則A的狀態機,DFA(B)為規則B的狀態機,DFA(L)為詞法分析器A|B的狀態機:

             

            1:如果DFA(L)存在一個或多個狀態同時包含了DFA(A)DFA(B)的結束狀態,那么AB所代表的字符串存在交集。

            2:如果DFA(L)不存在同時包含了DFA(A)DFA(B)的結束狀態的狀態,那么AB所代表的字符串不存在交集。

            3:如果DFA(L)的某些狀態包含了DFA(A)的結束狀態,并且這些狀態都無一例外地包含了DFA(B)的結束狀態的話,那么AB的子集。

            4:如果DFA(L)的某些狀態包含了DFA(A)的結束狀態,但是這些狀態并沒有無一例外地包含DFA(B)的結束狀態的話,那么A不是B的子集。

             

            在圖6.1的詞法分析器中,我們可以很清楚地看出INRO四個規則兩兩之間都不存在交集。我們可以嘗試構造一個沖突的規則,并看一看詞法分析器的DFA是什么樣子的:

             

            假設詞法分析器包含以下規則:

            A”if”

            B[a-z]+

             

            A|B構造DFA,我們將會得到如下狀態機:



            6.2

             

            通過圖6.2我們可以看出,這個狀態圖滿足了上述的條件3:包含了狀態A的結束狀態的狀態都包含了B的結束狀態,因此AB的子集。顯然”if”[a-z]+的一個子集。在處理這種有沖突的規則的時候,既可以報錯,也可以根據指定的優先級進行挑選。

             

            七、尾聲

             

            使用DFA的方法完成的可配置詞法分析器的性能是相當好的。筆者前不久曾經做過實驗,首先使用本文提到的算法開發一個這樣的詞法分析器,然后在一份C++代碼(這份代碼經過多次復制而成件,一共有3.12M)中抽取所有數字、標識符和注釋,吞吐速度高達46萬記號/秒(筆者的臺式電腦配置是奔騰4的超線程2.99GHz處理器,1G內存),其中抽取出來的記號一共有22萬個。在分析的過程中,只有10%的時間花在了DFA上,90%的時間花在了處理結果的工作上。DFA本身造成的消耗是很小的。不過詞法分析的性能在很大程度上跟DFA的實現有很大關系。三個月前筆者也實現過一個同類的程序,但是吞吐速度僅有1.1萬記號/秒。

             

            一般來說,比較高性能的DFA的實現是一張二維的表。行代表字符,列代表DFA的狀態,單元格代表該狀態經輸入某個字符之后進行轉移的目標狀態。此外還有一張表用來記錄哪些狀態對應哪些規則的結束狀態。筆者的詞法分析器是基于UTF-16編碼的字符串,一張表一共有65535行顯然是不現實的,因此還有另一張表把字符轉換成字符類。字符類是這樣定義的:假設現在已經存在了65535行的一張大表,如果在某個字符區間所對應的子表內,任意一列的單元格的數據都一樣的話,那么這個區間內的所有字符就可以被視為是等價的,這些字符就屬于同一個字符類。于是僅需要另外一張65535個單元的表用來把一個字符映射到字符類。這種做法可以大大的壓縮DFA所需要的空間。在筆者的程序里,識別字符類的算法被融入了DFA的構造算法中

             

            參考文獻

             

            1Alfred V.Aho  Ravi Sethi  Jeffrey D.Ullman Compilers Principles , Techniques , and Tools

            2】:Dick Grune  Ceriel Jacobs Parsing Techniques ---- A Practical Guide

            3】:Kenneth C.Louden Compiler Construction Principles and Practice


             


            posted on 2007-11-09 10:41 空明流轉 閱讀(2404) 評論(19)  編輯 收藏 引用

            評論

            # re: [轉貼]構造可配置的詞法分析器 2007-11-09 17:17 gatt
            寫得很好,可惜圖沒貼完,不然一口氣將他看完
            gatt@21cn.com
            謝謝!!  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器 2007-11-10 08:26 converse
            也給我一份,謝謝
            converse_lc@163.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器 2007-11-10 11:52 vczh
            惡哈哈……被我看到了  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器 2007-11-10 12:21 空明流轉
            vc他老人家親自審查該頁面,你們要的這廝跟你們發過去了。。。  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2007-12-16 19:27 XY
            頂一個,寫得非常精彩!
            請給我發一份,謝謝:
            xiaoyangde AT 163.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-04-03 16:12 leafyoung
            呵呵,寫得很不錯啊,厚顏求相關代碼一份,謝謝o(∩_∩)o...  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-04-03 16:17 leafyoung
            忘了寫郵箱地址了,呵呵 leafyoung88#hotmail.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-04-12 13:38 jiuyue
            寫得很不錯,想仔細研究研究,求一份,十分感謝
            jiuyuesanqiu@163.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-04-12 14:27 螞蟻終結者
            很不錯,能不能幫忙發個源代碼,非常感謝!!!
            antterminator@gmail.com
              回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-04-18 22:27 幻景
            可以發我一份不啦,先謝過啦
            jsj_leilei@126.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-04-20 10:28 xiaoxiao
            寫得很好!佩服! 希望可以得到一份源代碼! 謝謝
            mshnpl@126.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-05-09 13:52 小龍
            寫的不錯,我也想了解下 麻煩了~~~
            aiwushang521@126.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結)[未登錄] 2008-05-11 17:39 rebol
            最近正在學習這方面的內容,自己也想寫個parser,希望發我一份源碼學習學習,謝謝
            rebol@126.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-05-20 15:53 謝謝
            最近正在學習這方面的內容,希望發我一份源碼學習學習,謝謝
            zengfanna27@126.com
              回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-05-21 11:04 h31home
            希望可以得到一份源代碼! 謝謝
            h31h31@163.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結)[未登錄] 2008-05-22 13:41 111
            我也想要biosxjj@163.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-09-27 09:21 songsb
            最近正在學習這方面的內容,希望發我一份源碼學習學習,謝謝 謝謝 謝謝!!!!!!!!!!!
            ssb_it@126.com  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2008-10-12 12:36 giantman
            不錯!獲益匪淺!只可惜只有這一部分.
            希望你也能發一個給我 shaofeng.88@163.com 勞駕了!  回復  更多評論
              

            # re: [轉貼]構造可配置的詞法分析器(已完結) 2010-10-23 15:36 xrd
            謝謝,很不錯,請麻煩也給我一份,謝謝!!
            xrdmly@163.com

            xrd#ustc.edu
            #請改成@
            ^_^  回復  更多評論
              

            久久久这里只有精品加勒比| 午夜久久久久久禁播电影| 久久亚洲国产最新网站| 国产成人精品久久亚洲高清不卡 | 久久无码国产| 久久国产影院| 思思久久99热免费精品6| 久久久久久久久久久久中文字幕 | 人妻丰满AV无码久久不卡| 久久热这里只有精品在线观看| 日韩久久无码免费毛片软件| 久久本道综合久久伊人| 色婷婷综合久久久久中文字幕| 四虎国产精品成人免费久久| 亚洲国产一成久久精品国产成人综合 | 久久精品国产99国产电影网| 国内精品人妻无码久久久影院| 久久亚洲欧美国产精品| 丁香狠狠色婷婷久久综合| 国产AV影片久久久久久| 精品久久综合1区2区3区激情| 色综合久久中文字幕综合网| 模特私拍国产精品久久| 无码国内精品久久人妻| 久久青青草原精品影院| 久久久久无码精品| 一本久久综合亚洲鲁鲁五月天亚洲欧美一区二区 | 久久免费视频一区| 中文字幕精品无码久久久久久3D日动漫| 狠狠色丁香久久婷婷综合蜜芽五月| 久久久久久久女国产乱让韩| 97久久精品人妻人人搡人人玩| 久久97久久97精品免视看| 97视频久久久| 国产精品无码久久久久| 东方aⅴ免费观看久久av| 91久久精品电影| 久久婷婷五月综合成人D啪| 久久久青草久久久青草| 久久久SS麻豆欧美国产日韩| 久久97久久97精品免视看|