package compiler_test import ( "testing" "git.sharkk.net/Sharkk/Mako/compiler" "git.sharkk.net/Sharkk/Mako/parser" ) // Helper function to compile source code and return chunk func compileSource(t *testing.T, source string) *compiler.Chunk { lexer := parser.NewLexer(source) p := parser.NewParser(lexer) program := p.ParseProgram() if p.HasErrors() { t.Fatalf("Parser errors: %v", p.ErrorStrings()) } comp := compiler.NewCompiler() chunk, errors := comp.Compile(program) if len(errors) > 0 { t.Fatalf("Compiler errors: %v", errors) } return chunk } // Helper to check instruction at position func checkInstruction(t *testing.T, chunk *compiler.Chunk, pos int, expected compiler.Opcode, operands ...uint16) { if pos >= len(chunk.Code) { t.Fatalf("Position %d out of bounds (code length: %d)", pos, len(chunk.Code)) } op, actualOperands, _ := compiler.DecodeInstruction(chunk.Code, pos) if op != expected { t.Errorf("Expected opcode %v at position %d, got %v", expected, pos, op) } if len(actualOperands) != len(operands) { t.Errorf("Expected %d operands, got %d", len(operands), len(actualOperands)) return } for i, expected := range operands { if actualOperands[i] != expected { t.Errorf("Expected operand %d to be %d, got %d", i, expected, actualOperands[i]) } } } // Test literal compilation func TestNumberLiteral(t *testing.T) { chunk := compileSource(t, "echo 42") // Should have one constant (42) and load it if len(chunk.Constants) != 1 { t.Fatalf("Expected 1 constant, got %d", len(chunk.Constants)) } if chunk.Constants[0].Type != compiler.ValueNumber { t.Errorf("Expected number constant, got %v", chunk.Constants[0].Type) } if chunk.Constants[0].Data.(float64) != 42.0 { t.Errorf("Expected constant value 42, got %v", chunk.Constants[0].Data) } // Check bytecode: OpLoadConst 0, OpEcho, OpReturnNil checkInstruction(t, chunk, 0, compiler.OpLoadConst, 0) checkInstruction(t, chunk, 3, compiler.OpEcho) checkInstruction(t, chunk, 4, compiler.OpReturnNil) } func TestStringLiteral(t *testing.T) { chunk := compileSource(t, `echo "hello"`) if len(chunk.Constants) != 1 { t.Fatalf("Expected 1 constant, got %d", len(chunk.Constants)) } if chunk.Constants[0].Type != compiler.ValueString { t.Errorf("Expected string constant, got %v", chunk.Constants[0].Type) } if chunk.Constants[0].Data.(string) != "hello" { t.Errorf("Expected constant value 'hello', got %v", chunk.Constants[0].Data) } checkInstruction(t, chunk, 0, compiler.OpLoadConst, 0) } func TestBooleanLiterals(t *testing.T) { chunk := compileSource(t, "echo true") if chunk.Constants[0].Type != compiler.ValueBool { t.Errorf("Expected bool constant, got %v", chunk.Constants[0].Type) } if chunk.Constants[0].Data.(bool) != true { t.Errorf("Expected true, got %v", chunk.Constants[0].Data) } } func TestNilLiteral(t *testing.T) { chunk := compileSource(t, "echo nil") if chunk.Constants[0].Type != compiler.ValueNil { t.Errorf("Expected nil constant, got %v", chunk.Constants[0].Type) } } // Test arithmetic operations func TestArithmetic(t *testing.T) { tests := []struct { source string expected compiler.Opcode }{ {"echo 1 + 2", compiler.OpAdd}, {"echo 5 - 3", compiler.OpSub}, {"echo 4 * 6", compiler.OpMul}, {"echo 8 / 2", compiler.OpDiv}, } for _, test := range tests { chunk := compileSource(t, test.source) // Should have: LoadConst 0, LoadConst 1, OpArithmetic, OpEcho, OpReturnNil checkInstruction(t, chunk, 0, compiler.OpLoadConst, 0) checkInstruction(t, chunk, 3, compiler.OpLoadConst, 1) checkInstruction(t, chunk, 6, test.expected) checkInstruction(t, chunk, 7, compiler.OpEcho) } } // Test comparison operations func TestComparison(t *testing.T) { tests := []struct { source string expected compiler.Opcode }{ {"echo 1 == 2", compiler.OpEq}, {"echo 1 != 2", compiler.OpNeq}, {"echo 1 < 2", compiler.OpLt}, {"echo 1 <= 2", compiler.OpLte}, {"echo 1 > 2", compiler.OpGt}, {"echo 1 >= 2", compiler.OpGte}, } for _, test := range tests { chunk := compileSource(t, test.source) checkInstruction(t, chunk, 6, test.expected) } } // Test prefix operations func TestPrefixOperations(t *testing.T) { tests := []struct { source string expected compiler.Opcode }{ {"echo -42", compiler.OpNeg}, {"echo not true", compiler.OpNot}, } for _, test := range tests { chunk := compileSource(t, test.source) checkInstruction(t, chunk, 3, test.expected) } } // Test variable assignment func TestLocalAssignment(t *testing.T) { // Test local assignment within a function scope chunk := compileSource(t, ` fn test() x: number = 42 end `) // This tests function compilation which is not yet implemented // For now, just check that it doesn't crash if chunk == nil { t.Skip("Function compilation not yet implemented") } } func TestGlobalAssignment(t *testing.T) { chunk := compileSource(t, "x = 42") // Should have: LoadConst 0, StoreGlobal 1, OpReturnNil // Constants: [42, "x"] if len(chunk.Constants) != 2 { t.Fatalf("Expected 2 constants, got %d", len(chunk.Constants)) } // Check that we have the number and variable name if chunk.Constants[0].Data.(float64) != 42.0 { t.Errorf("Expected first constant to be 42, got %v", chunk.Constants[0].Data) } if chunk.Constants[1].Data.(string) != "x" { t.Errorf("Expected second constant to be 'x', got %v", chunk.Constants[1].Data) } checkInstruction(t, chunk, 0, compiler.OpLoadConst, 0) // Load 42 checkInstruction(t, chunk, 3, compiler.OpStoreGlobal, 1) // Store to "x" } // Test echo statement func TestEchoStatement(t *testing.T) { chunk := compileSource(t, "echo 42") // Should have: LoadConst 0, OpEcho, OpReturnNil checkInstruction(t, chunk, 0, compiler.OpLoadConst, 0) checkInstruction(t, chunk, 3, compiler.OpEcho) checkInstruction(t, chunk, 4, compiler.OpReturnNil) } // Test if statement func TestIfStatement(t *testing.T) { chunk := compileSource(t, ` if true then echo 1 end `) // Should start with: LoadConst, JumpIfFalse (with offset), Pop checkInstruction(t, chunk, 0, compiler.OpLoadConst, 0) // Load true // JumpIfFalse has 1 operand (the jump offset), but we don't need to check the exact value op, operands, _ := compiler.DecodeInstruction(chunk.Code, 3) if op != compiler.OpJumpIfFalse { t.Errorf("Expected OpJumpIfFalse at position 3, got %v", op) } if len(operands) != 1 { t.Errorf("Expected 1 operand for JumpIfFalse, got %d", len(operands)) } checkInstruction(t, chunk, 6, compiler.OpPop) // Pop condition } // Test while loop func TestWhileLoop(t *testing.T) { chunk := compileSource(t, ` while true do break end `) // Should have condition evaluation and loop structure checkInstruction(t, chunk, 0, compiler.OpLoadConst, 0) // Load true // JumpIfFalse has 1 operand (the jump offset) op, operands, _ := compiler.DecodeInstruction(chunk.Code, 3) if op != compiler.OpJumpIfFalse { t.Errorf("Expected OpJumpIfFalse at position 3, got %v", op) } if len(operands) != 1 { t.Errorf("Expected 1 operand for JumpIfFalse, got %d", len(operands)) } } // Test table creation func TestTableLiteral(t *testing.T) { chunk := compileSource(t, "echo {1, 2, 3}") // Should start with OpNewTable checkInstruction(t, chunk, 0, compiler.OpNewTable) } // Test table with key-value pairs func TestTableWithKeys(t *testing.T) { chunk := compileSource(t, `echo {x = 1, y = 2}`) checkInstruction(t, chunk, 0, compiler.OpNewTable) // Should have subsequent operations to set fields } // Test function call func TestFunctionCall(t *testing.T) { chunk := compileSource(t, "print(42)") // Should have: LoadGlobal "print", LoadConst 42, Call 1 // The exact positions depend on constant ordering found := false for i := 0; i < len(chunk.Code)-2; i++ { op, operands, _ := compiler.DecodeInstruction(chunk.Code, i) if op == compiler.OpCall && len(operands) > 0 && operands[0] == 1 { found = true break } } if !found { t.Error("Expected OpCall with 1 argument") } } // Test constant deduplication func TestConstantDeduplication(t *testing.T) { chunk := compileSource(t, "echo 42\necho 42\necho 42") // Should only have one constant despite multiple uses if len(chunk.Constants) != 1 { t.Errorf("Expected 1 constant (deduplicated), got %d", len(chunk.Constants)) } } // Test short-circuit evaluation func TestShortCircuitAnd(t *testing.T) { chunk := compileSource(t, "echo true and false") // Should have conditional jumping for short-circuit found := false for i := 0; i < len(chunk.Code); i++ { op, _, _ := compiler.DecodeInstruction(chunk.Code, i) if op == compiler.OpJumpIfFalse { found = true break } } if !found { t.Error("Expected JumpIfFalse for short-circuit and") } } func TestShortCircuitOr(t *testing.T) { chunk := compileSource(t, "echo false or true") // Should have conditional jumping for short-circuit foundFalseJump := false foundJump := false for i := 0; i < len(chunk.Code); i++ { op, _, _ := compiler.DecodeInstruction(chunk.Code, i) if op == compiler.OpJumpIfFalse { foundFalseJump = true } if op == compiler.OpJump { foundJump = true } } if !foundFalseJump || !foundJump { t.Error("Expected JumpIfFalse and Jump for short-circuit or") } } // Test complex expressions func TestComplexExpression(t *testing.T) { chunk := compileSource(t, "echo 1 + 2 * 3") // Should follow correct precedence: Load 1, Load 2, Load 3, Mul, Add if len(chunk.Constants) != 3 { t.Fatalf("Expected 3 constants, got %d", len(chunk.Constants)) } // Verify constants expected := []float64{1, 2, 3} for i, exp := range expected { if chunk.Constants[i].Data.(float64) != exp { t.Errorf("Expected constant %d to be %v, got %v", i, exp, chunk.Constants[i].Data) } } }