官术网_书友最值得收藏!

1.4 擴展詞法單元和詞法分析器

為了避免以后編寫語法分析器時需要在多個語言包之間跳轉,需要擴展詞法分析器,以便識別更多的 Monkey 代碼并輸出更多的詞法單元。因此本節將添加對==!!=-/*<>和關鍵字truefalseifelsereturn的支持。

需要添加、構建和輸出的新詞法單元可以分為以下三種:單字符詞法單元(例如-)、雙字符詞法單元(例如==)和關鍵字詞法單元(例如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

為了讓測試通過,還需要修改LexerNextToken()里面的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

成功添加新的單字符詞法單元后,下一步來添加新的關鍵字truefalseifelsereturn

同樣,第一步是擴展測試中的輸入,添加這些新關鍵字。下面是TestNextTokeninput現在的內容:

// 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.positionl.readPosition。它的目的只是窺視一下輸入中的下一個字符,不會移動位于輸入中的指針位置,這樣就能知道下一步在調用readChar()時會返回什么。大多數詞法分析器和語法分析器具有這樣的“窺視”函數,且大部分情況是用來向前看一個字符的。

在對不同的編程語言進行語法分析時,通常的難點就是必須在源代碼中向前或向后多看幾個字符才能確定代碼的含義。

添加peekChar()后,測試代碼還無法編譯。這是由于測試中引用了未定義的詞法單元常量。需要再次解決這個問題,這很容易:

// token/token.go

const (
// [...]

    EQ     = "=="
    NOT_EQ = "!="

// [...]
)

修復了詞法分析器測試中對token.EQtoken.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.positionl.readPosition保持正確的狀態。這兩個雙字符的處理方式非常相似。如果要在 Monkey 語言中支持更多的雙字符詞法單元,則應該使用名為makeTwoCharToken的方法把處理步驟抽象出來。該方法會在找到某些詞法單元時繼續前看一個字符。對于 Monkey 來說,目前僅有==!=這兩個雙字符詞法單元,所以先保持原樣。現在再次運行測試以確保其有效:

$ go test ./lexer
ok      monkey/lexer 0.006s

測試正常通過。我們成功地擴展了詞法分析器!現在詞法分析器可以生成擴展的詞法單元,接下來就能夠編寫語法分析器了。但在此之前,再做一些額外的工作來為后續章節打好基礎。

主站蜘蛛池模板: 孟连| 土默特右旗| 沁阳市| 商河县| 吴堡县| 桃园市| 瑞安市| 洞口县| 织金县| 天全县| 儋州市| 延川县| 黔西县| 昌邑市| 新绛县| 昌宁县| 曲靖市| 环江| 江西省| 阿合奇县| 博野县| 道孚县| 重庆市| 天气| 连州市| 沁水县| 盘锦市| 克什克腾旗| 崇文区| 信宜市| 商南县| 泌阳县| 米脂县| 萝北县| 东平县| 神木县| 五峰| 淅川县| 天峨县| 定州市| 化德县|