From 90d40977a0a73c63ab5a2f1c359511411b834f28 Mon Sep 17 00:00:00 2001 From: ryotaro oda Date: Sat, 7 Dec 2019 22:14:11 +0900 Subject: [PATCH] feat: eval --- evaluator/builtin.go | 106 +++++++ evaluator/evaluator.go | 391 +++++++++++++++++++++++++ evaluator/evaluator_test.go | 551 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1048 insertions(+) create mode 100644 evaluator/builtin.go create mode 100644 evaluator/evaluator.go create mode 100644 evaluator/evaluator_test.go diff --git a/evaluator/builtin.go b/evaluator/builtin.go new file mode 100644 index 0000000..2039381 --- /dev/null +++ b/evaluator/builtin.go @@ -0,0 +1,106 @@ +package evaluator + +import ( + "fmt" + + "github.com/0daryo/ody/object" +) + +var builtins = map[string]*object.Builtin{ + "len": &object.Builtin{ + Fn: func(args ...object.Object) object.Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", len(args)) + } + switch arg := args[0].(type) { + case *object.String: + return &object.Integer{Value: int64(len(arg.Value))} + case *object.Array: + return &object.Integer{Value: int64(len(arg.Elements))} + default: + return newError("argument to `len` not supported, got %s", + args[0].Type()) + } + }, + }, + "first": &object.Builtin{ + Fn: func(args ...object.Object) object.Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", len(args)) + } + if args[0].Type() != object.ARRAY_OBJ { + return newError("argument to `first` must be ARRAY, got %s", args[0].Type()) + } + + arr := args[0].(*object.Array) + if len(arr.Elements) > 0 { + return arr.Elements[0] + } + return NULL + }, + }, + "last": &object.Builtin{ + Fn: func(args ...object.Object) object.Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", len(args)) + } + if args[0].Type() != object.ARRAY_OBJ { + return newError("argument to `last` must be ARRAY, got %s", args[0].Type()) + } + + arr := args[0].(*object.Array) + length := len(arr.Elements) + if length > 0 { + return arr.Elements[length-1] + } + return NULL + }, + }, + "rest": &object.Builtin{ + Fn: func(args ...object.Object) object.Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", len(args)) + } + if args[0].Type() != object.ARRAY_OBJ { + return newError("argument to `rest` must be ARRAY, got %s", args[0].Type()) + } + + arr := args[0].(*object.Array) + length := len(arr.Elements) + if length > 0 { + newElements := make([]object.Object, length-1) + copy(newElements, arr.Elements[1:length]) + return &object.Array{Elements: newElements} + } + return NULL + }, + }, + "push": &object.Builtin{ + Fn: func(args ...object.Object) object.Object { + if len(args) != 2 { + return newError("wrong number of arguments. got=%d, want=2", len(args)) + } + if args[0].Type() != object.ARRAY_OBJ { + return newError("argument to `push` must be ARRAY, got %s", args[0].Type()) + } + + arr := args[0].(*object.Array) + length := len(arr.Elements) + if length > 0 { + newElements := make([]object.Object, length+1) + copy(newElements, arr.Elements) + newElements[length] = args[1] + return &object.Array{Elements: newElements} + } + return NULL + }, + }, + "puts": &object.Builtin{ + Fn: func(args ...object.Object) object.Object { + for _, arg := range args { + fmt.Println(arg.Inspect()) + } + return NULL + }, + }, +} diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go new file mode 100644 index 0000000..9ec18d5 --- /dev/null +++ b/evaluator/evaluator.go @@ -0,0 +1,391 @@ +package evaluator + +import ( + "fmt" + + "github.com/0daryo/ody/ast" + "github.com/0daryo/ody/object" +) + +var ( + NULL = &object.Null{} + TRUE = &object.Boolean{Value: true} + FALSE = &object.Boolean{Value: false} +) + +func isError(obj object.Object) bool { + if obj != nil { + return obj.Type() == object.ERROR_OBJ + } + return false +} + +func Eval(node ast.Node, env *object.Environment) object.Object { + switch node := node.(type) { + case *ast.HashLiteral: + return evalHashLiteral(node, env) + case *ast.FunctionLiteral: + params := node.Parameters + body := node.Body + return &object.Function{Parameters: params, Env: env, Body: body} + case *ast.Identifier: + return evalIdentifier(node, env) + case *ast.PrefixExpression: + right := Eval(node.Right, env) + if isError(right) { + return right + } + return evalPrefixExpression(node.Operator, right) + case *ast.CallExpression: + function := Eval(node.Function, env) + if isError(function) { + return function + } + args := evalExpressions(node.Arguments, env) + if len(args) == 1 && isError(args[0]) { + return args[0] + } + return applyFunction(function, args) + case *ast.Program: + return evalProgram(node, env) + case *ast.BlockStatement: + return evalBlockStatement(node, env) + case *ast.IfExpression: + return evalIfExpression(node, env) + case *ast.ExpressionStatement: + return Eval(node.Expression, env) + case *ast.IntegerLiteral: + return &object.Integer{Value: node.Value} + case *ast.Boolean: + return nativeBoolToBooleanObject(node.Value) + case *ast.ReturnStatement: + val := Eval(node.ReturnValue, env) + if isError(val) { + return val + } + return &object.ReturnValue{Value: val} + case *ast.InfixExpression: + left := Eval(node.Left, env) + if isError(left) { + return left + } + right := Eval(node.Right, env) + if isError(right) { + return right + } + return evalInfixExpression(node.Operator, left, right) + case *ast.LetStatement: + val := Eval(node.Value, env) + if isError(val) { + return val + } + env.Set(node.Name.Value, val) + case *ast.StringLiteral: + return &object.String{Value: node.Value} + case *ast.ArrayLiteral: + elements := evalExpressions(node.Elements, env) + if len(elements) == 1 && isError(elements[0]) { + return elements[0] + } + return &object.Array{Elements: elements} + case *ast.IndexExpression: + left := Eval(node.Left, env) + if isError(left) { + return left + } + index := Eval(node.Index, env) + if isError(index) { + return index + } + return evalIndexExpression(left, index) + } + return nil +} + +func evalHashLiteral( + node *ast.HashLiteral, + env *object.Environment, +) object.Object { + pairs := make(map[object.HashKey]object.HashPair) + + for keyNode, valueNode := range node.Pairs { + key := Eval(keyNode, env) + if isError(key) { + return key + } + + hashKey, ok := key.(object.Hashable) + if !ok { + return newError("unusable as hash key: %s", key.Type()) + } + value := Eval(valueNode, env) + if isError(value) { + return value + } + + hashed := hashKey.HashKey() + pairs[hashed] = object.HashPair{Key: key, Value: value} + } + return &object.Hash{Pairs: pairs} +} + +func evalIndexExpression(left, index object.Object) object.Object { + switch { + case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ: + return evalArrayIndexExpression(left, index) + case left.Type() == object.HASH_OBJ: + return evalHashIndexExpression(left, index) + default: + return newError("index operator not supprted: %s", left.Type()) + } +} + +func evalHashIndexExpression(hash, index object.Object) object.Object { + hashObject := hash.(*object.Hash) + + key, ok := index.(object.Hashable) + + if !ok { + return newError("unusable as hash key: %s", index.Type()) + } + + pair, ok := hashObject.Pairs[key.HashKey()] + if !ok { + return NULL + } + return pair.Value +} + +func evalArrayIndexExpression(array, index object.Object) object.Object { + arrayObject := array.(*object.Array) + idx := index.(*object.Integer).Value + max := int64(len(arrayObject.Elements) - 1) + if idx < 0 || idx > max { + return NULL + } + return arrayObject.Elements[idx] +} + +func applyFunction(fn object.Object, args []object.Object) object.Object { + switch fn := fn.(type) { + case *object.Function: + extededEnv := extendFunctionEnv(fn, args) + evaluated := Eval(fn.Body, extededEnv) + return unwrapReturnValue(evaluated) + case *object.Builtin: + return fn.Fn(args...) + + default: + return newError("not a function: %s", fn.Type()) + } +} + +func extendFunctionEnv(fn *object.Function, args []object.Object) *object.Environment { + env := object.NewEnclosedEnvironment(fn.Env) + for paramIdx, param := range fn.Parameters { + env.Set(param.Value, args[paramIdx]) + } + return env +} + +func unwrapReturnValue(obj object.Object) object.Object { + if returnValue, ok := obj.(*object.ReturnValue); ok { + return returnValue.Value + } + + return obj +} + +func evalExpressions(exps []ast.Expression, env *object.Environment) []object.Object { + var result []object.Object + + for _, e := range exps { + evaluated := Eval(e, env) + if isError(evaluated) { + return []object.Object{evaluated} + } + result = append(result, evaluated) + } + return result +} + +func evalIdentifier(node *ast.Identifier, env *object.Environment) object.Object { + val, ok := env.Get(node.Value) + if ok { + return val + } + + if builtin, ok := builtins[node.Value]; ok { + return builtin + } + return newError("identifier not found: " + node.Value) +} + +func evalBlockStatement(block *ast.BlockStatement, env *object.Environment) object.Object { + var result object.Object + + for _, statement := range block.Statements { + result = Eval(statement, env) + if result != nil { + rt := result.Type() + if rt == object.RETURN_VALUE_OBJECT || rt == object.ERROR_OBJ { + return result + } + } + } + return result +} + +func evalProgram(program *ast.Program, env *object.Environment) object.Object { + var result object.Object + for _, statement := range program.Statements { + result = Eval(statement, env) + switch result := result.(type) { + case *object.ReturnValue: + return result.Value + case *object.Error: + return result + } + } + return result +} + +func evalIfExpression(ie *ast.IfExpression, env *object.Environment) object.Object { + condition := Eval(ie.Condition, env) + if isError(condition) { + return condition + } + if isTruthy(condition) { + return Eval(ie.Consequence, env) + } else if ie.Alternative != nil { + return Eval(ie.Alternative, env) + } else { + return NULL + } +} + +func isTruthy(obj object.Object) bool { + switch obj { + case NULL: + return false + case TRUE: + return true + case FALSE: + return false + default: + return true + } +} + +func evalInfixExpression(operator string, left, right object.Object) object.Object { + switch { + case left.Type() == object.INTEGER_OBJ && right.Type() == object.INTEGER_OBJ: + return evalIntegerInfixExpression(operator, left, right) + case operator == "==": + return nativeBoolToBooleanObject(left == right) + case operator == "!=": + return nativeBoolToBooleanObject(left != right) + case left.Type() != right.Type(): + return newError("type mismatch: %s %s %s", left.Type(), operator, right.Type()) + case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ: + return evalStringInfixExpression(operator, left, right) + default: + return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type()) + } +} + +func evalStringInfixExpression( + operator string, + left, right object.Object, +) object.Object { + if operator != "+" { + return newError("unknown operator: %s %s %s", + left.Type(), operator, right.Type()) + } + leftVal := left.(*object.String).Value + rightVal := right.(*object.String).Value + return &object.String{Value: leftVal + rightVal} +} + +func evalIntegerInfixExpression( + operator string, + left, right object.Object, +) object.Object { + leftVal := left.(*object.Integer).Value + rightVal := right.(*object.Integer).Value + + switch operator { + case "+": + return &object.Integer{Value: leftVal + rightVal} + case "-": + return &object.Integer{Value: leftVal - rightVal} + case "*": + return &object.Integer{Value: leftVal * rightVal} + case "/": + return &object.Integer{Value: leftVal / rightVal} + case "<": + return nativeBoolToBooleanObject(leftVal < rightVal) + case ">": + return nativeBoolToBooleanObject(leftVal > rightVal) + case "==": + return nativeBoolToBooleanObject(leftVal == rightVal) + case "!=": + return nativeBoolToBooleanObject(leftVal != rightVal) + default: + return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type()) + } +} + +func evalPrefixExpression(operator string, right object.Object) object.Object { + switch operator { + case "!": + return evalBangOperatorExpression(right) + case "-": + return evalMinusPrefixOperatorExpression(right) + default: + return newError("unknown operator: %s%s", operator, right.Type()) + } +} + +func evalMinusPrefixOperatorExpression(right object.Object) object.Object { + if right.Type() != object.INTEGER_OBJ { + return newError("unknown operator: -%s", right.Type()) + } + value := right.(*object.Integer).Value + return &object.Integer{Value: -value} +} + +func evalBangOperatorExpression(right object.Object) object.Object { + switch right { + case TRUE: + return FALSE + case FALSE: + return TRUE + case NULL: + return TRUE + default: + return FALSE + } +} + +func evalStatements(stmts []ast.Statement, env *object.Environment) object.Object { + var result object.Object + for _, statement := range stmts { + result = Eval(statement, env) + if returnValue, ok := result.(*object.ReturnValue); ok { + return returnValue.Value + } + } + return result +} + +func nativeBoolToBooleanObject(input bool) *object.Boolean { + if input { + return TRUE + } + return FALSE +} + +func newError(format string, a ...interface{}) *object.Error { + return &object.Error{Message: fmt.Sprintf(format, a...)} +} diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go new file mode 100644 index 0000000..93f4052 --- /dev/null +++ b/evaluator/evaluator_test.go @@ -0,0 +1,551 @@ +package evaluator + +import ( + "github.com/0daryo/ody/lexer" + "github.com/0daryo/ody/object" + "github.com/0daryo/ody/parser" + "testing" +) + +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", + }, + { + "foobar", + "identifier not found: foobar", + }, + { + `"Hello" - "World"'`, + "unknown operator: STRING - STRING", + }, + { + `{"name": "Monkey"}[fn(x) { x }];`, + "unusable as hash key: FUNCTION", + }, + } + + 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 TestEvalIntegerExpression(t *testing.T) { + tests := []struct { + input string + expected int64 + }{ + {"5", 5}, + {"10", 10}, + {"-5", -5}, + {"-10", -10}, + {"5 + 5 + 5 + 5 - 10", 10}, + {"2 * 2 * 2 * 2 * 2", 32}, + {"-50 + 100 + -50", 0}, + {"5 * 2 + 10", 20}, + {"5 + 2 * 10", 25}, + {"20 + 2 * -10", 0}, + {"50 / 2 * 2 + 10", 60}, + {"2 * (5 + 10)", 30}, + {"3 * 3 * 3 + 10", 37}, + {"3 * (3 * 3) + 10", 37}, + {"(5 + 10 * 2 + 15 / 3) * 2 + -10", 50}, + } + + 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() + env := object.NewEnvironment() + return Eval(program, env) +} + +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 +} + +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}, + {"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}, + } + + 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 +} + +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) + } +} + +func TestIfElseExpressions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + {"if (true) { 10 }", 10}, + {"if (false) { 10 }", nil}, + {"if (1) { 10 }", 10}, + {"if (1 < 2) { 10 }", 10}, + {"if (1 > 2) { 10 }", nil}, + {"if (1 > 2) { 10 } else { 20 }", 20}, + {"if (1 < 2) { 10 } else { 20 }", 10}, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + integer, ok := tt.expected.(int) + if ok { + testIntegerObject(t, evaluated, int64(integer)) + } else { + testNullObject(t, evaluated) + } + } +} + +func testNullObject(t *testing.T, obj object.Object) bool { + if obj != NULL { + t.Errorf("object is not NULL. got=%T (%+v)", obj, obj) + return false + } + return true +} + +func TestReturnStatements(t *testing.T) { + tests := []struct { + input string + expected int64 + }{ + {"return 10;", 10}, + {"return 10; 9;", 10}, + {"return 2 * 5; 9;", 10}, + {"9; return 2 * 5; 9;", 10}, + {"if (10 > 1) { return 10; }", 10}, + { + ` +if (10 > 1) { + if (10 > 1) { + return 10; + } + + return 1; +} +`, + 10, + }, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + testIntegerObject(t, evaluated, tt.expected) + } +} + +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) + } +} + +func TestFunctionObject(t *testing.T) { + input := "fn(x) { x + 2; };" + + evaluated := testEval(input) + fn, ok := evaluated.(*object.Function) + if !ok { + t.Fatalf("object is not Function. got=%T (%+v)", evaluated, evaluated) + } + + if len(fn.Parameters) != 1 { + t.Fatalf("function has wrong parameters. Parameters=%+v", + fn.Parameters) + } + + if fn.Parameters[0].String() != "x" { + t.Fatalf("parameter is not 'x'. got=%q", fn.Parameters[0]) + } + + expectedBody := "(x + 2)" + + if fn.Body.String() != expectedBody { + t.Fatalf("body is not %q. got=%q", expectedBody, fn.Body.String()) + } +} + +func TestFunctionApplication(t *testing.T) { + tests := []struct { + input string + expected int64 + }{ + {"let identity = fn(x) { x; }; identity(5);", 5}, + {"let identity = fn(x) { return x; }; identity(5);", 5}, + {"let double = fn(x) { x * 2; }; double(5);", 10}, + {"let add = fn(x, y) { x + y; }; add(5, 5);", 10}, + {"let add = fn(x, y) { x + y; }; add(5 + 5, add(5, 5));", 20}, + {"fn(x) { x; }(5)", 5}, + } + + for _, tt := range tests { + testIntegerObject(t, testEval(tt.input), tt.expected) + } +} + +func TestStringLiteral(t *testing.T) { + input := `"Hello World!"` + + evaluated := testEval(input) + str, ok := evaluated.(*object.String) + if !ok { + t.Fatalf("object is not String. got=%T (%+v)", evaluated, evaluated) + } + + if str.Value != "Hello World!" { + t.Errorf("String has wrong value. got=%q", str.Value) + } +} + +func TestStringConcatenation(t *testing.T) { + input := `"Hello" + " " + "World!"` + + evaluated := testEval(input) + str, ok := evaluated.(*object.String) + if !ok { + t.Fatalf("object is not String. got=%T (%+v)", evaluated, evaluated) + } + + if str.Value != "Hello World!" { + t.Errorf("String has wrong value. got=%q", str.Value) + } +} + +func TestBuiltinFunctions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + {`len("")`, 0}, + {`len("four")`, 4}, + {`len("hello world")`, 11}, + {`len(1)`, "argument to `len` not supported, got INTEGER"}, + {`len("one", "two")`, "wrong number of arguments. got=2, want=1"}, + {`first([1, 2, 3])`, 1}, + {`first([])`, nil}, + {`first(1)`, "argument to `first` must be ARRAY, got INTEGER"}, + {`last([1, 2, 3])`, 3}, + {`last([])`, nil}, + {`last(1)`, "argument to `last` must be ARRAY, got INTEGER"}, + {`rest([1, 2, 3])`, []int{2, 3}}, + {`rest([])`, nil}, + {`push([], 1)`, []int{1}}, + {`push(1, 1)`, "argument to `push` must be ARRAY, got INTEGER"}, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + + switch expected := tt.expected.(type) { + case int: + testIntegerObject(t, evaluated, int64(expected)) + case string: + errObj, ok := evaluated.(*object.Error) + if !ok { + t.Errorf("object is not Error. got=%T (%+v)", + evaluated, evaluated) + continue + } + if errObj.Message != expected { + t.Errorf("wrong error message. expected=%q, got=%q", + expected, errObj.Message) + } + } + } +} + +func TestArrayLiterals(t *testing.T) { + input := "[1, 2 * 2, 3 + 3]" + + evaluated := testEval(input) + result, ok := evaluated.(*object.Array) + if !ok { + t.Fatalf("object is not Array. got=%T (%+v)", evaluated, evaluated) + } + + if len(result.Elements) != 3 { + t.Fatalf("array has wrong num of elements. got=%d", + len(result.Elements)) + } + + testIntegerObject(t, result.Elements[0], 1) + testIntegerObject(t, result.Elements[1], 4) + testIntegerObject(t, result.Elements[2], 6) +} + +func TestArrayIndexExpressions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + { + "[1, 2, 3][0]", + 1, + }, + { + "[1, 2, 3][1]", + 2, + }, + { + "[1, 2, 3][2]", + 3, + }, + { + "let i = 0; [1][i];", + 1, + }, + { + "[1, 2, 3][1 + 1];", + 3, + }, + { + "let myArray = [1, 2, 3]; myArray[2];", + 3, + }, + { + "let myArray = [1, 2, 3]; myArray[0] + myArray[1] + myArray[2];", + 6, + }, + { + "let myArray = [1, 2, 3]; let i = myArray[0]; myArray[i]", + 2, + }, + { + "[1, 2, 3][3]", + nil, + }, + { + "[1, 2, 3][-1]", + nil, + }, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + integer, ok := tt.expected.(int) + if ok { + testIntegerObject(t, evaluated, int64(integer)) + } else { + testNullObject(t, evaluated) + } + } +} + +func TestHashLiterals(t *testing.T) { + input := `let two = "two"; + { + "one": 10 - 9, + two: 1 + 1, + "thr" + "ee": 6 / 2, + 4: 4, + true: 5, + false: 6 + }` + + evaluated := testEval(input) + result, ok := evaluated.(*object.Hash) + if !ok { + t.Fatalf("Eval didn't return Hash. got=%T (%+v)", evaluated, evaluated) + } + + expected := map[object.HashKey]int64{ + (&object.String{Value: "one"}).HashKey(): 1, + (&object.String{Value: "two"}).HashKey(): 2, + (&object.String{Value: "three"}).HashKey(): 3, + (&object.Integer{Value: 4}).HashKey(): 4, + TRUE.HashKey(): 5, + FALSE.HashKey(): 6, + } + + if len(result.Pairs) != len(expected) { + t.Fatalf("Hash has wrong num of pairs. got=%d", len(result.Pairs)) + } + + for expectedKey, expectedValue := range expected { + pair, ok := result.Pairs[expectedKey] + if !ok { + t.Errorf("no pair for given key in Pairs") + } + + testIntegerObject(t, pair.Value, expectedValue) + } +} + +func TestHashIndexExpressions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + { + `{"foo": 5}["foo"]`, + 5, + }, + { + `{"foo": 5}["bar"]`, + nil, + }, + { + `let key = "foo"; {"foo": 5}[key]`, + 5, + }, + { + `{}["foo"]`, + nil, + }, + { + `{5: 5}[5]`, + 5, + }, + { + `{true: 5}[true]`, + 5, + }, + { + `{false: 5}[false]`, + 5, + }, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + integer, ok := tt.expected.(int) + if ok { + testIntegerObject(t, evaluated, int64(integer)) + } else { + testNullObject(t, evaluated) + } + } +}