Monkey 01 lexer 詞法分析器

qbning發表於2024-07-15

此處可以下載每章程式碼https://interpreterbook.com/waiig_code_1.7.zip

首先,詞法分析器是`輸入字串,輸出詞法單元token`.

要定義詞法分析器,首先要定義token

token具有兩個屬性,一個是token的型別,另一個是token的字面量或者說能列印出來的值

//token/token.go
package token
type TokenType string

type Token struct {
	Type    TokenType
	Literal string
}

const (
	ILLEGAL = "ILLEGAL"
	EOF     = "EOF"

	IDENT = "IDENT" //識別符號
	INT   = "INT"   //目前僅處理整型

	//運算子
	ASSIGN   = "="
	PLUS     = "+"
	MINUS    = "-"
	BANG     = "!"
	ASTERISK = "*"
	SLASH    = "/"

	LT = "<"
	GT = ">"

	EQ     = "==" //多字元的運算子
	NOT_EQ = "!="

	//分隔符
	COMMA     = ","
	SEMICOLON = ";"

	LPAREN = "("
	RPAREN = ")"
	LBRACE = "{"
	RBRACE = "}"

	//關鍵字
	FUNCTION = "FUNCTION"
	LET      = "LET"
	TRUE     = "TRUE"
	FALSE    = "FALSE"
	IF       = "IF"
	ELSE     = "ELSE"
	RETURN   = "RETURN"
)

對於一個詞法分析器,需要知道字串、當前位置、當前字元,也可以加一個下個字元來方便操作

//lexer/lexer.go
package lexer

type Lexer struct {
	input        string
	position     int  // 輸入字串的當前位置
	readPosition int  // 讀取位置,即當前字元的下一個位置
	ch           byte // 當前字元,要想支援UTF8等需要換成rune,同時修改獲取下一個字元的函式
}

然後增加一個讀取一個字元和檢視下一個字元的函式

//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++
}
func (l *Lexer) peekChar() byte { //檢視下一個字元,不移動指標,用來讀取兩個字元的token
	if l.readPosition >= len(l.input) {
		return 0
	} else {
		return l.input[l.readPosition]
	}
}

詞法分析器初始化後就可以讀取一個字元

//lexer/lexer.go
func New(input string) *Lexer { // 初始化
	l := &Lexer{input: input}
	l.readChar()
	return l
}

然後接下來的核心就是不斷讀取字元,判斷是哪個識別符號,然後獲得詞法單元token了

func newToken(tokenType token.TokenType, ch byte) token.Token {//獲得一個新的詞法單元token
	return token.Token{Type: tokenType, Literal: string(ch)}
}

空格、換行等對我們來說應該跳過,遇到就應該果斷讀取下一個字元

//lexer/lexer.go
func (l *Lexer) skipWhitespace() { //跳過空格
	for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
		l.readChar()
	}
}

對於每個詞法單元,獲取它的字面量

//lexer/lexer.go
func (l *Lexer) readIdentifier() string { //讀取一個字串作為識別符號的字面量
	position := l.position
	for isLetter(l.ch) {
		l.readChar()
	}
	return l.input[position:l.position]
}

語句中的詞法單元無外乎是字母、數字和符號。

如果遇到字母,得到的要麼是關鍵字,要麼就是識別符號(變數名),當然下劃線也是合法的識別符號的一部分

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

需要判斷是否是某個識別符號,直接在token.go裡完成這個判斷

//token/token.go
var keywords = map[string]TokenType{
	"fn":     FUNCTION,
	"let":    LET,
	"true":   TRUE,
	"false":  FALSE,
	"if":     IF,
	"else":   ELSE,
	"return": RETURN,
}

func LookupIdent(ident string) TokenType { //檢查是否為關鍵字
	if tok, ok := keywords[ident]; ok { //在keywords裡找indent的值賦給tok,如果不行ok為false
		return tok
	}
	return IDENT
}

然後把獲取到的型別,字面量賦給新的token返回即可。

如果遇到數字就讀取數字,然後型別設定為整型,字面量讀進去返回。

//lexer/lexer.go
func isDigit(ch byte) bool {
	return '0' <= ch && ch <= '9'
}
func (l *Lexer) readNumber() string { 
	position := l.position
	for isDigit(l.ch) {
		l.readChar()
	}
	return l.input[position:l.position]
}

剩下就是各種符號了,有單字元符號和雙字元符號之分

綜上,核心函式就是這樣了

//lexer/lexer.go
func (l *Lexer) NextToken() token.Token { // 返回下一個token
	var tok token.Token
	l.skipWhitespace() //跳過空格

	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 '+':
		tok = newToken(token.PLUS, l.ch)
	case '-':
		tok = newToken(token.MINUS, 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)
		}
	case '*':
		tok = newToken(token.ASTERISK, l.ch)
	case '/':
		tok = newToken(token.SLASH, 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.LBRACE, l.ch)
	case '}':
		tok = newToken(token.RBRACE, l.ch)
	case '<':
		tok = newToken(token.LT, l.ch)
	case '>':
		tok = newToken(token.GT, l.ch)
	case 0:
		tok.Literal = ""
		tok.Type = token.EOF
	default:
		if isLetter(l.ch) {
			tok.Literal = l.readIdentifier()          //字面量
			tok.Type = token.LookupIdent(tok.Literal) //是否關鍵字
			return tok                                //呼叫readIdentifier已經讀完了這個字串,無需繼續執行readchar了
		} else if isDigit(l.ch) {
			tok.Type = token.INT
			tok.Literal = l.readNumber()
			return tok
		} else {
			tok = newToken(token.ILLEGAL, l.ch)        //非法字元
		}
	}

	l.readChar()
	return tok
}

然後可以寫測試程式碼或者寫一個類似python控制檯那樣的東西來驗證我們的詞法分析repl(Read-Eval-Print Loop (REPL) )

//repl/repl.go
package repl

import (
	"bufio"
	"fmt"
	"io"
	"monkey/lexer"
	"monkey/token"
)

const PROMPT = ">> "

func Start(in io.Reader, out io.Writer) {
	scanner := bufio.NewScanner(in)

	for { //無限迴圈
		fmt.Fprint(out, PROMPT)
		scanned := scanner.Scan()
		if !scanned { //讀取失敗
			return
		}

		line := scanner.Text() // 獲取新的一行的文字
		l := lexer.New(line)   // 新的詞法解析器

		for tok := l.NextToken(); tok.Type != token.EOF; tok = l.NextToken() {
			fmt.Fprintf(out, "%+v\n", tok) //+列印結構體的欄位和值
		}
	}
}

然後寫一個main.go裡就能啟動這個repl了

//main.go
package main

import (
	"fmt"
	"monkey/repl"
	"os"
	"os/user"
)

func main() {
	user, err := user.Current()
	if err != nil {
		panic(err)
	}
	fmt.Printf("Hello %s! \n歡迎使用Monkey直譯器語言!\n",
		user.Username)
	fmt.Printf("測試REPL\n")
	repl.Start(os.Stdin, os.Stdout)
}

效果如下

Monkey 01 lexer 詞法分析器

相關文章