• No results found

}

func evalBlockStatement(block *ast.BlockStatement) object.Object { var result object.Object

for _, statement := range block.Statements { result = Eval(statement)

if result != nil && result.Type() == object.RETURN_VALUE_OBJ { return result

} }

return result }

Here we explicitly don’t unwrap the return value and only check theType()of each evaluation result. If it’s object.RETURN_VALUE_OBJ we simply return the *object.ReturnValue, without unwrapping its .Value, so it stops execution in a possible outer block statement and bubbles up to evalProgram, where it finally get’s unwrapped. (That last part will change when we implement the evaluation of function calls.)

And with that the tests pass:

$ go test ./evaluator

ok monkey/evaluator 0.007s

Return statements are implemented. Now we’re definitely not building a calculator anymore.

And since evalProgramand evalBlockStatementare still so fresh in our mind let’s keep working on them.

3.8 - Abort! Abort! There’s been a mistake!, or:

Error Handling

Remember all theNULLs we were returning earlier and I said that you shouldn’t worry and we’ll come back to them? Here we are. It’s time to implement some real error handling in Monkey before it’s too late and we’d have to backpedal too much. Granted, we have to backpedal a little bit and correct previous code, but not much. We didn’t implement error handling as the first thing in our interpreter, because, and to be completely honest, I thought implementing expressions first is a lot more fun than error handling. But we’re now at a point where we need to add it, otherwise debugging and using our interpreter becomes too cumbersome in the near future.

First of all, let’s define what I mean with “real error handling”. It is not user-defined exceptions.

It’s internal error handling. Errors for wrong operators, unsupported operations, and other user or internal errors that may arise during execution.

As for the implementation of such errors: this will probably sound weird, but the error handling is implemented in nearly the same way as handling return statements is. The reason for this similarity is easy to find: errors and return statements both stop the evaluation of a series of statements.

The first thing we need is an error object:

// object/object.go

const ( // [...]

ERROR_OBJ = "ERROR"

)

type Error struct { Message string }

func (e *Error) Type() ObjectType { return ERROR_OBJ }

func (e *Error) Inspect() string { return "ERROR: " + e.Message }

As you can see, object.Erroris really, really simple. It only wraps a string that serves as error message. In a production-ready interpreter we’d want to attach a stack trace to such error objects, add the line and column numbers of its origin and provide more than just a message.

That’s not so hard to do, provided that line and column numbers are attached to the tokens by the lexer. Since our lexer doesn’t do that, to keep things simple, we only use an error message, which still serves us a great deal by giving us some feedback and stopping execution.

We will add support for errors in a few places now. Later, with increased capability of our interpreter, we’ll add more where appropriate. For now, this test function shows what we expect the error handling to do:

// evaluator/evaluator_test.go

func TestErrorHandling(t *testing.T) { tests := []struct {

input string

expectedMessage string }{

{

"5 + true;",

"type mismatch: INTEGER + BOOLEAN", },

{

"5 + true; 5;",

"type mismatch: INTEGER + BOOLEAN", },

{

"-true",

"unknown operator: -BOOLEAN", },

{

"true + false;",

"unknown operator: BOOLEAN + BOOLEAN", },

{

"5; true + false; 5",

"unknown operator: BOOLEAN + BOOLEAN", },

{

"if (10 > 1) { true + false; }",

"unknown operator: BOOLEAN + BOOLEAN", },

{

`

if (10 > 1) { if (10 > 1) {

return true + false;

}

return 1;

}

`,

"unknown operator: BOOLEAN + BOOLEAN", },

}

for _, tt := range tests {

evaluated := testEval(tt.input)

errObj, ok := evaluated.(*object.Error) if !ok {

t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)

continue }

if errObj.Message != tt.expectedMessage {

t.Errorf("wrong error message. expected=%q, got=%q", tt.expectedMessage, errObj.Message)

} } }

When we run the tests we meet our old friend NULLagain:

$ go test ./evaluator

--- FAIL: TestErrorHandling (0.00s)

evaluator_test.go:193: no error object returned. got=*object.Null(&{}) evaluator_test.go:193: no error object returned.\

got=*object.Integer(&{Value:5})

