package compiler import ( "fmt" "github/runnignwater/monkey/ast" "github/runnignwater/monkey/code" "github/runnignwater/monkey/lexer" "github/runnignwater/monkey/object" "github/runnignwater/monkey/parser" "testing" ) type compilerTestCase struct { input string expectedConstants []interface{} expectedInstructions []code.Instructions } 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 + 10)", expectedConstants: []interface{}{5, 2, 10}, 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.OpMul), 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), }, }, } runCompilerTests(t, tests) } func TestBooleanExpression(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: "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.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", expectedConstants: []interface{}{}, expectedInstructions: []code.Instructions{ code.Make(code.OpTrue), code.Make(code.OpBang), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestConditionals(t *testing.T) { tests := []compilerTestCase{ { input: "if (true) {10} ; 3333;", expectedConstants: []interface{}{10, 3333}, expectedInstructions: []code.Instructions{ // 0000 code.Make(code.OpTrue), // 0001 code.Make(code.OpJumpNotTruthy, 10), // 0004 code.Make(code.OpConstant, 0), // 0007 code.Make(code.OpJump, 11), // 0010 code.Make(code.OpNull), // 0011 code.Make(code.OpPop), // 0012 code.Make(code.OpConstant, 1), // 0015 code.Make(code.OpPop), }, }, { input: "if(true) {10} else {20}; 3333", expectedConstants: []interface{}{10, 20, 3333}, expectedInstructions: []code.Instructions{ // 0000 code.Make(code.OpTrue), // 0001 code.Make(code.OpJumpNotTruthy, 10), // 0004 code.Make(code.OpConstant, 0), // 0007 code.Make(code.OpJump, 13), // 0010 code.Make(code.OpConstant, 1), // 0013 code.Make(code.OpPop), // 0014 code.Make(code.OpConstant, 2), // 0017 code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestGlobalLetStatements(t *testing.T) { tests := []compilerTestCase{ { input: `let one = 1; let two = 2;`, expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetGlobal, 0), code.Make(code.OpConstant, 1), code.Make(code.OpSetGlobal, 1), }, }, { input: `let one = 1; one;`, expectedConstants: []interface{}{1}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetGlobal, 0), code.Make(code.OpGetGlobal, 0), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestLetStatement(t *testing.T) { tests := []compilerTestCase{ { input: `let num = 55; fn(){num};`, expectedConstants: []interface{}{ 55, []code.Instructions{ code.Make(code.OpGetGlobal, 0), code.Make(code.OpReturnValue), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetGlobal, 0), code.Make(code.OpConstant, 1), code.Make(code.OpPop), }, }, { input: ` fn () { let num = 55; num; } `, expectedConstants: []interface{}{ 55, []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetLocal, 0), code.Make(code.OpGetLocal, 0), code.Make(code.OpReturnValue), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 1), code.Make(code.OpPop), }, }, { input: ` fn () { let a = 55; let b = 77; a + b; } `, expectedConstants: []interface{}{ 55, 77, []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpSetLocal, 0), code.Make(code.OpConstant, 1), code.Make(code.OpSetLocal, 1), code.Make(code.OpGetLocal, 0), code.Make(code.OpGetLocal, 1), code.Make(code.OpAdd), code.Make(code.OpReturnValue), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 2), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestStringExpression(t *testing.T) { tests := []compilerTestCase{ { input: `"monkey"`, expectedConstants: []interface{}{"monkey"}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpPop), }, }, { input: `"monkey" + "language"`, expectedConstants: []interface{}{"monkey", "language"}, 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 TestArrayLiteral(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.OpIndex), 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.OpIndex), 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.OpReturnValue), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 2), code.Make(code.OpPop), }, }, { input: `fn(){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.OpReturnValue), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 2), code.Make(code.OpPop), }, }, { input: `fn(){1;2}`, expectedConstants: []interface{}{ 1, 2, []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpPop), code.Make(code.OpConstant, 1), code.Make(code.OpReturnValue), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 2), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestCompilerScopes(t *testing.T) { compiler := New() if compiler.scopeIndex != 0 { t.Errorf("scopeIndex wrong. got=%d, want=%d", compiler.scopeIndex, 0) } globalSymbolTable := compiler.symbolTable compiler.emit(code.OpMul) // enterScope ====================================================================================================== compiler.enterScope() if compiler.scopeIndex != 1 { t.Errorf("scopeIndex wrong. got=%d, want=%d", compiler.scopeIndex, 0) } compiler.emit(code.OpSub) if len(compiler.scopes[compiler.scopeIndex].instructions) != 1 { t.Errorf("instructions length wrong. got=%d, want = 1", len(compiler.scopes[compiler.scopeIndex].instructions)) } last := compiler.scopes[compiler.scopeIndex].lastInstruction if last.Opcode != code.OpSub { t.Errorf("lastInstruction.Opcde wrong. got=%d, want=%d", last.Opcode, code.OpSub) } if compiler.symbolTable.Outer != globalSymbolTable { t.Errorf("compiler did not enclose symboTable") } compiler.leaveScope() if compiler.scopeIndex != 0 { t.Errorf("scopeIndex wrong. got=%d, want=%d", compiler.scopeIndex, 0) } // leaveScope ====================================================================================================== if compiler.symbolTable != globalSymbolTable { t.Errorf("compiler did not restore global symbol table") } if compiler.symbolTable.Outer != nil { t.Errorf("compiler modified global symbol table incorrectly") } compiler.emit(code.OpAdd) if len(compiler.scopes[compiler.scopeIndex].instructions) != 2 { t.Errorf("instructions length 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.OpAdd) } previous := compiler.scopes[compiler.scopeIndex].previousInstruction if previous.Opcode != code.OpMul { t.Errorf("previousInstruction.Opcode wrong. got=%d, want=%d", previous.Opcode, code.OpMul) } } func TestFunctionWithoutReturnValue(t *testing.T) { tests := []compilerTestCase{ {input: `fn() {}`, expectedConstants: []interface{}{ []code.Instructions{code.Make(code.OpReturn)}, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpPop), }, }, } runCompilerTests(t, tests) } func TestFunctionCalls(t *testing.T) { tests := []compilerTestCase{ { input: `fn () { 24 } ()`, expectedConstants: []interface{}{ 24, []code.Instructions{ code.Make(code.OpConstant, 0), // The literal "24" code.Make(code.OpReturnValue), }, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 1), // The compiled function code.Make(code.OpCall), code.Make(code.OpPop), }, }, { input: `let noArg = fn(){24};noArg();`, expectedConstants: []interface{}{ 24, []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpReturnValue)}, }, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 1), // The compiled function code.Make(code.OpSetGlobal, 0), code.Make(code.OpGetGlobal, 0), code.Make(code.OpCall), 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 testConstants( t *testing.T, expected []interface{}, actual []object.Object, ) error { t.Helper() 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.CompileFunction) 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 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=%s, want=%q", result.Value, expected) } return nil } func testIntegerObject(expected int64, actual object.Object) error { 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 testInstructions( expected []code.Instructions, actual code.Instructions, ) error { concatted := concatInstructions(expected) if len(actual) != len(concatted) { return fmt.Errorf("wrong instructions length.\nwant=%q\n got=%q", concatted, actual) } for i, ins := range concatted { if actual[i] != ins { return fmt.Errorf("wrong instructions at %d.\nwant=%q\n got=%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 parse(input string) *ast.Program { l := lexer.New(input) p := parser.New(l) return p.ParseProgram() }