Skip to content

Latest commit

 

History

History
196 lines (173 loc) · 7.38 KB

File metadata and controls

196 lines (173 loc) · 7.38 KB

3.9绑定和环境

接下来我们将通过添加对 let 语句的支持来为我们的解释器添加绑定。但我们不仅需要支持 let 语句,不,我们还需要支持标识符的评估。 假设我们已经评估了以下代码:

let x = 5 * 5;

仅添加对该语句的评估的支持是不够的。 我们还需要确保在解释了上面的行之后 x 的计算结果为 10。

因此,我们在本节中的任务是评估 let 语句和标识符。 我们通过评估 let 语句的值生成表达式并跟踪指定名称下的生成值来评估 let 语句。 为了评估标识符,我们检查是否已经有一个绑定到名称的值。 如果我们这样做,标识符会评估为这个值,如果我们不这样做,我们会返回一个错误。

听起来是个好计划? 好的,让我们开始进行一些测试:

// evaluator/evaluator_test.go

func TestLetStatements(t *testing.T) {
    tests := []struct {
        input string
        expected int64
    }{
        {"let a = 5; a;", 5},
        {"let a = 5 * 5; a;", 25},
        {"let a = 5; let b = a; b;", 5},
        {"let a = 5; let b = a; let c = a + b + 5; c;", 15},
    }

    for _, tt := range tests {
        testIntegerObject(t, testEval(tt.input), tt.expected)
    }
}

测试用例断言这两件事应该起作用:评估 let 语句中产生值的表达式和评估绑定到名称的标识符。 但是我们还需要测试以确保在尝试评估未绑定标识符时会出现错误。 为此,我们可以简单地扩展我们现有的 TestErrorHandling 函数:

// evaluator/evaluator_test.go

func TestErrorHandling(t *testing.T) {
    tests := []struct {
        input string
        expectedMessage string
    }{
// [...]
        {
        "foobar",
        "identifier not found: foobar",
        },
    }
// [...]
}

我们如何让这些测试通过? 显然,我们要做的第一件事就是为 *ast.LetStatement 添加一个新的 case 分支到 Eval。 在这个分支中,我们需要评估 let 语句的表达式,对吗? 所以让我们从那个开始:

// evaluator/evaluator.go
func Eval(node ast.Node) object.Object {
// [...]
    case *ast.LetStatement:
        val := Eval(node.Value)
        if isError(val) {
            return val
        }
// Huh? Now what?
// [...]
}

评论是对的:现在呢? 我们如何跟踪值? 我们有值,我们也有我们应该绑定它的名称,node.Name.Value。 我们如何将一个与另一个联系起来?

这就是所谓的环境发挥作用的地方。 环境是我们用来通过将它们与名称相关联来跟踪价值的东西。 “环境”这个名字是一个经典的名字,在许多其他解释器中使用,尤其是 Lispy 解释器。 但是,尽管名称听起来很复杂,但其核心环境是将字符串与对象相关联的哈希映射。 这正是我们将用于实现的内容。

我们将向对象包添加一个新的 Environment 结构。 是的,现在它真的只是地图周围的一个薄包装:

// object/environment.go

package object

func NewEnvironment() *Environment {
    s := make(map[string]Object)
    return &Environment{store: s}
}

type Environment struct {
    store map[string]Object
}

func (e *Environment) Get(name string) (Object, bool) {
    obj, ok := e.store[name]
    return obj, ok
}

func (e *Environment) Set(name string, val Object) Object {
    e.store[name] = val
    return val
}

让我猜猜你在想什么:为什么不使用地图? 为什么是包装纸? 我保证,一旦我们在下一节开始实现函数和函数调用,一切都会变得有意义。这是我们稍后将建立的基础。

事实上, object.Environment 的用法本身是不言自明的。 但是我们如何在 Eval 中使用它呢? 我们如何以及在哪里跟踪环境? 我们通过使它成为一个Eval 的参数:

// evaluator/evaluator.go

func Eval(node ast.Node, env *object.Environment) object.Object {
// [...]
}

有了这个改变,任何东西都不再编译了,因为我们必须改变对 Eval 的每次调用以利用环境。 不仅是Eval本身对Eval的调用,还有evalProgram、evalIfExpression等函数中的调用。 这比其他任何事情都需要更多的手动编辑器工作,因此我不会通过在此处显示更改列表来使您感到厌烦。

当然,在我们的 REPL 和我们的测试套件中对 Eval 的调用也需要使用环境。在 REPL 中,我们使用单一环境:

// repl/repl.go
func Start(in io.Reader, out io.Writer) {
    scanner := bufio.NewScanner(in)
    env := object.NewEnvironment()
    
    for {
// [...]
        evaluated := evaluator.Eval(program, env)
        if evaluated != nil {
            io.WriteString(out, evaluated.Inspect())
            io.WriteString(out, "\n")
        }
    }
}

我们在这里使用的环境 env 在对 Eval 的调用之间持续存在。 如果没有,将值绑定到 REPL 中的名称将没有任何效果。 一旦评估下一行,关联就不会在新环境中。

不过,这正是我们在测试套件中想要的。 我们不想为每个测试函数和每个测试用例保留状态。 每次调用 testEval 都应该有一个新的环境,这样我们就不会遇到由测试运行顺序引起的涉及全局状态的奇怪错误。 每次调用 Eval 都会得到一个全新的环境:

// evaluator/evaluator_test.go

func testEval(input string) object.Object {
    l := lexer.New(input)
    p := parser.New(l)
    program := p.ParseProgram()
    env := object.NewEnvironment()

    return Eval(program, env)
}

随着更新的 Eval 调用再次编译测试,我们可以开始让它们通过,这在 *object.Environemnt 可用的情况下并不难。 在 *ast.LetStatement 的 case 分支中,我们可以使用我们已有的名称和值并将它们保存在当前环境中:

// evaluator/evaluator.go

func Eval(node ast.Node, env *object.Environment) object.Object {
// [...]
    case *ast.LetStatement:
        val := Eval(node.Value, env)
        if isError(val) {
            return val
        }
        env.Set(node.Name.Value, val)
// [...]
}

现在我们在评估 let 语句时向环境添加关联。 但是在评估标识符时,我们也需要将这些值取出来。 这样做也很容易:

// evaluator/evaluator.go

func Eval(node ast.Node, env *object.Environment) object.Object {
// [...]
    case *ast.Identifier:
        return evalIdentifier(node, env)
// [...]
}
func evalIdentifier(
    node *ast.Identifier,
    env *object.Environment,
    ) object.Object {
        val, ok := env.Get(node.Value)
        if !ok {
            return newError("identifier not found: " + node.Value)
        }
        
        return val
}

evalIdentifier 将在下一节中扩展。 现在它只是检查一个值是否与当前环境中的给定名称相关联。 如果是这种情况,它会返回值,否则报错。

看一下:

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

是的,你是对的,这正是这意味着:我们现在坚定地站在编程语言领域。

$ go run main.go
Hello mrnugget! This is the Monkey programming language!
Feel free to type in commands
>> let a = 5;
>> let b = a > 3;
>> let c = a * 99;
>> if (b) { 10 } else { 1 };
10
>> let d = if (c > a) { 99 } else { 100 };
>> d
99
>> d * c * a;
245025
< 3.8中止! 中止! 有错误!,或:错误处理 > 3.10函数和函数调用