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

1.3 詞法分析器

在開始編寫代碼之前,先了解本節的目標。在本節中,我們將編寫詞法分析器。詞法分析器將源代碼作為輸入,并輸出對應的詞法單元。詞法分析器會遍歷輸入的字符,然后逐個輸出識別出的詞法單元。這個過程既無須用到緩沖區,也無須保存詞法單元,只會用到一個名為NextToken()的方法來輸出下一個詞法單元。

也就是說,詞法分析器在接收源代碼之后,會在其中重復調用NextToken(),逐個字符遍歷源代碼來生成詞法單元。這里的源代碼還是使用字符串,因此可以省去不少處理工作。再次提醒,在生產環境中,應該將文件名和行號附加到詞法單元中,以便更好地跟蹤可能出現的詞法分析錯誤和語法分析錯誤。在這種情況下,最好使用io.Reader加上文件名來初始化詞法分析器。但因為這樣做會增加復雜性,所以這里從簡單處著手,僅使用字符串作為輸入,忽略文件名和行號。

經過這些分析,現在詞法分析器的任務就很清楚了。接下來創建一個新語言包并添加第一個測試。測試可以重復運行,以獲取詞法分析器當前的工作狀態信息。這里依然從簡單處著手,后面隨著詞法分析器功能的完善,測試用例也會隨之擴展:

// lexer/lexer_test.go

package lexer

import (
    "testing"

    "monkey/token"
)

func TestNextToken(t *testing.T) {
    input :=`=+(){},;`

    tests := []struct {
        expectedType    token.TokenType
        expectedLiteral string
    }{
        {token.ASSIGN, "="},
        {token.PLUS, "+"},
        {token.LPAREN, "("},
        {token.RPAREN, ")"},
        {token.LBRACE, "{"},
        {token.RBRACE, "}"},
        {token.COMMA, ","},
        {token.SEMICOLON, ";"},
        {token.EOF, ""},
    }

    l := New(input)

    for i, tt := range tests {
        tok := l.NextToken()

        if tok.Type != tt.expectedType {
            t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q",
                i, tt.expectedType, tok.Type)
        }

        if tok.Literal != tt.expectedLiteral {
            t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q",
                i, tt.expectedLiteral, tok.Literal)
        }
    }
}

現在的測試肯定會失敗,因為尚未編寫任何實際代碼:

$ go test ./lexer
# monkey/lexer
lexer/lexer_test.go:27: undefined: New
FAIL    monkey/lexer [build failed]

首先,定義New()函數,用來返回*Lexer

// lexer/lexer.go
package lexer

type Lexer struct {
    input        string
    position     int  // 所輸入字符串中的當前位置(指向當前字符)
    readPosition int  // 所輸入字符串中的當前讀取位置(指向當前字符之后的一個字符)
    ch           byte // 當前正在查看的字符
}

func New(input string) *Lexer {
    l := &Lexer{input: input}
    return l
}

Lexer中的大多數字段很容易理解,但positionreadPosition的含義可能讓人困惑。兩者都可以用作索引來訪問input中的字符,例如l.input[l.readPosition]。這里之所以用兩個“指針”來指向所輸入的字符串,是因為詞法分析器除了查看當前字符,還需要進一步“查看”字符串,即查看字符串中的下一個字符。readPosition始終指向所輸入字符串中的“下一個”字符,position則指向所輸入字符串中與ch字節對應的字符。

第一個輔助方法是readChar(),讀懂這個方法就能理解這些字段了:

// lexer/lexer.go

func (l *Lexer) readChar() {
    if l.readPosition >= len(l.input) {
        l.ch = 0
    } else {
        l.ch = l.input[l.readPosition]
    }
    l.position = l.readPosition
    l.readPosition += 1
}

readChar的目的是讀取input中的下一個字符,并前移其在input中的位置。這個過程的第一件事就是檢查是否已經到達input的末尾。如果是,則將l.ch設置為0,這是NUL字符的ASCII編碼,用來表示“尚未讀取任何內容”或“文件結尾”。如果還沒有到達input的末尾,則將l.ch設置為下一個字符,即l.input[l.readPosition]指向的字符。

