add tests
This commit is contained in:
parent
1bc3357aff
commit
1f2522d9fc
2
go.mod
2
go.mod
@ -1,3 +1,5 @@
|
||||
module git.sharkk.net/Sharkk/Mako
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require git.sharkk.net/Go/Assert v0.0.0-20250426205601-1b0e5ea6e7ac // indirect
|
||||
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
||||
git.sharkk.net/Go/Assert v0.0.0-20250426205601-1b0e5ea6e7ac h1:B6iLK3nv2ubDfk5Ve9Z2sRPqpTgPWgsm7PyaWlwr3NY=
|
||||
git.sharkk.net/Go/Assert v0.0.0-20250426205601-1b0e5ea6e7ac/go.mod h1:7AMVm0RCtLlQfWsnKs6h/IdSfzj52/o0nR03rCW68gM=
|
368
tests/compiler_test.go
Normal file
368
tests/compiler_test.go
Normal file
@ -0,0 +1,368 @@
|
||||
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)
|
||||
}
|
247
tests/edge_test.go
Normal file
247
tests/edge_test.go
Normal file
@ -0,0 +1,247 @@
|
||||
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/vm"
|
||||
)
|
||||
|
||||
// Helper function to execute Mako code
|
||||
func executeMakoWithErrors(code string) (*vm.VM, []string) {
|
||||
lex := lexer.New(code)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
errors := p.Errors()
|
||||
bytecode := compiler.Compile(program)
|
||||
virtualMachine := vm.New()
|
||||
virtualMachine.Run(bytecode)
|
||||
return virtualMachine, errors
|
||||
}
|
||||
|
||||
func TestTypeConversions(t *testing.T) {
|
||||
// Test automatic type conversions in operations
|
||||
_, errors := executeMakoWithErrors(`
|
||||
// String concatenation
|
||||
echo "Hello " + "World"; // Should work
|
||||
|
||||
// Using boolean in a condition
|
||||
if true then
|
||||
echo "Boolean works in condition";
|
||||
end
|
||||
|
||||
// Numeric conditions
|
||||
if 1 then
|
||||
echo "Numeric 1 is truthy";
|
||||
end
|
||||
|
||||
if 0 then
|
||||
echo "This should not execute";
|
||||
else
|
||||
echo "Numeric 0 is falsy";
|
||||
end
|
||||
`)
|
||||
|
||||
assert.Equal(t, 0, len(errors))
|
||||
}
|
||||
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
// Test edge cases that might cause issues
|
||||
_, errors := executeMakoWithErrors(`
|
||||
// Division by zero
|
||||
// echo 5 / 0; // Should not crash VM, would just return null
|
||||
|
||||
// Deep nesting
|
||||
table = {
|
||||
level1 = {
|
||||
level2 = {
|
||||
level3 = {
|
||||
level4 = {
|
||||
value = "Deep nesting"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
echo table["level1"]["level2"]["level3"]["level4"]["value"];
|
||||
|
||||
// Empty tables
|
||||
emptyTable = {};
|
||||
echo emptyTable["nonexistent"]; // Should return null
|
||||
|
||||
// Table with invalid access
|
||||
someTable = { key = "value" };
|
||||
// echo someTable[123]; // Should not crash
|
||||
`)
|
||||
|
||||
assert.Equal(t, 0, len(errors))
|
||||
}
|
||||
|
||||
func TestErrorHandling(t *testing.T) {
|
||||
// Test error handling in the parser
|
||||
_, errors := executeMakoWithErrors(`
|
||||
// Invalid syntax - missing semicolon
|
||||
x = 5
|
||||
y = 10;
|
||||
`)
|
||||
|
||||
// Should have at least one error
|
||||
assert.True(t, len(errors) > 0)
|
||||
|
||||
// Test parser recovery
|
||||
_, errors = executeMakoWithErrors(`
|
||||
// Missing end keyword
|
||||
if x < 10 then
|
||||
echo "x is less than 10";
|
||||
// end - missing
|
||||
`)
|
||||
|
||||
// Should have at least one error
|
||||
assert.True(t, len(errors) > 0)
|
||||
}
|
||||
|
||||
func TestNestedScopes(t *testing.T) {
|
||||
// Test nested scopes and variable shadowing
|
||||
_, errors := executeMakoWithErrors(`
|
||||
x = "global";
|
||||
|
||||
{
|
||||
echo x; // Should be "global"
|
||||
x = "outer";
|
||||
echo x; // Should be "outer"
|
||||
|
||||
{
|
||||
echo x; // Should be "outer"
|
||||
x = "inner";
|
||||
echo x; // Should be "inner"
|
||||
|
||||
{
|
||||
echo x; // Should be "inner"
|
||||
x = "innermost";
|
||||
echo x; // Should be "innermost"
|
||||
}
|
||||
|
||||
echo x; // Should be "inner" again
|
||||
}
|
||||
|
||||
echo x; // Should be "outer" again
|
||||
}
|
||||
|
||||
echo x; // Should be "global" again
|
||||
`)
|
||||
|
||||
assert.Equal(t, 0, len(errors))
|
||||
}
|
||||
|
||||
func TestComplexExpressions(t *testing.T) {
|
||||
// Test complex expressions with multiple operators
|
||||
_, errors := executeMakoWithErrors(`
|
||||
// Arithmetic precedence
|
||||
result = 5 + 10 * 2; // Should be 25, not 30
|
||||
echo result;
|
||||
|
||||
// Parentheses override precedence
|
||||
result = (5 + 10) * 2; // Should be 30
|
||||
echo result;
|
||||
|
||||
// Combined comparison and logical operators
|
||||
x = 5;
|
||||
y = 10;
|
||||
z = 15;
|
||||
|
||||
result = x < y and y < z; // Should be true
|
||||
echo result;
|
||||
|
||||
result = x > y or y < z; // Should be true
|
||||
echo result;
|
||||
|
||||
result = not (x > y); // Should be true
|
||||
echo result;
|
||||
|
||||
// Complex conditional
|
||||
if x < y and y < z then
|
||||
echo "Condition passed";
|
||||
end
|
||||
`)
|
||||
|
||||
assert.Equal(t, 0, len(errors))
|
||||
}
|
||||
|
||||
func TestNestedTables(t *testing.T) {
|
||||
// Test nested tables and complex access patterns
|
||||
_, errors := executeMakoWithErrors(`
|
||||
// Define a nested table
|
||||
config = {
|
||||
server = {
|
||||
host = "localhost",
|
||||
port = 8080,
|
||||
settings = {
|
||||
timeout = 30,
|
||||
retries = 3
|
||||
}
|
||||
},
|
||||
database = {
|
||||
host = "db.example.com",
|
||||
port = 5432,
|
||||
credentials = {
|
||||
username = "admin",
|
||||
password = "secret"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Access nested values
|
||||
echo config["server"]["host"];
|
||||
echo config["server"]["settings"]["timeout"];
|
||||
echo config["database"]["credentials"]["username"];
|
||||
|
||||
// Update nested values
|
||||
config["server"]["settings"]["timeout"] = 60;
|
||||
echo config["server"]["settings"]["timeout"];
|
||||
|
||||
// Add new nested values
|
||||
config["logging"] = {
|
||||
level = "info",
|
||||
file = "app.log"
|
||||
};
|
||||
echo config["logging"]["level"];
|
||||
`)
|
||||
|
||||
assert.Equal(t, 0, len(errors))
|
||||
}
|
||||
|
||||
func TestTableAsArguments(t *testing.T) {
|
||||
// Test using tables as arguments
|
||||
_, errors := executeMakoWithErrors(`
|
||||
// Define a table
|
||||
person = {
|
||||
name = "John",
|
||||
age = 30
|
||||
};
|
||||
|
||||
// Use as index
|
||||
lookup = {
|
||||
John = "Developer",
|
||||
Jane = "Designer"
|
||||
};
|
||||
|
||||
// Access using value from another table
|
||||
role = lookup[person["name"]];
|
||||
echo role; // Should print "Developer"
|
||||
|
||||
// Test table as complex index
|
||||
matrix = {};
|
||||
matrix[{x=0, y=0}] = "origin";
|
||||
echo matrix[{x=0, y=0}]; // This might not work as expected yet
|
||||
`)
|
||||
|
||||
// Check if there are errors related to complex table indexing
|
||||
// The test might legitimately fail as complex indexing with tables
|
||||
// depends on the implementation details of how table equality is defined
|
||||
// If it works without errors, that's fine too
|
||||
t.Logf("Found %d errors in table indexing test", len(errors))
|
||||
}
|
202
tests/integration_test.go
Normal file
202
tests/integration_test.go
Normal file
@ -0,0 +1,202 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.sharkk.net/Sharkk/Mako/compiler"
|
||||
"git.sharkk.net/Sharkk/Mako/lexer"
|
||||
"git.sharkk.net/Sharkk/Mako/parser"
|
||||
"git.sharkk.net/Sharkk/Mako/vm"
|
||||
)
|
||||
|
||||
// Helper function to execute Mako code
|
||||
func executeMako(code string) *vm.VM {
|
||||
lex := lexer.New(code)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
bytecode := compiler.Compile(program)
|
||||
virtualMachine := vm.New()
|
||||
virtualMachine.Run(bytecode)
|
||||
return virtualMachine
|
||||
}
|
||||
|
||||
func TestBasicExecution(t *testing.T) {
|
||||
// We can't directly validate output, but we can check for absence of errors
|
||||
executeMako(`
|
||||
// Variables and echo
|
||||
x = 5;
|
||||
y = 10;
|
||||
echo x + y;
|
||||
`)
|
||||
}
|
||||
|
||||
func TestTableOperations(t *testing.T) {
|
||||
executeMako(`
|
||||
// Table creation and access
|
||||
person = {
|
||||
name = "John",
|
||||
age = 30,
|
||||
isActive = true
|
||||
};
|
||||
|
||||
echo person["name"];
|
||||
person["location"] = "New York";
|
||||
echo person["location"];
|
||||
`)
|
||||
}
|
||||
|
||||
func TestConditionalExecution(t *testing.T) {
|
||||
executeMako(`
|
||||
// If-else statements
|
||||
x = 5;
|
||||
|
||||
if x < 10 then
|
||||
echo "x is less than 10";
|
||||
else
|
||||
echo "x is not less than 10";
|
||||
end
|
||||
|
||||
// Nested if-else
|
||||
y = 20;
|
||||
|
||||
if x > y then
|
||||
echo "x is greater than y";
|
||||
elseif x < y then
|
||||
echo "x is less than y";
|
||||
else
|
||||
echo "x equals y";
|
||||
end
|
||||
`)
|
||||
}
|
||||
|
||||
func TestScopes(t *testing.T) {
|
||||
executeMako(`
|
||||
// Global scope
|
||||
x = 10;
|
||||
echo x;
|
||||
|
||||
// Enter a new scope
|
||||
{
|
||||
// Local scope - variable shadowing
|
||||
x = 20;
|
||||
echo x;
|
||||
|
||||
// New local variable
|
||||
y = 30;
|
||||
echo y;
|
||||
}
|
||||
|
||||
// Back to global scope
|
||||
echo x;
|
||||
`)
|
||||
}
|
||||
|
||||
func TestArithmeticOperations(t *testing.T) {
|
||||
executeMako(`
|
||||
// Basic arithmetic
|
||||
echo 5 + 10;
|
||||
echo 20 - 5;
|
||||
echo 4 * 5;
|
||||
echo 20 / 4;
|
||||
|
||||
// Compound expressions
|
||||
echo (5 + 10) * 2;
|
||||
echo 5 + 10 * 2;
|
||||
echo -5 + 10;
|
||||
`)
|
||||
}
|
||||
|
||||
func TestComparisonOperations(t *testing.T) {
|
||||
executeMako(`
|
||||
// Basic comparisons
|
||||
echo 5 == 5;
|
||||
echo 5 != 10;
|
||||
echo 5 < 10;
|
||||
echo 10 > 5;
|
||||
echo 5 <= 5;
|
||||
echo 5 >= 5;
|
||||
|
||||
// Compound comparisons
|
||||
echo 5 + 5 == 10;
|
||||
echo 5 * 2 != 15;
|
||||
`)
|
||||
}
|
||||
|
||||
func TestLogicalOperations(t *testing.T) {
|
||||
executeMako(`
|
||||
// Logical operators
|
||||
echo true and true;
|
||||
echo true and false;
|
||||
echo true or false;
|
||||
echo false or false;
|
||||
echo not true;
|
||||
echo not false;
|
||||
|
||||
// Short-circuit evaluation
|
||||
x = 5;
|
||||
echo true or (x = 10); // Should not change x
|
||||
echo x; // Should still be 5
|
||||
|
||||
echo false and (x = 15); // Should not change x
|
||||
echo x; // Should still be 5
|
||||
`)
|
||||
}
|
||||
|
||||
func TestComplexProgram(t *testing.T) {
|
||||
executeMako(`
|
||||
// Define a table to store data
|
||||
data = {
|
||||
users = {
|
||||
admin = {
|
||||
name = "Admin User",
|
||||
access = "full",
|
||||
active = true
|
||||
},
|
||||
guest = {
|
||||
name = "Guest User",
|
||||
access = "limited",
|
||||
active = true
|
||||
},
|
||||
blocked = {
|
||||
name = "Blocked User",
|
||||
access = "none",
|
||||
active = false
|
||||
}
|
||||
},
|
||||
settings = {
|
||||
theme = "dark",
|
||||
notifications = true,
|
||||
language = "en"
|
||||
}
|
||||
};
|
||||
|
||||
// Function to check if user has access
|
||||
// Since Mako doesn't have actual functions yet, we'll simulate with code blocks
|
||||
|
||||
// Get the user type from input (simulated)
|
||||
userType = "admin";
|
||||
|
||||
// Check access and print message
|
||||
if data["users"][userType]["active"] then
|
||||
access = data["users"][userType]["access"];
|
||||
|
||||
if access == "full" then
|
||||
echo "Welcome, Administrator!";
|
||||
elseif access == "limited" then
|
||||
echo "Welcome, Guest!";
|
||||
else
|
||||
echo "Access denied.";
|
||||
end
|
||||
else
|
||||
echo "User account is inactive.";
|
||||
end
|
||||
|
||||
// Update a setting
|
||||
data["settings"]["theme"] = "light";
|
||||
echo "Theme changed to: " + data["settings"]["theme"];
|
||||
|
||||
// Toggle notifications
|
||||
data["settings"]["notifications"] = not data["settings"]["notifications"];
|
||||
echo "Notifications: " + data["settings"]["notifications"];
|
||||
`)
|
||||
}
|
237
tests/lexer_test.go
Normal file
237
tests/lexer_test.go
Normal file
@ -0,0 +1,237 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "git.sharkk.net/Go/Assert"
|
||||
"git.sharkk.net/Sharkk/Mako/lexer"
|
||||
)
|
||||
|
||||
func TestLexerSimpleTokens(t *testing.T) {
|
||||
input := `= + - * / ( ) { } [ ] , ; "hello" 123 if then else end true false`
|
||||
|
||||
lex := lexer.New(input)
|
||||
|
||||
expected := []struct {
|
||||
expectedType lexer.TokenType
|
||||
expectedValue string
|
||||
}{
|
||||
{lexer.TokenEqual, "="},
|
||||
{lexer.TokenPlus, "+"},
|
||||
{lexer.TokenMinus, "-"},
|
||||
{lexer.TokenStar, "*"},
|
||||
{lexer.TokenSlash, "/"},
|
||||
{lexer.TokenLeftParen, "("},
|
||||
{lexer.TokenRightParen, ")"},
|
||||
{lexer.TokenLeftBrace, "{"},
|
||||
{lexer.TokenRightBrace, "}"},
|
||||
{lexer.TokenLeftBracket, "["},
|
||||
{lexer.TokenRightBracket, "]"},
|
||||
{lexer.TokenComma, ","},
|
||||
{lexer.TokenSemicolon, ";"},
|
||||
{lexer.TokenString, "hello"},
|
||||
{lexer.TokenNumber, "123"},
|
||||
{lexer.TokenIf, "if"},
|
||||
{lexer.TokenThen, "then"},
|
||||
{lexer.TokenElse, "else"},
|
||||
{lexer.TokenEnd, "end"},
|
||||
{lexer.TokenTrue, "true"},
|
||||
{lexer.TokenFalse, "false"},
|
||||
{lexer.TokenEOF, ""},
|
||||
}
|
||||
|
||||
for _, exp := range expected {
|
||||
tok := lex.NextToken()
|
||||
assert.Equal(t, exp.expectedType, tok.Type)
|
||||
assert.Equal(t, exp.expectedValue, tok.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLexerCompoundTokens(t *testing.T) {
|
||||
input := `== != < > <= >= and or not elseif`
|
||||
|
||||
lex := lexer.New(input)
|
||||
|
||||
expected := []struct {
|
||||
expectedType lexer.TokenType
|
||||
expectedValue string
|
||||
}{
|
||||
{lexer.TokenEqualEqual, "=="},
|
||||
{lexer.TokenNotEqual, "!="},
|
||||
{lexer.TokenLessThan, "<"},
|
||||
{lexer.TokenGreaterThan, ">"},
|
||||
{lexer.TokenLessEqual, "<="},
|
||||
{lexer.TokenGreaterEqual, ">="},
|
||||
{lexer.TokenAnd, "and"},
|
||||
{lexer.TokenOr, "or"},
|
||||
{lexer.TokenNot, "not"},
|
||||
{lexer.TokenElseIf, "elseif"},
|
||||
{lexer.TokenEOF, ""},
|
||||
}
|
||||
|
||||
for _, exp := range expected {
|
||||
tok := lex.NextToken()
|
||||
assert.Equal(t, exp.expectedType, tok.Type)
|
||||
assert.Equal(t, exp.expectedValue, tok.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLexerIdentifiersAndKeywords(t *testing.T) {
|
||||
input := `variable echo if then else end true false and or not x y_1 _var UPPERCASE`
|
||||
|
||||
lex := lexer.New(input)
|
||||
|
||||
expected := []struct {
|
||||
expectedType lexer.TokenType
|
||||
expectedValue string
|
||||
}{
|
||||
{lexer.TokenIdentifier, "variable"},
|
||||
{lexer.TokenEcho, "echo"},
|
||||
{lexer.TokenIf, "if"},
|
||||
{lexer.TokenThen, "then"},
|
||||
{lexer.TokenElse, "else"},
|
||||
{lexer.TokenEnd, "end"},
|
||||
{lexer.TokenTrue, "true"},
|
||||
{lexer.TokenFalse, "false"},
|
||||
{lexer.TokenAnd, "and"},
|
||||
{lexer.TokenOr, "or"},
|
||||
{lexer.TokenNot, "not"},
|
||||
{lexer.TokenIdentifier, "x"},
|
||||
{lexer.TokenIdentifier, "y_1"},
|
||||
{lexer.TokenIdentifier, "_var"},
|
||||
{lexer.TokenIdentifier, "UPPERCASE"},
|
||||
{lexer.TokenEOF, ""},
|
||||
}
|
||||
|
||||
for _, exp := range expected {
|
||||
tok := lex.NextToken()
|
||||
assert.Equal(t, exp.expectedType, tok.Type)
|
||||
assert.Equal(t, exp.expectedValue, tok.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLexerNumbers(t *testing.T) {
|
||||
input := `0 123 999999`
|
||||
|
||||
lex := lexer.New(input)
|
||||
|
||||
expected := []struct {
|
||||
expectedType lexer.TokenType
|
||||
expectedValue string
|
||||
}{
|
||||
{lexer.TokenNumber, "0"},
|
||||
{lexer.TokenNumber, "123"},
|
||||
{lexer.TokenNumber, "999999"},
|
||||
{lexer.TokenEOF, ""},
|
||||
}
|
||||
|
||||
for _, exp := range expected {
|
||||
tok := lex.NextToken()
|
||||
assert.Equal(t, exp.expectedType, tok.Type)
|
||||
assert.Equal(t, exp.expectedValue, tok.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLexerStrings(t *testing.T) {
|
||||
input := `"" "hello" "one two three" "special chars: !@#$%^&*()"`
|
||||
|
||||
lex := lexer.New(input)
|
||||
|
||||
expected := []struct {
|
||||
expectedType lexer.TokenType
|
||||
expectedValue string
|
||||
}{
|
||||
{lexer.TokenString, ""},
|
||||
{lexer.TokenString, "hello"},
|
||||
{lexer.TokenString, "one two three"},
|
||||
{lexer.TokenString, "special chars: !@#$%^&*()"},
|
||||
{lexer.TokenEOF, ""},
|
||||
}
|
||||
|
||||
for _, exp := range expected {
|
||||
tok := lex.NextToken()
|
||||
assert.Equal(t, exp.expectedType, tok.Type)
|
||||
assert.Equal(t, exp.expectedValue, tok.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLexerComments(t *testing.T) {
|
||||
input := `x = 5 // This is a comment
|
||||
y = 10 // Another comment`
|
||||
|
||||
lex := lexer.New(input)
|
||||
|
||||
expected := []struct {
|
||||
expectedType lexer.TokenType
|
||||
expectedValue string
|
||||
}{
|
||||
{lexer.TokenIdentifier, "x"},
|
||||
{lexer.TokenEqual, "="},
|
||||
{lexer.TokenNumber, "5"},
|
||||
{lexer.TokenIdentifier, "y"},
|
||||
{lexer.TokenEqual, "="},
|
||||
{lexer.TokenNumber, "10"},
|
||||
{lexer.TokenEOF, ""},
|
||||
}
|
||||
|
||||
for _, exp := range expected {
|
||||
tok := lex.NextToken()
|
||||
assert.Equal(t, exp.expectedType, tok.Type)
|
||||
assert.Equal(t, exp.expectedValue, tok.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLexerCompositeCode(t *testing.T) {
|
||||
input := `
|
||||
// Sample Mako code
|
||||
x = 10;
|
||||
y = 20;
|
||||
if x < y then
|
||||
echo "x is less than y";
|
||||
else
|
||||
echo "x is not less than y";
|
||||
end
|
||||
|
||||
// Table example
|
||||
table = {
|
||||
name = "John",
|
||||
age = 30,
|
||||
isActive = true
|
||||
};
|
||||
|
||||
echo table["name"];
|
||||
`
|
||||
|
||||
lex := lexer.New(input)
|
||||
|
||||
// Spot check some tokens to avoid an overly verbose test
|
||||
assert.Equal(t, lexer.TokenIdentifier, lex.NextToken().Type) // x
|
||||
assert.Equal(t, lexer.TokenEqual, lex.NextToken().Type) // =
|
||||
assert.Equal(t, lexer.TokenNumber, lex.NextToken().Type) // 10
|
||||
assert.Equal(t, lexer.TokenSemicolon, lex.NextToken().Type) // ;
|
||||
|
||||
// Skip ahead to check table creation
|
||||
for i := 0; i < 13; i++ {
|
||||
lex.NextToken()
|
||||
}
|
||||
|
||||
assert.Equal(t, lexer.TokenIdentifier, lex.NextToken().Type) // table
|
||||
assert.Equal(t, lexer.TokenEqual, lex.NextToken().Type) // =
|
||||
assert.Equal(t, lexer.TokenLeftBrace, lex.NextToken().Type) // {
|
||||
|
||||
// Check echo table["name"]
|
||||
for i := 0; i < 15; i++ {
|
||||
lex.NextToken()
|
||||
}
|
||||
|
||||
tok := lex.NextToken()
|
||||
assert.Equal(t, lexer.TokenEcho, tok.Type)
|
||||
tok = lex.NextToken()
|
||||
assert.Equal(t, lexer.TokenIdentifier, tok.Type)
|
||||
assert.Equal(t, "table", tok.Value)
|
||||
tok = lex.NextToken()
|
||||
assert.Equal(t, lexer.TokenLeftBracket, tok.Type)
|
||||
tok = lex.NextToken()
|
||||
assert.Equal(t, lexer.TokenString, tok.Type)
|
||||
assert.Equal(t, "name", tok.Value)
|
||||
}
|
47
tests/main_test.go
Normal file
47
tests/main_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "git.sharkk.net/Go/Assert"
|
||||
)
|
||||
|
||||
// This file serves as a meta-test to make sure all our tests are working
|
||||
|
||||
func TestTestFramework(t *testing.T) {
|
||||
// Verify that our assert package works
|
||||
assert.Equal(t, 5, 5)
|
||||
assert.NotEqual(t, 5, 10)
|
||||
assert.True(t, true)
|
||||
assert.False(t, false)
|
||||
assert.NotNil(t, "not nil")
|
||||
assert.Contains(t, "Hello World", "World")
|
||||
assert.NotContains(t, "Hello World", "Goodbye")
|
||||
|
||||
// Create a failing test context that doesn't actually fail
|
||||
testFailContext := new(testing.T)
|
||||
failingTestContext := &testContext{T: testFailContext}
|
||||
|
||||
// These should not cause the overall test to fail
|
||||
assert.Equal(failingTestContext, 1, 2)
|
||||
assert.NotEqual(failingTestContext, 5, 5)
|
||||
|
||||
// Check that the failing tests did record failures
|
||||
assert.True(t, failingTestContext.failed)
|
||||
assert.Equal(t, 2, failingTestContext.failCount)
|
||||
}
|
||||
|
||||
// Helper type to capture test failures without actually failing the test
|
||||
type testContext struct {
|
||||
*testing.T
|
||||
failed bool
|
||||
failCount int
|
||||
}
|
||||
|
||||
func (t *testContext) Errorf(format string, args ...any) {
|
||||
t.failCount++
|
||||
}
|
||||
|
||||
func (t *testContext) FailNow() {
|
||||
t.failed = true
|
||||
}
|
345
tests/parser_test.go
Normal file
345
tests/parser_test.go
Normal file
@ -0,0 +1,345 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "git.sharkk.net/Go/Assert"
|
||||
"git.sharkk.net/Sharkk/Mako/lexer"
|
||||
"git.sharkk.net/Sharkk/Mako/parser"
|
||||
)
|
||||
|
||||
func TestParseVariableStatement(t *testing.T) {
|
||||
input := `x = 5;`
|
||||
|
||||
lex := lexer.New(input)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
|
||||
assert.Equal(t, 0, len(p.Errors()))
|
||||
assert.Equal(t, 1, len(program.Statements))
|
||||
|
||||
stmt, ok := program.Statements[0].(*parser.VariableStatement)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "x", stmt.Name.Value)
|
||||
|
||||
// Test the expression value
|
||||
numLit, ok := stmt.Value.(*parser.NumberLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 5.0, numLit.Value)
|
||||
}
|
||||
|
||||
func TestParseEchoStatement(t *testing.T) {
|
||||
input := `echo "hello";`
|
||||
|
||||
lex := lexer.New(input)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
|
||||
assert.Equal(t, 0, len(p.Errors()))
|
||||
assert.Equal(t, 1, len(program.Statements))
|
||||
|
||||
stmt, ok := program.Statements[0].(*parser.EchoStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
strLit, ok := stmt.Value.(*parser.StringLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "hello", strLit.Value)
|
||||
}
|
||||
|
||||
func TestParseTableLiteral(t *testing.T) {
|
||||
input := `table = { name = "John", age = 30 };`
|
||||
|
||||
lex := lexer.New(input)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
|
||||
assert.Equal(t, 0, len(p.Errors()))
|
||||
assert.Equal(t, 1, len(program.Statements))
|
||||
|
||||
stmt, ok := program.Statements[0].(*parser.VariableStatement)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "table", stmt.Name.Value)
|
||||
|
||||
tableLit, ok := stmt.Value.(*parser.TableLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 2, len(tableLit.Pairs))
|
||||
|
||||
// Check that the table has the expected keys and values
|
||||
// We need to find the entries in the pairs map
|
||||
foundName := false
|
||||
foundAge := false
|
||||
|
||||
for key, value := range tableLit.Pairs {
|
||||
if ident, ok := key.(*parser.Identifier); ok && ident.Value == "name" {
|
||||
foundName = true
|
||||
strLit, ok := value.(*parser.StringLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "John", strLit.Value)
|
||||
}
|
||||
|
||||
if ident, ok := key.(*parser.Identifier); ok && ident.Value == "age" {
|
||||
foundAge = true
|
||||
numLit, ok := value.(*parser.NumberLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 30.0, numLit.Value)
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, foundName)
|
||||
assert.True(t, foundAge)
|
||||
}
|
||||
|
||||
func TestParseIndexExpression(t *testing.T) {
|
||||
input := `table["key"];`
|
||||
|
||||
lex := lexer.New(input)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
|
||||
assert.Equal(t, 0, len(p.Errors()))
|
||||
assert.Equal(t, 1, len(program.Statements))
|
||||
|
||||
exprStmt, ok := program.Statements[0].(*parser.ExpressionStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
indexExpr, ok := exprStmt.Expression.(*parser.IndexExpression)
|
||||
assert.True(t, ok)
|
||||
|
||||
ident, ok := indexExpr.Left.(*parser.Identifier)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "table", ident.Value)
|
||||
|
||||
strLit, ok := indexExpr.Index.(*parser.StringLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "key", strLit.Value)
|
||||
}
|
||||
|
||||
func TestParseInfixExpression(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
leftValue float64
|
||||
operator string
|
||||
rightValue float64
|
||||
}{
|
||||
{"5 + 5;", 5, "+", 5},
|
||||
{"5 - 5;", 5, "-", 5},
|
||||
{"5 * 5;", 5, "*", 5},
|
||||
{"5 / 5;", 5, "/", 5},
|
||||
{"5 < 5;", 5, "<", 5},
|
||||
{"5 > 5;", 5, ">", 5},
|
||||
{"5 == 5;", 5, "==", 5},
|
||||
{"5 != 5;", 5, "!=", 5},
|
||||
{"5 <= 5;", 5, "<=", 5},
|
||||
{"5 >= 5;", 5, ">=", 5},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
lex := lexer.New(tt.input)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
|
||||
assert.Equal(t, 0, len(p.Errors()))
|
||||
assert.Equal(t, 1, len(program.Statements))
|
||||
|
||||
exprStmt, ok := program.Statements[0].(*parser.ExpressionStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
infixExpr, ok := exprStmt.Expression.(*parser.InfixExpression)
|
||||
assert.True(t, ok)
|
||||
|
||||
leftLit, ok := infixExpr.Left.(*parser.NumberLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tt.leftValue, leftLit.Value)
|
||||
|
||||
assert.Equal(t, tt.operator, infixExpr.Operator)
|
||||
|
||||
rightLit, ok := infixExpr.Right.(*parser.NumberLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tt.rightValue, rightLit.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePrefixExpression(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
operator string
|
||||
value float64
|
||||
}{
|
||||
{"-5;", "-", 5},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
lex := lexer.New(tt.input)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
|
||||
assert.Equal(t, 0, len(p.Errors()))
|
||||
assert.Equal(t, 1, len(program.Statements))
|
||||
|
||||
exprStmt, ok := program.Statements[0].(*parser.ExpressionStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
prefixExpr, ok := exprStmt.Expression.(*parser.PrefixExpression)
|
||||
assert.True(t, ok)
|
||||
|
||||
assert.Equal(t, tt.operator, prefixExpr.Operator)
|
||||
|
||||
rightLit, ok := prefixExpr.Right.(*parser.NumberLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tt.value, rightLit.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBooleanLiteral(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
value bool
|
||||
}{
|
||||
{"true;", true},
|
||||
{"false;", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
lex := lexer.New(tt.input)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
|
||||
assert.Equal(t, 0, len(p.Errors()))
|
||||
assert.Equal(t, 1, len(program.Statements))
|
||||
|
||||
exprStmt, ok := program.Statements[0].(*parser.ExpressionStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
boolLit, ok := exprStmt.Expression.(*parser.BooleanLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tt.value, boolLit.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIfExpression(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()
|
||||
|
||||
assert.Equal(t, 0, len(p.Errors()))
|
||||
assert.Equal(t, 1, len(program.Statements))
|
||||
|
||||
exprStmt, ok := program.Statements[0].(*parser.ExpressionStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
ifExpr, ok := exprStmt.Expression.(*parser.IfExpression)
|
||||
assert.True(t, ok)
|
||||
|
||||
// Check condition
|
||||
condition, ok := ifExpr.Condition.(*parser.InfixExpression)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "<", condition.Operator)
|
||||
|
||||
// Check consequence
|
||||
assert.Equal(t, 1, len(ifExpr.Consequence.Statements))
|
||||
|
||||
consEchoStmt, ok := ifExpr.Consequence.Statements[0].(*parser.EchoStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
consStrLit, ok := consEchoStmt.Value.(*parser.StringLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "x is less than 10", consStrLit.Value)
|
||||
|
||||
// Check alternative
|
||||
assert.NotNil(t, ifExpr.Alternative)
|
||||
assert.Equal(t, 1, len(ifExpr.Alternative.Statements))
|
||||
|
||||
altEchoStmt, ok := ifExpr.Alternative.Statements[0].(*parser.EchoStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
altStrLit, ok := altEchoStmt.Value.(*parser.StringLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "x is not less than 10", altStrLit.Value)
|
||||
}
|
||||
|
||||
func TestParseElseIfExpression(t *testing.T) {
|
||||
input := `if x < 10 then
|
||||
echo "x is less than 10";
|
||||
elseif x < 20 then
|
||||
echo "x is less than 20";
|
||||
else
|
||||
echo "x is not less than 20";
|
||||
end`
|
||||
|
||||
lex := lexer.New(input)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
|
||||
assert.Equal(t, 0, len(p.Errors()))
|
||||
assert.Equal(t, 1, len(program.Statements))
|
||||
|
||||
exprStmt, ok := program.Statements[0].(*parser.ExpressionStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
ifExpr, ok := exprStmt.Expression.(*parser.IfExpression)
|
||||
assert.True(t, ok)
|
||||
|
||||
// Check that we have an alternative block
|
||||
assert.NotNil(t, ifExpr.Alternative)
|
||||
assert.Equal(t, 1, len(ifExpr.Alternative.Statements))
|
||||
|
||||
// The alternative should contain another IfExpression (the elseif)
|
||||
altExprStmt, ok := ifExpr.Alternative.Statements[0].(*parser.ExpressionStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
nestedIfExpr, ok := altExprStmt.Expression.(*parser.IfExpression)
|
||||
assert.True(t, ok)
|
||||
|
||||
// Check nested if condition
|
||||
condition, ok := nestedIfExpr.Condition.(*parser.InfixExpression)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "<", condition.Operator)
|
||||
|
||||
rightLit, ok := condition.Right.(*parser.NumberLiteral)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 20.0, rightLit.Value)
|
||||
|
||||
// Check that the nested if has an alternative (the else)
|
||||
assert.NotNil(t, nestedIfExpr.Alternative)
|
||||
}
|
||||
|
||||
func TestParseLogicalOperators(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
operator string
|
||||
}{
|
||||
{"true and false;", "and"},
|
||||
{"true or false;", "or"},
|
||||
{"not true;", "not"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
lex := lexer.New(tt.input)
|
||||
p := parser.New(lex)
|
||||
program := p.ParseProgram()
|
||||
|
||||
assert.Equal(t, 0, len(p.Errors()))
|
||||
assert.Equal(t, 1, len(program.Statements))
|
||||
|
||||
exprStmt, ok := program.Statements[0].(*parser.ExpressionStatement)
|
||||
assert.True(t, ok)
|
||||
|
||||
if tt.operator == "not" {
|
||||
// Test prefix NOT
|
||||
prefixExpr, ok := exprStmt.Expression.(*parser.PrefixExpression)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tt.operator, prefixExpr.Operator)
|
||||
} else {
|
||||
// Test infix AND/OR
|
||||
infixExpr, ok := exprStmt.Expression.(*parser.InfixExpression)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tt.operator, infixExpr.Operator)
|
||||
}
|
||||
}
|
||||
}
|
307
tests/vm_test.go
Normal file
307
tests/vm_test.go
Normal file
@ -0,0 +1,307 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.sharkk.net/Sharkk/Mako/types"
|
||||
"git.sharkk.net/Sharkk/Mako/vm"
|
||||
)
|
||||
|
||||
func TestVMPushPop(t *testing.T) {
|
||||
vm := vm.New()
|
||||
|
||||
// Create basic bytecode that pushes constants and then pops
|
||||
bytecode := &types.Bytecode{
|
||||
Constants: []any{5.0, "hello", true},
|
||||
Instructions: []types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push "hello"
|
||||
{Opcode: types.OpConstant, Operand: 2}, // Push true
|
||||
{Opcode: types.OpPop, Operand: 0}, // Pop true
|
||||
{Opcode: types.OpPop, Operand: 0}, // Pop "hello"
|
||||
{Opcode: types.OpPop, Operand: 0}, // Pop 5.0
|
||||
},
|
||||
}
|
||||
|
||||
// Run the VM
|
||||
vm.Run(bytecode)
|
||||
|
||||
// VM doesn't expose stack, so we can only test that it completes without error
|
||||
// This is a simple smoke test
|
||||
}
|
||||
|
||||
func TestVMArithmetic(t *testing.T) {
|
||||
tests := []struct {
|
||||
constants []any
|
||||
instructions []types.Instruction
|
||||
expected float64
|
||||
}{
|
||||
{
|
||||
// 5 + 10
|
||||
[]any{5.0, 10.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push 10.0
|
||||
{Opcode: types.OpAdd, Operand: 0}, // Add
|
||||
},
|
||||
15.0,
|
||||
},
|
||||
{
|
||||
// 5 - 10
|
||||
[]any{5.0, 10.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push 10.0
|
||||
{Opcode: types.OpSubtract, Operand: 0}, // Subtract
|
||||
},
|
||||
-5.0,
|
||||
},
|
||||
{
|
||||
// 5 * 10
|
||||
[]any{5.0, 10.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push 10.0
|
||||
{Opcode: types.OpMultiply, Operand: 0}, // Multiply
|
||||
},
|
||||
50.0,
|
||||
},
|
||||
{
|
||||
// 10 / 5
|
||||
[]any{10.0, 5.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 10.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push 5.0
|
||||
{Opcode: types.OpDivide, Operand: 0}, // Divide
|
||||
},
|
||||
2.0,
|
||||
},
|
||||
{
|
||||
// -5
|
||||
[]any{5.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpNegate, Operand: 0}, // Negate
|
||||
},
|
||||
-5.0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
vm := vm.New()
|
||||
|
||||
// To test VM operations, we need to expose the result
|
||||
// So we add an OpSetGlobal instruction to save the result
|
||||
// Then we can retrieve it and check
|
||||
constants := append(tt.constants, "result")
|
||||
instructions := append(tt.instructions,
|
||||
types.Instruction{Opcode: types.OpSetGlobal, Operand: len(constants) - 1})
|
||||
|
||||
bytecode := &types.Bytecode{
|
||||
Constants: constants,
|
||||
Instructions: instructions,
|
||||
}
|
||||
|
||||
vm.Run(bytecode)
|
||||
|
||||
// Now we need to retrieve the global variable 'result'
|
||||
// Create bytecode to get the result
|
||||
retrieveBytecode := &types.Bytecode{
|
||||
Constants: []any{"result"},
|
||||
Instructions: []types.Instruction{
|
||||
{Opcode: types.OpGetGlobal, Operand: 0}, // Get result
|
||||
{Opcode: types.OpSetGlobal, Operand: 0}, // Set result again (will keep the value for examination)
|
||||
},
|
||||
}
|
||||
|
||||
vm.Run(retrieveBytecode)
|
||||
|
||||
// Access the VM's global values map
|
||||
// This requires modifying vm.go to expose this, so for now we just skip validation
|
||||
// In a real test, we'd add a method to VM to retrieve global values
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMComparisons(t *testing.T) {
|
||||
tests := []struct {
|
||||
constants []any
|
||||
instructions []types.Instruction
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
// 5 == 5
|
||||
[]any{5.0, 5.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push 5.0
|
||||
{Opcode: types.OpEqual, Operand: 0}, // Equal
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
// 5 != 10
|
||||
[]any{5.0, 10.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push 10.0
|
||||
{Opcode: types.OpNotEqual, Operand: 0}, // Not Equal
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
// 5 < 10
|
||||
[]any{5.0, 10.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push 10.0
|
||||
{Opcode: types.OpLessThan, Operand: 0}, // Less Than
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
// 10 > 5
|
||||
[]any{10.0, 5.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 10.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push 5.0
|
||||
{Opcode: types.OpGreaterThan, Operand: 0}, // Greater Than
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
// 5 <= 5
|
||||
[]any{5.0, 5.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push 5.0
|
||||
{Opcode: types.OpLessEqual, Operand: 0}, // Less Than or Equal
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
// 5 >= 5
|
||||
[]any{5.0, 5.0},
|
||||
[]types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push 5.0
|
||||
{Opcode: types.OpGreaterEqual, Operand: 0}, // Greater Than or Equal
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
vm := vm.New()
|
||||
|
||||
// Similar to arithmetic test, we store the result in a global variable
|
||||
constants := append(tt.constants, "result")
|
||||
instructions := append(tt.instructions,
|
||||
types.Instruction{Opcode: types.OpSetGlobal, Operand: len(constants) - 1})
|
||||
|
||||
bytecode := &types.Bytecode{
|
||||
Constants: constants,
|
||||
Instructions: instructions,
|
||||
}
|
||||
|
||||
vm.Run(bytecode)
|
||||
|
||||
// We would check the result, but again we'd need to expose vm.globals
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMTableOperations(t *testing.T) {
|
||||
// Create bytecode for: table = {}; table["key"] = "value"; result = table["key"]
|
||||
bytecode := &types.Bytecode{
|
||||
Constants: []any{"table", "key", "value", "result"},
|
||||
Instructions: []types.Instruction{
|
||||
{Opcode: types.OpNewTable, Operand: 0}, // Create new table
|
||||
{Opcode: types.OpSetGlobal, Operand: 0}, // Store in global "table"
|
||||
{Opcode: types.OpGetGlobal, Operand: 0}, // Push table onto stack
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push "key"
|
||||
{Opcode: types.OpConstant, Operand: 2}, // Push "value"
|
||||
{Opcode: types.OpSetIndex, Operand: 0}, // Set table["key"] = "value"
|
||||
{Opcode: types.OpPop, Operand: 0}, // Pop the result of SetIndex
|
||||
{Opcode: types.OpGetGlobal, Operand: 0}, // Push table onto stack again
|
||||
{Opcode: types.OpConstant, Operand: 1}, // Push "key"
|
||||
{Opcode: types.OpGetIndex, Operand: 0}, // Get table["key"]
|
||||
{Opcode: types.OpSetGlobal, Operand: 3}, // Store in global "result"
|
||||
},
|
||||
}
|
||||
|
||||
vm := vm.New()
|
||||
vm.Run(bytecode)
|
||||
|
||||
// Again, we'd check the result, but we need to expose vm.globals
|
||||
}
|
||||
|
||||
func TestVMConditionalJumps(t *testing.T) {
|
||||
// Create bytecode for: if true then result = "true" else result = "false" end
|
||||
bytecode := &types.Bytecode{
|
||||
Constants: []any{true, "result", "true", "false"},
|
||||
Instructions: []types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push true
|
||||
{Opcode: types.OpJumpIfFalse, Operand: 6}, // Jump to else branch if false
|
||||
{Opcode: types.OpConstant, Operand: 2}, // Push "true"
|
||||
{Opcode: types.OpSetGlobal, Operand: 1}, // Set result = "true"
|
||||
{Opcode: types.OpJump, Operand: 8}, // Jump to end
|
||||
{Opcode: types.OpConstant, Operand: 3}, // Push "false"
|
||||
{Opcode: types.OpSetGlobal, Operand: 1}, // Set result = "false"
|
||||
},
|
||||
}
|
||||
|
||||
vm := vm.New()
|
||||
vm.Run(bytecode)
|
||||
|
||||
// We'd check result == "true", but we need to expose vm.globals
|
||||
}
|
||||
|
||||
func TestVMScopes(t *testing.T) {
|
||||
// Create bytecode for:
|
||||
// x = 5
|
||||
// {
|
||||
// x = 10
|
||||
// y = 20
|
||||
// }
|
||||
// result = x
|
||||
bytecode := &types.Bytecode{
|
||||
Constants: []any{5.0, "x", 10.0, 20.0, "y", "result"},
|
||||
Instructions: []types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push 5.0
|
||||
{Opcode: types.OpSetGlobal, Operand: 1}, // Set global x = 5
|
||||
{Opcode: types.OpEnterScope, Operand: 0}, // Enter new scope
|
||||
{Opcode: types.OpConstant, Operand: 2}, // Push 10.0
|
||||
{Opcode: types.OpSetLocal, Operand: 1}, // Set local x = 10
|
||||
{Opcode: types.OpConstant, Operand: 3}, // Push 20.0
|
||||
{Opcode: types.OpSetLocal, Operand: 4}, // Set local y = 20
|
||||
{Opcode: types.OpExitScope, Operand: 0}, // Exit scope
|
||||
{Opcode: types.OpGetGlobal, Operand: 1}, // Get global x
|
||||
{Opcode: types.OpSetGlobal, Operand: 5}, // Set result = x
|
||||
},
|
||||
}
|
||||
|
||||
vm := vm.New()
|
||||
vm.Run(bytecode)
|
||||
|
||||
// We'd check result == 5.0 (global x, not the shadowed local x)
|
||||
}
|
||||
|
||||
func TestVMLogicalOperators(t *testing.T) {
|
||||
// Test NOT operator
|
||||
notBytecode := &types.Bytecode{
|
||||
Constants: []any{true, "result"},
|
||||
Instructions: []types.Instruction{
|
||||
{Opcode: types.OpConstant, Operand: 0}, // Push true
|
||||
{Opcode: types.OpNot, Operand: 0}, // NOT operation
|
||||
{Opcode: types.OpSetGlobal, Operand: 1}, // Set result
|
||||
},
|
||||
}
|
||||
|
||||
vm := vm.New()
|
||||
vm.Run(notBytecode)
|
||||
|
||||
// We'd check result == false
|
||||
|
||||
// For AND and OR, we'd need to implement more complex tests that check
|
||||
// short-circuit behavior, but they're dependent on the conditional jumps
|
||||
// which we already tested separately
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user