Skip to content

Latest commit

 

History

History
257 lines (219 loc) · 8.92 KB

File metadata and controls

257 lines (219 loc) · 8.92 KB

1.4拓展我们的Token集和词法分析器

为了避免以后编写解析器时在包之间跳转的需要,我们需要去拓展我们的词法分析器以便于他能够识别更多Monkey语言和输出更多的Tokens。因此,在本节中,我们将添加对==,!,!=,-,、,*,<,>和关键字true,false,if,else和return的支持。

我们需要添加的新Tokens,建立和输出可以被分类作为以下三种之一:一个字符的Token(例如,-),两个字符的token(例如==)和关键字token(例如 return)。我们也知道如何处理一个字符tokens和关键字tokens,因此在额外添加对两个字符token之前,我们首先添加对这两者的支持。。

添加对-,/,*,<和>的支持是微不足道的。我们需要做的第一件事当然是修改我们的测试用例在lexer/lexer_test.go中输入以包含这些字符。就像以前我们做过的一样。在本章节随附的代码中,您还可以找到扩展测试表,我不会再本章节的其余部分展示,以节省空间并避免让你感到无聊。

// 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这样的胡言乱语。没关系,词法分析器的工作不是告诉我们代码是否有意义,有效或包含错误。那是以后的阶段。词法分析器应该只将此输入转换为tokens。出于这个原因,我为词法分析器编写的测试用例还改了所有的tokens并且还尝试引发一对一的错误,文件末尾的边缘情况、换行符处理、多位数字解析等。这就是为什么“代码”看起来像胡言乱语。

运行测试我们得到undefined:errors,因为测试包含对未定义TokenTypes的引用,为了修复他们,我们添加以下常量给token/token.go:

//token/token.go

const (
//[...]

// Operators
	ASSIGN   = "="
	PLUS     = "+"
	MINUS    = "-"
	BANG     = "!"
	ASTERISK = "*"
	SLASH    = "/"

	LT = "<"
	GT = ">"

//[...]
)

随着我们添加了新常量,测试仍然失败,因为我们没有返回具有预期TokenType的Tokens。

$ 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 {
// [...
    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)
// [...]
}   

现在我们添加了tokens,并且对switch语句的case进行了重新排序以反映token/token.go中的常量结构。这个小改动让我们的测试通过了:

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

新的一个字符的tokens已经成功添加了。下一步:添加新的关键字true,false,if,else和return。

同样,第一步是拓展我们测试中的输入以包含这些新的关键字。TestNextToken里新的输入看起来是这样的:

// 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 flase;
    }
`
// [...]

由于测试期望引用的新关键字未定义,测试甚至没有编译。修复此内容,意味着只添加新常量,在这种情况下,将关键字添加到查找表LookupIdent()。

// token/token.go

const (
// [...]

    // Keywords
	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.0

词法分析器现在认识了新的关键字,必要的变化是微不足道的,易于去预测和改变。我想说的是适当地拍拍背,我们做的不错!

但是在我们到下一个章节并且开始我们的解析器之前,我们仍然需要拓展词法分析器为了它能够识别两个字符的tokens。我们想要支持的tokens看起来像源代码里的:==和!=。

首先瞥一眼你可能会认为,为什么不添加新的case在我们的switch语句里并且和之前一起做?因为我们的switch语句将当前字符l.ch作为表达式与case比较,我们不能只添加新的case,例如case "=="-编译器不会让我们这样做。我们无法将我们的l.ch字节与诸如"=="之类的字符串进行比较。

我们可以做的是重用现有的'='和'!'分支并扩展它们。所以我们要做的是从输入中向前看,然后确定是否未=或==返回一个token。再一次扩展了lexer/lexer_test.go的输入后,它看起来像这样:

// 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 flase;
    }

    10 == 10;
    10 != 9;
`
// [...]

在我们开始我们switch语句之前,我们需要添加新的辅助函数定义peekChar给*Lexer:

// 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.positon和l.readPosition。我们只想在输入中“窥视”而不是在其中移动,所以我们直到调用readChar会返回什么。大多数语法分析器和解析器都有这样一个向前看的“窥视”功能,大多数情况下它只返回紧邻的下一个字符。解析不同语言的难度通常归结为你必须向前看(或向后看!)理解它的源代码。

随着peekChar添加,使用更新的测试输入的代码不能编译。当然,由于我们在测试中引用了未定义的token常量。修复它,事实证明,很容易:

// token/token.go

const (
// [...]
    EQ      = "=="
    NOT_EQ  = "!=!"

//[...]
)

修复了词法分析器测试中对token.EQ和token.NOT_EQ引用后,运行 go test 现在会返回正确的失败信息:

$ 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 tokens代替一个token.EQ token。解决方法是使用我们的新peekChar方法。在switch分支中在'='和'!'中,我们向前看。如果下一个令牌是也是=我们创建token.EQ或一个token.NOT_EQ token。

// lexer/lexer.go

func (l *Lexer) NextToken() token.Token {
// [...]
    switch l.ch {
	case '=':
		if l.peekChar() == '=' {
			ch := l.ch
			l.readChar()
			tok = token.Token{Type: token.EQ, Literal: string(ch) + string(l.ch)}
		} else {
			tok = newToken(token.ASSIGN, l.ch)
		}
// [...]
    case '!':
		if l.peekChar() == '=' {
			ch := l.ch
			l.readChar()
			tok = token.Token{Type: token.NOT_EQ, Literal: string(ch) + string(l.ch)}
		} else {
			tok = newToken(token.BANG, l.ch)
		}
// [...]
    }
}

请注意,我们在再一次调用l.readChar()之前将l.ch保存在局部变量中。这样我们就不会丢失当前字符并且可以安全地推进词法分析器,因此使它带有l.position和l.readPosition的NextToken()处于正确状态。如果我们要开始在Monkey中支持更多的两个字符的标记,我们可能应该在一个名为makeTwoCharToken的方法中抽象出行为,如果它找到正确的标记,它会查看并前进。因为那两个分支看起来非常相似。现在虽然==和!=是Monkey中唯一的两个字符标记,所以让我们保持原样并再次运行我们的测试以确保它有效:

$ go test ./lexer
ok  monkey/lexer 0.

它们通过了,我们做到了!词法分析器现在可以生产扩展的tokens,我们已经准备就绪写我们的解析器。但是在我们写之前,让我们在接下来的章节中铺设另一块基石...

< 1.3词法分析器 > 1.5开始一个REPL