separate core from cli into submodules, add support for boolean and and or operations, make more fields public, fix passing arguments to functions

This commit is contained in:
Neemek 2024-12-13 11:33:47 +01:00
parent 7498c85424
commit 62656c6dff
Signed by: neemek
GPG key ID: 28360A8951CD0E9B
19 changed files with 202 additions and 99 deletions

449
core/compiler_test.go Normal file
View file

@ -0,0 +1,449 @@
package core
import (
"fmt"
"testing"
)
func TestNewCompiler(t *testing.T) {
c := NewCompiler()
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()
}
}
type CompileTestData struct {
tree Node
expectedStack []Value
}
func GetCompileTestData() map[string]CompileTestData {
return map[string]CompileTestData{
"constant_string": {
&StringNode{
"Hello world!",
"\"Hello world!\"",
},
[]Value{
StringValue("Hello world!"),
},
},
"conditional_false": {
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
0,
},
true,
},
&ConditionalNode{
&BooleanNode{
false,
},
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
1,
},
false,
},
},
},
nil,
},
},
},
[]Value{
&VariableValue{
"a",
NumberValue(0),
0,
},
},
},
"conditional_true": {
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
0,
},
true,
},
&ConditionalNode{
&BooleanNode{
true,
},
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
1,
},
false,
},
},
},
nil,
},
},
},
[]Value{
&VariableValue{
"a",
NumberValue(1),
0,
},
},
},
"conditional_welse_false": {
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
0,
},
true,
},
&ConditionalNode{
&BooleanNode{
false,
},
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
1,
},
false,
},
},
},
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
2,
},
false,
},
},
},
},
},
},
[]Value{
&VariableValue{
"a",
NumberValue(2),
0,
},
},
},
"conditional_welse_true": {
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
0,
},
true,
},
&ConditionalNode{
&BooleanNode{
true,
},
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
1,
},
false,
},
},
},
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
2,
},
false,
},
},
},
},
},
},
[]Value{
&VariableValue{
"a",
NumberValue(1),
0,
},
},
},
"addition": {
&BinaryNode{
BinaryAddition,
&NumberNode{
1,
},
&NumberNode{
2,
},
},
[]Value{
NumberValue(3),
},
},
"sum_function": {&BlockNode{
[]Node{
&AssignNode{
"sum",
&FunctionNode{
"sum",
[]string{"a", "b"},
&BlockNode{
[]Node{
&ReturnNode{
&BinaryNode{
BinaryAddition,
&ReferenceNode{"a"},
&ReferenceNode{"b"},
},
},
},
},
},
true,
},
},
},
[]Value{
&VariableValue{
"sum",
FunctionValue{
"sum",
[]string{"a", "b"},
NewChunk(
[]Bytecode{
InstructionDescend,
InstructionGetLocal, 0,
InstructionGetLocal, 1,
InstructionAdd,
InstructionReturn,
InstructionAscend,
},
[]Value{
StringValue("a"), StringValue("b"),
},
),
},
0,
},
},
},
"remove_func_vars": {
&BlockNode{
[]Node{
&AssignNode{
"a",
&FunctionNode{
"a",
[]string{},
&BlockNode{
[]Node{
&AssignNode{
"b",
&NumberNode{1},
true,
},
&ReturnNode{
&ReferenceNode{"b"},
},
},
},
},
true,
},
&CallNode{
"a",
[]Node{},
false,
},
},
},
[]Value{
&VariableValue{
"a",
FunctionValue{
"a",
[]string{},
NewChunk(
[]Bytecode{
InstructionDescend,
InstructionConstant, 0,
InstructionDeclareLocal, 1,
InstructionGetLocal, 1,
InstructionReturn,
InstructionAscend,
},
[]Value{
NumberValue(1), StringValue("b"),
},
),
},
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)
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()
t.Log("Compiling node tree")
c.Compile(testCase.tree)
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.HasNext() && vm.Next() {
}
t.Log("Executed bytecode")
CompareStacks(t, testCase.expectedStack, vm.stack)
})
}
}
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()
c.Compile(testCase.tree)
}
})
}
}
func TestCompiler_AddU16(t *testing.T) {
for i := 0; i <= 0xffff; i++ {
t.Run(fmt.Sprint(i), func(t *testing.T) {
c := NewCompiler()
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 {
switch tc.tree.Type() {
// skip all expected unclean nodes
case StringNodeType, NumberNodeType, ReferenceNodeType, BooleanNodeType, NilNodeType, BinaryNodeType, ReturnNodeType:
continue
case CallNodeType:
if tc.tree.(*CallNode).keep {
// if we know it should be unclean, skip it
continue
}
// clean statements
case BlockNodeType:
case ConditionalNodeType:
case LoopNodeType:
case AssignNodeType:
case FunctionNodeType:
}
t.Run(name, func(t *testing.T) {
c := NewCompiler()
c.Compile(tc.tree)
vm := NewVM(c.Chunk, 256, 256)
for vm.HasNext() && vm.Next() {
}
// make sure stack has only assigned values
for i := 0; i < int(vm.stack.Current); i++ {
v := vm.stack.items[i]
if v == nil || v.Type() != VariableValueType {
t.Errorf("Unclean stack! value %v at %d on the stack is intermediary", v.String(), i)
}
}
})
}
}