編譯器實現(二)

1.詞法分析

編譯器的掃描或詞法分析( lexical analysis)階段可將源程序讀做字符文件並將其分爲若干個記號。html

1.掃描處理

  掃描程序的任務是從源代碼中讀取字符並造成由編譯器的之後部分(一般是分析程序)處理的邏輯單元。git

記號一般定義爲枚舉類型的邏輯項。例如,記號在C中可被定義爲:
typedef enum
{ IF, THEN, ELS,EPLUS, MINUS, NUM, ID, ...}
TokenType;正則表達式

  掃描程序是一種格式匹配,因此須要研究掃描過程當中的格式說明和識別方法,其中最主要的是正則表達式和有窮自動機。算法

2.正則表達式

  正則表達式表示字符串的格式。正則表達式r徹底由它所匹配的串集來定義。這個集合稱爲由正則表達式生成的語言( language generated by the regular expression),寫做L(r)。此處的語言只表示「串的集合」,它與程序設計語言並沒有特殊關係(至少在此處是這樣的)。express

  正則表達式r還包括字母表中的字符,但這些字符具備不一樣的含義:在正則表達式中,全部的符號指的都是模式。 數組

2.1 正則表達式的定義

參考:https://www.runoob.com/regexp/regexp-syntax.html數據結構

  如今經過講解每一個模式所生成的不一樣語言來描述正則表達式的含義。閉包

  1) 基本正則表達式 它們是字母表中的單個字符且自身匹配。空串用( epsilon )來表示,元符號ε(黑體)是經過設定L( ε) = {ε }來定義的,表示不包含任何字符的串。偶爾還須要寫出一個與任何串都不匹配的符號,它的語言爲空集(empty set),寫做{ }。咱們用符號Φ來表示,並寫做L( Φ) = {}。請注意{ }和{ε}的區別:{ }集不包括任何串,而{ε}則包含一個沒有任何字符的串。函數

  2) 正則表達式運算 在正則表達式中有3種基本運算:① 從各選擇對象中選擇,用元字符|(豎線)表示。②連結,由並置表示(不用元字符)。③重複或「閉包」,由元字符*表示。測試

  3) 從各選擇對象中選擇 若是r 和s 是正則表達式,那麼正則表達式r | s 可匹配被r 或s 匹配的任意串。從語言方面來看,r | s 語言是r 語言和s 語言的聯合(union),或L (r | s) = L (r)∪ (s)。

  4) 連結 正則表達式r 和正則表達式s 的連結可寫做rs,它匹配兩串連結的任何一個串,其中第1個匹配r,第2個匹配s。

  5) 重複 正則表達式的重複有時稱爲Kleene閉包((Kleene) closure),寫做r *,其中r 是一個正則表達式。正則表達式r * 匹配串的任意有窮連結,每一個連結均匹配r。

  6) 運算的優先和括號的使用  在這3個運算中, *優先權最高,連結其次,| 最末。所以,a | b c * 就可解釋爲a | ( b ( c* )),而a b | c * d 卻解釋爲( a b )| (( c* ) d )。

 2.2 正則表達式的擴展

(1) 一個或多個重複
  倘若有一個正則表達式r,r 的重複是經過使用標準的閉包運算來描述,並寫做r *。它容許r 被重複0次或更屢次。0次並不是是最典型的狀況,一次或屢次纔是,這就要求至少有一個串匹配r,但空串卻不行。例如在天然數中須要有一個數字序列,且至少要出現一個數字。如要匹配二進制數,就寫做( 0 | 1 ) *,它一樣也可匹配不是一個數的空串。固然也可寫做
  ( 0 | 1 ) ( 0 | 1 ) *
  可是這種狀況只出如今用+代替*的這個相關的標準表示法被開發以前: r +代表r 的一個或多個重複。所以,前面的二進制數的正則表達式可寫做:
  ( 0 | 1 ) +

(2) 任意字符
  爲字母表中的任意字符進行匹配須要一個一般情況:無需特別運算,它只要求字母表中的每一個字符都列在一個解中。句號「 .」表示任意字符匹配的典型元字符,它不要求真正將字母表寫出來。利用這個元字符就可爲全部包含了至少一個b 的串寫出一個正則表達式,以下所示:
  . * b . *

