前言
本文前四節主要講解理論部分,在最後一節的實踐部分中,實現了一個基於DFA法的支援多語言擴充套件的JS版開源詞法分析器,點選檢視lexer專案地址 ,歡迎大家關注與討論 !
讓機器理解程式碼並生成可執行檔案,這是一件很困難的事情,所以編譯器是一個非常浩大的工程,它內部工作過程十分複雜。
不過電腦科學中有句名言,任何一個問題都可以通過增加一箇中間層/抽象層來解決,這也是最常用的計算機分層思想。
All problems in computer science can be solved by another level of indirection. —— David John Wheeler
所以為了降低編譯器工程的複雜性,提高發展效率,一般把編譯器分為兩個完全解耦的兩個部分
- 編譯器前端 :只負責理解程式碼,不考慮各種CPU指令不相容等問題
- 編譯器後端 :只負責生成可執行檔案,不考慮各種語言特性
為了讓兩個部分可以連線在一起工作,所以必然存在一個部分的輸出是另一部分的輸入,很明顯,編譯器前端的輸出,就是編譯器後端的輸入。
這樣,兩個部分只需要約定好資料格式,就可以大幅度提高編譯器的發展效率,編譯器整個工作流程也變得非常簡單清晰了,如下圖所示
結論 :所以編譯器前端就是編譯器中的前端部分,負責理解程式碼並生成中間語言
既然編譯器前端的工作是理解程式碼,那麼它的原理是什麼呢,首先看下面一段簡單的C語言程式碼
int age = 18;
從人類閱讀程式碼的角度來看,我們是如何理解上述程式碼的呢
1、區域性理解
- 看到
int
關鍵字 - 看到
age
識別符號 - 看到
=
賦值符號 - 看到
18
字面量數值 - 看到
;
結束符
區域性理解的過程,就是詞法分析的過程
2、全域性理解
通過聯絡孤立的、區域性的理解結果,可得到一個整體 int
age
=
18
;
,對其在腦海中進行全域性理解後,發現它符合C語言的整型賦值語法
整型賦值語法 -> 關鍵字 識別符號 賦值符號 字面量
關鍵字 -> int | long
識別符號 -> [a-zA-Z_]+[a-zA-Z+_*0-9]*
字面量 ->[0-9]+
所以通過全域性理解,最終可知道上面程式碼是一個整型的賦值語句
全域性理解的過程,就是語法分析的過程
3、總結
結合人類理解程式碼的過程,編譯器前端主要可以分為區域性理解和全域性理解兩個步驟,其中
- 區域性理解會生成一個一個的理解結果,稱為
Token
- 全域性理解會把孤立的
token
聯絡成為一個整體,進行語法規則匹配,如果符合語法則理解成功,否則理解失敗
詞法分析器就是在做對字元序列的區域性理解,每完成一次區域性理解,就生成一個Token
。常見的Token
包括操作符、符號、空白符、關鍵字、識別符號、數字、浮點數、字串、字元等。
所以詞法分析器的工作內容也比較簡單,只要能把輸入的字元序列,生成一個有序的token
列表即可。
1、直接掃描法
直接掃描法的思路類似二重迴圈
的暴力法,每一輪的掃描都是如下過程
- 先第一層的掃描,根據第一個字元判斷屬於哪種型別的token
- 進入第二層的掃描,向後依次讀取,直到讀出一個完整的token為止,跳出第二層迴圈
- 繼續開始第一層的掃描
(1) 虛擬碼
let end = s.length-1;
for(let i=0; i<=end; i=n){
let tokenType = justTokenType(s[i]);
switch(tokenType){
case "string":
let queue = [];
for(let j = i; j<=end-1; ++j){
if(!match){
break;
}
queue.push(s[j]);
}
let token = queue.join('');
tokens.push(token);
n=j+1;
break;
}
}
(2) 缺點
- 邏輯非常複雜
- 可能存在回溯情況,可能需要記憶已匹配的前N個字元,效率低
- 大量IF判斷,可維護性差,擴充套件性差
2、DFA法
DFA即deterministic finite automaton
有限狀態自動機,其特點是可以實現狀態的自動轉移,可以用於解決字元匹配問題
(1) 核心構成
DFA的核心構成要素是狀態和狀態的轉移
- 定義自動機所具備的N種狀態,並定義初始狀態
- 定義上一個狀態轉移至下一個狀態的條件
(2) 應用實踐
如何用DFA
實現詞法分析器呢,步驟如下
先定義不同的狀態 :如操作符狀態、數字狀態、符號狀態等
再定義狀態轉移條件 :如當前狀態是初始狀態,遇到數字則轉移至數字狀態,遇到符號則轉移至符號狀態
如果字元讀取完成,則整個轉移過程結束
(3) 虛擬碼
let state = 0; // 當前狀態, 也是初始狀態 while ((ch = nextChar()) !== false) { let match = false; // 獲取下一個狀態, 如果下一個狀態不是初始狀態, 則說明匹配成功 let nextState = getNextState(ch, state); if (nextState) { match = true; } // 如果匹配成功, 則字元讀取序列的下標+1,並轉移至下一個狀態 if (match) { incrSeq(); flowtoNextState(ch, nextState); } else { // 不匹配則生成token,並轉移至初始狀態,開始重新匹配 produceToken(); flowtoResetState(); } }
(4) 優點
邏輯清晰簡單
可維護性高,可擴充套件能力高
時間複雜度為O(N),效率高
(5) 一些例子
1) 匹配字串
2) 匹配浮點數
3) 匹配字元
4) 匹配運算子
詞法分析器的詳細實現及原始碼講解,參見lexer專案
本文已結束,感謝閱讀,點選檢視原文
下期請關注文章《編譯器前端之如何實現一個基於LL(1)的語法分析器》
本作品採用《CC 協議》,轉載必須註明作者和本文連結