實現指令碼直譯器 - 詞法分析器

霖哲煌發表於2019-05-11

本系列介紹

筆者最近正學習編譯原理,為了將理論變為實踐,所以創作了本系列來記錄學習過程中的思考與問題,注意文章中為了理論上描述方便增加了自創的術語。

本系列使用 Java 語言來實現一個指令碼直譯器,該指令碼語言命名為 Foo,其語法參考 JavaScript 語言,本系列程式碼地址 Github

詞法分析器介紹

詞法分析器的作用是將輸入的字串轉變為一個個的記號(token),記號是由記號名(name)和屬性值(value)構成的二元組(unit doublet)。

通過構造有限自動機(finite automata, FA)來識別字串是否為匹配某種規則(模式),編譯原理書中用正規式來描述這種規則,但其描述性不強且不能描述匹配對,故本文統一採用擴充套件的巴斯克正規化(ABNF),具體語法參考 RFC5234

當有限自動機匹配或不匹配輸入串會執行不同的動作,具體實現時是匹配則返回對應的記號或者忽略該字串(例如註釋)否則報詞法錯誤,而有限自動機往往通過一段子程式(函式)來實現,將這些子程式組合起來就構成了詞法分析器(lexer)。

基本的準備

首先需要編寫一個記號類,其包含了記號名和屬性值,由於屬性值會被賦予不同的型別,所以使用 Object 型別,類中的常量來表示不同的記號名。

public class Token {
    public static final String TOKEN_EOF = "<eof>";
    // omit other token constants
    
    private private String name = TOKEN_EOF;
    private Object value = null;
    
    // getters and setters
}

接下來就可以來編寫 Lexer 詞法分析器類,先拋棄其他一些細節來分析下面定義的兩個私有屬性和兩個個私有方法的作用。其中屬性 currentChar 用來存放當前讀取的字元,而 nextChar 則是存放下一個字元 。

方法 char readChar() 用來讀取下一個字元,當返回 -1 時表明讀取完畢,其過載方法 char readChar(int offset) 用來指定偏移多少位置後讀取字元,從 0 開始且 0 相當於呼叫了該方法的無參過載。

public class Lexer {
    private char currentChar = '\0';
    private char nextChar = '\0';
    
    private char readChar() {
        // ...
    }
    private char readChar(int offset) {
        // ...
    }
}

分析字串流程

接下來定義 Lexer 類的公有方法 Token nextToken() 來讀取一個記號,它分析字串的流程如下:

  1. currentChar 存放當前需要匹配的字元,若讀取到檔案末尾則返回 EOF 記號。
  2. 根據匹配的單字元或雙字元,呼叫確定的子過程。
  3. 子過程匹配完畢,讀取下一個字元,並返回相對應的記號或者跳轉回步驟 1 。

注意若是程式碼較短,則這裡的子過程並不一定需要寫成函式。

匹配字首與匹配狀態

整個詞法分析器其實就是個不確定的有限自動機(NFA),開始時並不知道匹配何種記號,這裡稱之為 不確定匹配狀態 。通過單個或多個字元就能確定匹配何種記號並可以呼叫子過程,這時進入了 確定匹配狀態,而子過程就是個確定的有限自動機(DFA),稱這些字元或字元序列為 匹配字首

記號可以分為以下幾類,這些記號根據匹配字首可以分為需要雙字元和只需單字元確定,雙字元確定的記號只有註釋和雙字元符號,其他都為單字元確定的,這也是為什麼前面需要宣告 nextChar 變數存放下一個字元。其中的識別符號包含了保留字,而符號分為運算子及界符。

  • 註釋
  • 空白符號
  • 換行
  • 識別符號
  • 數字
  • 字串
  • 雙字元符號
  • 單字元符號
  • 終止記號

消除歧義

有些情況下,單字元確定的匹配會影響雙字元確定的匹配,為了消除這種歧義,就需要先進行雙字元匹配再進行單字元匹配。

例如單行註釋以雙字元 // 作為匹配字首,而單字元符號除號 / 會影響該雙字元確定的匹配,若是將單字元確定的匹配放前面,則會匹配成兩個除號記號。

匹配換行

在不同的系統中,檔案的換行有以下三種:

  • CRLF Windows
  • LF Linux
  • CR Unix

為了相容考慮,匹配換行具體程式碼如下所示:

if (currentChar == '\r' || currentChar == '\n') {
    newLine();
    continue;
}

private void newLine() {
    nextChar = readChar();
    if (nextChar == '\n') {
        currentChar = readChar();
    } else {
        currentChar = nextChar;
        nextChar = '\0';
    }
}

待續

相關文章