package parser_test import ( "strings" "testing" "git.sharkk.net/Sharkk/Mako/parser" ) func TestBreakStatement(t *testing.T) { tests := []struct { input string desc string }{ {"break", "standalone break"}, {"while true do\nbreak\nend", "break in while loop"}, {"for i = 1, 10 do\nbreak\nend", "break in numeric for loop"}, {"for k, v in table do\nbreak\nend", "break in for-in loop"}, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { l := parser.NewLexer(tt.input) p := parser.NewParser(l) program := p.ParseProgram() checkParserErrors(t, p) found := false for _, stmt := range program.Statements { if _, ok := stmt.(*parser.BreakStatement); ok { found = true break } // Check nested statements in loops if whileStmt, ok := stmt.(*parser.WhileStatement); ok { for _, bodyStmt := range whileStmt.Body { if _, ok := bodyStmt.(*parser.BreakStatement); ok { found = true break } } } if forStmt, ok := stmt.(*parser.ForStatement); ok { for _, bodyStmt := range forStmt.Body { if _, ok := bodyStmt.(*parser.BreakStatement); ok { found = true break } } } if forInStmt, ok := stmt.(*parser.ForInStatement); ok { for _, bodyStmt := range forInStmt.Body { if _, ok := bodyStmt.(*parser.BreakStatement); ok { found = true break } } } } if !found { t.Error("expected to find BreakStatement") } }) } } func TestBreakStringRepresentation(t *testing.T) { tests := []struct { input string expected string desc string }{ {"break", "break", "simple break"}, {"while true do\nbreak\nend", "while true do\n\tbreak\nend", "break in while"}, {"for i = 1, 10 do\necho i\nbreak\nend", "for i = 1.00, 10.00 do\n\techo i\n\tbreak\nend", "break with other statements"}, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { l := parser.NewLexer(tt.input) p := parser.NewParser(l) program := p.ParseProgram() checkParserErrors(t, p) result := strings.TrimSpace(program.String()) if result != tt.expected { t.Errorf("expected %q, got %q", tt.expected, result) } }) } } func TestExitStatement(t *testing.T) { tests := []struct { input string hasValue bool desc string }{ {"exit", false, "exit without value"}, {"exit 0", true, "exit with number"}, {"exit 1", true, "exit with error code"}, {"exit \"success\"", true, "exit with string"}, {"exit x", true, "exit with variable"}, {"exit 1 + 2", true, "exit with expression"}, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { l := parser.NewLexer(tt.input) p := parser.NewParser(l) program := p.ParseProgram() checkParserErrors(t, p) if len(program.Statements) != 1 { t.Fatalf("expected 1 statement, got %d", len(program.Statements)) } stmt, ok := program.Statements[0].(*parser.ExitStatement) if !ok { t.Fatalf("expected ExitStatement, got %T", program.Statements[0]) } if tt.hasValue && stmt.Value == nil { t.Error("expected exit value but got nil") } if !tt.hasValue && stmt.Value != nil { t.Error("expected no exit value but got one") } }) } } func TestExitStringRepresentation(t *testing.T) { tests := []struct { input string expected string desc string }{ {"exit", "exit", "simple exit"}, {"exit 0", "exit 0.00", "exit with number"}, {"exit \"message\"", "exit \"message\"", "exit with string"}, {"exit x + 1", "exit (x + 1.00)", "exit with expression"}, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { l := parser.NewLexer(tt.input) p := parser.NewParser(l) program := p.ParseProgram() checkParserErrors(t, p) result := strings.TrimSpace(program.String()) if result != tt.expected { t.Errorf("expected %q, got %q", tt.expected, result) } }) } } func TestNestedBreakInComplexLoops(t *testing.T) { input := `for i = 1, 10 do while condition do if found then break end x = x + 1 end for k, v in data do if v == target then break end end end` l := parser.NewLexer(input) p := parser.NewParser(l) program := p.ParseProgram() checkParserErrors(t, p) if len(program.Statements) != 1 { t.Fatalf("expected 1 statement, got %d", len(program.Statements)) } outerFor, ok := program.Statements[0].(*parser.ForStatement) if !ok { t.Fatalf("expected ForStatement, got %T", program.Statements[0]) } if len(outerFor.Body) != 2 { t.Fatalf("expected 2 body statements, got %d", len(outerFor.Body)) } // First: while loop with break whileStmt, ok := outerFor.Body[0].(*parser.WhileStatement) if !ok { t.Fatalf("expected WhileStatement, got %T", outerFor.Body[0]) } // Check that while body contains if with break ifStmt, ok := whileStmt.Body[0].(*parser.IfStatement) if !ok { t.Fatalf("expected IfStatement in while body, got %T", whileStmt.Body[0]) } _, ok = ifStmt.Body[0].(*parser.BreakStatement) if !ok { t.Fatalf("expected BreakStatement in if body, got %T", ifStmt.Body[0]) } // Second: for-in loop with break forInStmt, ok := outerFor.Body[1].(*parser.ForInStatement) if !ok { t.Fatalf("expected ForInStatement, got %T", outerFor.Body[1]) } // Check that for-in body contains if with break ifStmt2, ok := forInStmt.Body[0].(*parser.IfStatement) if !ok { t.Fatalf("expected IfStatement in for-in body, got %T", forInStmt.Body[0]) } _, ok = ifStmt2.Body[0].(*parser.BreakStatement) if !ok { t.Fatalf("expected BreakStatement in if body, got %T", ifStmt2.Body[0]) } } func TestExitInMixedProgram(t *testing.T) { input := `x = 42 if x > 50 then exit 1 else echo "continuing" end y = "done" exit "success"` l := parser.NewLexer(input) p := parser.NewParser(l) program := p.ParseProgram() checkParserErrors(t, p) if len(program.Statements) != 4 { t.Fatalf("expected 4 statements, got %d", len(program.Statements)) } // First: assignment _, ok := program.Statements[0].(*parser.AssignStatement) if !ok { t.Fatalf("statement 0: expected AssignStatement, got %T", program.Statements[0]) } // Second: if statement with exit in body ifStmt, ok := program.Statements[1].(*parser.IfStatement) if !ok { t.Fatalf("statement 1: expected IfStatement, got %T", program.Statements[1]) } _, ok = ifStmt.Body[0].(*parser.ExitStatement) if !ok { t.Fatalf("if body: expected ExitStatement, got %T", ifStmt.Body[0]) } // Third: assignment _, ok = program.Statements[2].(*parser.AssignStatement) if !ok { t.Fatalf("statement 2: expected AssignStatement, got %T", program.Statements[2]) } // Fourth: final exit _, ok = program.Statements[3].(*parser.ExitStatement) if !ok { t.Fatalf("statement 3: expected ExitStatement, got %T", program.Statements[3]) } } func TestExitWithComplexExpressions(t *testing.T) { tests := []struct { input string desc string }{ {"exit table.errorCode", "exit with member access"}, {"exit arr[0]", "exit with index access"}, {"exit status + 1", "exit with arithmetic"}, {"exit {code = 1, msg = \"error\"}", "exit with table"}, {"exit obj.nested.value", "exit with chained access"}, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { l := parser.NewLexer(tt.input) p := parser.NewParser(l) program := p.ParseProgram() checkParserErrors(t, p) if len(program.Statements) != 1 { t.Fatalf("expected 1 statement, got %d", len(program.Statements)) } stmt, ok := program.Statements[0].(*parser.ExitStatement) if !ok { t.Fatalf("expected ExitStatement, got %T", program.Statements[0]) } if stmt.Value == nil { t.Error("expected exit value but got nil") } }) } } func TestControlFlowErrors(t *testing.T) { tests := []struct { input string expectedError string desc string }{ {"break x", "unexpected identifier", "break with argument"}, {"exit +", "unexpected token '+'", "exit with invalid expression"}, {"exit (", "unexpected end of input", "exit with incomplete expression"}, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { l := parser.NewLexer(tt.input) p := parser.NewParser(l) p.ParseProgram() if !p.HasErrors() { t.Fatal("expected parsing errors") } errors := p.Errors() found := false for _, err := range errors { if strings.Contains(err.Message, tt.expectedError) { found = true break } } if !found { errorMsgs := make([]string, len(errors)) for i, err := range errors { errorMsgs[i] = err.Message } t.Errorf("expected error containing %q, got %v", tt.expectedError, errorMsgs) } }) } } func TestProgramExitCode(t *testing.T) { input := `x = 42 echo x` l := parser.NewLexer(input) p := parser.NewParser(l) program := p.ParseProgram() checkParserErrors(t, p) // Program should have default exit code of 0 if program.ExitCode != 0 { t.Errorf("expected default exit code 0, got %d", program.ExitCode) } } func TestBreakAndExitInSameProgram(t *testing.T) { input := `for i = 1, 100 do if i == 10 then break end if i > 50 then exit 1 end echo i end exit "completed"` l := parser.NewLexer(input) p := parser.NewParser(l) program := p.ParseProgram() checkParserErrors(t, p) if len(program.Statements) != 2 { t.Fatalf("expected 2 statements, got %d", len(program.Statements)) } // First: for loop with break and exit forStmt, ok := program.Statements[0].(*parser.ForStatement) if !ok { t.Fatalf("statement 0: expected ForStatement, got %T", program.Statements[0]) } if len(forStmt.Body) != 3 { t.Fatalf("expected 3 body statements, got %d", len(forStmt.Body)) } // Check for break in first if if1, ok := forStmt.Body[0].(*parser.IfStatement) if !ok { t.Fatalf("body[0]: expected IfStatement, got %T", forStmt.Body[0]) } _, ok = if1.Body[0].(*parser.BreakStatement) if !ok { t.Fatalf("if1 body: expected BreakStatement, got %T", if1.Body[0]) } // Check for exit in second if if2, ok := forStmt.Body[1].(*parser.IfStatement) if !ok { t.Fatalf("body[1]: expected IfStatement, got %T", forStmt.Body[1]) } _, ok = if2.Body[0].(*parser.ExitStatement) if !ok { t.Fatalf("if2 body: expected ExitStatement, got %T", if2.Body[0]) } // Second: final exit _, ok = program.Statements[1].(*parser.ExitStatement) if !ok { t.Fatalf("statement 1: expected ExitStatement, got %T", program.Statements[1]) } }