Skip to content

Commit

Permalink
3.8 Evaluation (error handling)
Browse files Browse the repository at this point in the history
  • Loading branch information
cedrickchee committed Mar 31, 2020
1 parent 398a7b2 commit fb7f562
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 13 deletions.
72 changes: 59 additions & 13 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package evaluator
// the nodes according to their semantic meaning.

import (
"fmt"

"github.com/cedrickchee/hou/ast"
"github.com/cedrickchee/hou/object"
)
Expand Down Expand Up @@ -47,6 +49,9 @@ func Eval(node ast.Node) object.Object {
case *ast.ReturnStatement:
// Evaluate the expression associated with the return statement.
val := Eval(node.ReturnValue)
if isError(val) {
return val
}
return &object.ReturnValue{Value: val}

// Expressions
Expand All @@ -60,11 +65,22 @@ func Eval(node ast.Node) object.Object {
// The first step is to evaluate its operand and then use the result of
// this evaluation with the operator.
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)

case *ast.IfExpression:
Expand All @@ -84,10 +100,14 @@ func evalProgram(program *ast.Program) object.Object {
for _, statement := range program.Statements {
result = Eval(statement)

// Check if the last evaluation result is such an object.ReturnValue and
// if so, we stop the evaluation and return the unwrapped value.
if returnValue, ok := result.(*object.ReturnValue); ok {
return returnValue.Value
switch result := result.(type) {
case *object.ReturnValue:
// Check if the last evaluation result is such an object.ReturnValue
// and if so, we stop the evaluation and return the unwrapped value.
return result.Value
case *object.Error:
// Error handling — stop the evaluation.
return result
}
}

Expand All @@ -107,8 +127,11 @@ func evalBlockStatement(block *ast.BlockStatement) object.Object {
// 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.
if result != nil && result.Type() == object.RETURN_VALUE_OBJ {
return result
if result != nil {
rt := result.Type()
if rt == object.RETURN_VALUE_OBJ || rt == object.ERROR_OBJ {
return result
}
}
}

Expand Down Expand Up @@ -139,10 +162,9 @@ func evalPrefixExpression(operator string, right object.Object) object.Object {
case "-":
return evalMinusPrefixOperatorExpression(right)
default:
// If the operator is not supported we return NULL. Is that the best
// choice? Maybe, maybe not. For now, it's definitely the easiest
// choice, since we don't have any error handling implemented yet.
return NULL
// If the operator is not supported we don't return NULL since we now
// have error handling implemented.
return newError("unknown operator: %s%s", operator, right.Type())
}
}

Expand All @@ -163,7 +185,7 @@ func evalBangOperatorExpression(right object.Object) object.Object {
func evalMinusPrefixOperatorExpression(right object.Object) object.Object {
// Check if the operand is an integer.
if right.Type() != object.INTEGER_OBJ {
return NULL
return newError("unknown operator: -%s", right.Type())
}

value := right.(*object.Integer).Value
Expand All @@ -190,8 +212,12 @@ func evalInfixExpression(
case operator == "!=":
// Using pointer comparison to check for equality between booleans.
return nativeBoolToBooleanObject(left != right)
case left.Type() != right.Type():
return newError("type mismatch: %s %s %s",
left.Type(), operator, right.Type())
default:
return NULL
return newError("unknown operator: %s %s %s",
left.Type(), operator, right.Type())
}
}

Expand Down Expand Up @@ -220,14 +246,18 @@ func evalIntegerInfixExpression(
case "!=":
return nativeBoolToBooleanObject(leftVal != rightVal)
default:
return NULL
return newError("unknown operator: %s %s %s",
left.Type(), operator, right.Type())
}
}

func evalIfExpression(ie *ast.IfExpression) object.Object {
// Deciding what to evaluate.

condition := Eval(ie.Condition)
if isError(condition) {
return condition
}

if isTruthy(condition) {
return Eval(ie.Consequence)
Expand All @@ -250,3 +280,19 @@ func isTruthy(obj object.Object) bool {
return true
}
}

func newError(format string, a ...interface{}) *object.Error {
// Helper function to help create new Error type.
// Error type wraps the formatted error messages.
//
// This function finds its use in every place where we didn't know what to
// do before and returned NULL instead.
return &object.Error{Message: fmt.Sprintf(format, a...)}
}

func isError(obj object.Object) bool {
if obj != nil {
return obj.Type() == object.ERROR_OBJ
}
return false
}
60 changes: 60 additions & 0 deletions evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,66 @@ if (10 > 1) {
}
}

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)
}
}
}

func testEval(input string) object.Object {
l := lexer.New(input)
p := parser.New(l)
Expand Down
18 changes: 18 additions & 0 deletions object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const (

// RETURN_VALUE_OBJ is the Return value object type.
RETURN_VALUE_OBJ = "RETURN_VALUE"

// ERROR_OBJ is the Error object type.
ERROR_OBJ = "ERROR"
)

// ObjectType represents the type of an object.
Expand Down Expand Up @@ -83,3 +86,18 @@ func (rv *ReturnValue) Type() ObjectType { return RETURN_VALUE_OBJ }

// Inspect returns a stringified version of the object for debugging.
func (rv *ReturnValue) Inspect() string { return rv.Value.Inspect() }

// Error is the error type and used to hold a message denoting the details of
// error encountered. This object is tracked through the evaluator and when
// encountered stops evaulation of the program or body of a function.
// 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.
type Error struct {
Message string
}

// Type returns the type of the object.
func (e *Error) Type() ObjectType { return ERROR_OBJ }

// Inspect returns a stringified version of the object for debugging.
func (e *Error) Inspect() string { return "ERROR:" + e.Message }

0 comments on commit fb7f562

Please sign in to comment.