2025-01-06 18:24:23 -08:00
|
|
|
package evaluator
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"code.jmug.me/jmug/interpreter-in-go/pkg/lexer"
|
|
|
|
|
"code.jmug.me/jmug/interpreter-in-go/pkg/object"
|
|
|
|
|
"code.jmug.me/jmug/interpreter-in-go/pkg/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(tt.input)
|
|
|
|
|
testIntegerObject(t, evaluated, tt.expected)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEvalBooleanExpression(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
input string
|
|
|
|
|
expected bool
|
|
|
|
|
}{
|
|
|
|
|
{"true", true},
|
|
|
|
|
{"false", false},
|
2025-01-07 18:15:12 -08:00
|
|
|
{"1 < 2", true},
|
|
|
|
|
{"1 > 2", false},
|
|
|
|
|
{"1 < 1", false},
|
|
|
|
|
{"1 > 1", false},
|
|
|
|
|
{"1 == 1", true},
|
|
|
|
|
{"1 != 1", false},
|
|
|
|
|
{"1 == 2", false},
|
|
|
|
|
{"1 != 2", true},
|
2025-01-06 18:24:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
evaluated := testEval(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},
|
2025-01-07 18:15:12 -08:00
|
|
|
{"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},
|
2025-01-06 18:24:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
evaluated := testEval(tt.input)
|
|
|
|
|
testBooleanObject(t, evaluated, tt.expected)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-07 18:15:12 -08:00
|
|
|
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 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) {
|
|
|
|
|
if (10 > 1) {
|
|
|
|
|
return 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
10,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
evaluated := testEval(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",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"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",
|
|
|
|
|
},
|
2025-01-08 19:41:36 -08:00
|
|
|
{
|
|
|
|
|
"foobar",
|
|
|
|
|
"identifier not found: foobar",
|
|
|
|
|
},
|
2025-01-10 17:48:54 -08:00
|
|
|
{
|
|
|
|
|
`"Hello" - "World"`,
|
|
|
|
|
"unknown operator: STRING - STRING",
|
|
|
|
|
},
|
2025-01-07 18:15:12 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-08 19:41:36 -08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-09 16:33:19 -08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-10 17:48:54 -08:00
|
|
|
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 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"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-07 18:15:12 -08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-06 18:24:23 -08:00
|
|
|
func testEval(input string) object.Object {
|
|
|
|
|
l := lexer.New(input)
|
|
|
|
|
p := parser.New(l)
|
|
|
|
|
program := p.ParseProgram()
|
|
|
|
|
|
2025-01-08 19:41:36 -08:00
|
|
|
return Eval(program, object.NewEnvironment())
|
2025-01-06 18:24:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 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
|
|
|
|
|
}
|