diff --git a/go.mod b/go.mod index 9130d44..90b6eaa 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..115793b --- /dev/null +++ b/go.sum @@ -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= diff --git a/tests/compiler_test.go b/tests/compiler_test.go new file mode 100644 index 0000000..f588168 --- /dev/null +++ b/tests/compiler_test.go @@ -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) +} diff --git a/tests/edge_test.go b/tests/edge_test.go new file mode 100644 index 0000000..a2b6f5a --- /dev/null +++ b/tests/edge_test.go @@ -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)) +} diff --git a/tests/integration_test.go b/tests/integration_test.go new file mode 100644 index 0000000..20b95eb --- /dev/null +++ b/tests/integration_test.go @@ -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"]; + `) +} diff --git a/tests/lexer_test.go b/tests/lexer_test.go new file mode 100644 index 0000000..504ca2d --- /dev/null +++ b/tests/lexer_test.go @@ -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) +} diff --git a/tests/main_test.go b/tests/main_test.go new file mode 100644 index 0000000..212a1db --- /dev/null +++ b/tests/main_test.go @@ -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 +} diff --git a/tests/parser_test.go b/tests/parser_test.go new file mode 100644 index 0000000..e2174dd --- /dev/null +++ b/tests/parser_test.go @@ -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) + } + } +} diff --git a/tests/vm_test.go b/tests/vm_test.go new file mode 100644 index 0000000..d7ec22f --- /dev/null +++ b/tests/vm_test.go @@ -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 +}