package core import ( "fmt" "testing" ) func TestNewCompiler(t *testing.T) { c := NewCompiler([]rune{}) if c == nil { t.Fatal("NewCompiler returned nil") } if c.ip != 0 { t.Error("compiler ip doesn't start at zero") } if c.Chunk == nil { t.Error("compiler chunk initialized to nil") } } func BenchmarkNewCompiler(b *testing.B) { for i := 0; i < b.N; i++ { _ = NewCompiler([]rune{}) } } type CompileTestData struct { program *Program expectedStack []Value expectedScope []map[string]Value } func GetCompileTestData() map[string]CompileTestData { return map[string]CompileTestData{ "constant_string": { &Program{ &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &StringNode{ "Hello world!", "\"Hello world!\"", 0, 0, }, true, 0, 0, }, &ReferenceNode{"a", 0, 0}, }, 0, 0, }, "", }, []Value{&StringValue{"Hello world!"}}, []map[string]Value{}, }, /* these tests are so fucking unmaintainable "conditional_false": { &Program{ []Import{}, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FloatNode{ 0, 0, 0, }, true, 0, 0, }, &ConditionalNode{ &BooleanNode{ false, 0, 0, }, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FloatNode{ 1, 0, 0, }, false, 0, 0, }, }, 0, 0, }, nil, 0, 0, }, }, 0, 0, }, "", }, []Value{ &VariableValue{ "a", &FloatValue{0}, 0, }, }, }, "conditional_true": { &Program{ []Import{}, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FloatNode{ 0, 0, 0, }, true, 0, 0, }, &ConditionalNode{ &BooleanNode{ true, 0, 0, }, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FloatNode{ 1, 0, 0, }, false, 0, 0, }, }, 0, 0, }, nil, 0, 0, }, }, 0, 0, }, "", }, []Value{ &VariableValue{ "a", &FloatValue{1}, 0, }, }, }, "conditional_else_false": { &Program{ []Import{}, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FloatNode{ 0, 0, 0, }, true, 0, 0, }, &ConditionalNode{ &BooleanNode{ false, 0, 0, }, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FloatNode{ 1, 0, 0, }, false, 0, 0, }, }, 0, 0, }, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FloatNode{ 2, 0, 0, }, false, 0, 0, }, }, 0, 0, }, 0, 0, }, }, 0, 0, }, "", }, []Value{ &VariableValue{ "a", &FloatValue{2}, 0, }, }, }, "conditional_else_true": { &Program{ []Import{}, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FloatNode{ 0, 0, 0, }, true, 0, 0, }, &ConditionalNode{ &BooleanNode{ true, 0, 0, }, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FloatNode{ 1, 0, 0, }, false, 0, 0, }, }, 0, 0, }, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FloatNode{ 2, 0, 0, }, false, 0, 0, }, }, 0, 0, }, 0, 0, }, }, 0, 0, }, "", }, []Value{ &VariableValue{ "a", &FloatValue{1}, 0, }, }, }, "addition": { &Program{ []Import{}, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &BinaryNode{ BinaryAddition, &FloatNode{ 1, 0, 0, }, &FloatNode{ 2, 0, 0, }, 0, 0, }, true, 0, 0, }, }, 0, 0, }, "", }, []Value{ &VariableValue{ "a", &FloatValue{3}, 0, }, }, }, "sum_function": { &Program{ []Import{}, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{name: "sum"}, &FunctionNode{ "sum", []FunctionParameter{ { "a", &FloatSignature{}, }, { "b", &FloatSignature{}, }, }, &FloatSignature{}, &BlockNode{ []Node{ &ReturnNode{ &BinaryNode{ BinaryAddition, &ReferenceNode{ "a", 0, 0, }, &ReferenceNode{ "b", 0, 0, }, 0, 0, }, 0, 0, }, }, 0, 0, }, 0, 0, }, true, 0, 0, }, }, 0, 0, }, "", }, []Value{ &VariableValue{ "sum", &FunctionValue{ "sum", []FunctionParameter{ { "a", &FloatSignature{}, }, { "b", &FloatSignature{}, }, }, &FloatSignature{}, NewChunk( []Bytecode{ InstructionDescend, InstructionGetLocal, 0, InstructionGetLocal, 1, InstructionAddFloat, InstructionReturn, InstructionAscend, }, []Value{ &StringValue{"a"}, &StringValue{"b"}, }, ), nil, }, 0, }, }, }, "remove_func_vars": { &Program{ []Import{}, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"a", 0, 0}, &FunctionNode{ "a", []FunctionParameter{}, &FloatSignature{}, &BlockNode{ []Node{ &AssignNode{ &ReferenceNode{"b", 0, 0}, &FloatNode{ 1, 0, 0, }, true, 0, 0, }, &ReturnNode{ &ReferenceNode{ "b", 0, 0, }, 0, 0, }, }, 0, 0, }, 0, 0, }, true, 0, 0, }, &CallNode{ &ReferenceNode{ "a", 0, 0, }, []Node{}, 0, 0, }, }, 0, 0, }, "", }, []Value{ &VariableValue{ "a", &FunctionValue{ "a", []FunctionParameter{}, &FloatSignature{}, NewChunk( []Bytecode{ InstructionDescend, InstructionConstant, 0, InstructionDeclareLocal, 1, InstructionGetLocal, 1, InstructionReturn, InstructionAscend, }, []Value{ &FloatValue{1}, &StringValue{"b"}, }, ), nil, }, 0, }, }, }, "two_lists": { program: &Program{ []Import{}, &BlockNode{ statements: []Node{ &AssignNode{ dest: &ReferenceNode{"a", 0, 0}, value: &ListNode{ items: []Node{ &FloatNode{value: 1}, &FloatNode{value: 2}, }, }, declare: true, }, &AssignNode{ dest: &ReferenceNode{"b", 0, 0}, value: &ListNode{ items: []Node{ &StringNode{value: "Hello"}, &StringNode{value: "world"}, }, }, declare: true, }, }, }, "", }, expectedStack: []Value{ &VariableValue{ name: "a", value: &ListValue{ Items: []Value{ &FloatValue{1}, &FloatValue{2}, }, }, scope: 0, }, &VariableValue{ name: "b", value: &ListValue{ Items: []Value{ &StringValue{"Hello"}, &StringValue{"world"}, }, }, scope: 0, }, }, }, "unary/negate": { program: &Program{ []Import{}, &BlockNode{ []Node{ &AssignNode{ dest: &ReferenceNode{"a", 0, 0}, value: &UnaryNode{ UnaryNegate, &FloatNode{ 1, 0, 0, }, 0, 0, }, declare: true, }, }, 0, 0, }, "", }, expectedStack: []Value{ &VariableValue{ name: "a", value: &FloatValue{-1}, scope: 0, }, }, }, "unary/not": { program: &Program{ []Import{}, &BlockNode{ []Node{ &AssignNode{ dest: &ReferenceNode{"a", 0, 0}, value: &UnaryNode{ UnaryNot, &BooleanNode{ value: true, }, 0, 0, }, declare: true, }, }, 0, 0, }, "", }, expectedStack: []Value{ &VariableValue{ name: "a", value: &BoolValue{false}, scope: 0, }, }, }, */ } } func printChunk(t *testing.T, name string, chunk *Chunk) { t.Logf("=v= %s =v=", name) for i, bc := range chunk.Bytecode { t.Logf("i=%d \t%d \t(%s)", i, bc, bc) } t.Logf("=-= constants =-=") for i, ct := range chunk.Constants { t.Logf("c=%d \t%s", i, ct.DebugString()) f, ok := ct.(*FunctionValue) if ok { printChunk(t, f.Name, f.Chunk) } } t.Logf("=^= %s =^=", name) } func TestCompile(t *testing.T) { data := GetCompileTestData() for name, testCase := range data { t.Run(name, func(t *testing.T) { t.Log("Initializing compiler") c := NewCompiler([]rune(testCase.program.String())) t.Log("Compiling node tree") _, err := c.Compile(testCase.program) if err != nil { t.Fatalf("Compiling failed: %v", err) } t.Log("Initializing vm") vm := NewVM(c.Chunk, 256, 256) t.Log("Printing chunk for debug") printChunk(t, name, c.Chunk) t.Log("Executing bytecode") for vm.Next() { } t.Log("Executed bytecode") CompareStacks(t, testCase.expectedStack, vm.Stack) CompareScope(t, testCase.expectedScope, vm.scope) }) } } func BenchmarkCompile(b *testing.B) { data := GetCompileTestData() for name, testCase := range data { b.Run(name, func(b *testing.B) { for i := 0; i < b.N; i++ { c := NewCompiler([]rune{}) _, _ = c.Compile(testCase.program) } }) } } func TestCompiler_AddU16(t *testing.T) { for i := 0; i <= 0xffff; i++ { t.Run(fmt.Sprint(i), func(t *testing.T) { c := NewCompiler([]rune{}) c.addU16(uint16(i)) if c.Chunk.Bytecode[0] != Bytecode(i>>8) { t.Errorf("first 8 bits don't match (got %s, expected %b)", c.Chunk.Bytecode[0], byte(i>>8)) } if c.Chunk.Bytecode[1] != Bytecode(i&0xff) { t.Errorf("last 8 bits don't match (got %s, expected %b)", c.Chunk.Bytecode[1], byte(i&0xff)) } }) } } func TestCompiler_CleanStack(t *testing.T) { cases := GetCompileTestData() for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewCompiler([]rune(tc.program.String())) _, err := c.Compile(tc.program) if err != nil { t.Fatalf("Compiling failed: %v", err) } vm := NewVM(c.Chunk, 256, 256) for vm.Next() { } // make sure stack has only assigned values for i := 1; i < int(vm.Stack.Current); i++ { v := vm.Stack.items[i] if v == nil { t.Errorf("Unclean stack! value %v at %d on the stack is intermediary", v.String(), i) } } }) } }