- 用Go語言自制解釋器
- (德)索斯藤·鮑爾
- 1761字
- 2022-06-17 10:50:32
1.4 擴展詞法單元和詞法分析器
為了避免以后編寫語法分析器時需要在多個語言包之間跳轉,需要擴展詞法分析器,以便識別更多的 Monkey 代碼并輸出更多的詞法單元。因此本節將添加對==
、!
、 !=
、-
、/
、*
、<
、>
和關鍵字true
、false
、if
、else
和return
的支持。
需要添加、構建和輸出的新詞法單元可以分為以下三種:單字符詞法單元(例如-
)、雙字符詞法單元(例如==
)和關鍵字詞法單元(例如return
)。前面已經介紹了如何處理單字符和關鍵字的詞法單元,所以現在先添加這兩種,之后再為詞法分析器添加雙字符詞法單元。
添加對-
、/
、*
、<
和>
的支持很簡單。當然,與之前一樣,第一件事是在lexer/lexer_test.go中修改測試用例的輸入來添加這些字符。另外還要修改tests
表,在本章隨附的代碼中可以找到擴展后的tests
表。為了節省篇幅且不讓讀者感到枯燥,本章后續部分不會再列出tests
表。
// lexer/lexer_test.go
func TestNextToken(t *testing.T) {
input :=`let five = 5;
let ten = 10;
let add = fn(x, y) {
x + y;
};
let result = add(five, ten);
!-/*5;
5 < 10 > 5;
`
// [...]
}
注意,盡管這個輸入看起來像是一段真實的 Monkey 源代碼,但是實際上有些代碼行并沒有意義,比如!-/*5
這樣的亂碼。不過沒關系,詞法分析器的任務不是檢查代碼是否有意義、能否運行,或者有沒有錯誤,這些都是后續階段的任務。詞法分析器應該僅用來將輸入轉換為詞法單元。因此,為詞法分析器編寫的這個測試用例涵蓋了所有詞法單元,并且還嘗試引發詞法單元位置的差一錯誤、文件末尾的邊緣情形、換行符處理、多位數字解析等問題。這就是為什么這段用作測試的代碼看起來像亂碼。
運行該測試會得到許多undefined:
錯誤,因為測試包含對未定義TokenType
的引用。為了解決這些問題,需要在token/token.go中添加以下常量:
// token/token.go
const (
// [...]
// 運算符
ASSIGN = "="
PLUS = "+"
MINUS = "-"
BANG = "!"
ASTERISK = "*"
SLASH = "/"
LT = "<"
GT = ">"
// [...]
)
添加了新的常量后,測試仍然會失敗,因為還沒有返回帶有預期TokenType
的詞法單元。
$ go test ./lexer
--- FAIL: TestNextToken (0.00s)
lexer_test.go:84: tests[36] - tokentype wrong. expected="!", got="ILLEGAL"
FAIL
FAIL monkey/lexer 0.007s
為了讓測試通過,還需要修改Lexer
中NextToken()
里面的switch
語句:
// lexer/lexer.go
func (l *Lexer) NextToken() token.Token {
// [...]
switch l.ch {
case '=':
tok = newToken(token.ASSIGN, l.ch)
case '+':
tok = newToken(token.PLUS, l.ch)
case '-':
tok = newToken(token.MINUS, l.ch)
case '!':
tok = newToken(token.BANG, l.ch)
case '/':
tok = newToken(token.SLASH, l.ch)
case '*':
tok = newToken(token.ASTERISK, l.ch)
case '<':
tok = newToken(token.LT, l.ch)
case '>':
tok = newToken(token.GT, l.ch)
case ';':
tok = newToken(token.SEMICOLON, l.ch)
case ',':
tok = newToken(token.COMMA, l.ch)
// [...]
}
這里添加了新的詞法單元,并且對switch
語句的各個分支進行了重新排序,以便與token/token.go中的常量結構相對應。有了這個小小的修改,測試就能通過了:
$ go test ./lexer
ok monkey/lexer 0.007s
成功添加新的單字符詞法單元后,下一步來添加新的關鍵字true
、false
、if
、else
和return
。
同樣,第一步是擴展測試中的輸入,添加這些新關鍵字。下面是TestNextToken
中input
現在的內容:
// lexer/lexer_test.go
func TestNextToken(t *testing.T) {
input :=`let five = 5;
let ten = 10;
let add = fn(x, y) {
x + y;
};
let result = add(five, ten);
!-/*5;
5 < 10 > 5;
if (5 < 10) {
return true;
} else {
return false;
}`
// [...]
}
由于測試的期望結果中還沒有添加對新關鍵字的引用,因此測試無法編譯。為了再次解決這個問題,需要添加新的常量。而對于當前情況,需要將關鍵字添加到LookupIdent()
的關鍵字表中。
// token/token.go
const (
// [...]
// 關鍵字
FUNCTION = "FUNCTION"
LET = "LET"
TRUE = "TRUE"
FALSE = "FALSE"
IF = "IF"
ELSE = "ELSE"
RETURN = "RETURN"
)
var keywords = map[string]TokenType{
"fn": FUNCTION,
"let": LET,
"true": TRUE,
"false": FALSE,
"if": IF,
"else": ELSE,
"return": RETURN,
}
結果是,不僅通過修復對未定義變量的引用解決了編譯錯誤,測試也通過了:
$ go test ./lexer
ok monkey/lexer 0.007s
詞法分析器現在可以識別新的關鍵字了,所做的修改不大,很容易就能想到并實現。現在可以自夸一下,我們做得很好!
但是在進入第2章接觸語法分析器之前,還需要進一步擴展詞法分析器,以便識別由兩個字符組成的詞法單元。所要支持的詞法單元在源代碼中看起來像==
和!=
這樣。
乍一看,讀者可能會想:為什么不向switch
語句中添加新的case
來達到這個目的呢?由于switch
語句使用的表達式是單個字符l.ch
,與它相比較的case
也需要是單個字符,因此編譯器不允許使用case "=="
這樣的形式,即字節類型的l.ch
與==
之類的字符串不能互相比較。因此不能直接添加類似的新case
語句。
實際可以做的是,復用并擴展現有的=
分支和!
分支。因此,所要做的是根據前一步輸入中的下一個字符,決定是返回=
,還是==
的詞法單元。再次擴展lexer/lexer_test.go中的input
,現在的代碼如下:
// lexer/lexer_test.go
func TestNextToken(t *testing.T) {
input :=`let five = 5;
let ten = 10;
let add = fn(x, y) {
x + y;
};
let result = add(five, ten);
!-/*5;
5 < 10 > 5;
if (5 < 10) {
return true;
} else {
return false;
}
10 == 10;
10 != 9;
`
// [...]
}
在開始修改NextToken()
中的switch
語句之前,需要在 *Lexer
上定義名為peekChar()
的新輔助方法:
// lexer/lexer.go
func (l *Lexer) peekChar() byte {
if l.readPosition >= len(l.input) {
return 0
} else {
return l.input[l.readPosition]
}
}
peekChar()
與readChar()
非常類似,但這個函數不會前移l.position
和l.readPosition
。它的目的只是窺視一下輸入中的下一個字符,不會移動位于輸入中的指針位置,這樣就能知道下一步在調用readChar()
時會返回什么。大多數詞法分析器和語法分析器具有這樣的“窺視”函數,且大部分情況是用來向前看一個字符的。
在對不同的編程語言進行語法分析時,通常的難點就是必須在源代碼中向前或向后多看幾個字符才能確定代碼的含義。
添加peekChar()
后,測試代碼還無法編譯。這是由于測試中引用了未定義的詞法單元常量。需要再次解決這個問題,這很容易:
// token/token.go
const (
// [...]
EQ = "=="
NOT_EQ = "!="
// [...]
)
修復了詞法分析器測試中對token.EQ
和token.NOT_EQ
的引用后,運行該測試會得到一條失敗消息:
$ go test ./lexer
--- FAIL: TestNextToken (0.00s)
lexer_test.go:118: tests[66] - tokentype wrong. expected="==", got="="
FAIL
FAIL monkey/lexer 0.007s
現在,當詞法分析器在輸入中遇到==
時,會創建兩個token.ASSIGN
詞法單元,而不是一個token.EQ
詞法單元。解決方案是使用新的peekChar()
方法。在switch
語句的=
分支和!
分支中,向前多看一個字符。如果下一個詞法單元是=
,那么就分別創建token.EQ
詞法單元或token.NOT_EQ
詞法單元:
// lexer/lexer.go
func (l *Lexer) NextToken() token.Token {
// [...]
switch l.ch {
case '=':
if l.peekChar() == '=' {
ch := l.ch
l.readChar()
literal := string(ch) + string(l.ch)
tok = token.Token{Type: token.EQ, Literal: literal}
} else {
tok = newToken(token.ASSIGN, l.ch)
}
// [...]
case '!':
if l.peekChar() == '=' {
ch := l.ch
l.readChar()
literal := string(ch) + string(l.ch)
tok = token.Token{Type: token.NOT_EQ, Literal: literal}
} else {
tok = newToken(token.BANG, l.ch)
}
// [...]
}
注意,再次調用l.readChar()
之前,需要將l.ch
保存在局部變量中。這樣就不會丟失當前字符,可以安全地前移詞法分析器,以使NextToken()
的l.position
和l.readPosition
保持正確的狀態。這兩個雙字符的處理方式非常相似。如果要在 Monkey 語言中支持更多的雙字符詞法單元,則應該使用名為makeTwoCharToken
的方法把處理步驟抽象出來。該方法會在找到某些詞法單元時繼續前看一個字符。對于 Monkey 來說,目前僅有==
和!=
這兩個雙字符詞法單元,所以先保持原樣。現在再次運行測試以確保其有效:
$ go test ./lexer
ok monkey/lexer 0.006s
測試正常通過。我們成功地擴展了詞法分析器!現在詞法分析器可以生成擴展的詞法單元,接下來就能夠編寫語法分析器了。但在此之前,再做一些額外的工作來為后續章節打好基礎。
- 手機安全和可信應用開發指南:TrustZone與OP-TEE技術詳解
- JavaScript前端開發模塊化教程
- Java EE 6 企業級應用開發教程
- 微服務與事件驅動架構
- Interactive Data Visualization with Python
- Windows系統管理與服務配置
- PostgreSQL Replication(Second Edition)
- SQL Server從入門到精通(第3版)
- UVM實戰
- SQL Server數據庫管理與開發兵書
- PHP與MySQL權威指南
- C語言程序設計實踐
- Functional Python Programming
- Android應用程序設計
- 創新工場講AI課:從知識到實踐