(3) 字符範圍
  咱們常常須要寫出字符的範圍,例如全部的小寫字母或全部的數字。直到如今都是在用表示法a | b | . . . | z 來表示小寫字母,用0 | 1 | . . . | 9來表示數字。還可針對這種狀況使用一個特殊表示法,但常見的表示法是利用方括號和一個連字符,如[ a - z ]是指全部小寫字母,[ 0 -9 ]則指數字。這種表示法還可用做表示單個的解,所以a | b | c可寫成[ a b c ],它還可用於多個範圍,如[ a - z A - Z ]表明全部的大小寫字母。這種廣泛表示法稱爲字符類( character class)。例如,[ A - Z ]是假設位於A和Z之間的字符B、C等(一個可能的假設)且必須只能是A和Z之間的大寫字母(ASCII字符集也可)。但[ A - z ]則與[ A - Z a - z ]中的字符不匹配,甚至與ASCII字符集中的字符也不匹配。

(4) 不在給定集合中的任意字符
  正如前面所見的,可以使要匹配的字符集中不包括單個字符頗有用,這點可由用元字符表示「非」或解集合的互補運算來作到。例如,在邏輯中表示「非」的標準字符是波形符「 ~」,那麼表示字母表中非a 字符的正則表達式就是~ a。非a、b 及c 表示爲:
  ~ ( a | b | c )
  在Lex中使用的表示法是在連結中使用插入符「^」和上面所提的字符類來表示互補。

  例如,任何非a 的字符可寫做[ ^ a ],任何非a、b 及c 的字符則寫做:
    [ ^ a b c ]

(5) 可選的子表達式
  有關串的最後一個常見的狀況是在特定的串中包括既可能出現又可能不出現的可選部分。例如,數字前既可有一個諸如+或-的先行符也能夠沒有。這可用解來表示,同在正則定義中是同樣的:
  natural = [0-9]+
  signedNatural = natural | + natural | - natural
  但這會很快變得麻煩起來,如今引入問號元字符r?來表示由r 匹配的串是可選的(或顯示r 的0個或1個拷貝)。所以上面那個先行符號的例子可寫成:
  natural = [0-9]+
  signedNatural= (+|-)? natural

3.有限自動機

3.1 NFA:不肯定的有限自動機

1.定義

 

3.2 DFA:肯定的有限自動機

1.定義

 

2.解釋:

  • 初始狀態: 只有一個,由start箭頭指向
  • 終止狀態: 能夠有多個,用雙圈表示
  • L(M): 由一個有窮自動機M接收的全部串構成的集合,稱爲是該FA定義(或接收)的語言
  • 最長前綴匹配原則

 

3.3 用代碼實現有窮自定機

 

1.方法一

{starting in state 1}
if the next character is a letter then
    advance the input;
    { now in state 2 }
    while the next character is a letter or a digit do
        advance the input; { stay in state 2 }
    end while;
    { go to state 3 without advancing the input }
    accept ;
else
    { error or other cases }
end if;

這個代碼使用代碼中的位置(嵌套於測試中)來隱含狀態,這與由註釋所指出的同樣。若是沒有太多的狀態(要求有許多嵌套層)且DFA中的循環較小,那麼就合適了。

2.方法二

 

利用狀態變量和嵌套的case測試實現標識符DFA。

3.方法三

將DFA表示爲數據結構並寫成實現來自該數據結構的行爲的「類」代碼。轉換表( transition table),或二維數組就

是符合這個目標的簡單數據結構,它由表示轉換函數T值的狀態和輸入字符來索引:

 

 

 

若給定了恰當的數據結構和表項,就能夠在一個將會實現任何DFA的格式中編寫代碼了。下面的代碼圖解假設了轉換被保存在一個轉換數組T中,而T由狀態和輸入字符索引;先行輸入的轉換(即:那些在表格中未被括號標出的)是由布爾數組Advance給出,它們也由狀態和輸入字符索引;而由布爾數組A c c e p t給出的接受狀態則由狀態索引。下面就是代碼圖解:

state := 1;
ch : = next input character;
while not Accept[state] and not error(state) do
    newstate := T [s t a t e , c h];
    if Advance [state,ch] then ch := next input char;
    state := newstate;
