在我们开始扩展解析器之前,我们首先要做的就是清楚并且拓展我们的测试点。我不会通过列出完整的更改让您感到厌烦,但我会向您展示一些小的帮助函数,使测试更容易理解。
我们已经有一个辅助函数testItegerLiteral。第二个名为 testIdentifier 的函数可以清理许多其他测试:
// parser/parser_test.go
func testIdentifier(t *testing.T, exp ast.Expression, value string) bool {
ident, ok := exp.(*ast.Identifier)
if !ok {
t.Errorf("exp not *ast.Identifier. got=%T", exp)
return false
}
if ident.Value != value {
t.Errorf("ident.Value not %s. got=%s", value, ident.Value)
return false
}
if ident.TokenLiteral() != value {
t.Errorf("ident.TokenLiteral not %s. got=%s", value,
ident.TokenLiteral())
return false
}
return true
}
有趣的部分是现在使用 testIntegerLiteral 和 testIdentifier 来构建更通用的辅助函数。
// parser/parser_test.go
func testLiteralExpression(
t *testing.T,
exp ast.Expression,
expected interface{},
) bool {
switch v := expected.(type) {
case int:
return testIntegerLiteral(t, exp, int64(v))
case int64:
return testIntegerLiteral(t, exp, v)
case string:
return testIdentifier(t, exp, v)
case bool:
return testBooleanLiteral(t, exp, v)
}
t.Errorf("type of exp not handled. got=%T", exp)
return false
}
func testInfixExpression(t *testing.T, exp ast.Expression, left interface{},
operator string, right interface{}) bool {
opExp, ok := exp.(*ast.InfixExpression)
if !ok {
t.Errorf("exp is not ast.OperatorExpression. got=%T(%s)", exp, exp)
return false
}
if !testLiteralExpression(t, opExp.Left, left) {
return false
}
if opExp.Operator != operator {
t.Errorf("exp.Operator is not '%s'. got=%q", operator, opExp.Operator)
return false
}
if !testLiteralExpression(t, opExp.Right, right) {
return false
}
return true
}
有了这些,就可以像这样编写测试代码:
testInfixExpression(t, stmt.Expression, 5, "+", 10)
testInfixExpression(t, stmt.Expression, "alice", "*", "bob")
这使得测试由我们的解析器生成的 AST 的属性变得更加容易。 有关清理和扩展的测试套件,请参阅 parser/parser_test.go。
在Monkey编程语言中有很多仍然在我们的解析器和AST需要实现的事情。最简单的是布尔文字。在Monkey我们能使用布尔值替代其他语句:
true;
false;
let foobar = true;
let barfoo = false;
比起标识符和整数文字,它们的AST是简单的和小的:
// ast/ast.go
type Boolean struct {
Token token.Token
Value bool
}
func (b *Boolean) expressionNode() {}
func (b *Boolean) TokenLiteral() string { return b.Token.Literal }
func (b *Boolean) String() string { return b.Token.Literal
Value 字段可以保存 bool 类型的值,这意味着我们将在其中保存 true 或 false(Go bool 值,而不是 Monkey 文字)。
随着AST节点的定义我们现在能添加我们的测试。简单的TestBooleanExpression测试函数是如此的相似和我将不会在这里展示的TestIdentifierExpression和TestIntegerLiteralExpression。这对于显示错误消息就足够了,它为我们指明了如何实现布尔文字解析的正确方向:
$ go test ./parser
--- FAIL: TestBooleanExpression (0.00s)
parser_test.go:470: parser has 1 errors
parser_test.go:472: parser error: "no prefix parse function for true found"
FAIL
FAIL monkey/parser 0.008s
当然,是的,我们需要注册一个prefixParseFn对于token.TRUE和token.FALSE tokens。
// parser/parser.go
func New(l *lexer.Lexer) *Parser {
// [...]
p.registerPrefix(token.TRUE, p.parseBoolean)
p.registerPrefix(token.FALSE, p.parseBoolean)
// [...]
}
并且parseBoolean方法也正如你想象的:
// parser/parser.go
func (p *Parser) parseBoolean() ast.Expression {
return &ast.Boolean{Token: p.curToken, Value: p.curTokenIs(token.TRUE)}
}
这个方法唯一有点有趣的部分是 p.curTokenIs(token.TRUE) 调用的内联,这并不是很有趣。 除此之外,它很简单,甚至可能很无聊。 或者换句话说:解析器的结构很好地为我们服务! 这实际上是 Pratt 方法的优点之一:它很容易扩展。 And boom!测试变绿了:
$ go test ./parser
ok monkey/parser 0.006s
但有趣的是,我们现在可以扩展几个测试来合并新实现的布尔文字。 第一个候选是TestOperatorPrecedenceParsing,它的字符串比较机制:
// parser/parser_test.go
func TestOperatorPrecedenceParsing(t *testing.T) {
tests := []struct {
input string
expected string
}{
//[...]
{
"true",
"true",
},
{
"false",
"false",
},
{
"3 > 5 == false",
"((3 > 5) == false)",
},
{
"3 < 5 == true",
"((3 < 5) == true)",
},
//[...]
}
我们能为布尔文字测试,即使更多的测试扩展在testLiteralExpression辅助函数,并且提供一个新的testBooleanLiteral函数:
// parser_test.go
func testLiteralExpression(
t *testing.T,
exp ast.Expression,
expected interface{},
) bool {
switch v := expected.(type) {
// [...]
case bool:
return testBooleanLiteral(t, exp, v)
}
// [...]
func testBooleanLiteral(t *testing.T, exp ast.Expression, value bool) bool {
bo, ok := exp.(*ast.Boolean)
if !ok {
t.Errorf("exp not *ast.Boolean. got=%T", exp)
return false
}
if bo.Value != value {
t.Errorf("bo.Value not %t. got=%t", value, bo.Value)
return false
}
if bo.TokenLiteral() != fmt.Sprintf("%t", value) {
t.Errorf("bo.TokenLiteral not %t. got=%s",
value, bo.TokenLiteral())
return false
}
return true
}
这里没有令人惊讶的事情,仅有另一个在switch语句里的case和一个新的辅助函数。但是随着代码写下,这是简单的扩展TestParsingInfixExpressions:
// parser/parser_test.go
func TestParsingInfixExpressions(t *testing.T) {
infixTests := []struct {
input string
leftValue interface{}
operator string
rightValue interface{}
}{
//[...]
{"true == true", true, "==", true},
{"true != false", true, "!=", false},
{"false == false", false, "==", false},
}
//[...]
if !testLiteralExpression(t, exp.Left, tt.leftValue) {
return
}
if !testLiteralExpression(t, exp.Right, tt.rightValue) {
return
}
// [...]
}
而且 TestParsingPrefixExpressions 很容易扩展,只需向测试表添加新条目:
// parser/parser_test.go
func TestParsingPrefixExpressions(t *testing.T) {
prefixTests := []struct {
input string
operator string
value interface{}
}{
// [...]
{"!true;", "!", true},
{"!false;", "!", false},
}
// [...]
}
是时候拍拍自己的背了! 我们实现了布尔值的解析并扩展了我们的测试以一种现在为我们提供更多测试覆盖率和以后提供更好工具的方式。 做得好!
我们接下来要看到的有时被称为“沃恩普拉特有史以来最伟大的把戏”。 实际上,不,我只是躺在那里,没有人这么说。 但他们应该! 当然,我说的是解析分组表达式。 在 Monkey 中,我们可以用括号对表达式进行分组,以影响它们的优先级,从而影响它们在上下文中的计算顺序。 我们之前已经看过这个规范的例子:
(5 + 5) * 2;
5 + 5组的括号为了给它们一个更高的优先级并且放到AST树种更深的位置,导致此数学表达式的正确评估顺序。
现在你可能认为“Oh,来吧,不要再来优先级的东西了,我的头还在疼!这个家伙...”然后你考虑是否要跳到本章的结尾。别这样,你必须要看这个!
我们不会为分组表达式编写单元测试,因为它们不是由单独的 AST 节点类型表示的。 恩,那就对了。 我们不需要为了正确解析分组表达式而改变我们的 AST! 相反,我们要做的是扩展我们的 TestOperatorPrecedenceParsing 测试函数,以确保括号实际上对表达式进行分组并对结果 AST 产生影响。
// parser/parser_test.go
func TestOperatorPrecedenceParsing(t *testing.T) {
tests := []struct {
input string
expected string
}{
// [...]
{
"1 + (2 + 3) + 4",
"((1 + (2 + 3)) + 4)",
},
{
"(5 + 5) * 2",
"((5 + 5) * 2)",
},
{
"2 / (5 + 5)",
"(2 / (5 + 5))",
},
{
"-(5 + 5)",
"(-(5 + 5))",
},
{
"!(true == true)",
"(!(true == true))",
},
}
// [...]
}
它们失败了,正如预期:
$ go test ./parser
--- FAIL: TestOperatorPrecedenceParsing (0.00s)
parser_test.go:531: parser has 3 errors
parser_test.go:533: parser error: "no prefix parse function for ( found"
parser_test.go:533: parser error: "no prefix parse function for ) found"
parser_test.go:533: parser error: "no prefix parse function for + found"
FAIL
FAIL monkey/parser 0.007s
下面是令人兴奋的部分。 为了让这些测试通过,我们需要做的就是添加:
// parser/parser.go
func New(l *lexer.Lexer) *Parser {
// [...]
p.registerPrefix(token.LPAREN, p.parseGroupedExpression)
// [...]
}
func (p *Parser) parseGroupedExpression() ast.Expression {
p.nextToken()
exp := p.parseExpression(LOWEST)
if !p.expectPeek(token.RPAREN) {
return nil
}
return exp
}
就是这样! 是的,确实如此。 通过提高封闭表达式的优先级,测试通过并且括号按预期工作。 将token类型与函数相关联的概念在这里真正闪耀。 这里的所有都是它的。 这里没有发生我们以前没见过的事情。
我告诉过你,不是吗? 这是一个很好的技巧。 话虽如此,让我们保持一些魔力并继续前进。
在Monkey中我们能使用if和else就像我们做了数百次在其他语言中做的那样:
if (x > y) {
return x;
} else {
return y;
}
else是可选项它可以被这样输出:
if (x > y) {
return x;
}
这都是非常熟悉的。 但是在 Monkey 中,if-else-conditionals 是表达式。 这意味着它们产生一个值,在 if 表达式的情况下,它是最后计算的行。 我们这里不需要 return 语句:
let foobar = if (x > y) { x } else { y };
解释if-else结构似乎并不需要,但为了清楚命名,它在这里:
if (<condition>) <consequence> else <alternative>
大括号是结果和替代的一部分,因为两者都是块语句。 块语句是一系列语句(就像 Monkey 中的程序一样),由开头 { 和结尾 } 包围。
到目前为止,我们成功的秘诀是“定义 AST 节点,编写测试,让测试通过 写解析代码,庆祝,拍拍自己的背,互相祝贺,告诉大家”而且,现在没有理由改变它。
下面是 ast.IfExpression AST 节点的定义:
// ast/ast.go
type IfExpression struct {
Token token.Token // The 'if' token
Condition Expression
Consequence *BlockStatement
Alternative *BlockStatement
}
func (ie *IfExpression) expressionNode() {}
func (ie *IfExpression) TokenLiteral() string { return ie.Token.Literal }
func (ie *IfExpression) String() string {
var out bytes.Buffer
out.WriteString("if")
out.WriteString(ie.Condition.String())
out.WriteString(" ")
out.WriteString(ie.Consequence.String())
if ie.Alternative != nil {
out.WriteString("else ")
out.WriteString(ie.Alternative.String())
}
return out.String()
}
这里没有惊喜。 ast.IfExpression 实现了 ast.Expression 接口并具有三个字段可以代表一个if-else-conditional。 Condition 持有条件,可以是任何表达式、后果和替代指向结果和替代 分别有条件。 但是它们引用了一种新类型 ast.BlockStatement。 正如我们之前看到的,if-else-condition 的结果/替代只是一系列语句。 那正是ast.BlockStatement 代表什么:
// ast/ast.go
type BlockStatement struct {
Token token.Token // the { token
Statements []Statement
}
func (bs *BlockStatement) statementNode() {}
func (bs *BlockStatement) TokenLiteral() string { return bs.Token.Literal }
func (bs *BlockStatement) String() string {
var out bytes.Buffer
for _, s := range bs.Statements {
out.WriteString(s.String())
}
return out.String()
}
我们成功秘诀的下一步是添加测试。 到现在为止,我们知道演习和测试看起来很熟悉:
// parser/parser_test.go
func TestIfExpression(t *testing.T) {
input := `if (x < y) { x }`
l := lexer.New(input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("program.Body does not contain %d statements. got=%d\n",
1, len(program.Statements))
}
stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
if !ok {
t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T",
program.Statements[0])
}
exp, ok := stmt.Expression.(*ast.IfExpression)
if !ok {
t.Fatalf("stmt.Expression is not ast.IfExpression. got=%T",
stmt.Expression)
}
if !testInfixExpression(t, exp.Condition, "x", "<", "y") {
return
}
if len(exp.Consequence.Statements) != 1 {
t.Errorf("consequence is not 1 statements. got=%d\n",
len(exp.Consequence.Statements))
}
consequence, ok := exp.Consequence.Statements[0].(*ast.ExpressionStatement)
if !ok {
t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T",
exp.Consequence.Statements[0])
}
if !testIdentifier(t, consequence.Expression, "x") {
return
}
if exp.Alternative != nil {
t.Errorf("exp.Alternative.Statements was not nil. got=%+v", exp.Alternative)
}
}
我已经添加一个TestIfElseExpression函数使用以下输入:
if (x < y) { x } else { y }
在 TestIfElseExpression 的 Alternative 字段上有额外的断言 *ast.If 表达式。 两个测试都对结果的结构做出断言 *ast.IfExpression 节点并使用辅助函数 testInfixExpression 和 testI dentifier 来保持对条件本身的关注,同时确保我们的其余部分解析器已正确集成。两个测试都失败并显示大量错误消息。 但我们现在已经熟悉了所有这些:
$ go test ./parser
--- FAIL: TestIfExpression (0.00s)
parser_test.go:659: parser has 3 errors
parser_test.go:661: parser error: "no prefix parse function for IF found"
parser_test.go:661: parser error: "no prefix parse function for { found"
parser_test.go:661: parser error: "no prefix parse function for } found"
--- FAIL: TestIfElseExpression (0.00s)
parser_test.go:659: parser has 6 errors
parser_test.go:661: parser error: "no prefix parse function for IF found"
parser_test.go:661: parser error: "no prefix parse function for { found"
parser_test.go:661: parser error: "no prefix parse function for } found"
parser_test.go:661: parser error: "no prefix parse function for ELSE found"
parser_test.go:661: parser error: "no prefix parse function for { found"
parser_test.go:661: parser error: "no prefix parse function for } found"
FAIL
FAIL monkey/parser 0.007s
我们将开始第一个失败测试:TestIfExpression。很清楚,我们需要注册一个prefixParseFn对于token.IF token。
// parser/parser.go
func New(l *lexer.Lexer) *Parser {
// [...]
p.registerPrefix(token.IF, p.parseIfExpression)
// [...]
}
unc (p *Parser) parseIfExpression() ast.Expression {
expression := &ast.IfExpression{Token: p.curToken}
if !p.expectPeek(token.LPAREN) {
return nil
}
p.nextToken()
expression.Condition = p.parseExpression(LOWEST)
if !p.expectPeek(token.RPAREN) {
return nil
}
if !p.expectPeek(token.LBRACE) {
return nil
}
expression.Consequence = p.parseBlockStatement()
if !p.expectPeek(token.RPAREN) {
return nil
}
if !p.expectPeek(token.LBRACE) {
return nil
}
expression.Alternative = p.parseBlockStatement()
}
return expression
}
在其他任何解析函数中,我们都没有如此广泛地使用 expectPeek。 只是没有必要。 这里说得通。 如果 p.peekToken 不是预期的类型,expectPeek 会向解析器添加一个错误,但如果是,则它通过调用 nextToken 方法来推进标记。 这正是我们在这里所需要的。 我们需要有一个 ( 就在 if 之后,如果它在那里,我们需要跳过它。同样适用于 ) 表达式和 { 标记块语句的开头。
这个方法也遵循我们的解析函数协议:token变得足够先进,使得 parseBlockStatement 位于 { 并且 p.curToken 是 token.LBRACE 类型。 这是 parseBlockStatement:
// parser/parser.go
func (p *Parser) parseBlockStatement() *ast.BlockStatement {
block := &ast.BlockStatement{Token: p.curToken}
block.Statements = []ast.Statement{}
p.nextToken()
for !p.curTokenIs(token.RBRACE) && !p.curTokenIs(token.EOF) {
stmt := p.parseStatement()
if stmt != nil {
block.Statements = append(block.Statements, stmt)
}
p.nextToken()
}
return block
}
parseBlockStatement 调用 parseStatement 直到它遇到一个 },它表示块语句的结束,或者一个 token.EOF,它告诉我们没有更多的标记要解析。 在这种情况下,我们无法成功解析块语句,也没有必要在无限循环中继续调用 parseStatement。
这看起来与我们的顶级 ParseProgram 方法非常相似,我们也重复调用 parseStatement 直到遇到“结束标记”,在 ParseProgram 的情况下,它只是token.EOF token。 循环的重复虽然没有坏处,所以我们留下这两个 方法是,而不是照顾我们的测试:
$ go test ./parser
--- FAIL: TestIfElseExpression (0.00s)
parser_test.go:659: parser has 3 errors
parser_test.go:661: parser error: "no prefix parse function for ELSE found"
parser_test.go:661: parser error: "no prefix parse function for { found"
parser_test.go:661: parser error: "no prefix parse function for } found"
FAIL
FAIL monkey/parser 0.007s
TestIfExpression 通过而 TestIfElseExpression 没有通过,完全符合预期。 现在,为了支持 if-else 条件的 else 部分,我们需要检查它是否存在,如果存在,我们需要解析直接在 else 之后的块语句:
// parser/parser.go
func (p *Parser) parseIfExpression() ast.Expression {
// [...]
expression.Consequence = p.parseBlockStatement()
if p.peekTokenIs(token.ELSE) {
p.nextToken()
if !p.expectPeek(token.LBRACE) {
return nil
}
expression.Alternative = p.parseBlockStatement()
}
return expression
}
这里的所有都是它的。 此方法的整个部分以允许可选的 else 但不添加解析器错误(如果没有)的方式构造。 在我们解析结果块语句之后,我们检查下一个标记是否是 token.ELSE 标记。 请记住,在parseBlockStatement 的末尾,我们坐在 } 上。 如果我们有一个 token.ELSE,我们将这个 token 推进两次。 第一次调用 nextToken,因为我们已经知道 p.peekToken 是 else。 然后调用expectPeek,因为现在下一个标记必须是块语句的左大括号,否则程序无效。
是的,解析很容易出现一对一错误。 很容易忘记推进令牌或对、 nextToken 进行错误调用。 拥有一个严格的协议来规定每个解析函数必须如何推进令牌有很大帮助。 幸运的是,我们还有一个很棒的测试套件,可以让我们知道一切正常:
$ go test ./parser
ok monkey/parser 0.007s
我想我不必再告诉你了:干得好! 我们做到了——又一次。
您可能已经注意到,我们刚刚添加的 parseIfExpression 方法比我们之前编写的任何 prefixParseFns 或 infixParseFns 方法都包含更多内容。 主要原因是我们必须使用许多不同的token和表达式类型,甚至是可选部分。 我们接下来要做的事情在难度和涉及的代币类型的多样性上是相似的。 我们将解析函数文字。
在 Monkey 中,函数字面量是我们定义函数的方式:它们具有哪些参数以及函数的作用。 函数字面量如下所示:
fn(x, y) {
return x + y;
}
它以关键字 fn 开头,后跟参数列表,后跟块语句, 这是函数的主体,在调用函数时执行。 函数字面量的抽象结构是这样的:
fn <parameters> <block statement>
我们已经知道什么是块语句以及如何解析它们。 参数是虽然是新的,但并不难解析。 它们只是一个标识符列表逗号分隔并用括号括起来:
(<parameter one>, <parameter two>, <parameter three>, ...)
参数列表也可以是空的:
fn() {
return foobar + barfoo;
}
这就是函数字面量的结构。 但它们是什么类型的 AST 节点? 当然是表情! 我们可以在任何其他表达式有效的任何地方使用函数文字。 例如,这里有一个函数字面量作为 let 语句中的表达式
let myFunction = fn(x, y) { return x + y; }
这是一个函数文字作为另一个函数内的 return 语句中的表达式文字:
fn() {
return fn(x, y) { return x > y; };
}
也可以在调用另一个函数时使用函数字面量作为参数:
myFunc(x, y, fn(x, y) { return x > y; });
这听起来确实很复杂,但事实并非如此。 我们的解析器的一大优点是一旦我们将函数文字定义为表达式并提供一个函数来正确解析它们其余的工作。 听起来很神奇? 我同意。
我们刚刚看到函数字面量的两个主要部分是参数列表和作为函数体的块语句。 这就是我们在定义 AST 节点时需要记住的全部内容:
// ast/ast.go
type FunctionLiteral struct {
Token token.Token // The 'fn' token
Parameters []*Identifier
Body *BlockStatement
}
func (fl *FunctionLiteral) expressionNode() {}
func (fl *FunctionLiteral) TokenLiteral() string { return fl.Token.Literal }
func (fl *FunctionLiteral) String() string {
var out bytes.Buffer
params := []string{}
for _, p := range fl.Parameters {
params = append(params, p.String())
}
out.WriteString(fl.TokenLiteral())
out.WriteString("(")
out.WriteString(strings.Join(params, ", "))
out.WriteString(") ")
out.WriteString(fl.Body.String())
return out.String()
}
Parameters 字段是 *ast.Identifiers 的一部分,因为这就是它的全部内容,而 Body 是一个 *ast.BlockStatement,我们之前见过并使用过。
这是测试,我们可以再次使用我们的辅助函数 testLiteralExpression 和 testInfixExpression:
// parser/parser_test.go
func TestFunctionLiteralParsing(t *testing.T) {
input := `fn(x, y) { x + y; }`
l := lexer.New(input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("program.Body does not contain %d statements. got=%d\n",
1, len(program.Statements))
}
stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
if !ok {
t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T",
program.Statements[0])
}
function, ok := stmt.Expression.(*ast.FunctionLiteral)
if !ok {
t.Fatalf("stmt.Expression is not ast.FunctionLiteral. got=%T",
stmt.Expression)
}
if len(function.Parameters) != 2 {
t.Fatalf("function literal parameters wrong. want 2, got=%d\n",
len(function.Parameters))
}
testLiteralExpression(t, function.Parameters[0], "x")
testLiteralExpression(t, function.Parameters[1], "y")
if len(function.Body.Statements) != 1 {
t.Fatalf("function.Body.Statements has not 1 statements. got=%d\n",
len(function.Body.Statements))
}
bodyStmt, ok := function.Body.Statements[0].(*ast.ExpressionStatement)
if !ok {
t.Fatalf("function body stmt is not ast.ExpressionStatement. got=%T",
function.Body.Statements[0])
}
testInfixExpression(t, bodyStmt.Expression, "x", "+", "y")
}
因此,测试包含三个主要部分:检查 *ast.FunctionLiteral 是否存在,检查参数列表是否正确,并确保函数体包含正确的语句。 最后一部分不是绝对必要的,因为我们之前已经在 IfExpressions 测试中测试了解析块语句。 但是我可以在这里复制一些测试断言,这些断言可能会在连接块语句的解析失败时提醒我们。
只定义了 ast.FunctionLiteral 并且解析器中没有任何更改,测试失败:
$ go test ./parser
--- FAIL: TestFunctionLiteralParsing (0.00s)
parser_test.go:755: parser has 6 errors
parser_test.go:757: parser error: "no prefix parse function for FUNCTION found"
parser_test.go:757: parser error: "expected next token to be ), got , instead"
parser_test.go:757: parser error: "no prefix parse function for , found"
parser_test.go:757: parser error: "no prefix parse function for ) found"
parser_test.go:757: parser error: "no prefix parse function for { found"
parser_test.go:757: parser error: "no prefix parse function for } found"
FAIL
FAIL monkey/parser 0.007s
很清楚我们需要注册一个新的token.FUNCTION prefixParseFn tokens。
// parser/parser.go
func New(l *lexer.Lexer) *Parser {
// [...]
p.registerPrefix(token.FUNCTION, p.parseFunctionLiteral)
// [...]
}
func (p *Parser) parseFunctionLiteral() ast.Expression {
lit := &ast.FunctionLiteral{Token: p.curToken}
if !p.expectPeek(token.LPAREN) {
return nil
}
lit.Parameters = p.parseFunctionParameters()
if !p.expectPeek(token.LBRACE) {
return nil
}
lit.Body = p.parseBlockStatement()
return lit
}
我们在这里用来解析文字参数的 parseFunctionParameters 方法看起来像:
// parser/parser.go
func (p *Parser) parseFunctionParameters() []*ast.Identifier {
identifiers := []*ast.Identifier{}
if p.peekTokenIs(token.RPAREN) {
p.nextToken()
return identifiers
}
p.nextToken()
ident := &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
identifiers = append(identifiers, ident)
for p.peekTokenIs(token.COMMA) {
p.nextToken()
p.nextToken()
ident := &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
identifiers = append(identifiers, ident)
}
if !p.expectPeek(token.RPAREN) {
return nil
}
return identifiers
}
这就是问题的核心。 parseFunctionParameters 通过从逗号分隔列表中重复构建标识符来构造参数切片。 如果列表为空,它也会提前退出,并且它会仔细处理不同大小的列表。
对于这样的方法,使用另一组检查边缘情况的测试确实是值得的:一个空参数列表、一个带有一个参数的列表和一个带有多个参数的列表。
// parser/parser_test.go
func TestFunctionParameterParsing(t *testing.T) {
tests := []struct {
input string
expectedParams []string
}{
{input: "fn() {};", expectedParams: []string{}},
{input: "fn(x) {};", expectedParams: []string{"x"}},
{input: "fn(x, y, z) {};", expectedParams: []string{"x", "y", "z"}},
}
for _, tt := range tests {
l := lexer.New(tt.input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
stmt := program.Statements[0].(*ast.ExpressionStatement)
function := stmt.Expression.(*ast.FunctionLiteral)
if len(function.Parameters) != len(tt.expectedParams) {
t.Errorf("length parameters wrong. want %d, got=%d\n",
len(tt.expectedParams), len(function.Parameters))
}
for i, ident := range tt.expectedParams {
testLiteralExpression(t, function.Parameters[i], ident)
}
}
}
这些测试函数现在通过了:
$ go test ./parser
ok monkey/parser 0.007s
函数文字在包里! 甜的! 在我们离开解析器并开始讨论对 AST 的评估之前,现在只有最后一件事要做。
现在我们知道如何解析函数文字,下一步是解析函数的调用:调用表达式。 这是它们的结构:
<expression>(<comma separated expressions>)
什么? 是的,就是这样,但当然,还需要一些例子。 下面是我们都知道的正常调用表达式:
add(2,3)
现在想一想:add 是一个标识符。 标识符是表达式。 参数 2 和 3 也是表达式 - 整数文字。 但它们不一定是,参数只是一个表达式列表:
add(2 + 2,3 * 3 * 3)
这也有道理。 第一个参数是中缀表达式 2 + 2,第二个参数是 3 * 3 * 3。到目前为止,一切都很好。 现在,让我们看看这里调用的函数。 在这种情况下,函数绑定到标识符 add。 标识符 add 在评估时返回此函数。 这意味着,我们可以直接转到源代码,跳过标识符并用函数文字替换 add:
fn(x, y) { x + y; }(2, 3)
是的,这是有效的。 我们也可以使用函数字面量作为参数:
callsFunction(2, 3, fn(x, y) { x + y; });
让我们再来看结构:
<expression>(<comma separated expressions>)
调用表达式由求值时生成函数的表达式和作为此函数调用参数的表达式列表组成。 作为 AST 节点,它们如下所示:
// ast/ast.go
type CallExpression struct {
Token token.Token // The '(' token
Function Expression // Identifier or FunctionLiteral
Arguments []Expression
}
func (ce *CallExpression) expressionNode() {}
func (ce *CallExpression) TokenLiteral() string { return ce.Token.Literal }
func (ce *CallExpression) String() string {
var out bytes.Buffer
args := []string{}
for _, a := range ce.Arguments {
args = append(args, a.String())
}
out.WriteString(ce.Function.String())
out.WriteString("(")
out.WriteString(strings.Join(args, ", "))
out.WriteString(")")
return out.String()
}
调用表达式的测试用例就像我们测试套件的其余部分一样,对 *ast.CallExpression 结构进行断言:
// parser/parser_test.go
func TestCallExpressionParsing(t *testing.T) {
input := "add(1, 2 * 3, 4 + 5);"
l := lexer.New(input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("program.Statements does not contain %d statements. got=%d\n",
1, len(program.Statements))
}
stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
if !ok {
t.Fatalf("stmt is not ast.ExpressionStatement. got=%T",
program.Statements[0])
}
exp, ok := stmt.Expression.(*ast.CallExpression)
if !ok {
t.Fatalf("stmt.Expression is not ast.CallExpression. got=%T",
stmt.Expression)
}
if !testIdentifier(t, exp.Function, "add") {
return
}
if len(exp.Arguments) != 3 {
t.Fatalf("wrong length of arguments. got=%d", len(exp.Arguments))
}
testLiteralExpression(t, exp.Arguments[0], 1)
testInfixExpression(t, exp.Arguments[1], 2, "*", 3)
testInfixExpression(t, exp.Arguments[2], 4, "+", 5)
}
与函数文字和参数解析一样,为参数解析添加单独的测试也是一个好主意。 只是为了确保每个极端情况都能正常工作并被测试覆盖。 我添加了一个 TestCallExpressionParameterParsing 测试函数,它正是这样做的。 你可以在本章的代码中看到它。
到此为止,太熟悉了。 但现在转折来了。 如果我们运行测试,我们会收到以下错误消息:
$ go test ./parser
--- FAIL: TestCallExpressionParsing (0.00s)
parser_test.go:853: parser has 4 errors
parser_test.go:855: parser error: "expected next token to be ), got , instead"
parser_test.go:855: parser error: "no prefix parse function for , found"
parser_test.go:855: parser error: "no prefix parse function for , found"
parser_test.go:855: parser error: "no prefix parse function for ) found"
FAIL
FAIL monkey/parser 0.007s
呵呵,没多大意思。 为什么没有错误消息告诉我们为调用表达式注册一个 prefixParseFn? 因为调用表达式中没有新的标记类型。 那么我们该怎么做而不是注册一个 prefixParseFn 呢? 看看这个:
add(2,3)
add 是一个由 prefixParseFn 解析的标识符。 在标识符之后是一个 token.LPAREN,就在标识符和参数列表之间,就在中间,在中缀位置……是的,我们需要为 token.LPAREN 注册一个 infixParseFn。 通过这种方式,我们解析作为函数的表达式(标识符或函数文字),然后检查与 token.LPAREN 关联的 aninfixParseFn 并使用已经解析的表达式作为参数调用它。 在这个 infixParseFn 中,我们可以解析参数列表。 完美的!
// parser/parser.go
func New(l *lexer.Lexer) *Parser {
// [...]
p.registerInfix(token.LPAREN, p.parseCallExpression)
// [...]
}
func (p *Parser) parseCallExpression(function ast.Expression) ast.Expression {
exp := &ast.CallExpression{Token: p.curToken, Function: function}
exp.Arguments = p.parseCallArguments()
return exp
}
func (p *Parser) parseCallArguments() []ast.Expression {
args := []ast.Expression{}
if p.peekTokenIs(token.RPAREN) {
p.nextToken()
return args
}
p.nextToken()
args = append(args, p.parseExpression(LOWEST))
for p.peekTokenIs(token.COMMA) {
p.nextToken()
p.nextToken()
args = append(args, p.parseExpression(LOWEST))
}
if !p.expectPeek(token.RPAREN) {
return nil
}
return args
}
parseCallExpression 接收已经解析的函数作为参数,并使用它来构造一个 *ast.CallExpression 节点。 为了解析参数列表,我们调用 parseCallArguments,它看起来与 parseFunctionParameters 惊人地相似,除了它更通用并且返回 ast.Expression 的切片而不是 *ast.Identifier。
这里没有我们以前没见过的东西。 我们所做的只是注册一个新的 infixParseFn。 但测试仍然失败:
$ go test ./parser
--- FAIL: TestCallExpressionParsing (0.00s)
parser_test.go:853: parser has 4 errors
parser_test.go:855: parser error: "expected next token to be ), got , instead"
parser_test.go:855: parser error: "no prefix parse function for , found"
parser_test.go:855: parser error: "no prefix parse function for , found"
parser_test.go:855: parser error: "no prefix parse function for ) found"
FAIL
FAIL monkey/parser 0.007s
它仍然不起作用的原因是 ( in add(1, 2) 现在就像一个中缀运算符,但我们还没有为其分配优先级。它还没有正确的“粘性”, 所以 parseExpression 不会返回我们想要的东西。但是 call 表达式具有最高的优先级,所以我们修复我们的优先级表很重要:
// parser/parser.go
var precedences = map[token.TokenType]int{
// [...]
token.LPAREN: CALL,
}
为了确保调用表达式确实具有最高优先级,我们可以扩展我们的 TestOperatorPrecedenceParsing 测试函数:
// parser/parser_test.go
func TestOperatorPrecedenceParsing(t *testing.T) {
tests := []struct {
input string
expected string
}{
// [...]
{
"a + add(b * c) + d",
"((a + add((b * c))) + d)",
},
{
"add(a, b, 1, 2 * 3, 4 + 5, add(6, 7 * 8))",
"add(a, b, 1, (2 * 3), (4 + 5), add(6, (7 * 8)))",
},
{
"add(a + b + c * d / f + g)",
"add((((a + b) + ((c * d) / f)) + g))",
},
}
// [...]
}
如果我们现在再一次运行测试,我们可以看到它们全部通过了:
$ go test ./parser
ok monkey/parser 0.008s
是的,所有这些:单元测试、参数解析测试和优先级测试 - 哇!他们都通过了! 如果这还不够,这里还有一些好消息:我们已经完成了。 是的,解析器完成了。 诚然,我们稍后会在本书结尾处再次讨论它,以再次扩展它。 但现在:就是这样! AST 已完全定义并且解析器可以工作 - 是时候进入评估主题了。
在我们这样做之前,让我们删除我们留在代码中的 TODO 并扩展我们的 REPL集成解析器。
当我们编写解析 let 和 return 语句的代码时,我们通过跳过表达式来走捷径:
// parser/parser.go
func (p *Parser) parseLetStatement() *ast.LetStatement {
stmt := &ast.LetStatement{Token: p.curToken}
if !p.expectPeek(token.IDENT) {
return nil
}
stmt.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
if !p.expectPeek(token.ASSIGN) {
return nil
}
// TODO: We're skipping the expressions until we
// encounter a semicolon
for !p.curTokenIs(token.SEMICOLON) {
p.nextToken()
}
return stmt
}
相同的 TODO 位于 parseReturnStatement 中。 是时候摆脱它们了。 没有捷径。 首先,我们需要扩展我们现有的测试,以确保被解析为 let 或 return 语句的一部分的表达式确实存在。 我们通过使用我们的辅助函数(不会分散测试的重点)和不同的表达式类型来做到这一点,因此我们知道 parseExpression 已正确集成。
下面是 TestLetStatement 函数的样子:
// parser/parser_test.go
func TestLetStatements(t *testing.T) {
tests := []struct {
input string
expectedIdentifier string
expectedValue interface{}
}{
{"let x = 5;", "x", 5},
{"let y = true;", "y", true},
{"let foobar = y;", "foobar", "y"},
}
for _, tt := range tests {
l := lexer.New(tt.input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("program.Statements does not contain 1 statements. got=%d",
len(program.Statements))
}
stmt := program.Statements[0]
if !testLetStatement(t, stmt, tt.expectedIdentifier) {
return
}
val := stmt.(*ast.LetStatement).Value
if !testLiteralExpression(t, val, tt.expectedValue) {
return
}
}
}
对 TestReturnStatements 也需要这样做。 修复是微不足道的,因为我们以前做过如此出色的工作。 我们只需要在 parseReturnStatement 和 parseLetStatement 中连接 parseExpression 即可。 而且我们还需要处理可选的分号,我们已经从 parseExpressionStatement 中知道如何做。 parseReturnStatement 和 parseLetStatement 的更新的、完全可用的版本如下所示:
// parser/parser.go
func (p *Parser) parseReturnStatement() *ast.ReturnStatement {
stmt := &ast.ReturnStatement{Token: p.curToken}
p.nextToken()
stmt.ReturnValue = p.parseExpression(LOWEST)
if p.peekTokenIs(token.SEMICOLON) {
p.nextToken()
}
return stmt
}
func (p *Parser) parseLetStatement() *ast.LetStatement {
stmt := &ast.LetStatement{Token: p.curToken}
if !p.expectPeek(token.IDENT) {
return nil
}
stmt.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
if !p.expectPeek(token.ASSIGN) {
return nil
}
p.nextToken()
stmt.Value = p.parseExpression(LOWEST)
if p.peekTokenIs(token.SEMICOLON) {
p.nextToken()
}
return stmt
}
啊! 从代码中删除了所有 TODO。 让我们将此解析器用于测试驱动器。
< 2.7Pratt解析是如何运行的? | > 2.9RPPL |
---|