This commit is contained in:
Sky Johnson 2025-06-10 09:57:52 -05:00
parent 3cb856889a
commit b2a1b7a79b
6 changed files with 282 additions and 0 deletions

View File

@ -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

View File

@ -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:

121
parser/tests/echo_test.go Normal file
View File

@ -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")
}
})
}
}

View File

@ -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)
}
})
}
}

View File

@ -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)
}
}

View File

@ -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 {