之后,將l.position更新為剛用過的l.readPosition,然后將l.readPosition1。這樣一來,l.readPosition就始終指向下一個將讀取的字符位置,而l.position始終指向剛剛讀取的位置。這個特性很快就會派上用場。

在談到readChar時,值得指出的是,該詞法分析器僅支持ASCII字符,不能支持所有的Unicode字符。這么做也是為了讓事情保持簡單,讓我們能夠專注于解釋器的基礎部分。如果要完全支持Unicode和UTF-8,就要將l.ch的類型從byte改為rune,同時還要修改讀取下一個字符的方式。因為字符此時可能為多字節,所以l.input[l.readPosition]將無法工作。除此之外,還需要修改其他一些后面會介紹的方法和函數。這里將在Monkey中全面支持Unicode和表情符號作為練習留給讀者來實現。

New()函數中使用readChar,初始化l.chl.positionl.readPosition,以便在調用NextToken()之前讓*Lexer完全就緒:

// lexer/lexer.go

func New(input string) *Lexer {
    l := &Lexer{input: input}
    l.readChar()
    return l
}

此時運行測試會發現,調用New(input)后一切正常,但現在還缺少NextToken()方法。下面就通過添加第一版的NextToken()來解決這個問題:

// lexer/lexer.go

package lexer

import "monkey/token"

func (l *Lexer) NextToken() token.Token {
    var tok token.Token

    switch l.ch {
    case '=':
        tok = newToken(token.ASSIGN, l.ch)
    case ';':
        tok = newToken(token.SEMICOLON, l.ch)
    case '(':
        tok = newToken(token.LPAREN, l.ch)
    case ')':
        tok = newToken(token.RPAREN, l.ch)
    case ',':
        tok = newToken(token.COMMA, l.ch)
    case '+':
        tok = newToken(token.PLUS, l.ch)
    case '{':
        tok = newToken(token.LBRACE, l.ch)
    case '}':
        tok = newToken(token.RBRACE, l.ch)
    case 0:
        tok.Literal = ""
        tok.Type = token.EOF
    }

    l.readChar()
    return tok
}

func newToken(tokenType token.TokenType, ch byte) token.Token {
    return token.Token{Type: tokenType, Literal: string(ch)}
}

這就是NextToken()方法的基本結構。它首先檢查了當前正在查看的字符l.ch,根據具體的字符來返回對應的詞法單元。在返回詞法單元之前,位于所輸入字符串中的指針會前移,所以之后再次調用NextToken()時,l.ch字段就已經更新過了。最后,名為newToken的小型函數可以幫助初始化這些詞法單元。

運行測試,可以看到測試通過:

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

很好!現在來擴展測試用例,讓其開始處理Monkey源代碼:

