anglais/core/vm_test.go
2026-07-11 12:51:39 +02:00

857 lines
15 KiB
Go

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) {}