atitit.詞法分析原理 詞法分析器 (Lexer)
2. ;實現詞法分析程序的常用途徑:自動生成,手工生成.[1] 2
2.4. Token的類型,根據程序設計語言的特點,單詞可以分為五類:關鍵字、標識符、常量、運算符、界符。以4
2.10. 不確定”(Nondeterministic Finite Automata ,NFA 8
2.11. 轉換圖(transition graph)的表示9
2.15. 正則表達式如何轉換為NFA呢?有幾個公式(MLS2007[1]):13
1. 詞法分析(英語:lexical analysis)
是計算機科學中將字符序列轉換為單詞(Token)序列的過程。進行詞法分析的程序或者函數叫作詞法分析器(Lexical analyzer,簡稱Lexer),也叫掃描器(Scanner
詞法分析階段是編譯過程的第一個階段,是編譯的基礎。這個階段的任務是從左到右一個字符一個字符地讀入源程序,即對構成源程序的字符流進行掃描然后根據構詞規則識別單詞(也稱單詞符號或符號)。詞法分析程序實現這個任務。詞法分析程序可以使用Lex等工具自動生成。
詞法分析是編譯程序的第一個階段且是必要階段;詞法分析的核心任務是掃描、識別單詞且對識別出的單詞給出定性、定長的處理
一段對計算機來說豪無意義的字符串,經過語法分析后就得到了略微有意義的 Token 流。digit 就表示這個詞法單元對應的是數字,operator 則表示操作符,后面相應的數字和符號(粉色背景)就是詞素。同時,程序中一些不必要的空白、注釋也可以由詞法分析器來過濾掉,這樣,之后的語法分析等步驟處理起來就會容易得多
作者:: ★(attilax)>>> 綽號:老哇的爪子 ( 全名::Attilax Akbar Al Rapanui 阿提拉克斯 阿克巴 阿爾 拉帕努伊 ) 漢字名:艾龍, EMAIL:1466519819@qq.com
轉載請注明來源: http://www.cnblogs.com/attilax/
2. ;實現詞法分析程序的常用途徑:自動生成,手工生成.[1]
盡管在某些情況下需要手工編寫詞法分析器,使用狀態模式,,一般情況下詞法分析器都用自動化工具生成。
2.1. 詞法分析程序的功能
完成詞法分析任務的程序稱為詞法分析程序或詞法分析器或掃描器。[1]
從左至右地對源程序進行掃描,按照語言的詞法規則識別各類單詞,並產生相應單詞的屬性字。[1]
詞法分析器通常不會關心單詞之間的關系(屬於語法分析的范疇),舉例來說:詞法分析器能夠將括號識別為單詞,但並不保證括號是否匹配。
。語法分析器讀取輸入字符流、從中識別出語素、最后生成不同類型的單詞。其間一旦發現無效單詞,便會報錯。
詞法分析器可以做諸如
1). 去掉注釋,自動生成文檔(c#中的///注釋)
2). 提供錯誤位置(可以通過記錄行號來提供),當字符流變成詞法記號流以后,就沒有了行的概念
3). 完成預處理,比如宏定義
2.2. 如何描述詞素
現在知道了詞法分析可以將詞素分割開來,那么詞素是怎么描述的?或者說,為什么 12、+ 和 34 都是詞素,而 1、 2+3 和 4 就不是詞素呢?這就需要用到模式了。
模式(pattern)描述了一個詞法單元的詞素可能具有的形式。
也就是說,我定義了 digit 模式為“由一個或多個數字組成的序列”,和 operator 模式為“單個 + 或 * 字符”,詞法分析器就知道 12 是一個詞素,而 2+3 則不是詞素了。
現在,模式一般都是用正則表達式(regular expression)表示的,這里所謂的正則表達式,與平常所說的正則表達式(例如 System.Text.RegularExpressions.Regex 類)形式完全相同,功能卻更有限,它只包含了字符串的匹配能力,而沒有分組、引用和替換的能力。簡單的舉個例子,a+ 這個正則表達式就表示“由一個或多個字符 a 組成的序列”。
2.3. 單詞token
這里的單詞是一個字符串,是構成源代碼的最小單位。從輸入字符流中生成單詞的過程叫作單詞化(Tokenization),在這個過程中,詞法分析器還會對單詞進行分類。
分析詞素的同時還會同時記錄下這些詞素所在的行、列以便輸出錯誤信息供用戶查看,也會同時記錄詞素的類型。
{
"channel":0,
"charPositionInLine":15,
"inputStream":{"$ref":"$.tokenSource.charStream"},
"line":1,
"startIndex":15,
"stopIndex":15,
"text":"<EOF>",
"tokenIndex":2,
"type":-1
}
]
2.4. Token的類型,根據程序設計語言的特點,單詞可以分為五類:關鍵字、標識符、常量、運算符、界符。以
讀者可能對"單詞"感到有點疑惑,不明白到底什么才是詞法分析中所說的"單詞"。試圖回答這個問題就必須了解幾個基本概念。這里,引入幾個程序設計語言相關的名詞。
(1)標識符:用戶自定義的變量名、函數名等字符串。
(2)關鍵字:具有特殊含義的標識符。
(3)運算符:例如+、-、*、/ 等。
(4)常量:例如3.24、92等。
(5)界符:具有特殊含義的符號,如分號、括號等。
詞法分析的結果是識別出如下的單詞符號:
關鍵字 |
界符 |
標識符 |
運算符 |
常量 |
運算符 |
if |
( |
aa |
&& |
10 |
== |
常量 |
界符 |
標識符 |
運算符 |
常量 |
界符 |
0 |
) |
aa |
= |
100 |
; |
這里,讀者只需了解詞法分析的任務即可。其算法實現將在第2章中詳述
2.5. 詞法分析的第一階段即掃描器
詞法分析的第一階段即掃描器,通常基於有限狀態自動機。掃描器能夠識別其所能處理的單詞中可能包含的所有字符序列(單個這樣的字符序列即前面所說的“語素”)。例如“整數”單詞可以包含所有數字字符序列。很多情況下,根據第一個非空白字符便可以推導出該單詞的類型,於是便可逐個處理之后的字符,直到出現不屬於該類型單詞字符集中的字符(即最長一致原則
2.6. 詞法分析的第二階段評估器(Evaluator)
,語法分析器需要第二階段的評估器(Evaluator)。評估器根據語素中的字符序列生成一個“值”,這個“值”和語素的類型便構成了可以送入語法分析器的單詞。一些諸如括號的語素並沒有“值”,評估器函數便可以什么都不返回。整數、標識符、字符串的評估器則要復雜的多。評估器有時會抑制語素,被抑制的語素(例如空白語素和注釋語素)隨后不會被送入語法分析器。
2.7. 例如C語言程序段的詞法分析結果
例2-1 C語言程序段的詞法分析結果見表2-1。
表2-1 詞法分析的單詞流
源程序字符流 |
詞法分析的邏輯結果 |
||||||||||||||||||||||||
int i,j; for (i=1;i<10;i++) j=j+1; |
|
注意,表2-1的單詞流並不是詞法分析器真正的實際輸出結果,只是一種邏輯表示而已。更詳細的形式將在后續章節中討論。根據單詞的分類標准,可以將單詞作如下歸類,見表2-2。
表2-2 例2-1單詞流的分類
關 鍵 字 |
int |
for |
|
|
標識符 |
i |
j |
|
|
運算符 |
= |
++ |
< |
+ |
常量 |
10 |
1 |
|
|
界符 |
, |
; |
( |
) |
這里,讀者可能會有兩個疑問:
(1)為什么"++"運算符不會分解為兩個"+"運算符呢?
(2)為什么將"int i"分解為"int"和"i",而不是"int i"呢?
最長原則
在實際編譯器設計中,任何詞法分析器都必須滿足一個原則,就是在符合詞法定義的情況下進行超前搜索識別。例如,當C語言詞法分析器讀入了一個字符"+"后,由於C語言中存在"++"、"+="運算符,那么,詞法分析器會繼續讀入下一個字符。如果下一個字符是"+"或"="時,詞法分析器就將這兩個字符作為一個運算符。然而,如果下一個字符不是"+"或"="時,詞法分析器就將前一個字符"+"作為一個運算符記錄下來后,繼續識別下一個單詞。
根據這個原則,就可以解釋為什么"int"沒有被識別為"i"、"n"、"t"了。根據C語言標識符(關鍵字只是有特殊含義的標識符)定義的規則,標識符必須以字母或下畫線開頭,后跟字母、數字、下畫線的任意組合。因此,當讀入"i"后,繼續讀入"n",由於"in"是合法的標識符,則繼續讀入"t"。直到讀到" "時,發現"int "不滿足標識符的定義,則將"int"記錄下來即可。
2.8. 最長原則
不過,詞法分析器的設計難度很大程度上依賴於程序設計語言本身的規范
在設計一門程序設計語言時,應該盡可能避免關鍵字非保留字、空格忽略等類似情況的發生,否則將給詞法、語法分析造成相當的障礙
2.9. 詞法單元的識別
某些狀態為接受狀態或最終狀態,表明已經找到一個詞素。
1)關系符轉換圖
2)保留字和標識符轉換圖
3)無符號樹轉換圖
4)空白轉換圖
2.10. 不確定”(Nondeterministic Finite Automata ,NFA
有窮自動機
1)有窮自動機可用作描述在輸入串中識別模式的過程,因此也能用作構造掃描程序。當然有窮自動機與正則表達式之間有着很密切的關系
2)有限自動機分成確定的和不確定的兩種情況。“不確定”(Nondeterministic Finite Automata ,NFA)的含義是,存在這樣的狀態,對於某個輸入符號,它存在不只一種轉換。 確定的和不確定的有限自動機都正好能識別正規集,也就是它們能識別的語言正好是正規式所能表達的語言。
假定一個輸入符號(symbol),可以得到2個或者2個以上的可能狀態,那么這個finite automaton就是不確定的,反之就是確定的。例如:
這就是一個不確定的無限自動機,在symbol a輸入的時候,無法確定狀態應該轉向0,還是1
不論是確定的finite automaton還是非確定的finite automaton,它們都可以精確的描述正規集(regular sets)
我們可以很方便的把正規表達式(regular expressions)轉換成為不確定 finite automaton
下面關於FA和NFA的描述是抄襲AMRJ2010[1]的:
轉換的核心是被稱為有窮自動機(finite automata)的表示方法。這些自動機在本質上是與狀態轉換圖類似的圖,但有如下幾點不同:
· 有窮自動機是識別器,它們只能對每個可能的輸入串簡單的回答“是”或“否”。
2.11. 轉換圖(transition graph)的表示
我們知道,計算機是無法直接表示一個圖,我們應該如何來表示一個轉換圖?使用表格就是一個最簡單的方法,每行表示一個狀態,每列表示一個input symbol,這種表格被叫做 transtion table(轉換表)
可以說使用表格是最簡單的表示方式,但是我們可以注意到在這個圖中狀態1和input symbol a,是沒有下一個狀態的(空集合),也就是,對於一個大的狀態圖,我們可能花費大量的空間,而其中空集合會消耗不少空間,但是這種消耗又不是必須的,所以,作為最簡單的一種實現方式,卻不是最優的
語言(language)被NFA定義成為一個input string的集合,而這個集合中的元素則是被NFA受接受的所有的字符串(那些可以從開始狀態到某接受狀態的input string)
至於存儲的方式,可以試試鄰接表。注意,使用什么樣的數據結構來保存NFA按情況不同而不同,在一些特殊情況下,某些數據結構會變得很方便使用,而換入其他情況,則不可以使用了。
2.12. 詞法分析(3)---DFA
1. DFA(Deterministic Finite automaton)
DFA就是確定的有限自動機,因為DFA和NFA關系密切,我們經常需要把他們拿到一起來講,NFA可以轉化成為一個DFA,DFA依然是一個數學model,它和NFA有以下區別
1. 不存在ε-transition,也就是說,不存在ε為input symbol的邊
2. 對於move函數,move : (state, symbol) -> S,具體來說就是,一個狀態和一個特定的input symbol,不會映射到2個不同的狀態。這樣的結果是,每個狀態,關於每個特定的input symbol,只有一條出邊
下圖就是一個DFA:
接受語言(a|b)*ab,注意一下,接受語言(a|b)*ab的DFA我們前面見過,就是這張圖:
2. DFA的行為
我們用一個算法來模擬DFA的行為
s = s0;
c = nextchar();
while(c != EOF){
s = move(s,c);
c = nextchar();
}
if(s屬於F)
return "yes"
else
return "no"
識別詞法的過程是用DFA實現的,DFA是類似於下圖所表示的東西(其實就是一個狀態轉換圖):
這個DFA只能處理IF、INSERT、INTO三個詞,它的運行過程大至描述如下:
1. 聲名一個變量(s)用來保存當前的狀態。
2. 把開始狀態(開始狀態就是圖中的實心圓點兒)負值給s。
3. 從字符流中讀一個字符(c),如果讀不出字符就終止算法。
4. s的邊上有字符,就代表s輸入這個字符之后可以沿着這個邊走到下一個狀態。此時看一下s輸入c可以到哪個新狀態里去。如果不能到到達一個新狀態,則說明這個DFA不能解析這個字符流(到此終止算法),否則s的值變成新的狀態。
5. 看一下s是否為終止狀態(也叫接受狀態,圖中用帶白邊的圓點兒表示),如果是終止狀態,則解析到一個字符,然后回到第2步,如果不是終止狀態,則回到第3步。
差不多就是這樣的,實際情況比上面所說的要稍復雜一點(比如沖突解決、匹配原則),后面會詳細講。
這個DFA只能識別三個單詞,實際的編譯器中肯定是要能識別一個語言中所有的詞素,那樣一個DFA是很龐大的,如何去來概造這個完整的DFA也是后面要講的內容
2.13. 為什么要NFA轉DFA
到此正則表達式轉NFA的內容就全講完了。雖然NFA也可以運行,並且也可以用來識別語言的詞素,但其運行過程要比DFA復雜得多,而且除非我們可以並發的運行NFA的每個分支,否則NFA的執行速度絕對分比NFA的執行速度要慢。我們現在擁有的計算機一般都只是PC機,還沒有那么強的並發能力,所以NFA轉DFA就成了詞法分析的一個必要的程。
另外,某些正則引擎用NFA來運行,這是基於引擎使用的實際情況來考慮的。因為NFA轉DFA也是要時間的,並且如果引擎經常使用在高並發能力的計算機上,那么直接用NFA來運行還會快一些。而編譯器通常不這么做是因為編譯器在發布時只發布DFA就行了,NFA轉DFA的過程最終用戶並不會接觸到。這也是詞法分析程序與正則引擎的不同之處。
下一節來講一下NFA轉DFA的方法。
2.14. 則表達式轉NFA
正則表達式是什么?這個問題不在這里詳述。上網搜一下,很快就能了解基本概念。有一本書《精通正則表達式》,這本書第一章(20多頁)看完就會寫基本的正則表達式了。其電子版在網上有下載。
直接做一個可以識別一個語言所有詞素的DFA是非常困難的,而且即使做出來,日后的修改同樣非常麻煩。而用正則表達式(正則文法)來描述詞素就簡單得多,同時日后這個語言要修改或增加新的詞素都很簡單。所以現在的詞法分析器的構造方式都是先用一種基於正則文法的語言來描述所有詞素,再把這一描述轉換成DFA。正則文法轉DFA的常規方法是需要一個中間過程的,即先把正則文法的描述轉成NFA,而從NFA到DFA的轉換方法是存在的。
2.15. 正則表達式如何轉換為NFA呢?有幾個公式(MLS2007[1]):
公式1:如果一個正則表達式只有一個字符'a',那么NFA如下圖:
即:從開始狀態,輸入一個字符a,就到達了接受狀態。
公式2:如果一個正則表達式是兩個表達式連成的,如ab,那么NFA如下圖:
即:從開始狀態,輸入a,到達狀態1,再輸入b到達接受狀態。這個公式相當於把兩個“公式1”前后連接而成的。
公式3:如果一個正則表達式是這樣的:a|b,即二選一的情況,那么NFA如下圖:
圖中我有幾條邊是沒有畫輸入的,那么就是Ɛ,即:空輸入或無輸入,以后為了畫圖方便,Ɛ輸入就不畫在圖中了。
這個圖描述的就是:從開始狀態,可以向上走1,也可以向下走3,如果走1,那輸入a就走到2,如果走3,那么輸入b就走到4,2和4都有一個空輸出到接受狀態。
這個圖相當於把兩個“公式1”的並排放到一起,前面接一個狀態做為開始,后面接一個狀態做為結束。
公式4:如果一個正則表達式是Kleen必包:a*,那么其對應的NFA如圖:
這個圖稍微解釋一下:從開始有兩條空輸入邊,一條直接到接受狀態,這表示一個a都不接受,另一個空輸入邊到1,1只有一個出口就是輸入一個a到2,2狀態可以直接到達接受狀態,也可以回到1,這樣就可以達到接受任意多個a的情況。
有了上面四個公式,就可以達到匹配任何字符的目的了(還不能匹配位置,不過對於編譯器的詞法分析是不需要匹配位置的),舉個例子a*|bc就可以用“公式4”把a*的圖畫出來,用“公式2”把bc的圖畫出來,再用“公式3”把前兩個圖連接上就行了,如圖:
上面四個公式上最基本的公式。大多數正則表達式也會識別其它的結構,如:a?、a+,其實這也可以用以上公式來做:a?可以等價於a|Ɛ(其實這個只要把a表示的NFA從開始狀態拉一個空輸入的邊到接受狀態就可以了,不需要使用“公式2”的,“公式2”主要是使用於兩個正則表達式之前的或關系,如果兩個表達式有一個為空,可以簡便一點處理),a+等價於aa*,這樣我們還是可以用基本公式來處理。
基本公式有了之后,還需要處理一些括號,下面分別講一下:
方括號[]:代表字符組,就是指方括號中的字符任選其一的意思。例如:[abc]就是指匹配a或匹配b或匹配c,即與a|b|c等價。特殊情況是當方括號內的第一個字符是^時,表示排除形字符組,就是指廣括號中,除了第一個^之外的其它字符都不匹配,例如[^abc]就是指不能匹配a,也不能匹配b,也不能匹配c。另外,在字符組中可以使用連字符(-),例如[a-d]和[abcd]是等價的。
方括號轉NFA的一個比較簡單的做法是把整個字符組做為一條邊的輸入,這樣做的話,那么表示NFA的某狀態的輸入就不是單個字符,而是一個字符串,只要當前字符是(或者不是,當是排除形字符組時)這個字符串中的字符即可。這樣的處理方式就可以套用前面的“公式1”了。
對於連字符(-)的處理一般有兩種方法。如果語言的字母表比較小(比如ASCII),那么只要把連字符展開就可以了,例如:[a-z]就直接用[abcdefghijklmnopqrstuvwxyz]來替換。如果語言的字母表很大(比如Unicode),那么就不展開,如果這樣展開,那這一個字符串就要占用非常大的內存,這時的做法是把連字符直接放到輸入里,不在轉換與此同時文法的時候處理,而在運行的時候用“大於等於”和“小於等於”來判斷。
小括號():代表在正則表達式中限定一個范圍,也就是改變有限級的做用。例如:a*|bc和(a*|b)c這兩個表達式,我們知道“合取”的有限級是高於“析取”的(這里用“合取”和“析取”不太標准,不過因為我想到如果用“與”和“或”仍然不太標准,所以我選擇用兩個稍生僻點的名詞,可以多吸引一下讀者的眼球,或許可以因此減少對這里的不准確的描述的誤解),所以a*|bc對應的NFA圖是這樣的:
而(a*|b)c改變了優先級,此時要先做“析取”再做合取,其對應的NFA圖是這樣的:
對於小括號的處理方式是先把括號內的部分做為一個整體再處理。例如:(a*|b)c,先把a*|b做為一個整體A,那么就變成了(A)c此時小括號就沒用了,可以去掉,就變成了Ac,這樣就可以套用“公式2”了。之后再處理a*|b,此時沒有括號,也可以套用基本公式(如果有嵌套的小括號,則前面的辦法,把括號內的部分做為一個整體)。之后再把轉換完a*|b的NFA放到之前A在圖中的位置就可以了。
花括號{}:用來引用前面已經定義過的正則表達式(我在寫代碼的時候用了尖括號<>,flex用的是花括號,我打算以后重寫的時候用花括號,因為花括號好看一點)。正則文法的准確定義我不在這里詳述,用我的話簡單說來就是一系列的正則表達式(每個表達式有一個名字和一個定義),后面的表達式不但可以包含字母表中的內容,還可以包含前面已經定義過的表達式。這里我們就用花括號來引用前面已經定義過的正則表達式的名字。
對於花括號的處理比較簡單:我們只要把花括號部分用前面的定義來替換就行了。實際寫代碼的時候我們可能在轉換NFA的時候把前面已經轉換完成的NFA圖拿過來用就行了,而不需要去替換其定義。
2.16. 構造詞法分析器了。大致的流程如下:
圖 3 構造詞法分析器
Regexpre >>nfa>>dfa>>simple dfa>>convert table>>dfa simulaer>>tokens..
從上圖來看,定義了模式的正則表達式,經過 NFA 轉換、DFA 轉換和 DFA 化簡,得到了一張轉換表。這張轉換表再加上一個固定的 DFA 模擬器,就組成了詞法分析器。它不斷的從輸入緩沖區中讀取字符,利用自動機來識別詞素並輸出。可以說,詞法分析的精華就是如何得到這張轉換表
2.17. 常用的token scanner
Hb 使用antlr...mysql 使用的customez..,但是語法分析卻用了yacc
2.18. 詞法分析器也能檢測到源代碼里邊的一些錯誤
詞法錯誤:
詞法分析器是很難(有些錯誤還是可以檢測)檢測錯誤的,因為詞法分析器的目的是產生詞法記號流,它沒有能力去分析程序結構,因此無法檢測到和程序結構有關的錯誤
從詞法分析階段中,詞法分析器也能檢測到源代碼里邊的一些錯誤。例如在Zend引擎的詞法分析階段就有這樣一段代碼:
zend_error(E_COMPILE_WARNING, “Unterminated comment starting line %d”, CG(zend_lineno));
當檢測到/*開頭,但是沒有*/結尾時,Zend引擎會拋出一個Waring提示
但是並不影響接下來的詞法解析,詞法分析階段一般都不會造成嚴重的解析錯誤,因為詞法分析階段的職責就是識別出Token序列而已,它並不需要知道Token跟Token之間是否具備什么聯系(那個應該是語法分析階段的職責)。在Zend引擎的詞法分析器中也會拋出致命的解析錯誤而終止詞法分析階段,如下代碼:
zend_error_noreturn(E_COMPILE_ERROR, “Could not convert the script from the detected “
“encoding \”%s\” to a compatible encoding”, zend_multibyte_get_encoding_name(LANG_SCNG(script_encoding)));
這個解析錯誤是因為從輸入流里邊檢測到的代碼的編碼不合法,顯然,這里是應該終止掉整個解析過程的。
Zend引擎的詞法分析器re2c來生成,詞法分析的階段會涉及到各個狀態,其變量命名均為yy開頭(下文會說明)。
2.19. 參考
2.1.1 詞法分析的任務 - 51CTO.COM.html
【編譯原理】第三章 詞法分析 - 小田的專欄 - 博客園.html
C# 詞法分析器(一)詞法分析介紹 update 2014.1.8 - CYJB - 博客園.html
2、JavaScript高級之詞法分析 - Javascript教程_JS教程_技術文章 - 紅黑聯盟.html
3、詞法分析(NFA與DFA) - woaidongmao - C++博客.html
4、一個編譯器的實現(02)——詞法分析(1.正則轉NFA)-naturemickey-ChinaUnix博客.html