// 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);
 `

    tests := []struct {
        expectedType    token.TokenType
        expectedLiteral string
    }{
        {token.LET, "let"},
        {token.IDENT, "five"},
        {token.ASSIGN, "="},
        {token.INT, "5"},
        {token.SEMICOLON, ";"},
        {token.LET, "let"},
        {token.IDENT, "ten"},
        {token.ASSIGN, "="},
        {token.INT, "10"},
        {token.SEMICOLON, ";"},
        {token.LET, "let"},
        {token.IDENT, "add"},
        {token.ASSIGN, "="},
        {token.FUNCTION, "fn"},
        {token.LPAREN, "("},
        {token.IDENT, "x"},
        {token.COMMA, ","},
        {token.IDENT, "y"},
        {token.RPAREN, ")"},
        {token.LBRACE, "{"},
        {token.IDENT, "x"},
        {token.PLUS, "+"},
        {token.IDENT, "y"},
        {token.SEMICOLON, ";"},
        {token.RBRACE, "}"},
        {token.SEMICOLON, ";"},
        {token.LET, "let"},
        {token.IDENT, "result"},
        {token.ASSIGN, "="},
        {token.IDENT, "add"},
        {token.LPAREN, "("},
        {token.IDENT, "five"},
        {token.COMMA, ","},
        {token.IDENT, "ten"},
        {token.RPAREN, ")"},
        {token.SEMICOLON, ";"},
        {token.EOF, ""},
    }
// [...]
}

注意,現在測試用例中的input發生了變化,看起來像是Monkey語言的子集。它不僅包含所有已經能成功轉換成詞法單元的符號,還有一些新內容,如標識符、關鍵字和數字。這些新內容會導致測試失敗。

先處理標識符和關鍵字。對于這兩者,詞法分析器需要識別當前字符是否為字母。如果是,則還需要讀取標識符/關鍵字的剩余部分,直到遇見非字母字符為止。讀取完該標識符/關鍵字之后,還需要判斷它到底是標識符還是關鍵字,以便使用正確的token.TokenType。因此第一步是擴展switch語句:

// lexer/lexer.go

import "monkey/token"

func (l *Lexer) NextToken() token.Token {
    var tok token.Token

    switch l.ch {
// [...]
    default:
        if isLetter(l.ch) {
            tok.Literal = l.readIdentifier()
            return tok
        } else {
            tok = newToken(token.ILLEGAL, l.ch)
        }
    }
// [...]
}

func (l *Lexer) readIdentifier() string {
    position := l.position
    for isLetter(l.ch) {
        l.readChar()
    }
    return l.input[position:l.position]
}

func isLetter(ch byte) bool {
    return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_'
}

這里的switch語句中添加了一個default分支,因此只要l.ch不是前面可識別的字符,就可以檢查是不是標識符了。這里還添加了用于生成token.ILLEGAL詞法單元的代碼。如果代碼走到此處,就將不知道如何處理的字符聲明成類型為token.ILLEGAL的詞法單元。

isLetter輔助函數用來判斷給定的參數是否為字母。值得注意的是,這個函數雖然看起來簡短,但意義重大,其決定了解釋器所能處理的語言形式。比如示例中包含ch =='_',這意味著下劃線_會被視為字母,允許在標識符和關鍵字中使用。因此可以使用諸如foo_bar之類的變量名。其他編程語言甚至允許在標識符中使用問號和感嘆號。如果讀者也想這么做,那么可以修改這個isLetter函數。

readIdentifier()函數顧名思義,就是讀入一個標識符并前移詞法分析器的掃描位置,直到遇見非字母字符。

switch語句的default分支中,使用readIdentifier()設置了當前詞法單元的Literal字段,但Type還沒有處理。現在letfnfoobar之類的標識符已經被讀取,還需要將語言的關鍵字和用戶定義標識符區分開來,因此需要一個函數來為現有的詞法單元字面量返回正確的TokenType。添加這個函數最合適的地方是在token包中。

// token/token.go

var keywords = map[string]TokenType{
    "fn": FUNCTION,
    "let": LET,
}

func LookupIdent(ident string) TokenType {
    if tok, ok := keywords[ident]; ok {
        return tok
    }
    return IDENT
}

LookupIdent通過檢查關鍵字表來判斷給定的標識符是否是關鍵字。如果是,則返回關鍵字的TokenType常量。如果不是,則返回token.IDENT,這個TokenType表示當前是用戶定義的標識符。

有了這些,現在就可以完成標識符和關鍵字的詞法分析了:

// lexer/lexer.go

func (l *Lexer) NextToken() token.Token {
    var tok token.Token

    switch l.ch {
// [...]
    default:
        if isLetter(l.ch) {
            tok.Literal = l.readIdentifier()
            tok.Type = token.LookupIdent(tok.Literal)
            return tok
        } else {
            tok = newToken(token.ILLEGAL, l.ch)
        }
    }
// [...]
}

這里需要用return tok語句提前退出,因為在調用readIdentifier()時會重復調用readChar(),并將readPositionposition字段前移到當前標識符的最后一個字符之后,所以無須在后續的switch語句中再次調用readChar()

現在運行測試,可以看到let被正確識別了,但是測試仍然失敗:

$ go test ./lexer
--- FAIL: TestNextToken (0.00s)
  lexer_test.go:70: tests[1] - tokentype wrong. expected="IDENT", got="ILLEGAL"
FAIL
FAIL    monkey/lexer 0.008s

問題出在下一個詞法單元上:原本預期是IDENT詞法單元,其Literal字段中是five,而這里得到的是ILLEGAL詞法單元。出現這種情況是因為letfive之間有空白字符。在 Monkey 語言中,空白字符僅用作詞法單元的分隔符,沒有任何意義,因此需要添加代碼直接跳過空白:

// lexer/lexer.go

func (l *Lexer) NextToken() token.Token {
    var tok token.Token

    l.skipWhitespace()

    switch l.ch {
// [...]
}

func (l *Lexer) skipWhitespace() {
    for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
        l.readChar()
    }
}

很多分析器中有這個簡單的輔助函數,它有時稱為eatWhitespace,有時稱為consumeWhiteSpace,還有時是完全不同的名稱。這個函數實際跳過的字符根據具體分析的語言會有所不同。例如在某些語言的實現中,會為換行符創建詞法單元,如果它們不在詞法單元流中的正確位置,就會拋出解析錯誤。不過這里沒有處理換行符,是為了簡化后面的語法分析步驟。

添加了skipWhitespace()后,詞法分析器會停在測試代碼中let five = 5;5這里。是的,詞法分析器還不能將數字轉換為詞法單元。現在來添加這個功能。

就像之前處理標識符那樣,現在需要在switch語句的default分支中添加更多功能:

// lexer/lexer.go

func (l *Lexer) NextToken() token.Token {
    var tok token.Token

    l.skipWhitespace()

    switch l.ch {
// [...]
    default:
        if isLetter(l.ch) {
            tok.Literal = l.readIdentifier()
            tok.Type = token.LookupIdent(tok.Literal)
            return tok
        } else if isDigit(l.ch) {
            tok.Type = token.INT
            tok.Literal = l.readNumber()
            return tok
        } else {
            tok = newToken(token.ILLEGAL, l.ch)
        }
    }
// [...]
}

func (l *Lexer) readNumber() string {
    position := l.position
    for isDigit(l.ch) {
        l.readChar()
    }
    return l.input[position:l.position]
}

func isDigit(ch byte) bool {
    return '0' <= ch && ch <= '9'
}

從中可以看到,剛剛添加的代碼和上面讀取標識符和關鍵字的代碼很像。readNumber方法與readIdentifier幾乎完全相同,除了其中使用的是isDigit而不是isLetter。當然,也可以創建一個characteridentifying函數同時處理這兩種情況,但為了簡潔和易于理解,這里還是分開處理。

isDigit函數與isLetter一樣簡單,只是判斷傳入的內容是否為Latin字符集中09之間的數字。

添加完成后,測試就能通過了:

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

你是否注意到,readNumber中簡化了很多處理?它只能讀取整數,忽略了浮點數、十六進制數、八進制數,這也意味著 Monkey 語言不支持這些特性。當然,這樣做的原因還是出于教學目的,所以限定了介紹內容的范圍。

現在可以慶祝了,我們成功地將測試用例中的一段 Monkey 語言代碼轉換成了詞法單元!

有了這次勝利,就能很容易地擴展詞法分析器,來解析更多的 Monkey 源代碼。

主站蜘蛛池模板: 万盛区| 宿迁市| 昔阳县| 榆林市| 宁河县| 古浪县| 贵阳市| 南乐县| 蒙阴县| 南昌县| 云梦县| 财经| 时尚| 遂溪县| 虎林市| 阿拉善右旗| 霍林郭勒市| 会东县| 上犹县| 隆子县| 大洼县| 钟山县| 水富县| 来安县| 彭州市| 安庆市| 都江堰市| 云林县| 仙游县| 康马县| 英吉沙县| 明溪县| 黔江区| 元谋县| 怀安县| 扎鲁特旗| 平潭县| 西丰县| 汝阳县| 黑水县| 称多县|