package compiler import ( "fmt" "github.com/stretchr/testify/assert" "monkey/ast" "monkey/code" "monkey/lexer" "monkey/object" "monkey/parser" "testing" ) type Instructions string type compilerTestCase struct { input string expectedConstants []interface{} expectedInstructions []code.Instructions } type compilerTestCase2 struct { input string constants []interface{} instructions string } func TestIntegerArithmetic(t *testing.T) { tests := []compilerTestCase{ { input: "1 + 2", expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpAdd), code.Make(code.OpPop), }, }, { input: "1; 2", expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpPop), code.Make(code.OpConstant, 1), code.Make(code.OpPop), }, }, { input: "1 - 2", expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpSub), code.Make(code.OpPop), }, }, { input: "1 * 2", expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpMul), code.Make(code.OpPop), }, }, { input: "2 / 1", expectedConstants: []interface{}{2, 1}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpDiv), code.Make(code.OpPop), }, }, { input: "5 % 2", expectedConstants: []interface{}{5, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpMod), code.Make(code.OpPop), }, }, { input: "1 << 2", expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpLeftShift), code.Make(code.OpPop), }, }, { input: "4 >> 2", expectedConstants: []interface{}{4, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpRightShift), code.Make(code.OpPop), }, }, { input: "-1", expectedConstants: []interface{}{1}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpMinus), code.Make(code.OpPop), }, }, { input: "5 | 2", expectedConstants: []interface{}{5, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpBitwiseOR), code.Make(code.OpPop), }, }, { input: "5 ^ 2", expectedConstants: []interface{}{5, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpBitwiseXOR), code.Make(code.OpPop), }, }, { input: "5 & 2", expectedConstants: []interface{}{5, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpBitwiseAND), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestBooleanExpressions(t *testing.T) { tests := []compilerTestCase{ { input: "true", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpTrue), code.Make(code.OpPop), }, }, { input: "false", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpFalse), code.Make(code.OpPop), }, }, { input: "null", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpNull), code.Make(code.OpPop), }, }, { input: "1 > 2", expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpGreaterThan), code.Make(code.OpPop), }, }, { input: "1 < 2", expectedConstants: []interface{}{2, 1}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpGreaterThan), code.Make(code.OpPop), }, }, { input: "1 >= 2", expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpGreaterThanEqual), code.Make(code.OpPop), }, }, { input: "1 <= 2", expectedConstants: []interface{}{2, 1}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpGreaterThanEqual), code.Make(code.OpPop), }, }, { input: "1 == 2", expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpEqual), code.Make(code.OpPop), }, }, { input: "1 != 2", expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpNotEqual), code.Make(code.OpPop), }, }, { input: "true == false", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpTrue), code.Make(code.OpFalse), code.Make(code.OpEqual), code.Make(code.OpPop), }, }, { input: "true != false", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpTrue), code.Make(code.OpFalse), code.Make(code.OpNotEqual), code.Make(code.OpPop), }, }, { input: "true && false", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpTrue), code.Make(code.OpFalse), code.Make(code.OpAnd), code.Make(code.OpPop), }, }, { input: "true || false", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpTrue), code.Make(code.OpFalse), code.Make(code.OpOr), code.Make(code.OpPop), }, }, { input: "!true", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpTrue), code.Make(code.OpNot), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestConditionals(t *testing.T) { tests := []compilerTestCase2{ { input: ` if (true) { 10 }; 3333; `, constants: []interface{}{10, 3333}, instructions: "0000 OpTrue\n0001 OpJumpNotTruthy 10\n0004 OpConstant 0\n0007 OpJump 11\n0010 OpNull\n0011 OpPop\n0012 OpConstant 1\n0015 OpPop\n", }, { input: ` if (true) { 10 } else { 20 }; 3333; `, constants: []interface{}{10, 20, 3333}, instructions: "0000 OpTrue\n0001 OpJumpNotTruthy 10\n0004 OpConstant 0\n0007 OpJump 13\n0010 OpConstant 1\n0013 OpPop\n0014 OpConstant 2\n0017 OpPop\n", }, { input: ` x := 0; if (true) { x = 1; }; if (false) { x = 2; } `, constants: []interface{}{0, 1, 2}, instructions: "0000 OpConstant 0\n0003 OpSetGlobal 0\n0006 OpPop\n0007 OpTrue\n0008 OpJumpNotTruthy 20\n0011 OpConstant 1\n0014 OpAssignGlobal 0\n0017 OpJump 21\n0020 OpNull\n0021 OpPop\n0022 OpFalse\n0023 OpJumpNotTruthy 35\n0026 OpConstant 2\n0029 OpAssignGlobal 0\n0032 OpJump 36\n0035 OpNull\n0036 OpPop\n", }, } runCompilerTests2(t, tests) } func TestGlobalBindExpressions(t *testing.T) { tests := []compilerTestCase2{ { input: ` one := 1; two := 2; `, constants: []interface{}{1, 2}, instructions: "0000 OpConstant 0\n0003 OpSetGlobal 0\n0006 OpPop\n0007 OpConstant 1\n0010 OpSetGlobal 1\n0013 OpPop\n", }, { input: ` one := 1; one; `, constants: []interface{}{1}, instructions: "0000 OpConstant 0\n0003 OpSetGlobal 0\n0006 OpPop\n0007 OpGetGlobal 0\n0010 OpPop\n", }, { input: ` one := 1; two := one; two; `, constants: []interface{}{1}, instructions: "0000 OpConstant 0\n0003 OpSetGlobal 0\n0006 OpPop\n0007 OpGetGlobal 0\n0010 OpSetGlobal 1\n0013 OpPop\n0014 OpGetGlobal 1\n0017 OpPop\n", }, } runCompilerTests2(t, tests) } func TestStringExpressions(t *testing.T) { tests := []compilerTestCase{ { input: `"monkey"`, expectedConstants: []interface{}{"monkey"}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpPop), }, }, { input: `"mon" + "key"`, expectedConstants: []interface{}{"mon", "key"}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpAdd), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestArrayLiterals(t *testing.T) { tests := []compilerTestCase{ { input: "[]", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpArray, 0), code.Make(code.OpPop), }, }, { input: "[1, 2, 3]", expectedConstants: []interface{}{1, 2, 3}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpConstant, 2), code.Make(code.OpArray, 3), code.Make(code.OpPop), }, }, { input: "[1 + 2, 3 - 4, 5 * 6]", expectedConstants: []interface{}{1, 2, 3, 4, 5, 6}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpAdd), code.Make(code.OpConstant, 2), code.Make(code.OpConstant, 3), code.Make(code.OpSub), code.Make(code.OpConstant, 4), code.Make(code.OpConstant, 5), code.Make(code.OpMul), code.Make(code.OpArray, 3), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestHashLiterals(t *testing.T) { tests := []compilerTestCase{ { input: "{}", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpHash, 0), code.Make(code.OpPop), }, }, { input: "{1: 2, 3: 4, 5: 6}", expectedConstants: []interface{}{1, 2, 3, 4, 5, 6}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpConstant, 2), code.Make(code.OpConstant, 3), code.Make(code.OpConstant, 4), code.Make(code.OpConstant, 5), code.Make(code.OpHash, 6), code.Make(code.OpPop), }, }, { input: "{1: 2 + 3, 4: 5 * 6}", expectedConstants: []interface{}{1, 2, 3, 4, 5, 6}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpConstant, 2), code.Make(code.OpAdd), code.Make(code.OpConstant, 3), code.Make(code.OpConstant, 4), code.Make(code.OpConstant, 5), code.Make(code.OpMul), code.Make(code.OpHash, 4), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestIndexExpressions(t *testing.T) { tests := []compilerTestCase{ { input: "[1, 2, 3][1 + 1]", expectedConstants: []interface{}{1, 2, 3, 1, 1}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpConstant, 2), code.Make(code.OpArray, 3), code.Make(code.OpConstant, 3), code.Make(code.OpConstant, 4), code.Make(code.OpAdd), code.Make(code.OpGetItem), code.Make(code.OpPop), }, }, { input: "{1: 2}[2 - 1]", expectedConstants: []interface{}{1, 2, 2, 1}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpHash, 2), code.Make(code.OpConstant, 2), code.Make(code.OpConstant, 3), code.Make(code.OpSub), code.Make(code.OpGetItem), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestFunctions(t *testing.T) { tests := []compilerTestCase{ { input: `fn() { return 5 + 10 }`, expectedConstants: []interface{}{ 5, 10, []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpAdd), code.Make(code.OpReturn), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpClosure, 2, 0), code.Make(code.OpPop), }, }, { input: `fn() { 1; return 2 }`, expectedConstants: []interface{}{ 1, 2, []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpPop), code.Make(code.OpConstant, 1), code.Make(code.OpReturn), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpClosure, 2, 0), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestFunctionsWithoutReturn(t *testing.T) { tests := []compilerTestCase{ { input: `fn() { }`, expectedConstants: []interface{}{ []code.Instructions{ code.Make(code.OpNull), code.Make(code.OpReturn), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpClosure, 0, 0), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestClosures(t *testing.T) { tests := []compilerTestCase2{ { input: ` fn(a) { return fn(b) { return a + b } } `, constants: []interface{}{ Instructions("0000 OpGetFree 0\n0002 OpGetLocal 0\n0004 OpAdd\n0005 OpReturn\n"), Instructions("0000 OpGetLocal 0\n0002 OpClosure 0 1\n0006 OpReturn\n"), }, instructions: "0000 OpClosure 1 0\n0004 OpPop\n", }, { input: ` fn(a) { return fn(b) { return fn(c) { return a + b + c } } }; `, constants: []interface{}{ Instructions("0000 OpGetFree 0\n0002 OpGetFree 1\n0004 OpAdd\n0005 OpGetLocal 0\n0007 OpAdd\n0008 OpReturn\n"), Instructions("0000 OpGetFree 0\n0002 OpGetLocal 0\n0004 OpClosure 0 2\n0008 OpReturn\n"), Instructions("0000 OpGetLocal 0\n0002 OpClosure 1 1\n0006 OpReturn\n"), }, instructions: "0000 OpClosure 2 0\n0004 OpPop\n", }, { input: ` global := 55; fn() { a := 66; return fn() { b := 77; return fn() { c := 88; return global + a + b + c; } } } `, constants: []interface{}{ 55, 66, 77, 88, Instructions("0000 OpConstant 3\n0003 OpSetLocal 0\n0005 OpPop\n0006 OpGetGlobal 0\n0009 OpGetFree 0\n0011 OpAdd\n0012 OpGetFree 1\n0014 OpAdd\n0015 OpGetLocal 0\n0017 OpAdd\n0018 OpReturn\n"), Instructions("0000 OpConstant 2\n0003 OpSetLocal 0\n0005 OpPop\n0006 OpGetFree 0\n0008 OpGetLocal 0\n0010 OpClosure 4 2\n0014 OpReturn\n"), Instructions("0000 OpConstant 1\n0003 OpSetLocal 0\n0005 OpPop\n0006 OpGetLocal 0\n0008 OpClosure 5 1\n0012 OpReturn\n"), }, instructions: "0000 OpConstant 0\n0003 OpSetGlobal 0\n0006 OpPop\n0007 OpClosure 6 0\n0011 OpPop\n", }, } runCompilerTests2(t, tests) } func TestCompilerScopes(t *testing.T) { compiler := New() if compiler.scopeIndex != 0 { t.Errorf("scopeIndex wrong. got=%d, want=%d", compiler.scopeIndex, 0) } compiler.emit(code.OpMul) compiler.enterScope() if compiler.scopeIndex != 1 { t.Errorf("scopeIndex wrong. got=%d, want=%d", compiler.scopeIndex, 1) } compiler.emit(code.OpSub) if len(compiler.scopes[compiler.scopeIndex].instructions) != 1 { t.Errorf("instructions length is wrong. got=%d", len(compiler.scopes[compiler.scopeIndex].instructions)) } last := compiler.scopes[compiler.scopeIndex].lastInstruction if last.Opcode != code.OpSub { t.Errorf("lastInstruction.OpCode wrong. got=%d, want=%d", last.Opcode, code.OpSub) } compiler.leaveScope() if compiler.scopeIndex != 0 { t.Errorf("scopeIndex wrong. got=%d, want=%d", compiler.scopeIndex, 0) } compiler.emit(code.OpAdd) if len(compiler.scopes[compiler.scopeIndex].instructions) != 2 { t.Errorf("instructions length is wrong. got=%d", len(compiler.scopes[compiler.scopeIndex].instructions)) } last = compiler.scopes[compiler.scopeIndex].lastInstruction if last.Opcode != code.OpAdd { t.Errorf("lastInstruction.OpCode wrong. got=%d, want=%d", last.Opcode, code.OpSub) } previous := compiler.scopes[compiler.scopeIndex].previousInstruction if previous.Opcode != code.OpMul { t.Errorf("previousInstruction.OpCode wrong. got=%d, want=%d", last.Opcode, code.OpMul) } } func TestFunctionCalls(t *testing.T) { tests := []compilerTestCase2{ { input: `fn() { return 24 }();`, constants: []interface{}{ 24, Instructions("0000 OpConstant 0\n0003 OpReturn\n"), }, instructions: "0000 OpClosure 1 0\n0004 OpCall 0\n0006 OpPop\n", }, { input: ` noArg := fn() { return 24 }; noArg(); `, constants: []interface{}{ 24, Instructions("0000 OpConstant 0\n0003 OpReturn\n"), }, instructions: "0000 OpClosure 1 0\n0004 OpSetGlobal 0\n0007 OpPop\n0008 OpGetGlobal 0\n0011 OpCall 0\n0013 OpPop\n", }, { input: ` oneArg := fn(a) { return a }; oneArg(24); `, constants: []interface{}{ Instructions("0000 OpGetLocal 0\n0002 OpReturn\n"), 24, }, instructions: "0000 OpClosure 0 0\n0004 OpSetGlobal 0\n0007 OpPop\n0008 OpGetGlobal 0\n0011 OpConstant 1\n0014 OpCall 1\n0016 OpPop\n", }, { input: ` manyArg := fn(a, b, c) { a; b; return c }; manyArg(24, 25, 26); `, constants: []interface{}{ Instructions("0000 OpGetLocal 0\n0002 OpPop\n0003 OpGetLocal 1\n0005 OpPop\n0006 OpGetLocal 2\n0008 OpReturn\n"), 24, 25, 26, }, instructions: "0000 OpClosure 0 0\n0004 OpSetGlobal 0\n0007 OpPop\n0008 OpGetGlobal 0\n0011 OpConstant 1\n0014 OpConstant 2\n0017 OpConstant 3\n0020 OpCall 3\n0022 OpPop\n", }, } runCompilerTests2(t, tests) } func TestAssignmentExpressions(t *testing.T) { tests := []compilerTestCase2{ { input: ` x := 1 x = 2 `, constants: []interface{}{1, 2}, instructions: "0000 OpConstant 0\n0003 OpSetGlobal 0\n0006 OpPop\n0007 OpConstant 1\n0010 OpAssignGlobal 0\n0013 OpPop\n", }, } runCompilerTests2(t, tests) } func TestAssignmentStatementScopes(t *testing.T) { tests := []compilerTestCase{ { input: ` num := 0; fn() { num = 55; } `, expectedConstants: []interface{}{ 0, 55, []code.Instructions{ code.Make(code.OpConstant, 1), code.Make(code.OpAssignGlobal, 0), code.Make(code.OpNull), code.Make(code.OpReturn), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetGlobal, 0), code.Make(code.OpPop), code.Make(code.OpClosure, 2, 0), code.Make(code.OpPop), }, }, { input: ` fn() { num := 0; num = 55; } `, expectedConstants: []interface{}{ 0, 55, []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetLocal, 0), code.Make(code.OpPop), code.Make(code.OpConstant, 1), code.Make(code.OpAssignLocal, 0), code.Make(code.OpNull), code.Make(code.OpReturn), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpClosure, 2, 0), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestLetStatementScopes(t *testing.T) { tests := []compilerTestCase{ { input: ` num := 55; fn() { return num } `, expectedConstants: []interface{}{ 55, []code.Instructions{ code.Make(code.OpGetGlobal, 0), code.Make(code.OpReturn), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetGlobal, 0), code.Make(code.OpPop), code.Make(code.OpClosure, 1, 0), code.Make(code.OpPop), }, }, { input: ` fn() { num := 55; return num } `, expectedConstants: []interface{}{ 55, []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetLocal, 0), code.Make(code.OpPop), code.Make(code.OpGetLocal, 0), code.Make(code.OpReturn), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpClosure, 1, 0), code.Make(code.OpPop), }, }, { input: ` fn() { a := 55; b := 77; return a + b } `, expectedConstants: []interface{}{ 55, 77, []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetLocal, 0), code.Make(code.OpPop), code.Make(code.OpConstant, 1), code.Make(code.OpSetLocal, 1), code.Make(code.OpPop), code.Make(code.OpGetLocal, 0), code.Make(code.OpGetLocal, 1), code.Make(code.OpAdd), code.Make(code.OpReturn), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpClosure, 2, 0), code.Make(code.OpPop), }, }, { input: ` a := 0; a := a + 1; `, expectedConstants: []interface{}{ 0, 1, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetGlobal, 0), code.Make(code.OpPop), code.Make(code.OpGetGlobal, 0), code.Make(code.OpConstant, 1), code.Make(code.OpAdd), code.Make(code.OpSetGlobal, 0), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestBuiltins(t *testing.T) { tests := []compilerTestCase{ { input: ` len([]); push([], 1); `, expectedConstants: []interface{}{1}, expectedInstructions: []code.Instructions{ code.Make(code.OpGetBuiltin, 18), code.Make(code.OpArray, 0), code.Make(code.OpCall, 1), code.Make(code.OpPop), code.Make(code.OpGetBuiltin, 25), code.Make(code.OpArray, 0), code.Make(code.OpConstant, 0), code.Make(code.OpCall, 2), code.Make(code.OpPop), }, }, { input: `fn() { return len([]) }`, expectedConstants: []interface{}{ []code.Instructions{ code.Make(code.OpGetBuiltin, 18), code.Make(code.OpArray, 0), code.Make(code.OpCall, 1), code.Make(code.OpReturn), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpClosure, 0, 0), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestRecursiveFunctions(t *testing.T) { tests := []compilerTestCase{ { input: ` countDown := fn(x) { return countDown(x - 1); }; countDown(1); `, expectedConstants: []interface{}{ 1, []code.Instructions{ code.Make(code.OpCurrentClosure), code.Make(code.OpGetLocal, 0), code.Make(code.OpConstant, 0), code.Make(code.OpSub), code.Make(code.OpCall, 1), code.Make(code.OpReturn), }, 1, }, expectedInstructions: []code.Instructions{ code.Make(code.OpClosure, 1, 0), code.Make(code.OpSetGlobal, 0), code.Make(code.OpPop), code.Make(code.OpGetGlobal, 0), code.Make(code.OpConstant, 2), code.Make(code.OpCall, 1), code.Make(code.OpPop), }, }, { input: ` wrapper := fn() { countDown := fn(x) { return countDown(x - 1); }; return countDown(1); }; wrapper(); `, expectedConstants: []interface{}{ 1, []code.Instructions{ code.Make(code.OpCurrentClosure), code.Make(code.OpGetLocal, 0), code.Make(code.OpConstant, 0), code.Make(code.OpSub), code.Make(code.OpCall, 1), code.Make(code.OpReturn), }, 1, []code.Instructions{ code.Make(code.OpClosure, 1, 0), code.Make(code.OpSetLocal, 0), code.Make(code.OpPop), code.Make(code.OpGetLocal, 0), code.Make(code.OpConstant, 2), code.Make(code.OpCall, 1), code.Make(code.OpReturn), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpClosure, 3, 0), code.Make(code.OpSetGlobal, 0), code.Make(code.OpPop), code.Make(code.OpGetGlobal, 0), code.Make(code.OpCall, 0), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestIteration(t *testing.T) { tests := []compilerTestCase{ { input: ` while (true) { 10 }; `, expectedConstants: []interface{}{10}, expectedInstructions: []code.Instructions{ // 0000 code.Make(code.OpTrue), // 0001 code.Make(code.OpJumpNotTruthy, 11), // 0004 code.Make(code.OpConstant, 0), // 0007 code.Make(code.OpPop), // 0008 code.Make(code.OpJump, 0), // 0011 code.Make(code.OpNull), // 0012 code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func runCompilerTests(t *testing.T, tests []compilerTestCase) { t.Helper() for _, tt := range tests { program := parse(tt.input) compiler := New() err := compiler.Compile(program) if err != nil { t.Fatalf("compiler error: %s", err) } bytecode := compiler.Bytecode() err = testInstructions(tt.expectedInstructions, bytecode.Instructions) if err != nil { t.Fatalf("testInstructions failed: %s", err) } err = testConstants(t, tt.expectedConstants, bytecode.Constants) if err != nil { t.Fatalf("testConstants failed: %s", err) } } } func runCompilerTests2(t *testing.T, tests []compilerTestCase2) { t.Helper() assert := assert.New(t) for _, tt := range tests { program := parse(tt.input) compiler := New() err := compiler.Compile(program) assert.NoError(err) bytecode := compiler.Bytecode() assert.Equal(tt.instructions, bytecode.Instructions.String()) testConstants2(t, tt.constants, bytecode.Constants) } } func parse(input string) *ast.Program { l := lexer.New(input) p := parser.New(l) return p.ParseProgram() } func testInstructions(expected []code.Instructions, actual code.Instructions) error { concatted := concatInstructions(expected) if len(actual) != len(concatted) { return fmt.Errorf("wrong instructions length.\nwant=%q\ngot =%q", concatted, actual) } for i, ins := range concatted { if actual[i] != ins { return fmt.Errorf("wrong instruction at %d.\nwant=%q\ngot =%q", i, concatted, actual) } } return nil } func concatInstructions(s []code.Instructions) code.Instructions { out := code.Instructions{} for _, ins := range s { out = append(out, ins...) } return out } func testConstants(t *testing.T, expected []interface{}, actual []object.Object) error { if len(expected) != len(actual) { return fmt.Errorf("wrong number of constants. got=%d, want=%d", len(actual), len(expected)) } for i, constant := range expected { switch constant := constant.(type) { case int: err := testIntegerObject(int64(constant), actual[i]) if err != nil { return fmt.Errorf("constant %d = testIntegerObject failed : %s", i, err) } case string: err := testStringObject(constant, actual[i]) if err != nil { return fmt.Errorf("constant %d = testStringObject failed : %s", i, err) } case []code.Instructions: fn, ok := actual[i].(*object.CompiledFunction) if !ok { return fmt.Errorf("constant %d - not a function: %T", i, actual[i]) } err := testInstructions(constant, fn.Instructions) if err != nil { return fmt.Errorf("constant %d = testInstructions failed: %s", i, err) } } } return nil } func testConstants2(t *testing.T, expected []interface{}, actual []object.Object) { assert := assert.New(t) assert.Equal(len(expected), len(actual)) for i, constant := range expected { switch constant := constant.(type) { case Instructions: assert.Equal( string(constant), actual[i].(*object.CompiledFunction).Instructions.String(), ) case string: assert.Equal(constant, actual[i].(*object.String).Value) case int: assert.Equal(int64(constant), actual[i].(*object.Integer).Value) } } } func testIntegerObject(expected int64, actual object.Object) interface{} { result, ok := actual.(*object.Integer) if !ok { return fmt.Errorf("object is not Integer. got=%T (%+v", actual, actual) } if result.Value != expected { return fmt.Errorf("object has wrong value. got=%d, want=%d", result.Value, expected) } return nil } func testStringObject(expected string, actual object.Object) error { result, ok := actual.(*object.String) if !ok { return fmt.Errorf("object is not String. got %T (%+v", actual, actual) } if result.Value != expected { return fmt.Errorf("object has wrong value. got=%q, want=%q", result.Value, expected) } return nil }