evaluator_test.go:193: no error object returned. got=*object.Null(&{}) evaluator_test.go:193: no error object returned. got=*object.Null(&{}) evaluator_test.go:193: no error object returned.\

got=*object.Integer(&{Value:5})

evaluator_test.go:193: no error object returned. got=*object.Null(&{}) evaluator_test.go:193: no error object returned.\

got=*object.Integer(&{Value:10}) FAIL

FAIL monkey/evaluator 0.007s

But there are also unexpected*object.Integers. That’s because these test cases actually assert two things: that errors are created for unsupported operations and that errors prevent any further evaluation. When the test fails because of an *object.Integer being returned, the evaluation didn’t stop correctly.

Creating errors and passing them around inEvalis easy. We just need a helper function to help us create new *object.Errors and return them when we think we should:

// evaluator/evaluator.go

func newError(format string, a ...interface{}) *object.Error { return &object.Error{Message: fmt.Sprintf(format, a...)}

}

ThisnewErrorfunction finds its use in every place where we didn’t know what to do before and returnedNULL instead:

// evaluator/evaluator.go

func evalPrefixExpression(operator string, right object.Object) object.Object { switch operator {

// [...]

default:

return newError("unknown operator: %s%s", operator, right.Type()) }

}

func evalInfixExpression(

operator string,

left, right object.Object, ) object.Object {

switch { // [...]

case left.Type() != right.Type():

return newError("type mismatch: %s %s %s", left.Type(), operator, right.Type()) default:

return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type()) }

}

func evalMinusPrefixOperatorExpression(right object.Object) object.Object { if right.Type() != object.INTEGER_OBJ {

return newError("unknown operator: -%s", right.Type()) }

// [...]

}

func evalIntegerInfixExpression(

operator string,

left, right object.Object, ) object.Object {

// [...]

switch operator { // [...]

default:

return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type()) }

}

With these changes made the number of failing test cases has been reduced to just two:

$ go test ./evaluator

--- FAIL: TestErrorHandling (0.00s)

evaluator_test.go:193: no error object returned.\

got=*object.Integer(&{Value:5})

evaluator_test.go:193: no error object returned.\

got=*object.Integer(&{Value:5}) FAIL

FAIL monkey/evaluator 0.007s

That output tells us that creating errors poses no problem but stopping the evaluation still does. We already know where to look though, don’t we? Yes, that’s right: evalProgram and

evalBlockStatement. Here are both functions in their entirety, with newly added support for error handling:

// evaluator/evaluator.go

func evalProgram(program *ast.Program) object.Object { var result object.Object

for _, statement := range program.Statements { result = Eval(statement)

switch result := result.(type) { case *object.ReturnValue:

return result.Value case *object.Error:

return result }

}

return result }

func evalBlockStatement(block *ast.BlockStatement) object.Object { var result object.Object

for _, statement := range block.Statements { result = Eval(statement)

if result != nil { rt := result.Type()

if rt == object.RETURN_VALUE_OBJ || rt == object.ERROR_OBJ { return result

} } }

return result }

That did it. Evaluation is stopped at the right places and the tests now pass:

$ go test ./evaluator

ok monkey/evaluator 0.010s

There’s still one last thing we need to do. We need to check for errors whenever we call Eval

inside of Eval, in order to stop errors from being passed around and then bubbling up far away from their origin:

// evaluator/evaluator.go

func isError(obj object.Object) bool { if obj != nil {

return obj.Type() == object.ERROR_OBJ }

return false }

func Eval(node ast.Node) object.Object { switch node := node.(type) {

// [...]

case *ast.ReturnStatement:

val := Eval(node.ReturnValue) if isError(val) {

return val }

return &object.ReturnValue{Value: val}

// [...]

case *ast.PrefixExpression:

right := Eval(node.Right) if isError(right) {

return right }

return evalPrefixExpression(node.Operator, right)

case *ast.InfixExpression:

left := Eval(node.Left) if isError(left) {

return left }

right := Eval(node.Right) if isError(right) {

return right }

return evalInfixExpression(node.Operator, left, right) // [...]

}

func evalIfExpression(ie *ast.IfExpression) object.Object { condition := Eval(ie.Condition)

if isError(condition) { return condition }

// [...]

}

And that’s it. Error handling is in place.