diff --git a/parser/ast.go b/parser/ast.go index 73b91c1..bb09fc6 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -43,6 +43,16 @@ func (as *AssignStatement) String() string { return fmt.Sprintf("%s = %s", as.Name.String(), as.Value.String()) } +// EchoStatement represents echo output statements +type EchoStatement struct { + Value Expression +} + +func (es *EchoStatement) statementNode() {} +func (es *EchoStatement) String() string { + return fmt.Sprintf("echo %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 086e230..f921b55 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -105,6 +105,8 @@ func (p *Parser) parseStatement() Statement { return nil case IF: return p.parseIfStatement() + case ECHO: + return p.parseEchoStatement() case ASSIGN: p.addError("assignment operator '=' without left-hand side identifier") return nil @@ -145,6 +147,21 @@ func (p *Parser) parseAssignStatement() *AssignStatement { return stmt } +// parseEchoStatement parses echo statements +func (p *Parser) parseEchoStatement() *EchoStatement { + stmt := &EchoStatement{} + + p.nextToken() // move past 'echo' + + stmt.Value = p.ParseExpression(LOWEST) + if stmt.Value == nil { + p.addError("expected expression after 'echo'") + return nil + } + + return stmt +} + // parseIfStatement parses if/elseif/else/end statements func (p *Parser) parseIfStatement() *IfStatement { stmt := &IfStatement{} @@ -587,6 +604,8 @@ func tokenTypeString(t TokenType) string { return "else" case END: return "end" + case ECHO: + return "echo" case EOF: return "end of file" case ILLEGAL: diff --git a/parser/tests/echo_test.go b/parser/tests/echo_test.go new file mode 100644 index 0000000..807880a --- /dev/null +++ b/parser/tests/echo_test.go @@ -0,0 +1,121 @@ +package parser_test + +import ( + "testing" + + "git.sharkk.net/Sharkk/Mako/parser" +) + +func TestBasicEchoStatement(t *testing.T) { + tests := []struct { + input string + expectedVal any + isExpr bool + desc string + }{ + {`echo 42`, 42.0, false, "echo number"}, + {`echo "hello"`, "hello", false, "echo string"}, + {`echo true`, true, false, "echo boolean"}, + {`echo nil`, nil, false, "echo nil"}, + {`echo x`, "x", false, "echo identifier"}, + {`echo 1 + 2`, "(1.00 + 2.00)", true, "echo 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.EchoStatement) + if !ok { + t.Fatalf("expected EchoStatement, got %T", program.Statements[0]) + } + + if tt.isExpr { + if stmt.Value.String() != tt.expectedVal.(string) { + t.Errorf("expected %s, got %s", tt.expectedVal.(string), stmt.Value.String()) + } + } else { + switch expected := tt.expectedVal.(type) { + case float64: + testNumberLiteral(t, stmt.Value, expected) + case string: + if expected == "x" { + testIdentifier(t, stmt.Value, expected) + } else { + testStringLiteral(t, stmt.Value, expected) + } + case bool: + testBooleanLiteral(t, stmt.Value, expected) + case nil: + testNilLiteral(t, stmt.Value) + } + } + }) + } +} + +func TestEchoStringRepresentation(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {`echo "hello"`, `echo "hello"`}, + {`echo 42`, `echo 42.00`}, + {`echo x + y`, `echo (x + y)`}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*parser.EchoStatement) + if stmt.String() != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, stmt.String()) + } + }) + } +} + +func TestEchoWithComplexExpressions(t *testing.T) { + tests := []struct { + input string + desc string + }{ + {`echo {1, 2, 3}`, "echo table array"}, + {`echo {x = 1, y = 2}`, "echo table hash"}, + {`echo 0xFF + 0b1010`, "echo extended numbers"}, + {`echo [[multiline string]]`, "echo multiline string"}, + } + + 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.EchoStatement) + if !ok { + t.Fatalf("expected EchoStatement, got %T", program.Statements[0]) + } + + if stmt.Value == nil { + t.Error("expected non-nil echo value") + } + }) + } +} diff --git a/parser/tests/errors_test.go b/parser/tests/errors_test.go index 84f36ce..71bbf20 100644 --- a/parser/tests/errors_test.go +++ b/parser/tests/errors_test.go @@ -138,3 +138,75 @@ func TestErrorMessages(t *testing.T) { }) } } + +func TestEchoErrors(t *testing.T) { + tests := []struct { + input string + expectedError string + desc string + }{ + {"echo", "expected expression after 'echo'", "echo without expression"}, + {"echo +", "unexpected operator '+'", "echo with invalid expression"}, + {"echo (", "unexpected end of input", "echo 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 TestTokenTypeStringWithEcho(t *testing.T) { + tests := []struct { + input string + expectedMessage string + }{ + {"echo", "Parse error at line 1, column 1: expected expression after 'echo' (near '')"}, + } + + for _, tt := range tests { + t.Run(tt.input, 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() + if len(errors) == 0 { + t.Fatal("expected at least one error") + } + + errorStr := errors[0].Error() + if !strings.Contains(errorStr, "Parse error at line") { + t.Errorf("expected formatted error message, got: %s", errorStr) + } + }) + } +} diff --git a/parser/tests/parser_test.go b/parser/tests/parser_test.go index 6a61caf..5fb894c 100644 --- a/parser/tests/parser_test.go +++ b/parser/tests/parser_test.go @@ -201,3 +201,61 @@ end` t.Errorf("expected 4 table pairs, got %d", len(table.Pairs)) } } + +func TestMixedStatementsWithEcho(t *testing.T) { + input := `x = 42 +echo x +if x then + echo "found x" +end +echo {result = x}` + + 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: echo + echo1, ok := program.Statements[1].(*parser.EchoStatement) + if !ok { + t.Fatalf("statement 1: expected EchoStatement, got %T", program.Statements[1]) + } + testIdentifier(t, echo1.Value, "x") + + // Third: if statement with echo in body + ifStmt, ok := program.Statements[2].(*parser.IfStatement) + if !ok { + t.Fatalf("statement 2: expected IfStatement, got %T", program.Statements[2]) + } + + if len(ifStmt.Body) != 1 { + t.Fatalf("expected 1 body statement, got %d", len(ifStmt.Body)) + } + + bodyEcho, ok := ifStmt.Body[0].(*parser.EchoStatement) + if !ok { + t.Fatalf("if body: expected EchoStatement, got %T", ifStmt.Body[0]) + } + testStringLiteral(t, bodyEcho.Value, "found x") + + // Fourth: echo with table + echo2, ok := program.Statements[3].(*parser.EchoStatement) + if !ok { + t.Fatalf("statement 3: expected EchoStatement, got %T", program.Statements[3]) + } + + _, ok = echo2.Value.(*parser.TableLiteral) + if !ok { + t.Fatalf("expected TableLiteral in echo, got %T", echo2.Value) + } +} diff --git a/parser/token.go b/parser/token.go index 19400dc..87a1fc0 100644 --- a/parser/token.go +++ b/parser/token.go @@ -33,6 +33,7 @@ const ( ELSEIF ELSE END + ECHO // Special EOF @@ -79,6 +80,7 @@ func lookupIdent(ident string) TokenType { "elseif": ELSEIF, "else": ELSE, "end": END, + "echo": ECHO, } if tok, ok := keywords[ident]; ok {