package evaluator import ( "testing" "monkey/lexer" "monkey/object" "monkey/parser" ) 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(t, tt.input) testIntegerObject(t, evaluated, tt.expected) } } 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}, {`"A" == "A"`, true}, {`"A" != "A"`, false}, {`"A" == "B"`, false}, {`"A" != "B"`, true}, } for _, tt := range tests { evaluated := testEval(t, tt.input) testBooleanObject(t, evaluated, tt.expected) } } 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(t, tt.input) testBooleanObject(t, evaluated, tt.expected) } } func TestIfElseExpressions(t *testing.T) { tests := []struct { input string expected any }{ {"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(t, tt.input) integer, ok := tt.expected.(int) if ok { testIntegerObject(t, evaluated, int64(integer)) } else { testNullObject(t, evaluated) } } } 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, }, { ` let f = fn(x) { return x; x + 10; }; f(10);`, 10, }, { ` let f = fn(x) { let result = x + 10; return result; return 10; }; f(10);`, 20, }, } for _, tt := range tests { evaluated := testEval(t, tt.input) testIntegerObject(t, evaluated, tt.expected) } } 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", }, { "true + false + true + false;", "unknown operator: BOOLEAN + BOOLEAN", }, { "5; true + false; 5", "unknown operator: BOOLEAN + BOOLEAN", }, { `"Hello" - "World"`, "unknown operator: STRING - STRING", }, { "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", }, { `{"name": "Monkey"}[fn(x) { x }];`, "unusable as hash key: FUNCTION", }, { `999[1]`, "index operator not supported: INTEGER", }, } for _, tt := range tests { evaluated := testEval(t, 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 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(t, tt.input), tt.expected) } } func TestFunctionObject(t *testing.T) { input := "fn(x) { x + 2; };" evaluated := testEval(t, 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(t, tt.input), tt.expected) } } func TestEnclosingEnvironments(t *testing.T) { input := ` let first = 10; let second = 10; let third = 10; let ourFunction = fn(first) { let second = 20; first + second + third; }; ourFunction(20) + first + second;` testIntegerObject(t, testEval(t, input), 70) } func TestClosures(t *testing.T) { input := ` let newAdder = fn(x) { fn(y) { x + y }; }; let addTwo = newAdder(2); addTwo(2);` testIntegerObject(t, testEval(t, input), 4) } func TestStringLiteral(t *testing.T) { input := `"Hello World!"` evaluated := testEval(t, 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(t, 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 any }{ {`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"}, {`len([1, 2, 3])`, 3}, {`len([])`, 0}, {`puts("hello", "world!")`, nil}, {`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(t, tt.input) switch expected := tt.expected.(type) { case int: testIntegerObject(t, evaluated, int64(expected)) case nil: testNullObject(t, evaluated) 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) } case []int: array, ok := evaluated.(*object.Array) if !ok { t.Errorf("obj not Array. got=%T (%+v)", evaluated, evaluated) continue } if len(array.Elements) != len(expected) { t.Errorf("wrong num of elements. want=%d, got=%d", len(expected), len(array.Elements)) continue } for i, expectedElem := range expected { testIntegerObject(t, array.Elements[i], int64(expectedElem)) } } } } func TestArrayLiterals(t *testing.T) { input := "[1, 2 * 2, 3 + 3]" evaluated := testEval(t, 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 any }{ { "[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(t, 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(t, 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 any }{ { `{"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(t, tt.input) integer, ok := tt.expected.(int) if ok { testIntegerObject(t, evaluated, int64(integer)) } else { testNullObject(t, evaluated) } } } func testEval(t *testing.T, input string) object.Object { t.Helper() 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 { t.Helper() 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 testBooleanObject(t *testing.T, obj object.Object, expected bool) bool { t.Helper() 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 testNullObject(t *testing.T, obj object.Object) bool { t.Helper() if obj != NULL { t.Errorf("object is not NULL. got=%T (%+v)", obj, obj) return false } return true }