本系列介紹
筆者最近正學習編譯原理,為了將理論變為實踐,所以創作了本系列來記錄學習過程中的思考與問題,注意文章中為了理論上描述方便增加了自創的術語。
本系列使用 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()
來讀取一個記號,它分析字符串的流程如下:
currentChar
存放當前需要匹配的字符,若讀取到文件末尾則返回EOF
記號。- 根據匹配的單字符或雙字符,調用確定的子過程。
- 子過程匹配完畢,讀取下一個字符,並返回相對應的記號或者跳轉回步驟 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';
}
}