package core import ( "fmt" "math/big" "testing" ) func CompareChunks(t *testing.T, got *Chunk, want *Chunk) { if len(got.Constants) != len(want.Constants) { t.Errorf("constant count does not match; got %v, expected %v", len(got.Constants), len(want.Constants)) } for i, v := range got.Constants { if i < len(want.Constants) && !v.Equals(want.Constants[i]) { t.Errorf("constant %d does not match (%s and %s)", i, v.String(), want.Constants[i].String()) } } if len(got.Bytecode) != len(want.Bytecode) { t.Errorf("bytecode size does not match; got %v, expected %v", len(got.Bytecode), len(want.Bytecode)) } t.Log("instruction \t\tchunk got \t\tchunk want") i := 0 for ; i < len(got.Bytecode); i++ { v := got.Bytecode[i] if i < len(want.Bytecode) { if v != want.Bytecode[i] { t.Errorf("i=%d mismatch \t%d (%s) \t\t%d (%s)", i, v, v.String(), want.Bytecode[i], want.Bytecode[i].String()) } else { t.Logf("i=%d match \t%d (%s) \t\t%d (%s)", i, v, v.String(), want.Bytecode[i], want.Bytecode[i].String()) } } else { t.Errorf("i=%d mismatch \t%d (%s) \t\t- (None)", i, v, v.String()) } } // if want bytecode is greater than got bytecode for ; i < len(want.Bytecode); i++ { v := want.Bytecode[i] t.Errorf("i=%d mismatch \t- (None) \t\t%d (%s)", i, v, v.String()) } } func TestNewVM(t *testing.T) { // constants chunk := NewChunk([]Bytecode{ InstructionConstant, 0, }, []Value{ &FloatValue{0}, }) stackSize := Pos(256) callstackSize := Pos(256) vm := NewVM(chunk, stackSize, callstackSize) // should start at first instruction if vm.ip != 0 { t.Errorf("vm.ip = %d, want 0", vm.ip) } // should have given instructions for i, v := range chunk.Bytecode { if v != vm.chunk.Bytecode[i] { t.Errorf("vm.Bytecode[%d] = %d, want %d", i, vm.chunk.Bytecode[i], v) } } // should have given constants for i, v := range chunk.Constants { if v != vm.chunk.Constants[i] { t.Errorf("vm.Constants[%d] = %d, want %d", i, vm.chunk.Constants[i], v) } } // should have given stack size if vm.Stack.Capacity != stackSize { t.Errorf("vm.stack.Capacity = %d, want %d", vm.Stack.Capacity, stackSize) } // should have given call stack size if vm.call.Capacity != callstackSize { t.Errorf("vm.call.Capacity = %d, want %d", vm.call.Capacity, callstackSize) } } func BenchmarkNewVM(b *testing.B) { for i := 0; i < b.N; i++ { _ = NewVM(nil, 256, 256) } } func GetExecutionTestData() map[string]struct { chunk *Chunk resultingStack []Value resultingScope []map[string]Value } { return map[string]struct { chunk *Chunk resultingStack []Value resultingScope []map[string]Value }{ "two_plus_one": { NewChunk([]Bytecode{ InstructionConstant, 0, InstructionConstant, 1, InstructionAddFloat, }, []Value{ &FloatValue{1}, &FloatValue{2}, }), []Value{ &FloatValue{3}, }, []map[string]Value{}, }, "push_constant": { NewChunk( []Bytecode{ InstructionConstant, 0, }, []Value{ &FloatValue{1}, }, ), []Value{ &FloatValue{1}, }, []map[string]Value{}, }, "push_true": { NewChunk( []Bytecode{ InstructionTrue, }, []Value{}, ), []Value{ &BoolValue{true}, }, []map[string]Value{}, }, "push_false": { NewChunk( []Bytecode{ InstructionFalse, }, []Value{}, ), []Value{ &BoolValue{false}, }, []map[string]Value{}, }, "push_nil": { NewChunk( []Bytecode{ InstructionNil, }, []Value{}, ), []Value{ &NilValue{}, }, []map[string]Value{}, }, "empty": { NewChunk( []Bytecode{}, []Value{}, ), []Value{}, []map[string]Value{}, }, // (2 + 1) * 5 / (6 - 2) "full_arithmetic": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionConstant, 1, InstructionAddFloat, InstructionConstant, 2, InstructionMulFloat, InstructionConstant, 3, InstructionConstant, 0, InstructionSubFloat, InstructionDivFloat, }, []Value{ &FloatValue{2}, &FloatValue{1}, &FloatValue{5}, &FloatValue{6}, }, ), []Value{ &FloatValue{3.75}, }, []map[string]Value{}, }, "equality_true": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionConstant, 0, InstructionEquals, }, []Value{ &FloatValue{1}, }, ), []Value{ &BoolValue{true}, }, []map[string]Value{}, }, "equality_false": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionConstant, 1, InstructionEquals, }, []Value{ &FloatValue{1}, &FloatValue{2}, }, ), []Value{ &BoolValue{false}, }, []map[string]Value{}, }, "inequality_false": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionConstant, 0, InstructionNotEqual, }, []Value{ &FloatValue{1}, }, ), []Value{ &BoolValue{false}, }, []map[string]Value{}, }, "inequality_true": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionConstant, 1, InstructionNotEqual, }, []Value{ &FloatValue{1}, &FloatValue{2}, }, ), []Value{ &BoolValue{true}, }, []map[string]Value{}, }, "not_true": { NewChunk( []Bytecode{ InstructionTrue, InstructionNot, }, []Value{}, ), []Value{ &BoolValue{false}, }, []map[string]Value{}, }, "not_false": { NewChunk( []Bytecode{ InstructionFalse, InstructionNot, }, []Value{}, ), []Value{ &BoolValue{true}, }, []map[string]Value{}, }, "jump": { NewChunk( []Bytecode{ InstructionJump, 0, 2, InstructionConstant, 0, // should not execute InstructionConstant, 1, // should execute }, []Value{ &FloatValue{0}, &FloatValue{1}, }, ), []Value{ &FloatValue{1}, }, []map[string]Value{}, }, "jump_false/false": { NewChunk( []Bytecode{ InstructionFalse, InstructionJumpFalse, 0, 2, InstructionConstant, 0, // should not execute InstructionConstant, 1, // should execute }, []Value{ &FloatValue{0}, &FloatValue{1}, }, ), []Value{ &FloatValue{1}, }, []map[string]Value{}, }, "jump_false/true": { NewChunk( []Bytecode{ InstructionTrue, InstructionJumpFalse, 0, 2, InstructionConstant, 0, // should execute InstructionConstant, 1, // should execute }, []Value{ &FloatValue{0}, &FloatValue{1}, }, ), []Value{ &FloatValue{0}, &FloatValue{1}, }, []map[string]Value{}, }, "declare_local": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionDeclareLocal, 1, }, []Value{ &FloatValue{0}, &StringValue{"a"}, }, ), []Value{&FloatValue{0}}, []map[string]Value{ { "a": &FloatValue{0}, }, }, }, "assign_local": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionDeclareLocal, 1, InstructionPop, InstructionConstant, 2, InstructionSetLocal, 1, // reassign InstructionPop, }, []Value{ &FloatValue{0}, &StringValue{"a"}, &FloatValue{1}, }, ), []Value{}, []map[string]Value{ { "a": &FloatValue{1}, }, }, }, "get_local": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionDeclareLocal, 1, }, []Value{ &FloatValue{0}, &StringValue{"a"}, }, ), []Value{ &FloatValue{0}, }, []map[string]Value{ { "a": &FloatValue{0}, }, }, }, "get_reassigned_local": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionDeclareLocal, 1, InstructionPop, InstructionGetLocal, 1, InstructionConstant, 2, InstructionSetLocal, 1, // reassign InstructionPop, InstructionGetLocal, 1, }, []Value{ &FloatValue{0}, &StringValue{"a"}, &FloatValue{1}, }, ), []Value{ &FloatValue{0}, &FloatValue{1}, }, []map[string]Value{ { "a": &FloatValue{1}, }, }, }, "variable_scope": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionDeclareLocal, 1, InstructionPop, InstructionDescend, InstructionConstant, 2, InstructionDeclareLocal, 3, InstructionPop, InstructionDescend, InstructionConstant, 4, InstructionDeclareLocal, 5, InstructionPop, InstructionAscend, InstructionAscend, }, []Value{ &FloatValue{0}, &StringValue{"a"}, &FloatValue{1}, &StringValue{"b"}, &FloatValue{2}, &StringValue{"c"}, }, ), []Value{}, []map[string]Value{ { "a": &FloatValue{0}, }, }, }, "function_call": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionConstant, 1, InstructionConstant, 2, InstructionCall, }, []Value{ &FloatValue{1}, &FloatValue{2}, &FunctionValue{ Name: "sum", Params: []FunctionParameter{ { "a", &FloatSignature{}, }, { "b", &FloatSignature{}, }, }, Chunk: NewChunk( []Bytecode{ InstructionGetLocal, 0, InstructionGetLocal, 1, InstructionAddFloat, InstructionReturn, }, []Value{ &StringValue{"a"}, &StringValue{"b"}, }, ), }, }, ), []Value{ &FloatValue{3}, }, []map[string]Value{}, }, "function_calling_function": { NewChunk( []Bytecode{ InstructionConstant, 3, InstructionDeclareLocal, 4, InstructionPop, InstructionConstant, 0, InstructionConstant, 1, InstructionConstant, 2, InstructionCall, }, []Value{ &FloatValue{1}, &FloatValue{2}, &FunctionValue{ Name: "sum", Params: []FunctionParameter{ { "a", &FloatSignature{}, }, { "b", &FloatSignature{}, }, }, Chunk: NewChunk( []Bytecode{ InstructionGetLocal, 0, InstructionGetLocal, 2, InstructionCall, // square the number InstructionGetLocal, 1, InstructionGetLocal, 2, InstructionCall, // square the number InstructionAddFloat, InstructionReturn, }, []Value{ &StringValue{"a"}, &StringValue{"b"}, &StringValue{"square"}, }, ), }, &FunctionValue{ Name: "square", Params: []FunctionParameter{ { "n", &FloatSignature{}, }, }, Chunk: NewChunk( []Bytecode{ InstructionGetLocal, 0, InstructionGetLocal, 0, InstructionMulFloat, InstructionReturn, }, []Value{ &StringValue{"n"}, }, ), }, &StringValue{"square"}, }, ), []Value{ &FloatValue{5}, }, []map[string]Value{ { "square": &FunctionValue{ Name: "square", Params: []FunctionParameter{ { "n", &FloatSignature{}, }, }, Chunk: NewChunk( []Bytecode{ InstructionGetLocal, 0, InstructionGetLocal, 0, InstructionMulFloat, InstructionReturn, }, []Value{ &StringValue{"n"}, }, ), }, }, }, }, "list_concat": { NewChunk( []Bytecode{ InstructionConstant, 0, InstructionConstant, 1, InstructionConcatLists, }, []Value{ &ListValue{ []Value{ &FloatValue{1}, &FloatValue{2}, }, }, &ListValue{ []Value{ &FloatValue{3}, }, }, }, ), []Value{ &ListValue{ []Value{ &FloatValue{1}, &FloatValue{2}, &FloatValue{3}, }, }, }, []map[string]Value{}, }, "form_tuple": { &Chunk{ Bytecode: []Bytecode{ InstructionConstant, 0, InstructionConstant, 1, InstructionFormTuple, 0, 2, }, Constants: []Value{ &IntegerValue{big.NewInt(1)}, &IntegerValue{big.NewInt(2)}, }, }, []Value{ &TupleValue{ Items: []Value{ &IntegerValue{big.NewInt(1)}, &IntegerValue{big.NewInt(2)}, }, }, }, []map[string]Value{}, }, } } func TestVM_Execution(t *testing.T) { data := GetExecutionTestData() for name, test := range data { t.Run(name, func(t *testing.T) { vm := NewVM(test.chunk, 256, 256) for vm.Next() { } CompareStacks(t, test.resultingStack, vm.Stack) }) } } func BenchmarkVM_Execution(b *testing.B) { data := GetExecutionTestData() for name, test := range data { b.Run(name, func(b *testing.B) { for n := 0; n < b.N; n++ { vm := NewVM(test.chunk, 256, 256) for vm.Next() { } } }) } } func TestVM_NextByte(t *testing.T) { vm := NewVM( NewChunk( []Bytecode{ InstructionConstant, 0, }, []Value{ &FloatValue{0}, }, ), 16, 16, ) b, err := vm.TryNextByte() if err != nil { t.Fatal(err) } if b != InstructionConstant { t.Errorf("got %v; want %v", b, InstructionConstant) } b, err = vm.TryNextByte() if err != nil { t.Fatal(err) } if b != 0 { t.Errorf("got %v; want %v", b, 0) } b, err = vm.TryNextByte() if err == nil { t.Errorf("didn't get expected error") } if b != 0 { t.Errorf("got %v; want %v", b, nil) } } func TestVM_NextU16_Empty(t *testing.T) { defer func() { if r := recover(); r == nil { t.Fatalf("didn't panic when not enough bytes") } }() vm := NewVM( NewChunk( []Bytecode{}, []Value{}, ), 16, 16, ) vm.NextU16() } func TestVM_NextU16_One(t *testing.T) { defer func() { if r := recover(); r == nil { t.Fatalf("didn't panic when not enough bytes") } }() vm := NewVM( NewChunk( []Bytecode{ 0, }, []Value{}, ), 16, 16, ) vm.NextU16() } func TestVM_NextU16(t *testing.T) { for i := 0; i <= 0xFFFF; i++ { t.Run(fmt.Sprintf("value-%d", i), func(t *testing.T) { vm := NewVM( NewChunk( []Bytecode{ Bytecode((i >> 8) & 0xFF), Bytecode(i & 0xFF), }, []Value{}, ), 16, 16, ) b := vm.NextU16() if uint16(i) != b { t.Errorf("got %v; want %v", b, i) } }) } } func TestVM_Jump(t *testing.T) { vm := NewVM( NewChunk( []Bytecode{ InstructionJump, 0, 2, InstructionConstant, 0, InstructionConstant, 1, InstructionConstant, 2, }, []Value{ &FloatValue{0}, &FloatValue{1}, &FloatValue{2}, }, ), 16, 16, ) vm.Next() if vm.ip != 5 { t.Errorf("jumped got %v; want %v", vm.ip-3, 2) } } func TestVM_JumpFalse(t *testing.T) { vm := NewVM( NewChunk( []Bytecode{ InstructionFalse, InstructionJumpFalse, 0, 2, InstructionConstant, 0, InstructionConstant, 1, InstructionConstant, 2, }, []Value{ &FloatValue{0}, &FloatValue{1}, &FloatValue{2}, }, ), 16, 16, ) vm.Next() vm.Next() if vm.ip != 6 { t.Errorf("jumped got %v; want %v", vm.ip-4, 2) } } func TestVM_DontJumpFalse(t *testing.T) { vm := NewVM( NewChunk( []Bytecode{ InstructionTrue, InstructionJumpFalse, 0, 2, InstructionConstant, 0, InstructionConstant, 1, InstructionConstant, 2, }, []Value{ &FloatValue{0}, &FloatValue{1}, &FloatValue{2}, }, ), 16, 16, ) vm.Next() vm.Next() if vm.ip != 4 { t.Errorf("ip is %v; want %v", vm.ip, 4) } } func TestVM_GetGlobal(t *testing.T) {}