From e7a7d05e64aebaa0a56d5c0d17f78b45cc62f7af Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 10 Jun 2025 11:54:17 -0500 Subject: [PATCH] add break and exit, remove var --- parser/ast.go | 22 ++ parser/parser.go | 45 +++- parser/tests/breakexit_test.go | 428 +++++++++++++++++++++++++++++++++ parser/token.go | 6 +- 4 files changed, 496 insertions(+), 5 deletions(-) create mode 100644 parser/tests/breakexit_test.go diff --git a/parser/ast.go b/parser/ast.go index 602fd19..8d789be 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -22,6 +22,7 @@ type Expression interface { // Program represents the root of the AST type Program struct { Statements []Statement + ExitCode int } func (p *Program) String() string { @@ -53,6 +54,27 @@ func (es *EchoStatement) String() string { return fmt.Sprintf("echo %s", es.Value.String()) } +// BreakStatement represents break statements to exit loops +type BreakStatement struct{} + +func (bs *BreakStatement) statementNode() {} +func (bs *BreakStatement) String() string { + return "break" +} + +// ExitStatement represents exit statements to quit the script +type ExitStatement struct { + Value Expression // optional, can be nil +} + +func (es *ExitStatement) statementNode() {} +func (es *ExitStatement) String() string { + if es.Value == nil { + return "exit" + } + return fmt.Sprintf("exit %s", es.Value.String()) +} + // ElseIfClause represents an elseif condition type ElseIfClause struct { Condition Expression diff --git a/parser/parser.go b/parser/parser.go index 74f3900..33409ef 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -117,6 +117,10 @@ func (p *Parser) parseStatement() Statement { return p.parseWhileStatement() case ECHO: return p.parseEchoStatement() + case BREAK: + return p.parseBreakStatement() + case EXIT: + return p.parseExitStatement() case ASSIGN: p.addError("assignment operator '=' without left-hand side identifier") return nil @@ -187,6 +191,39 @@ func (p *Parser) parseEchoStatement() *EchoStatement { return stmt } +// parseBreakStatement parses break statements +func (p *Parser) parseBreakStatement() *BreakStatement { + return &BreakStatement{} +} + +// parseExitStatement parses exit statements +func (p *Parser) parseExitStatement() *ExitStatement { + stmt := &ExitStatement{} + + // Check if there's an optional expression after 'exit' + // Only parse expression if next token can start an expression + if p.canStartExpression(p.peekToken.Type) { + p.nextToken() // move past 'exit' + stmt.Value = p.ParseExpression(LOWEST) + if stmt.Value == nil { + p.addError("expected expression after 'exit'") + return nil + } + } + + return stmt +} + +// canStartExpression checks if a token type can start an expression +func (p *Parser) canStartExpression(tokenType TokenType) bool { + switch tokenType { + case IDENT, NUMBER, STRING, TRUE, FALSE, NIL, LPAREN, LBRACE, MINUS: + return true + default: + return false + } +} + // parseWhileStatement parses while loops: while condition do ... end func (p *Parser) parseWhileStatement() *WhileStatement { stmt := &WhileStatement{} @@ -739,7 +776,7 @@ func (p *Parser) expectPeekIdent() bool { // isKeyword checks if a token type is a keyword that can be used as identifier func (p *Parser) isKeyword(t TokenType) bool { switch t { - case TRUE, FALSE, NIL, VAR, IF, THEN, ELSEIF, ELSE, END, ECHO, FOR, WHILE, IN, DO: + case TRUE, FALSE, NIL, IF, THEN, ELSEIF, ELSE, END, ECHO, FOR, WHILE, IN, DO, BREAK, EXIT: return true default: return false @@ -873,8 +910,6 @@ func tokenTypeString(t TokenType) string { return "]" case COMMA: return "," - case VAR: - return "var" case IF: return "if" case THEN: @@ -895,6 +930,10 @@ func tokenTypeString(t TokenType) string { return "in" case DO: return "do" + case BREAK: + return "break" + case EXIT: + return "exit" case EOF: return "end of file" case ILLEGAL: diff --git a/parser/tests/breakexit_test.go b/parser/tests/breakexit_test.go new file mode 100644 index 0000000..f31f594 --- /dev/null +++ b/parser/tests/breakexit_test.go @@ -0,0 +1,428 @@ +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]) + } +} diff --git a/parser/token.go b/parser/token.go index f5b0faa..b61a3b9 100644 --- a/parser/token.go +++ b/parser/token.go @@ -38,7 +38,6 @@ const ( COMMA // , // Keywords - VAR IF THEN ELSEIF @@ -49,6 +48,8 @@ const ( WHILE IN DO + BREAK + EXIT // Special EOF @@ -97,7 +98,6 @@ var precedences = map[TokenType]Precedence{ // lookupIdent checks if an identifier is a keyword func lookupIdent(ident string) TokenType { keywords := map[string]TokenType{ - "var": VAR, "true": TRUE, "false": FALSE, "nil": NIL, @@ -111,6 +111,8 @@ func lookupIdent(ident string) TokenType { "while": WHILE, "in": IN, "do": DO, + "break": BREAK, + "exit": EXIT, } if tok, ok := keywords[ident]; ok {