Golang 词法分析器浅析
???? 淺析 Go 語言的詞法分析器
章節目錄
作者能力有限, 如果您在閱讀過程中發現任何錯誤, 還請您務必聯系本人,指出錯誤, 避免后來讀者再學習錯誤的知識.謝謝!
簡介##
在本文我們將簡單的走讀 Go 語言的詞法分析器實現(go/scanner/scanner.go).
本文基于 Go 1.11.4.
對于 Scanner 的作用, 就像 Java 中的 StringTokenizer 類型, 負責將一個輸入字符串按照特定的分隔符劃分為一個個獨立的單元. 不同的地方在于, 詞法分析器在劃分單元時依照的是 Go 語言的規范, 而不是指定的分隔符.
那么為什么要將源代碼進行劃分呢? 說來話長, 建議閱讀編譯原理相關書籍-.
在寫下本文之前, 本人有幸剛剛閱讀了 Writing an interpreter in go , 感謝該書作者, 這本書對于我寫下這篇文章有很多的幫助, 在這兒分享給大家.
Token##
維基百科中對詞法分析描述如下:
詞法分析(英語:lexical analysis)是計算機科學中將字符序列轉換為標記(token)序列的過程。進行詞法分析的程序或者函數叫作詞法分析器(lexical analyzer,簡稱lexer),也叫掃描器(scanner)。詞法分析器一般以函數的形式存在,供語法分析器調用。
對于編譯原理有了解的讀者肯定深知, 詞法分析是編譯的第一步. 它的主要作用就是將源代碼(這里就是我們編寫的 Go 語言源文件)進行掃描, 將源代碼解析為一個個的詞法單元. 而詞法單元就對應于我們源代碼中的變量,函數,關鍵字等.
舉個例子, 當我們使用 Go 語言編程時, 寫下如下代碼:
var a bool = false通過詞法解析器的解析, 我們將會得到如下 Token:
var // Go 語言關鍵字 a // 變量 bool // Go語言內置類型, 也屬于關鍵字 = // 賦值關鍵字 false // 常量關鍵字正如你所看到的那樣, 我們在解析過程中不僅僅解析出了不同的程序組件, 而且每個組件都有特定的類型標記, 這些信息將被語法分析器使用. 比如可以用來檢測語法錯誤.
好了, 本人知識有限,概述到此結束, 我們來看一下 token.go 的實現(該文件位于 Go 源碼目錄 go/token/token.go).
為了減少篇幅, 這里省略了部分代碼.
type Token int// 所有 Go 語言支持的 Token 類型列表 const (// 該類型的 Token 標識源文件中存在詞法錯誤, 詞法分析無法成功的解析源文件.// 也就是說我們編寫了不符合 Go 語言語法的源代碼ILLEGAL Token = iota// 該類型的 Token 表示源文件已經被遍歷完成// 在遇到這種類型的 Token 時, 表示著詞法分析的完成EOF COMMENT // 這個 Token 不必多說, 標識源代碼中的注釋literal_beg// IDENT 標識一個標識符, 比如方法名稱, 類型名稱, 變量名稱等. // 常量, 關鍵字都不屬于這個類型IDENT // mainINT // 12345FLOAT // 123.45IMAG // 123.45iCHAR // 'a'STRING // "abc"literal_endoperator_beg// Operators and delimitersADD // +SUB // -MUL // *QUO // /REM // %// AND, OR, +=, -=, &=, ^= 等省略ARROW // <-// 這里省略了各種括號對應的 Token 類型定義operator_endkeyword_beg// 以下聲明 Go 語言關鍵字 Token 類型, 這里省略了絕大部分FUNCGOGOTOIFIMPORTSELECTSTRUCTSWITCHTYPEVARkeyword_end )// tokens map 的用處在于將詞法分析過程中解析出來的單詞或者詞組 // 與上面剛剛定義的 Token 類型對應起來 // 比如, 當詞法分析器從源代碼中解析出一個單詞 func 時, 他將創建一個 Token, // 而該 Token 的類型將是 FUNC. 這一項就存在于下面這個 tokens map 中. // 值得一提的時, 這個 map 中包含了幾項 Token 類型, 這些類型僅僅在用來 // 輔助詞法分析器, 程序中并不會出現這樣的詞法單元.這幾個類型: // ILLEFAL: 用來標識不符合 Go 語言語法的詞法單元出現在源程序中 // EOF: 用來標識源程序解析完畢 var tokens = [...]string{ILLEGAL: "ILLEGAL",EOF: "EOF",COMMENT: "COMMENT",FUNC: "func",GO: "go",GOTO: "goto",IF: "if",IMPORT: "import",SELECT: "select",STRUCT: "struct",SWITCH: "switch",TYPE: "type",VAR: "var", }// 該方法返回 Token 的字符串表示形式 func (tok Token) String() string {s := ""if 0 <= tok && tok < Token(len(tokens)) {s = tokens[tok]}if s == "" {s = "token(" + strconv.Itoa(int(tok)) + ")"}return s }const (LowestPrec = 0 // non-operatorsUnaryPrec = 6HighestPrec = 7 )// 獲取當前 Token 的優先級 // 對于非操作符的 Token, 它的優先級最低, 為 LowestPrec. // 舉個例子說明以下. 比如我們在解析如下代碼: a := b + c * d // 我們會得到七個 Token. 分別對應 a, :=, b, +, c, *, d // 在語法分析中, 當我們要評估 a 在執行完該語句時,它的值是多少, // 我們就需要知道每個操作符 Token (:=, +, *) 的優先級, 以決定哪個操作將被優先執行, // 哪個操作實在另一個操作執行完之后執行. func (op Token) Precedence() int {switch op {case LOR:return 1case LAND:return 2case EQL, NEQ, LSS, LEQ, GTR, GEQ:return 3case ADD, SUB, OR, XOR:return 4case MUL, QUO, REM, SHL, SHR, AND, AND_NOT:return 5}return LowestPrec }var keywords map[string]Tokenfunc init() {keywords = make(map[string]Token)for i := keyword_beg + 1; i < keyword_end; i++ {keywords[tokens[i]] = i} }// 該方法用來判別一個解析出來的 Identifier 到底是一個 Identifier 還是一個關鍵字. // 關鍵字 map 在上一步已經被初始化了. func Lookup(ident string) Token {if tok, is_keyword := keywords[ident]; is_keyword {return tok}return IDENT }// 下面幾個方法很好理解, 不在贅述func (tok Token) IsLiteral() bool { return literal_beg < tok && tok < literal_end }func (tok Token) IsOperator() bool { return operator_beg < tok && tok < operator_end }func (tok Token) IsKeyword() bool { return keyword_beg < tok && tok < keyword_end }Scanner##
了解了 Token 之后, 我們就可以來看看 Scanner 的實現了.
就如簡述中所述, 詞法分析器的作用是將源程序分解為一個個 Token, 以便于語法分析器使用. 它的輸入肯定就是源程序了, 輸出自然是一個 Token 的集合.
詞法分析器僅能檢測出很少部分的程序錯誤, 比如 if 語句后未使用花括號’{’, 非法的操作符’…’ 等. 對于類型或變量重定義, 函數調用參數個數不正確等錯誤都需要在編譯器后續過程中才能發現.
這里我們先簡述一下詞法分析器的工作原理, 這樣將有助于學習源碼.
詞法分析器往往是一個一個字符的讀取輸入的代碼, 通過當前讀取到的字符, 搭配一個解析詞法的狀態機來決定當前讀取到的 Token 的類型.有時, 一個字符并不能提供足夠的信息來做出這種判斷, 此時就需要預先讀取下一個或多個字符來輔助詞法分析器做出判斷.
正如 <> 中所說的一樣, 它的工作原理與 JSON 解析器或者 XML 解析器的工作原理大體上是一致的, 只是得到的結果略有不同而已.
而這里提到的解析詞法的狀態機就是我們將要學習的核心了.
下來我們就來看看 scanner.go 源代碼.
和上一小節相同, 我們只留下程序的主干部分, 細枝末節的代碼我們將省略掉以換取相對的清晰整潔.
我們同時也根據需要調整了方法或者變量的聲明位置.
例子##
上面我們已經簡單的了解了整個解析器的工作原理, 下面我們來跑幾個測試來驗證一下.
測試用例位于 go/scanner/scanner_test.go
輸入輸出對照表:
| /* a comment */ | COMMENT | /* a comment */ |
| // a comment | COMMENT | // a comment |
| foobar | IDENT | foobar |
| 01234567 | INT | 01234567 |
| 0xcafebabe | INT | 0xcafebabe |
| 3.14159265 | FLOAT | 3.14159265 |
| 2.71828e-1000 | FLOAT | 2.71828e-1000 |
| ‘a’ | CHAR | a |
| `foobar` | STRING | `foobar` |
| + | ADD | EMPTY |
| / | QUO | EMPTY |
| %= | REM_ASSIGN | EMPTY |
| , | COMMA | EMPTY |
| { | LBRACK | EMPTY |
| break | BREAK | break |
| case | CASE | case |
| fallthrough | FALLTHROUGH | EMPTY |
省略部分測試用例
END!
總結
以上是生活随笔為你收集整理的Golang 词法分析器浅析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java反射基础(三)--Methods
- 下一篇: 基于ARM Cortex-M和Eclip