編譯器前端之如何實現基於DFA的詞法分析器

Kuper發表於2021-09-21

前言

本文前四節主要講解理論部分,在最後一節的實踐部分中,實現了一個基於DFA法的支援多語言擴充套件的JS版開源詞法分析器,點選檢視lexer專案地址 ,歡迎大家關注與討論 !

讓機器理解程式碼並生成可執行檔案,這是一件很困難的事情,所以編譯器是一個非常浩大的工程,它內部工作過程十分複雜。

不過電腦科學中有句名言,任何一個問題都可以通過增加一箇中間層/抽象層來解決,這也是最常用的計算機分層思想。

All problems in computer science can be solved by another level of indirection. —— David John Wheeler

所以為了降低編譯器工程的複雜性,提高發展效率,一般把編譯器分為兩個完全解耦的兩個部分

  • 編譯器前端 :只負責理解程式碼,不考慮各種CPU指令不相容等問題
  • 編譯器後端 :只負責生成可執行檔案,不考慮各種語言特性

為了讓兩個部分可以連線在一起工作,所以必然存在一個部分的輸出是另一部分的輸入,很明顯,編譯器前端的輸出,就是編譯器後端的輸入。

這樣,兩個部分只需要約定好資料格式,就可以大幅度提高編譯器的發展效率,編譯器整個工作流程也變得非常簡單清晰了,如下圖所示

image

結論 :所以編譯器前端就是編譯器中的前端部分,負責理解程式碼並生成中間語言

既然編譯器前端的工作是理解程式碼,那麼它的原理是什麼呢,首先看下面一段簡單的C語言程式碼

int age = 18;

從人類閱讀程式碼的角度來看,我們是如何理解上述程式碼的呢

1、區域性理解

  1. 看到 int 關鍵字
  2. 看到 age 識別符號
  3. 看到 = 賦值符號
  4. 看到 18 字面量數值
  5. 看到 ; 結束符

區域性理解的過程,就是詞法分析的過程

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種狀態,並定義初始狀態
  • 定義上一個狀態轉移至下一個狀態的條件

Laravel

(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) 匹配字串

Laravel

2) 匹配浮點數

Laravel

3) 匹配字元

Laravel

4) 匹配運算子

Laravel

詞法分析器的詳細實現及原始碼講解,參見lexer專案

本文已結束,感謝閱讀,點選檢視原文

下期請關注文章《編譯器前端之如何實現一個基於LL(1)的語法分析器》

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章