好吧,我们开始。让我们开始编写Eval!我们有了AST和一个新的对象系统,允许我们跟踪执行Monkey源代码时遇到值。是时候最终评估AST了。
以下是 Eval 的签名在其第一个版本中的样子:
func Eval(node ast.Node) object.Object
Eval将第一个ast.Node作为输入并返回一个object.Object。记住我们在ast包中定义的每个节点都满足ast.Node接口,因此可以传递给Eval,这允许我们递归地适用Eval并在评估AST的一部分时调用自身。每个AST节点都需要不同形式的评估,而Eval是我们决定这些形式是什么样子的地方。例如,假设我们将*ast.Program节点传递给Eval。然后Eval应该做的是通过使用单个调用自身来评估每个 *ast.Program.Statements语句。外部调用Eval的返回值是上次调用的返回值。
我们将从实现自我评估表达式开始。 这就是我们在 Eval 的土地上所说的文字。 具体来说,布尔和整数文字。 它们是 Monkey 中最容易评估的结构,因为它们对自己进行评估。 如果我在 REPL 中输入 5,那么 5 也是应该出现的。 如果我输入 true 那么 true 就是我想要的。、
听起来很容易吧?所以,让我们把“输入 5,取回 5”变成现实。
在编写代码之前,这到底是什么意思? 我们得到一个单独的表达式语句作为输入,它只包含一个整数文字,并希望对其求值以便返回整数本身。
翻译成我们系统的语言,这意味着,给定一个 *ast.IntegerLiteral,我们的 Eval 函数应该返回一个 *object.Integer,其 Value 字段包含与 *ast.IntegerLiteral.Value 相同的整数。
我们能简单地写一个测试为我们的新evaluator包:
// evaluator/evaluator_test.go
package evaluator
import (
"monkey/lexer"
"monkey/object"
"monkey/parser"
"testing"
)
func TestEvalIntegerExpression(t *testing.T) {
tests := []struct {
input string
expected int64
}{
{"5", 5},
{"10", 10},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
testIntegerObject(t, evaluated, tt.expected)
}
}
func testEval(input string) object.Object {
l := lexer.New(input)
p := parser.New(l)
program := p.ParseProgram()
return Eval(program)
}
func testIntegerObject(t *testing.T, obj object.Object, expected int64) bool {
result, ok := obj.(*object.Integer)
if !ok {
t.Errorf("object is not Integer. got=%T (%+v)", obj, obj)
return false
}
if result.Value != expected {
t.Errorf("object has wrong value. got=%d, want=%d",
result.Value, expected)
return false
}
return true
}
这么小的测试需要很多代码,不是吗? 与我们的解析器测试一样,我们在这里建立我们的测试基础设施。 TestEvalIntegerExpression 测试需要增长并且它的当前的结构使这非常容易。 testEval 和 testIntegerObject 也会有很多用处。
测试的核心是在 testEval 中调用 Eval。 我们接受我们的输入,将它传递给词法分析器,将词法分析器传递给解析器并返回一个 AST。 然后,这是新的,我们将 AST 传递给 Eval。 我们对 Eval 的返回值进行断言。 在这种情况下,我们希望返回值是一个带有正确 .Value 的 *object.Integer。 换句话说:我们希望 5 评估为 5。
当然,测试失败因为我们还没有定义Eval。但是我们已经知道了Eval应该戏曲一个ast.Node作为参数并且返回一个object.Object。并且每当它遇到 *ast.IntegerLiteral 时,它应该返回一个带有正确 .Value 的 *object.Integer。 将其转换为代码并在评估器包中使用此行为定义我们的新 Eval,我们得到:
// evaluator/evaluator.go
package evaluator
import (
"monkey/ast"
"monkey/object"
)
func Eval(node ast.Node) object.Object {
switch node := node.(type) {
case *ast.IntegerLiteral:
return &object.Integer{Value: node.Value}
}
return nil
}
这里没有特殊的,它所做的就是我们所说的。除了它不能运行。测试仍然失败了因为Eval返回nil而不是一个*object.Integer。
$ go test ./evaluator
--- FAIL: TestEvalIntegerExpression (0.00s)
evaluator_test.go:36: object is not Integer. got=<nil> (<nil>)
evaluator_test.go:36: object is not Integer. got=<nil> (<nil>)
FAIL
FAIL monkey/evaluator 0.006s
失败的原因是我们从未在 Eval 中遇到过 *ast.IntegerLiteral。 我们不遍历 AST。 我们应该始终从树的顶部开始,接收一个 *ast.Program,然后遍历其中的每个节点。 而这正是我们在这里没有做的。 我们只是在等待 *ast.IntegerLiteral。 解决方法是实际遍历树并评估 *ast.Program 的每个语句:
// evaluator/evaluator.go
func Eval(node ast.Node) object.Object {
switch node := node.(type) {
// Statements
case *ast.Program:
return evalStatements(node.Statements)
case *ast.ExpressionStatement:
return Eval(node.Expression)
// Expressions
case *ast.IntegerLiteral:
return &object.Integer{Value: node.Value}
}
return nil
}
func evalStatements(stmts []ast.Statement) object.Object {
var result object.Object
for _, statement := range stmts {
result = Eval(statement)
}
return result
}
随着这个改变,我们评估每个在Monkey程序语句。如果语句是*ast.ExpressionStatement我们评估它的表达式。这反映了我们得到的AST结构从一行输入,如5:由一个语句、一个表达式语句组成的程序(不是return语句,也不是let语句)以整数文字作为其表达式。
$ go test ./evaluator
ok monkey/evaluator 0.006s
测试通过了!我们能评估整数字段了!大家好,如果我们输入一个数字,就会出现一个数字,我们只需要几千行代码和测试就可以做到这一点!好吧,当然,它看起来并不多。但这是一个开始。我们开始看到评估是如何改变工作的以及我们如何扩展我们的评估器。Eval的结构不会改变,我们只会添加和扩展它。
在我们的自我评估表达式列表中,接下来是布尔文字。 但在我们这样做之前,我们应该庆祝我们的第一次评估成功并善待自己。 让我们把 E 放在 REPL 中!
到目前为止,我们的 REPL 中的 E 缺失,我们只有一个 RPPL - 一个读取解析打印循环。 现在我们有了 Eval,我们可以构建一个真正的 Read-Evaluate-Print-Loop!在 repl 包中使用评估器就像你想象的一样简单:
// repl/repl.go
import (
// [...]
"monkey/evaluator"
)
// [...]
func Start(in io.Reader, out io.Writer) {
scanner := bufio.NewScanner(in)
for {
fmt.Printf(PROMPT)
scanned := scanner.Scan()
if !scanned {
return
}
line := scanner.Text()
l := lexer.New(line)
p := parser.New(l)
program := p.ParseProgram()
if len(p.Errors()) != 0 {
printParserErrors(out, p.Errors())
continue
}
evaluated := evaluator.Eval(program)
if evaluated != nil {
io.WriteString(out, evaluated.Inspect())
io.WriteString(out, "\n")
}
}
}
我们将程序传递给 Eval,而不是打印程序(解析器返回的 AST)。 如果 Eval 返回一个非 nil 值,一个 object.Object,我们将打印其 Inspect() 方法的输出。 在 *object.Integer 的情况下,它将是它包装的整数的字符串表示。
有了这个,我们现在有了一个有效的 REPL:
$ go run main.go
Hello mrnugget! This is the Monkey programming language!
Feel free to type in commands
>> 5
5
>> 10
10
>> 999
999
>>
感觉很棒,这就是它吗?词法分析、解析、评估——都在那里。 我们已经走了很长一段路。
布尔字段就像他们的整数对应物一样,对自己求值。true评估为true
,false评估为false
。在 Eval 中实现它就像添加对整数文字的支持一样简单。 测试同样无聊:
// evaluator/evaluator_test.go
func TestEvalBooleanExpression(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"true", true},
{"false", false},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
testBooleanObject(t, evaluated, tt.expected)
}
}
func testBooleanObject(t *testing.T, obj object.Object, expected bool) bool {
result, ok := obj.(*object.Boolean)
if !ok {
t.Errorf("object is not Boolean. got=%T (%+v)", obj, obj)
return false
}
if result.Value != expected {
t.Errorf("object has wrong value. got=%t, want=%t",
result.Value, expected)
return false
}
return true
}
一旦我们支持更多导致布尔值的表达式,我们就会扩展测试切片。 现在,我们只确保在输入 true 或 false 时得到正确的输出。 测试失败:
$ go test ./evaluator
--- FAIL: TestEvalBooleanExpression (0.00s)
evaluator_test.go:42: Eval didn't return BooleanObject. got=<nil> (<nil>)
evaluator_test.go:42: Eval didn't return BooleanObject. got=<nil> (<nil>)
FAIL
FAIL monkey/evaluator 0.006s
使其通过就像从 *ast.IntegerLiteral 复制 case 分支并更改两个标识符一样简单:
// evaluator/evaluator.go
func Eval(node ast.Node) object.Object {
// [...]
case *ast.Boolean:
return &object.Boolean{Value: node.Value}
// [...]
}
这就对了!让我们在REPL里试一试:
$ go run main.go
Hello mrnugget! This is the Monkey programming language!
Feel free to type in commands
>> true
true
>> false
false
>>
很棒,但是,让我问你:每次遇到 true 或 false 时,我们都在创建一个新的 object.Boolean 是荒谬的,不是吗? 两个true之间没有区别。 false也是一样。 为什么每次都使用新实例? 只有两个可能的值,所以让我们引用它们而不是创建新的。
// evaluator/evaluator.go
var (
TRUE = &object.Boolean{Value: true}
FALSE = &object.Boolean{Value: false}
)
func Eval(node ast.Node) object.Object {
// [...]
case *ast.Boolean:
return nativeBoolToBooleanObject(node.Value)
// [...]
}
func nativeBoolToBooleanObject(input bool) *object.Boolean {
if input {
return TRUE
}
return FALSE
}
现在我们的包中只有两个 object.Boolean 实例:TRUE 和 FALSE,我们引用它们而不是分配新的 object.Booleans。 这更有意义,并且是我们无需大量工作即可获得的微小性能改进。 当我们在做的时候,让我们也处理 null。
正如只有一个真和一个假一样,应该只有一个对空值的引用。空值没有变化。 没有有点但不完全空,没有半空,也没有基本上与另一个空相同。 要么是这个 null,要么不是。 因此,让我们创建一个可以在整个评估器中引用的 NULL,而不是创建新的 object.Nulls。
// evaluator/evaluator.go
var (
NULL = &object.Null{}
TRUE = &object.Boolean{Value: true}
FALSE = &object.Boolean{Value: false}
)
这就是它在这里的全部。现在我们有一个我们指出的NULL。
有了整数文字和我们的 NULL、TRUE 和 FALSE 三个组合,我们就可以评估运算符表达式了。
Monkey 支持的最简单的运算符表达式形式是前缀表达式或一元运算符表达式,其中一个操作数跟在运算符之后。 在我们的解析器中,我们处理了很多语言结构,比如前缀表达式,因为这是解析它们的最简单方法。 但在本节中,前缀表达式只是一个运算符和一个操作数的运算符表达式。 Monkey 支持其中两个前缀运算符: ! 和 -。
评估运算符表达式(尤其是带有前缀运算符和一个操作数)并不难。我们将分步进行,并一点一点地构建所需的行为。 但我们也需要密切关注。 我们即将实施的措施具有深远的影响。 请记住:在评估过程中,输入语言获得意义; 我们正在定义 Monkey 编程语言的语义。 运算符表达式求值的微小变化可能会导致语言中似乎完全不相关的部分出现意外情况。 测试帮助我们确定所需的行为,并作为我们的规范。
我们将开始通过实现对!操作符的支持。测试展示了运算符应该“转换”对布尔值并将其取反。
// evaluator/evaluator_test.go
func TestBangOperator(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"!true", false},
{"!false", true},
{"!5", false},
{"!!true", true},
{"!!false", false},
{"!!5", true},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
testBooleanObject(t, evaluated, tt.expected)
}
}
正如我说的,这儿我们决定我们的语言如何工作的地方。!true
和!false
表达式和它们的预期结果似乎是常识,但!5可能是其他语言设计人员认为应该返回错误的地方。但是我们在这里要说的是5是“真实的”。
测试没有通过,当然,因为Eval返回nil而不是TRUE或者FALSE。第一步是评估前缀表达式是评估它的操作符并且然后使用评估操作符的结果:
// evaluator/evaluator.go
func Eval(node ast.Node) object.Object {
// [...]
case *ast.PrefixExpression:
right := Eval(node.Right)
return evalPrefixExpression(node.Operator, right)
// [...]
}
在第一次调用 Eval 之后,right 可能是 *object.Integer 或 *object.Boolean 甚至可能是 NULL。 然后我们将这个正确的操作数传递给 evalPrefixExpression,它检查是否支持该运算符:
// evaluator/evaluator.go
func evalPrefixExpression(operator string, right object.Object) object.Object {
switch operator {
case "!":
return evalBangOperatorExpression(right)
default:
return NULL
}
}
如果不支持该运算符,则返回 NULL。 那是最好的选择吗? 也许,也许不是。 目前,这绝对是最简单的选择,因为我们还没有实现任何错误处理。
evalBangOperatorExpression 函数是 ! 指定:
// evaluator/evaluator.go
func evalBangOperatorExpression(right object.Object) object.Object {
switch right {
case TRUE:
return FALSE
case FALSE:
return TRUE
case NULL:
return TRUE
default:
return FALSE
}
}
然后测试通过了。
$ go test ./evaluator
ok monkey/evaluator 0.007s
让我们移动-前缀操作符,我们能扩展我们的TestEvalIntegerExpression测试函数合并它:
// evaluator/evaluator_test.go
func TestEvalIntegerExpression(t *testing.T) {
tests := []struct {
input string
expected int64
}{
{"5", 5},
{"10", 10},
{"-5", -5},
{"-10", -10},
}
// [...]
}
出于两个原因,我选择扩展此测试而不是仅为 - 前缀运算符编写新的测试函数。 首先,整数是前缀位置的 - 运算符唯一支持的操作数。 其次,因为这个测试函数应该扩展到包含所有整数运算,以便有一个地方以清晰整洁的方式显示所需的行为。
我们必须扩展我们之前编写的 evalPrefixExpression 函数才能使测试用例通过。 switch 语句中需要一个新的分支:
// evaluator/evaluator.go
func evalPrefixExpression(operator string, right object.Object) object.Object {
switch operator {
case "!":
return evalBangOperatorExpression(right)
case "-":
return evalMinusPrefixOperatorExpression(right)
default:
return NULL
}
}
evalMinusPrefixOperatorExpression函数看起来像这样:
// evaluator/evaluator.go
func evalMinusPrefixOperatorExpression(right object.Object) object.Object {
if right.Type() != object.INTEGER_OBJ {
return NULL
}
value := right.(*object.Integer).Value
return &object.Integer{Value: -value}
}
我们在这里做的第一件事是检查操作数是否是整型。如果不是我们返回NULL。但如果是,我们提取*object.Integer 的值。 然后我们分配一个新对象来包装这个值的否定版本
那不是有很多代码不是吗?但继续,它工作了:
$ go test ./evaluator
ok monkey/evaluator 0.007s
优秀! 现在我们可以在 REPL 中给我们的前缀表达式一个旋转,然后再转到它们的中缀朋友:
$ go run main.go
Hello mrnugget! This is the Monkey programming language!
Feel free to type in commands
>> -5
-5
>> !true
false
>> !-5
false
>> !!-5
true
>> !!!!-5
true
>> -true
null
很棒!
复习一下,以下是 Monkey 支持的八个中缀运算符:
5 + 5;
5 - 5;
5 * 5;
5 / 5;
5 > 5;
5 < 5;
5 == 5;
5 != 5;
这八个运算符可以分为两组:一组运算符生成布尔值作为其结果,另一组不生成布尔值。 我们将从实现对第二组的支持开始:+、-、*、/。 并且首先仅与整数操作数结合使用。 一旦成功,我们将在运算符的任一侧添加对布尔值的支持
测试基础设施已经就位。 我们将使用这些新运算符的测试用例扩展我们的 TestEvalIntegerExpression 测试函数:
// evaluator/evaluator_test.go
func TestEvalBooleanExpression(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"true", true},
{"false", false},
{"1 < 2", true},
{"1 > 2", false},
{"1 < 1", false},
{"1 > 1", false},
{"1 == 1", true},
{"1 != 1", false},
{"1 == 2", false},
{"1 != 2", true},
}
// [...]
}
在 evalIntegerInfixExpression 中添加几行是让这些测试通过所需要的全部内容:
// evaluator/evaluator.go
func evalIntegerInfixExpression(
operator string,
left, right object.Object,
) object.Object {
leftVal := left.(*object.Integer).Value
rightVal := right.(*object.Integer).Value
switch operator {
// [...]
case "<":
return nativeBoolToBooleanObject(leftVal < rightVal)
case ">":
return nativeBoolToBooleanObject(leftVal > rightVal)
case "==":
return nativeBoolToBooleanObject(leftVal == rightVal)
case "!=":
return nativeBoolToBooleanObject(leftVal != rightVal)
default:
return NULL
}
}
我们已经用于布尔文字的 nativeBoolToBooleanObject 函数现在在我们需要根据未包装值之间的比较返回 TRUE 或 FALSE 时找到了一些重用。
就是这样! 好吧,至少对于整数。 当两个操作数都是整数时,我们现在完全支持八个中缀运算符。 本节剩下的是添加对布尔操作数的支持。
Monkey 仅支持相等运算符 == 和 != 的布尔操作数。 它不支持加、减、除和乘布尔值。 也不支持使用 < 或 > 检查 true 是否大于 false。 这将我们的任务简化为仅添加对两个运算符的支持。
如您所知,我们要做的第一件事就是添加测试。 而且,和以前一样,我们可以扩展现有的测试功能。 在这种情况下,我们将使用 TestEvalBooleanExpression 并为 == 和 != 运算符添加测试用例:
// evaluator/evaluator_test.go
func TestEvalBooleanExpression(t *testing.T) {
tests := []struct {
input string
expected bool
}{
// [...]
{"true == true", true},
{"false == false", true},
{"true == false", false},
{"true != false", true},
{"false != true", true},
{"(1 < 2) == true", true},
{"(1 < 2) == false", false},
{"(1 > 2) == true", false},
{"(1 > 2) == false", true},
}
// [...]
}
严格来说,只有前五个案例是测试新的和期望的行为所必需的。 但是让我们也加入其他四个来检查生成的布尔值之间的比较。
到现在为止还挺好。 这里没有什么令人惊讶的。 只是另一组失败的测试:
$ go test ./evaluator
--- FAIL: TestEvalBooleanExpression (0.00s)
evaluator_test.go:121: object is not Boolean. got=*object.Null (&{})
evaluator_test.go:121: object is not Boolean. got=*object.Null (&{})
evaluator_test.go:121: object is not Boolean. got=*object.Null (&{})
evaluator_test.go:121: object is not Boolean. got=*object.Null (&{})
evaluator_test.go:121: object is not Boolean. got=*object.Null (&{})
evaluator_test.go:121: object is not Boolean. got=*object.Null (&{})
evaluator_test.go:121: object is not Boolean. got=*object.Null (&{})
evaluator_test.go:121: object is not Boolean. got=*object.Null (&{})
evaluator_test.go:121: object is not Boolean. got=*object.Null (&{})
FAIL
FAIL monkey/evaluator 0.007s
这里有一些让这些测试通过的好方法:
// evaluator/evaluator.go
func evalInfixExpression(
operator string,
left, right object.Object,
) object.Object {
switch {
// [...]
case operator == "==":
return nativeBoolToBooleanObject(left == right)
case operator == "!=":
return nativeBoolToBooleanObject(left != right)
default:
return NULL
}
}
恩,那就对了。 我们只在现有的 evalInfixExpression 中添加四行,测试就通过了。 我们在这里使用指针比较来检查布尔值之间的相等性。 这是有效的,因为我们总是使用指向我们对象的指针,而在布尔值的情况下,我们只使用两个:TRUE 和 FALSE。 因此,如果某事物具有与 TRUE(即内存地址)相同的值,那么它就是真的。 这也适用于 NULL。
这不适用于我们稍后可能添加的整数或其他数据类型。 在 *object.Integer 的情况下,我们总是分配 object.Integer 的新实例,因此使用新的指针。 我们不能将这些指针与不同的实例进行比较,否则 5 == 5 将是错误的,这不是我们想要的。 在这种情况下,我们希望显式比较值而不是包装这些值的对象。
这就是为什么整数操作数的检查必须在 switch 语句中更高并且比这些新添加的 case 分支更早匹配。 只要我们照顾其他在到达这些指针比较之前的操作数类型我们很好并且它有效。
十年后,当 Monkey 是一门著名的编程语言,而关于研究忽略了设计编程语言的业余爱好者的讨论仍在进行中,我们既富有又出名时,有人会在 StackOverflow 上问为什么 Monkey 中的整数比较比布尔比较慢。 答案将由你或我写,我们中的一个人会说 Monkey 的对象系统不允许对整数对象进行指针比较。 在进行比较之前,它必须解开该值。 因此布尔值之间的比较更快。 我们将添加一个“来源:我写的”。 到我们答案的底部,并获得闻所未闻的业力。
我离题了。 回到主题,让我说:哇! 我们做到了! 我知道,我对我的赞美非常慷慨,可以很容易地找到庆祝的理由,但如果有时间开香槟,那就是现在。 是的,我们做到了。 看看我们的解释器现在可以做什么:
$ go run main.go
Hello mrnugget! This is the Monkey programming language!
Feel free to type in commands
>> 5 * 5 + 10
35
>> 3 + 4 * 5 == 3 * 1 + 4 * 5
true
>> 5 * 10 > 40 + 5
true
>> (10 + 2) * 30 == 300 + 20 * 3
true
>> (5 > 5 == true) != false
false
>> 500 / 2 != 250
false
所以,现在我们有一个功能齐全的计算器,可以做更多的事情。让我们给他更多。让我们让它看起来更像一种编程语言。
< 3.4表示对象 | > 3.6条件句 |
---|