为了避免以后编写解析器时在包之间跳转的需要,我们需要去拓展我们的词法分析器以便于他能够识别更多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 |
---|