筆者最近正學習編譯原理,爲了將理論變爲實踐,因此創做了本系列來記錄學習過程當中的思考與問題,注意文章中爲了理論上描述方便增長了自創的術語。html
本系列使用 Java 語言來實現一個腳本解釋器,該腳本語言命名爲 Foo,其語法參考 JavaScript 語言,本系列代碼地址 Github 。java
詞法分析器的做用是將輸入的字符串轉變爲一個個的記號(token),記號是由記號名(name)和屬性值(value)構成的二元組(unit doublet)。git
經過構造有限自動機(finite automata, FA)來識別字符串是否爲匹配某種規則(模式),編譯原理書中用正規式來描述這種規則,但其描述性不強且不能描述匹配對,故本文統一採用擴展的巴斯克範式(ABNF),具體語法參考 RFC5234。github
當有限自動機匹配或不匹配輸入串會執行不一樣的動做,具體實現時是匹配則返回對應的記號或者忽略該字符串(例如註釋)不然報詞法錯誤,而有限自動機每每經過一段子程序(函數)來實現,將這些子程序組合起來就構成了詞法分析器(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
則是存放下一個字符 。code
方法 char readChar()
用來讀取下一個字符,當返回 -1
時代表讀取完畢,其重載方法 char readChar(int offset)
用來指定偏移多少位置後讀取字符,從 0 開始且 0 至關於調用了該方法的無參重載。htm
public class Lexer { private char currentChar = '\0'; private char nextChar = '\0'; private char readChar() { // ... } private char readChar(int offset) { // ... } }
接下來定義 Lexer
類的公有方法 Token nextToken()
來讀取一個記號,它分析字符串的流程以下:token
currentChar
存放當前須要匹配的字符,若讀取到文件末尾則返回 EOF
記號。注意如果代碼較短,則這裏的子過程並不必定須要寫成函數。ip
整個詞法分析器其實就是個不肯定的有限自動機(NFA),開始時並不知道匹配何種記號,這裏稱之爲 不肯定匹配狀態
。經過單個或多個字符就能肯定匹配何種記號並能夠調用子過程,這時進入了 肯定匹配狀態
,而子過程就是個肯定的有限自動機(DFA),稱這些字符或字符序列爲 匹配前綴
。
記號能夠分爲如下幾類,這些記號根據匹配前綴能夠分爲須要雙字符和只需單字符肯定,雙字符肯定的記號只有註釋和雙字符符號,其餘都爲單字符肯定的,這也是爲何前面須要聲明 nextChar
變量存放下一個字符。其中的標識符包含了保留字,而符號分爲運算符及界符。
有些狀況下,單字符肯定的匹配會影響雙字符肯定的匹配,爲了消除這種歧義,就須要先進行雙字符匹配再進行單字符匹配。
例如單行註釋以雙字符 //
做爲匹配前綴,而單字符符號除號 /
會影響該雙字符肯定的匹配,如果將單字符肯定的匹配放前面,則會匹配成兩個除號記號。
在不一樣的系統中,文件的換行有如下三種:
爲了兼容考慮,匹配換行具體代碼以下所示:
if (currentChar == '\r' || currentChar == '\n') { newLine(); continue; } private void newLine() { nextChar = readChar(); if (nextChar == '\n') { currentChar = readChar(); } else { currentChar = nextChar; nextChar = '\0'; } }