369 lines
9.3 KiB
Go
369 lines
9.3 KiB
Go
package tests
|
|
|
|
import (
|
|
"testing"
|
|
|
|
assert "git.sharkk.net/Go/Assert"
|
|
"git.sharkk.net/Sharkk/Mako/compiler"
|
|
"git.sharkk.net/Sharkk/Mako/lexer"
|
|
"git.sharkk.net/Sharkk/Mako/parser"
|
|
"git.sharkk.net/Sharkk/Mako/types"
|
|
)
|
|
|
|
func TestCompileConstants(t *testing.T) {
|
|
input := `
|
|
5;
|
|
"hello";
|
|
true;
|
|
false;
|
|
`
|
|
|
|
lex := lexer.New(input)
|
|
p := parser.New(lex)
|
|
program := p.ParseProgram()
|
|
|
|
bytecode := compiler.Compile(program)
|
|
|
|
// Check that we have the right constants
|
|
assert.Equal(t, 4, len(bytecode.Constants))
|
|
|
|
// Check constant values
|
|
assert.Equal(t, 5.0, bytecode.Constants[0])
|
|
assert.Equal(t, "hello", bytecode.Constants[1])
|
|
assert.Equal(t, true, bytecode.Constants[2])
|
|
assert.Equal(t, false, bytecode.Constants[3])
|
|
|
|
// Check that we have the right instructions
|
|
assert.Equal(t, 10, len(bytecode.Instructions)) // 2 scope + 4 constants + 4 pops
|
|
|
|
// Check instruction opcodes - first instruction is enter scope
|
|
assert.Equal(t, types.OpEnterScope, bytecode.Instructions[0].Opcode)
|
|
|
|
// Check the constant loading and popping instructions
|
|
for i := 0; i < 4; i++ {
|
|
assert.Equal(t, types.OpConstant, bytecode.Instructions[1+i*2].Opcode)
|
|
assert.Equal(t, i, bytecode.Instructions[1+i*2].Operand)
|
|
assert.Equal(t, types.OpPop, bytecode.Instructions[2+i*2].Opcode)
|
|
}
|
|
|
|
// Last instruction is exit scope
|
|
assert.Equal(t, types.OpExitScope, bytecode.Instructions[len(bytecode.Instructions)-1].Opcode)
|
|
}
|
|
|
|
func TestCompileVariableAssignment(t *testing.T) {
|
|
input := `x = 5;`
|
|
|
|
lex := lexer.New(input)
|
|
p := parser.New(lex)
|
|
program := p.ParseProgram()
|
|
|
|
bytecode := compiler.Compile(program)
|
|
|
|
// Constants should be: 5, "x"
|
|
assert.Equal(t, 2, len(bytecode.Constants))
|
|
assert.Equal(t, 5.0, bytecode.Constants[0])
|
|
assert.Equal(t, "x", bytecode.Constants[1])
|
|
|
|
// Instructions should be:
|
|
// - OpEnterScope
|
|
// - OpConstant (5)
|
|
// - OpSetGlobal with operand pointing to "x"
|
|
// - OpExitScope
|
|
assert.Equal(t, 4, len(bytecode.Instructions))
|
|
|
|
assert.Equal(t, types.OpEnterScope, bytecode.Instructions[0].Opcode)
|
|
assert.Equal(t, types.OpConstant, bytecode.Instructions[1].Opcode)
|
|
assert.Equal(t, 0, bytecode.Instructions[1].Operand)
|
|
assert.Equal(t, types.OpSetGlobal, bytecode.Instructions[2].Opcode)
|
|
assert.Equal(t, 1, bytecode.Instructions[2].Operand)
|
|
assert.Equal(t, types.OpExitScope, bytecode.Instructions[3].Opcode)
|
|
}
|
|
|
|
func TestCompileArithmeticExpressions(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
constants []any
|
|
operations []types.Opcode
|
|
}{
|
|
{
|
|
"5 + 10;",
|
|
[]any{5.0, 10.0},
|
|
[]types.Opcode{types.OpConstant, types.OpConstant, types.OpAdd, types.OpPop},
|
|
},
|
|
{
|
|
"5 - 10;",
|
|
[]any{5.0, 10.0},
|
|
[]types.Opcode{types.OpConstant, types.OpConstant, types.OpSubtract, types.OpPop},
|
|
},
|
|
{
|
|
"5 * 10;",
|
|
[]any{5.0, 10.0},
|
|
[]types.Opcode{types.OpConstant, types.OpConstant, types.OpMultiply, types.OpPop},
|
|
},
|
|
{
|
|
"5 / 10;",
|
|
[]any{5.0, 10.0},
|
|
[]types.Opcode{types.OpConstant, types.OpConstant, types.OpDivide, types.OpPop},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
lex := lexer.New(tt.input)
|
|
p := parser.New(lex)
|
|
program := p.ParseProgram()
|
|
|
|
bytecode := compiler.Compile(program)
|
|
|
|
// Check constants
|
|
assert.Equal(t, len(tt.constants), len(bytecode.Constants))
|
|
for i, c := range tt.constants {
|
|
assert.Equal(t, c, bytecode.Constants[i])
|
|
}
|
|
|
|
// Check operations, skipping the first and last (scope instructions)
|
|
assert.Equal(t, len(tt.operations)+2, len(bytecode.Instructions))
|
|
for i, op := range tt.operations {
|
|
assert.Equal(t, op, bytecode.Instructions[i+1].Opcode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompileComparisonExpressions(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
constants []any
|
|
operations []types.Opcode
|
|
}{
|
|
{
|
|
"5 == 10;",
|
|
[]any{5.0, 10.0},
|
|
[]types.Opcode{types.OpConstant, types.OpConstant, types.OpEqual, types.OpPop},
|
|
},
|
|
{
|
|
"5 != 10;",
|
|
[]any{5.0, 10.0},
|
|
[]types.Opcode{types.OpConstant, types.OpConstant, types.OpNotEqual, types.OpPop},
|
|
},
|
|
{
|
|
"5 < 10;",
|
|
[]any{5.0, 10.0},
|
|
[]types.Opcode{types.OpConstant, types.OpConstant, types.OpLessThan, types.OpPop},
|
|
},
|
|
{
|
|
"5 > 10;",
|
|
[]any{5.0, 10.0},
|
|
[]types.Opcode{types.OpConstant, types.OpConstant, types.OpGreaterThan, types.OpPop},
|
|
},
|
|
{
|
|
"5 <= 10;",
|
|
[]any{5.0, 10.0},
|
|
[]types.Opcode{types.OpConstant, types.OpConstant, types.OpLessEqual, types.OpPop},
|
|
},
|
|
{
|
|
"5 >= 10;",
|
|
[]any{5.0, 10.0},
|
|
[]types.Opcode{types.OpConstant, types.OpConstant, types.OpGreaterEqual, types.OpPop},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
lex := lexer.New(tt.input)
|
|
p := parser.New(lex)
|
|
program := p.ParseProgram()
|
|
|
|
bytecode := compiler.Compile(program)
|
|
|
|
// Check constants
|
|
assert.Equal(t, len(tt.constants), len(bytecode.Constants))
|
|
for i, c := range tt.constants {
|
|
assert.Equal(t, c, bytecode.Constants[i])
|
|
}
|
|
|
|
// Check operations, skipping the first and last (scope instructions)
|
|
assert.Equal(t, len(tt.operations)+2, len(bytecode.Instructions))
|
|
for i, op := range tt.operations {
|
|
assert.Equal(t, op, bytecode.Instructions[i+1].Opcode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompileTable(t *testing.T) {
|
|
input := `table = { name = "John", age = 30 };`
|
|
|
|
lex := lexer.New(input)
|
|
p := parser.New(lex)
|
|
program := p.ParseProgram()
|
|
|
|
bytecode := compiler.Compile(program)
|
|
|
|
// Constants should be: "John", 30, "name", "age", "table"
|
|
assert.Equal(t, 5, len(bytecode.Constants))
|
|
assert.Equal(t, "John", bytecode.Constants[0])
|
|
assert.Equal(t, 30.0, bytecode.Constants[1])
|
|
assert.Equal(t, "name", bytecode.Constants[2])
|
|
assert.Equal(t, "age", bytecode.Constants[3])
|
|
assert.Equal(t, "table", bytecode.Constants[4])
|
|
|
|
// Check that we have the right instructions for table creation
|
|
expectedOpcodes := []types.Opcode{
|
|
types.OpEnterScope,
|
|
types.OpNewTable, // Create table
|
|
types.OpDup, // Duplicate to set first property
|
|
types.OpConstant, // Load name key
|
|
types.OpConstant, // Load "John" value
|
|
types.OpSetIndex, // Set name = "John"
|
|
types.OpPop, // Pop result of setindex
|
|
types.OpDup, // Duplicate for second property
|
|
types.OpConstant, // Load age key
|
|
types.OpConstant, // Load 30 value
|
|
types.OpSetIndex, // Set age = 30
|
|
types.OpPop, // Pop result of setindex
|
|
types.OpSetGlobal, // Set table variable
|
|
types.OpExitScope,
|
|
}
|
|
|
|
assert.Equal(t, len(expectedOpcodes), len(bytecode.Instructions))
|
|
for i, op := range expectedOpcodes {
|
|
assert.Equal(t, op, bytecode.Instructions[i].Opcode)
|
|
}
|
|
}
|
|
|
|
func TestCompileIfStatement(t *testing.T) {
|
|
input := `
|
|
if x < 10 then
|
|
echo "x is less than 10";
|
|
else
|
|
echo "x is not less than 10";
|
|
end
|
|
`
|
|
|
|
lex := lexer.New(input)
|
|
p := parser.New(lex)
|
|
program := p.ParseProgram()
|
|
|
|
bytecode := compiler.Compile(program)
|
|
|
|
// We should have constants for: "x", 10, "x is less than 10", "x is not less than 10", nil (for else block)
|
|
assert.Equal(t, 5, len(bytecode.Constants))
|
|
|
|
// Check for key opcodes in the bytecode
|
|
// This is simplified - in reality we'd check the full instruction flow
|
|
ops := bytecode.Instructions
|
|
|
|
// Check for variable lookup
|
|
foundGetGlobal := false
|
|
for _, op := range ops {
|
|
if op.Opcode == types.OpGetGlobal {
|
|
foundGetGlobal = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundGetGlobal)
|
|
|
|
// Check for comparison opcode
|
|
foundLessThan := false
|
|
for _, op := range ops {
|
|
if op.Opcode == types.OpLessThan {
|
|
foundLessThan = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundLessThan)
|
|
|
|
// Check for conditional jump
|
|
foundJumpIfFalse := false
|
|
for _, op := range ops {
|
|
if op.Opcode == types.OpJumpIfFalse {
|
|
foundJumpIfFalse = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundJumpIfFalse)
|
|
|
|
// Check for unconditional jump (for else clause)
|
|
foundJump := false
|
|
for _, op := range ops {
|
|
if op.Opcode == types.OpJump {
|
|
foundJump = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundJump)
|
|
|
|
// Check for echo statements
|
|
foundEcho := false
|
|
echoCount := 0
|
|
for _, op := range ops {
|
|
if op.Opcode == types.OpEcho {
|
|
foundEcho = true
|
|
echoCount++
|
|
}
|
|
}
|
|
assert.True(t, foundEcho)
|
|
assert.Equal(t, 2, echoCount)
|
|
}
|
|
|
|
func TestCompileLogicalOperators(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedOperator types.Opcode
|
|
}{
|
|
{"not true;", types.OpNot},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
lex := lexer.New(tt.input)
|
|
p := parser.New(lex)
|
|
program := p.ParseProgram()
|
|
|
|
bytecode := compiler.Compile(program)
|
|
|
|
foundOp := false
|
|
for _, op := range bytecode.Instructions {
|
|
if op.Opcode == tt.expectedOperator {
|
|
foundOp = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundOp)
|
|
}
|
|
|
|
// AND and OR are special as they involve jumps - test separately
|
|
andInput := "true and false;"
|
|
lex := lexer.New(andInput)
|
|
p := parser.New(lex)
|
|
program := p.ParseProgram()
|
|
bytecode := compiler.Compile(program)
|
|
|
|
// AND should involve a JumpIfFalse instruction
|
|
foundJumpIfFalse := false
|
|
for _, op := range bytecode.Instructions {
|
|
if op.Opcode == types.OpJumpIfFalse {
|
|
foundJumpIfFalse = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundJumpIfFalse)
|
|
|
|
// OR test
|
|
orInput := "true or false;"
|
|
lex = lexer.New(orInput)
|
|
p = parser.New(lex)
|
|
program = p.ParseProgram()
|
|
bytecode = compiler.Compile(program)
|
|
|
|
// OR should involve both Jump and JumpIfFalse instructions
|
|
foundJump := false
|
|
foundJumpIfFalse = false
|
|
for _, op := range bytecode.Instructions {
|
|
if op.Opcode == types.OpJump {
|
|
foundJump = true
|
|
}
|
|
if op.Opcode == types.OpJumpIfFalse {
|
|
foundJumpIfFalse = true
|
|
}
|
|
}
|
|
assert.True(t, foundJump)
|
|
assert.True(t, foundJumpIfFalse)
|
|
}
|