Monkey 01 lexer 词法分析器

此处可以下载每章代码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)
}

效果如下

 

posted @ 2024-07-15 22:00  qbning  阅读(6)  评论(0编辑  收藏  举报
描述