end while;
if Accept [state] then accept;

 

 

此算法稱做表驅動( table driven),這是由於它們利用表格來引導算法的過程。表驅動方法有若干優勢:代碼的長度縮短了,相同的代碼能夠解決許多不一樣的問題,代碼也較易改變(維護)了。但也有一些缺點:表格會變得很是大,使得程序要求使用的空間也變得很是大。

 

3.4 從正則表達式到DFA

將正則表達式翻譯成DFA的最簡單算法是經過中間構造,在它之中,正則表達式派生出一個NFA,接着就用該NFA構造一個同等的DFA。

3.4.1 從正則表達式到NFA

1) 基本正則表達式基本正則表達式格式a、ε或Φ,其中a表示字母表中單個字符的匹配,ε表示空串的匹配,Φ而則表示根本不是串的匹配。

    

    與正則表達式a等同的NFA                與ε等同的NFA

2) 並置咱們但願構造一個與正則表達式rs等同的NFA,其中r 和s 都是正則表達式。

  r的NFA:

          

 注:圓角矩形的左邊圓圈表示初始狀態,右邊的雙圓表示接受狀態,中間的3個點表示NFA中未顯示出的狀態和轉換。

  rs的NFA:

    

咱們已將r 機的接受狀態與s 機的接受狀態經過一個- 轉換鏈接在一塊兒。新機器將r 機的初始狀態做爲它的初始狀態,並將s 機的接受狀態做爲它的接受狀態。很明顯,該機可接受L (r s) =L (r) L (s) 的關係,它也對應於正則表達式rs。

3) 在各選項中選擇咱們但願在與前面相同的假設下構造一個與r | s 相對應的NFA。

該機接受語言L (r | s) = L (r)υL (s)。

4) 重複咱們須要構造與r *相對應的機器,現假設已有一臺與r 相對應的機器。那麼就以下進行:

 

這裏又添加了兩個新的狀態:一個初始狀態和一個接受狀態。該機中的重複由從r 機的接受狀態到它的初始狀態的新的-轉換提供。

3.4.2 從NFA到DFA

消除ε- 轉換涉及到了ε- 閉包的構造。子集構造----消除在單個輸入字符上的多重轉換涉及跟蹤可由匹配單個字符而達到的狀態的集合。

1) 狀態集合的ε- 閉包

  咱們將單個狀態s 的ε- 閉包定義爲可由一系列的零個或多個ε- 轉換能達到的狀態集合。

2) 子集構造

對給定的NFA--M來構造DFA--$\overline{M}$。首先計算M的初始狀態的ε-閉包,構成$\overline{M}$的初始狀態。對於這個集合以及隨後的每一個集合,計算a字符之上的轉化以下:

  假設有狀態的S集合字母表中的字符a,計算集合Sa={t|對

於S中的一些s,在a上有從s到t的轉換}。接着計算$\overline{S_{a}}$,它是Sa的閉包。

這就定義了子集構造中一個新狀態和一個新轉換$S-\dfrac{a}{}\rightarrow \overline{S_{a}}$。

繼續這個過程直至不產生新的狀態或轉換。

 

例:

初始狀態是$\overline{1}=\left\{1,2,4\right\}$,存在字符a上的由狀態2向狀態3轉換,而在a上沒有來自狀態1或狀態4的轉換,所以a上就有從{1,2,4}到 $\overline{\left\{1,2,4\right\}}_{a}$ $=\overline{\left\{ 3\right\}}$ $=\left\{2,3,4\right\}$的轉換。因爲再也沒有來自一個字符上的一、2或4狀態的轉換了,所以就可將注意力轉向新狀態{ 2 , 3 , 4 }。此時在a 上有從狀態2到狀態3的轉換,且也沒有來自3或4狀態的a- 轉換,所以就有從{ 2 , 3 , 4 } 到

$ \overline{\left\{1,2,4\right\}}_{a}=\overline{\left\{3\right\}}=\left\{2,3,4\right\} $  的轉換,於是也就有從{ 2 , 3 , 4 }到它自己的a- 轉換。

相關文章
相關標籤/搜索