Compare commits
No commits in common. "119cfceccecca3274979427502ae98473fe0e8d3" and "4b5faae944cd1647649eb7d8ed74c23aabb7938b" have entirely different histories.
119cfcecce
...
4b5faae944
@ -102,61 +102,6 @@ func (is *IfStatement) String() string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForStatement represents numeric for loops: for i = start, end, step do ... end
|
|
||||||
type ForStatement struct {
|
|
||||||
Variable *Identifier
|
|
||||||
Start Expression
|
|
||||||
End Expression
|
|
||||||
Step Expression // optional, nil means step of 1
|
|
||||||
Body []Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *ForStatement) statementNode() {}
|
|
||||||
func (fs *ForStatement) String() string {
|
|
||||||
var result string
|
|
||||||
if fs.Step != nil {
|
|
||||||
result += fmt.Sprintf("for %s = %s, %s, %s do\n",
|
|
||||||
fs.Variable.String(), fs.Start.String(), fs.End.String(), fs.Step.String())
|
|
||||||
} else {
|
|
||||||
result += fmt.Sprintf("for %s = %s, %s do\n",
|
|
||||||
fs.Variable.String(), fs.Start.String(), fs.End.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, stmt := range fs.Body {
|
|
||||||
result += "\t" + stmt.String() + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
result += "end"
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForInStatement represents iterator for loops: for k, v in expr do ... end
|
|
||||||
type ForInStatement struct {
|
|
||||||
Key *Identifier // optional, nil for single variable iteration
|
|
||||||
Value *Identifier
|
|
||||||
Iterable Expression
|
|
||||||
Body []Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fis *ForInStatement) statementNode() {}
|
|
||||||
func (fis *ForInStatement) String() string {
|
|
||||||
var result string
|
|
||||||
if fis.Key != nil {
|
|
||||||
result += fmt.Sprintf("for %s, %s in %s do\n",
|
|
||||||
fis.Key.String(), fis.Value.String(), fis.Iterable.String())
|
|
||||||
} else {
|
|
||||||
result += fmt.Sprintf("for %s in %s do\n",
|
|
||||||
fis.Value.String(), fis.Iterable.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, stmt := range fis.Body {
|
|
||||||
result += "\t" + stmt.String() + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
result += "end"
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identifier represents identifiers
|
// Identifier represents identifiers
|
||||||
type Identifier struct {
|
type Identifier struct {
|
||||||
Value string
|
Value string
|
||||||
@ -200,17 +145,6 @@ type NilLiteral struct{}
|
|||||||
func (nl *NilLiteral) expressionNode() {}
|
func (nl *NilLiteral) expressionNode() {}
|
||||||
func (nl *NilLiteral) String() string { return "nil" }
|
func (nl *NilLiteral) String() string { return "nil" }
|
||||||
|
|
||||||
// PrefixExpression represents prefix operations like -x
|
|
||||||
type PrefixExpression struct {
|
|
||||||
Operator string
|
|
||||||
Right Expression
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pe *PrefixExpression) expressionNode() {}
|
|
||||||
func (pe *PrefixExpression) String() string {
|
|
||||||
return fmt.Sprintf("(%s%s)", pe.Operator, pe.Right.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// InfixExpression represents binary operations
|
// InfixExpression represents binary operations
|
||||||
type InfixExpression struct {
|
type InfixExpression struct {
|
||||||
Left Expression
|
Left Expression
|
||||||
|
@ -209,37 +209,7 @@ func (l *Lexer) NextToken() Token {
|
|||||||
|
|
||||||
switch l.ch {
|
switch l.ch {
|
||||||
case '=':
|
case '=':
|
||||||
if l.peekChar() == '=' {
|
|
||||||
ch := l.ch
|
|
||||||
l.readChar()
|
|
||||||
tok = Token{Type: EQ, Literal: string(ch) + string(l.ch), Line: l.line, Column: l.column}
|
|
||||||
} else {
|
|
||||||
tok = Token{Type: ASSIGN, Literal: string(l.ch), Line: l.line, Column: l.column}
|
tok = Token{Type: ASSIGN, Literal: string(l.ch), Line: l.line, Column: l.column}
|
||||||
}
|
|
||||||
case '!':
|
|
||||||
if l.peekChar() == '=' {
|
|
||||||
ch := l.ch
|
|
||||||
l.readChar()
|
|
||||||
tok = Token{Type: NOT_EQ, Literal: string(ch) + string(l.ch), Line: l.line, Column: l.column}
|
|
||||||
} else {
|
|
||||||
tok = Token{Type: ILLEGAL, Literal: string(l.ch), Line: l.line, Column: l.column}
|
|
||||||
}
|
|
||||||
case '<':
|
|
||||||
if l.peekChar() == '=' {
|
|
||||||
ch := l.ch
|
|
||||||
l.readChar()
|
|
||||||
tok = Token{Type: LT_EQ, Literal: string(ch) + string(l.ch), Line: l.line, Column: l.column}
|
|
||||||
} else {
|
|
||||||
tok = Token{Type: LT, Literal: string(l.ch), Line: l.line, Column: l.column}
|
|
||||||
}
|
|
||||||
case '>':
|
|
||||||
if l.peekChar() == '=' {
|
|
||||||
ch := l.ch
|
|
||||||
l.readChar()
|
|
||||||
tok = Token{Type: GT_EQ, Literal: string(ch) + string(l.ch), Line: l.line, Column: l.column}
|
|
||||||
} else {
|
|
||||||
tok = Token{Type: GT, Literal: string(l.ch), Line: l.line, Column: l.column}
|
|
||||||
}
|
|
||||||
case '+':
|
case '+':
|
||||||
tok = Token{Type: PLUS, Literal: string(l.ch), Line: l.line, Column: l.column}
|
tok = Token{Type: PLUS, Literal: string(l.ch), Line: l.line, Column: l.column}
|
||||||
case '-':
|
case '-':
|
||||||
|
203
parser/parser.go
203
parser/parser.go
@ -48,19 +48,12 @@ func NewParser(lexer *Lexer) *Parser {
|
|||||||
p.registerPrefix(NIL, p.parseNilLiteral)
|
p.registerPrefix(NIL, p.parseNilLiteral)
|
||||||
p.registerPrefix(LPAREN, p.parseGroupedExpression)
|
p.registerPrefix(LPAREN, p.parseGroupedExpression)
|
||||||
p.registerPrefix(LBRACE, p.parseTableLiteral)
|
p.registerPrefix(LBRACE, p.parseTableLiteral)
|
||||||
p.registerPrefix(MINUS, p.parsePrefixExpression)
|
|
||||||
|
|
||||||
p.infixParseFns = make(map[TokenType]func(Expression) Expression)
|
p.infixParseFns = make(map[TokenType]func(Expression) Expression)
|
||||||
p.registerInfix(PLUS, p.parseInfixExpression)
|
p.registerInfix(PLUS, p.parseInfixExpression)
|
||||||
p.registerInfix(MINUS, p.parseInfixExpression)
|
p.registerInfix(MINUS, p.parseInfixExpression)
|
||||||
p.registerInfix(SLASH, p.parseInfixExpression)
|
p.registerInfix(SLASH, p.parseInfixExpression)
|
||||||
p.registerInfix(STAR, p.parseInfixExpression)
|
p.registerInfix(STAR, p.parseInfixExpression)
|
||||||
p.registerInfix(EQ, p.parseInfixExpression)
|
|
||||||
p.registerInfix(NOT_EQ, p.parseInfixExpression)
|
|
||||||
p.registerInfix(LT, p.parseInfixExpression)
|
|
||||||
p.registerInfix(GT, p.parseInfixExpression)
|
|
||||||
p.registerInfix(LT_EQ, p.parseInfixExpression)
|
|
||||||
p.registerInfix(GT_EQ, p.parseInfixExpression)
|
|
||||||
p.registerInfix(DOT, p.parseDotExpression)
|
p.registerInfix(DOT, p.parseDotExpression)
|
||||||
p.registerInfix(LBRACKET, p.parseIndexExpression)
|
p.registerInfix(LBRACKET, p.parseIndexExpression)
|
||||||
|
|
||||||
@ -111,8 +104,6 @@ func (p *Parser) parseStatement() Statement {
|
|||||||
return p.parseAssignStatement()
|
return p.parseAssignStatement()
|
||||||
case IF:
|
case IF:
|
||||||
return p.parseIfStatement()
|
return p.parseIfStatement()
|
||||||
case FOR:
|
|
||||||
return p.parseForStatement()
|
|
||||||
case ECHO:
|
case ECHO:
|
||||||
return p.parseEchoStatement()
|
return p.parseEchoStatement()
|
||||||
case ASSIGN:
|
case ASSIGN:
|
||||||
@ -185,144 +176,6 @@ func (p *Parser) parseEchoStatement() *EchoStatement {
|
|||||||
return stmt
|
return stmt
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseForStatement parses for loops (both numeric and for-in)
|
|
||||||
func (p *Parser) parseForStatement() Statement {
|
|
||||||
p.nextToken() // move past 'for'
|
|
||||||
|
|
||||||
if !p.curTokenIs(IDENT) {
|
|
||||||
p.addError("expected identifier after 'for'")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
firstVar := &Identifier{Value: p.curToken.Literal}
|
|
||||||
|
|
||||||
// Look ahead to determine which type of for loop
|
|
||||||
if p.peekTokenIs(ASSIGN) {
|
|
||||||
// Numeric for loop: for i = start, end, step do
|
|
||||||
return p.parseNumericForStatement(firstVar)
|
|
||||||
} else if p.peekTokenIs(COMMA) || p.peekTokenIs(IN) {
|
|
||||||
// For-in loop: for k, v in expr do or for v in expr do
|
|
||||||
return p.parseForInStatement(firstVar)
|
|
||||||
} else {
|
|
||||||
p.addError("expected '=', ',' or 'in' after for loop variable")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseNumericForStatement parses numeric for loops: for i = start, end, step do
|
|
||||||
func (p *Parser) parseNumericForStatement(variable *Identifier) *ForStatement {
|
|
||||||
stmt := &ForStatement{Variable: variable}
|
|
||||||
|
|
||||||
if !p.expectPeek(ASSIGN) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.nextToken() // move past '='
|
|
||||||
|
|
||||||
// Parse start expression
|
|
||||||
stmt.Start = p.ParseExpression(LOWEST)
|
|
||||||
if stmt.Start == nil {
|
|
||||||
p.addError("expected start expression in for loop")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.expectPeek(COMMA) {
|
|
||||||
p.addError("expected ',' after start expression in for loop")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.nextToken() // move past ','
|
|
||||||
|
|
||||||
// Parse end expression
|
|
||||||
stmt.End = p.ParseExpression(LOWEST)
|
|
||||||
if stmt.End == nil {
|
|
||||||
p.addError("expected end expression in for loop")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional step expression
|
|
||||||
if p.peekTokenIs(COMMA) {
|
|
||||||
p.nextToken() // move to ','
|
|
||||||
p.nextToken() // move past ','
|
|
||||||
|
|
||||||
stmt.Step = p.ParseExpression(LOWEST)
|
|
||||||
if stmt.Step == nil {
|
|
||||||
p.addError("expected step expression in for loop")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.expectPeek(DO) {
|
|
||||||
p.addError("expected 'do' after for loop header")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.nextToken() // move past 'do'
|
|
||||||
|
|
||||||
// Parse loop body
|
|
||||||
stmt.Body = p.parseBlockStatements(END)
|
|
||||||
|
|
||||||
if !p.curTokenIs(END) {
|
|
||||||
p.addError("expected 'end' to close for loop")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return stmt
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseForInStatement parses for-in loops: for k, v in expr do or for v in expr do
|
|
||||||
func (p *Parser) parseForInStatement(firstVar *Identifier) *ForInStatement {
|
|
||||||
stmt := &ForInStatement{}
|
|
||||||
|
|
||||||
if p.peekTokenIs(COMMA) {
|
|
||||||
// Two variables: for k, v in expr do
|
|
||||||
stmt.Key = firstVar
|
|
||||||
p.nextToken() // move to ','
|
|
||||||
p.nextToken() // move past ','
|
|
||||||
|
|
||||||
if !p.curTokenIs(IDENT) {
|
|
||||||
p.addError("expected identifier after ',' in for loop")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt.Value = &Identifier{Value: p.curToken.Literal}
|
|
||||||
} else {
|
|
||||||
// Single variable: for v in expr do
|
|
||||||
stmt.Value = firstVar
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.expectPeek(IN) {
|
|
||||||
p.addError("expected 'in' in for loop")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.nextToken() // move past 'in'
|
|
||||||
|
|
||||||
// Parse iterable expression
|
|
||||||
stmt.Iterable = p.ParseExpression(LOWEST)
|
|
||||||
if stmt.Iterable == nil {
|
|
||||||
p.addError("expected expression after 'in' in for loop")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.expectPeek(DO) {
|
|
||||||
p.addError("expected 'do' after for loop header")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.nextToken() // move past 'do'
|
|
||||||
|
|
||||||
// Parse loop body
|
|
||||||
stmt.Body = p.parseBlockStatements(END)
|
|
||||||
|
|
||||||
if !p.curTokenIs(END) {
|
|
||||||
p.addError("expected 'end' to close for loop")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return stmt
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseIfStatement parses if/elseif/else/end statements
|
// parseIfStatement parses if/elseif/else/end statements
|
||||||
func (p *Parser) parseIfStatement() *IfStatement {
|
func (p *Parser) parseIfStatement() *IfStatement {
|
||||||
stmt := &IfStatement{}
|
stmt := &IfStatement{}
|
||||||
@ -521,22 +374,6 @@ func (p *Parser) parseNilLiteral() Expression {
|
|||||||
return &NilLiteral{}
|
return &NilLiteral{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) parsePrefixExpression() Expression {
|
|
||||||
expression := &PrefixExpression{
|
|
||||||
Operator: p.curToken.Literal,
|
|
||||||
}
|
|
||||||
|
|
||||||
p.nextToken()
|
|
||||||
|
|
||||||
expression.Right = p.ParseExpression(PREFIX)
|
|
||||||
if expression.Right == nil {
|
|
||||||
p.addError(fmt.Sprintf("expected expression after prefix operator '%s'", expression.Operator))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return expression
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) parseGroupedExpression() Expression {
|
func (p *Parser) parseGroupedExpression() Expression {
|
||||||
p.nextToken()
|
p.nextToken()
|
||||||
|
|
||||||
@ -645,7 +482,7 @@ func (p *Parser) parseInfixExpression(left Expression) Expression {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) parseDotExpression(left Expression) Expression {
|
func (p *Parser) parseDotExpression(left Expression) Expression {
|
||||||
if !p.expectPeekIdent() {
|
if !p.expectPeek(IDENT) {
|
||||||
p.addError("expected identifier after '.'")
|
p.addError("expected identifier after '.'")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -694,26 +531,6 @@ func (p *Parser) expectPeek(t TokenType) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// expectPeekIdent accepts IDENT or keyword tokens as identifiers
|
|
||||||
func (p *Parser) expectPeekIdent() bool {
|
|
||||||
if p.peekTokenIs(IDENT) || p.isKeyword(p.peekToken.Type) {
|
|
||||||
p.nextToken()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
p.peekError(IDENT)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, IN, DO:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error handling methods
|
// Error handling methods
|
||||||
func (p *Parser) addError(message string) {
|
func (p *Parser) addError(message string) {
|
||||||
p.errors = append(p.errors, ParseError{
|
p.errors = append(p.errors, ParseError{
|
||||||
@ -815,18 +632,6 @@ func tokenTypeString(t TokenType) string {
|
|||||||
return "/"
|
return "/"
|
||||||
case DOT:
|
case DOT:
|
||||||
return "."
|
return "."
|
||||||
case EQ:
|
|
||||||
return "=="
|
|
||||||
case NOT_EQ:
|
|
||||||
return "!="
|
|
||||||
case LT:
|
|
||||||
return "<"
|
|
||||||
case GT:
|
|
||||||
return ">"
|
|
||||||
case LT_EQ:
|
|
||||||
return "<="
|
|
||||||
case GT_EQ:
|
|
||||||
return ">="
|
|
||||||
case LPAREN:
|
case LPAREN:
|
||||||
return "("
|
return "("
|
||||||
case RPAREN:
|
case RPAREN:
|
||||||
@ -855,12 +660,6 @@ func tokenTypeString(t TokenType) string {
|
|||||||
return "end"
|
return "end"
|
||||||
case ECHO:
|
case ECHO:
|
||||||
return "echo"
|
return "echo"
|
||||||
case FOR:
|
|
||||||
return "for"
|
|
||||||
case IN:
|
|
||||||
return "in"
|
|
||||||
case DO:
|
|
||||||
return "do"
|
|
||||||
case EOF:
|
case EOF:
|
||||||
return "end of file"
|
return "end of file"
|
||||||
case ILLEGAL:
|
case ILLEGAL:
|
||||||
|
@ -210,126 +210,3 @@ func TestTokenTypeStringWithEcho(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrefixOperatorErrors(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expectedError string
|
|
||||||
desc string
|
|
||||||
}{
|
|
||||||
{"-", "expected expression after prefix operator '-'", "minus without operand"},
|
|
||||||
{"-(", "unexpected end of input", "minus with incomplete expression"},
|
|
||||||
{"-+", "unexpected operator '+'", "minus followed by plus"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
|
||||||
l := parser.NewLexer(tt.input)
|
|
||||||
p := parser.NewParser(l)
|
|
||||||
p.ParseExpression(parser.LOWEST)
|
|
||||||
|
|
||||||
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 TestComparisonOperatorErrors(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expectedError string
|
|
||||||
desc string
|
|
||||||
}{
|
|
||||||
{"x ==", "expected expression after operator '=='", "== without right operand"},
|
|
||||||
{"!= 5", "unexpected token '!='", "!= without left operand"},
|
|
||||||
{"< y", "unexpected token '<'", "< without left operand"},
|
|
||||||
{"> z", "unexpected token '>'", "> without left operand"},
|
|
||||||
{"<= 10", "unexpected token '<='", "<= without left operand"},
|
|
||||||
{">= 20", "unexpected token '>='", ">= without left operand"},
|
|
||||||
{"x !=", "expected expression after operator '!='", "!= without right operand"},
|
|
||||||
{"a <", "expected expression after operator '<'", "< without right operand"},
|
|
||||||
{"b >", "expected expression after operator '>'", "> without right operand"},
|
|
||||||
{"c <=", "expected expression after operator '<='", "<= without right operand"},
|
|
||||||
{"d >=", "expected expression after operator '>='", ">= without right operand"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
|
||||||
l := parser.NewLexer(tt.input)
|
|
||||||
p := parser.NewParser(l)
|
|
||||||
p.ParseExpression(parser.LOWEST)
|
|
||||||
|
|
||||||
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 TestTokenTypeStringWithComparisons(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expectedMessage string
|
|
||||||
}{
|
|
||||||
{"!= 5", "Parse error at line 1, column 1: unexpected token '!=' (near '!=')"},
|
|
||||||
{"< y", "Parse error at line 1, column 1: unexpected token '<' (near '<')"},
|
|
||||||
{">= 20", "Parse error at line 1, column 1: unexpected token '>=' (near '>=')"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.input, func(t *testing.T) {
|
|
||||||
l := parser.NewLexer(tt.input)
|
|
||||||
p := parser.NewParser(l)
|
|
||||||
p.ParseExpression(parser.LOWEST)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -6,113 +6,6 @@ import (
|
|||||||
"git.sharkk.net/Sharkk/Mako/parser"
|
"git.sharkk.net/Sharkk/Mako/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrefixExpressions(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
operator string
|
|
||||||
value any
|
|
||||||
}{
|
|
||||||
{"-5", "-", 5.0},
|
|
||||||
{"-x", "-", "x"},
|
|
||||||
{"-true", "-", true},
|
|
||||||
{"-(1 + 2)", "-", "(1.00 + 2.00)"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.input, func(t *testing.T) {
|
|
||||||
l := parser.NewLexer(tt.input)
|
|
||||||
p := parser.NewParser(l)
|
|
||||||
expr := p.ParseExpression(parser.LOWEST)
|
|
||||||
checkParserErrors(t, p)
|
|
||||||
|
|
||||||
prefix, ok := expr.(*parser.PrefixExpression)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected PrefixExpression, got %T", expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prefix.Operator != tt.operator {
|
|
||||||
t.Errorf("expected operator %s, got %s", tt.operator, prefix.Operator)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch expected := tt.value.(type) {
|
|
||||||
case float64:
|
|
||||||
testNumberLiteral(t, prefix.Right, expected)
|
|
||||||
case string:
|
|
||||||
if expected == "x" {
|
|
||||||
testIdentifier(t, prefix.Right, expected)
|
|
||||||
} else {
|
|
||||||
// It's an expression string
|
|
||||||
if prefix.Right.String() != expected {
|
|
||||||
t.Errorf("expected %s, got %s", expected, prefix.Right.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case bool:
|
|
||||||
testBooleanLiteral(t, prefix.Right, expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestComparisonExpressions(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
leftValue any
|
|
||||||
operator string
|
|
||||||
rightValue any
|
|
||||||
}{
|
|
||||||
{"1 == 1", 1.0, "==", 1.0},
|
|
||||||
{"1 != 2", 1.0, "!=", 2.0},
|
|
||||||
{"x < y", "x", "<", "y"},
|
|
||||||
{"a > b", "a", ">", "b"},
|
|
||||||
{"5 <= 10", 5.0, "<=", 10.0},
|
|
||||||
{"10 >= 5", 10.0, ">=", 5.0},
|
|
||||||
{"true == false", true, "==", false},
|
|
||||||
{"nil != x", nil, "!=", "x"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.input, func(t *testing.T) {
|
|
||||||
l := parser.NewLexer(tt.input)
|
|
||||||
p := parser.NewParser(l)
|
|
||||||
expr := p.ParseExpression(parser.LOWEST)
|
|
||||||
checkParserErrors(t, p)
|
|
||||||
|
|
||||||
infix, ok := expr.(*parser.InfixExpression)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected InfixExpression, got %T", expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if infix.Operator != tt.operator {
|
|
||||||
t.Errorf("expected operator %s, got %s", tt.operator, infix.Operator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test left operand
|
|
||||||
switch leftVal := tt.leftValue.(type) {
|
|
||||||
case float64:
|
|
||||||
testNumberLiteral(t, infix.Left, leftVal)
|
|
||||||
case string:
|
|
||||||
testIdentifier(t, infix.Left, leftVal)
|
|
||||||
case bool:
|
|
||||||
testBooleanLiteral(t, infix.Left, leftVal)
|
|
||||||
case nil:
|
|
||||||
testNilLiteral(t, infix.Left)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test right operand
|
|
||||||
switch rightVal := tt.rightValue.(type) {
|
|
||||||
case float64:
|
|
||||||
testNumberLiteral(t, infix.Right, rightVal)
|
|
||||||
case string:
|
|
||||||
testIdentifier(t, infix.Right, rightVal)
|
|
||||||
case bool:
|
|
||||||
testBooleanLiteral(t, infix.Right, rightVal)
|
|
||||||
case nil:
|
|
||||||
testNilLiteral(t, infix.Right)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInfixExpressions(t *testing.T) {
|
func TestInfixExpressions(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
@ -144,38 +37,11 @@ func TestOperatorPrecedence(t *testing.T) {
|
|||||||
input string
|
input string
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
// Arithmetic precedence
|
|
||||||
{"1 + 2 * 3", "(1.00 + (2.00 * 3.00))"},
|
{"1 + 2 * 3", "(1.00 + (2.00 * 3.00))"},
|
||||||
{"2 * 3 + 4", "((2.00 * 3.00) + 4.00)"},
|
{"2 * 3 + 4", "((2.00 * 3.00) + 4.00)"},
|
||||||
{"(1 + 2) * 3", "((1.00 + 2.00) * 3.00)"},
|
{"(1 + 2) * 3", "((1.00 + 2.00) * 3.00)"},
|
||||||
{"1 + 2 - 3", "((1.00 + 2.00) - 3.00)"},
|
{"1 + 2 - 3", "((1.00 + 2.00) - 3.00)"},
|
||||||
{"2 * 3 / 4", "((2.00 * 3.00) / 4.00)"},
|
{"2 * 3 / 4", "((2.00 * 3.00) / 4.00)"},
|
||||||
|
|
||||||
// Prefix with arithmetic
|
|
||||||
{"-1 + 2", "((-1.00) + 2.00)"},
|
|
||||||
{"-(1 + 2)", "(-(1.00 + 2.00))"},
|
|
||||||
{"-x * 2", "((-x) * 2.00)"},
|
|
||||||
|
|
||||||
// Comparison precedence
|
|
||||||
{"1 + 2 == 3", "((1.00 + 2.00) == 3.00)"},
|
|
||||||
{"1 * 2 < 3 + 4", "((1.00 * 2.00) < (3.00 + 4.00))"},
|
|
||||||
{"a + b != c * d", "((a + b) != (c * d))"},
|
|
||||||
{"x <= y + z", "(x <= (y + z))"},
|
|
||||||
{"a * b >= c / d", "((a * b) >= (c / d))"},
|
|
||||||
|
|
||||||
// Comparison chaining
|
|
||||||
{"a == b != c", "((a == b) != c)"},
|
|
||||||
{"x < y <= z", "((x < y) <= z)"},
|
|
||||||
|
|
||||||
// Member access with operators
|
|
||||||
{"table.key + 1", "(table.key + 1.00)"},
|
|
||||||
{"arr[0] * 2", "(arr[0.00] * 2.00)"},
|
|
||||||
{"obj.x == obj.y", "(obj.x == obj.y)"},
|
|
||||||
{"-table.value", "(-table.value)"},
|
|
||||||
|
|
||||||
// Complex combinations
|
|
||||||
{"-x + y * z == a.b", "(((-x) + (y * z)) == a.b)"},
|
|
||||||
{"(a + b) * c <= d[0]", "(((a + b) * c) <= d[0.00])"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -191,35 +57,3 @@ func TestOperatorPrecedence(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestComplexExpressionsWithComparisons(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
desc string
|
|
||||||
}{
|
|
||||||
{"x + 1 == y * 2", "arithmetic comparison"},
|
|
||||||
{"table.count > arr[0] + 5", "member access comparison"},
|
|
||||||
{"-value <= max", "prefix comparison"},
|
|
||||||
{"(a + b) != (c - d)", "grouped comparison"},
|
|
||||||
{"obj.x < obj.y && obj.y > obj.z", "multiple comparisons"}, // Note: && not implemented yet
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
|
||||||
l := parser.NewLexer(tt.input)
|
|
||||||
p := parser.NewParser(l)
|
|
||||||
expr := p.ParseExpression(parser.LOWEST)
|
|
||||||
|
|
||||||
// Skip && test since it's not implemented
|
|
||||||
if tt.input == "obj.x < obj.y && obj.y > obj.z" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
checkParserErrors(t, p)
|
|
||||||
|
|
||||||
if expr == nil {
|
|
||||||
t.Error("expected non-nil expression")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,425 +0,0 @@
|
|||||||
package parser_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.sharkk.net/Sharkk/Mako/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNumericForLoop(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
variable string
|
|
||||||
hasStep bool
|
|
||||||
desc string
|
|
||||||
}{
|
|
||||||
{"for i = 1, 10 do\necho i\nend", "i", false, "basic numeric loop"},
|
|
||||||
{"for j = 0, 5, 2 do\nx = j\nend", "j", true, "numeric loop with step"},
|
|
||||||
{"for count = 1, 10, 2 do\necho count\nend", "count", true, "step 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)
|
|
||||||
|
|
||||||
if len(program.Statements) != 1 {
|
|
||||||
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt, ok := program.Statements[0].(*parser.ForStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected ForStatement, got %T", program.Statements[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if stmt.Variable.Value != tt.variable {
|
|
||||||
t.Errorf("expected variable %s, got %s", tt.variable, stmt.Variable.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stmt.Start == nil {
|
|
||||||
t.Error("expected start expression")
|
|
||||||
}
|
|
||||||
|
|
||||||
if stmt.End == nil {
|
|
||||||
t.Error("expected end expression")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.hasStep && stmt.Step == nil {
|
|
||||||
t.Error("expected step expression")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tt.hasStep && stmt.Step != nil {
|
|
||||||
t.Error("unexpected step expression")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(stmt.Body) == 0 {
|
|
||||||
t.Error("expected non-empty body")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForInLoop(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
hasKey bool
|
|
||||||
keyName string
|
|
||||||
valueName string
|
|
||||||
desc string
|
|
||||||
}{
|
|
||||||
{"for v in arr do\necho v\nend", false, "", "v", "single variable for-in"},
|
|
||||||
{"for k, v in table do\necho k\nend", true, "k", "v", "key-value for-in"},
|
|
||||||
{"for index, item in list do\necho item\nend", true, "index", "item", "descriptive names"},
|
|
||||||
}
|
|
||||||
|
|
||||||
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.ForInStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected ForInStatement, got %T", program.Statements[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.hasKey {
|
|
||||||
if stmt.Key == nil {
|
|
||||||
t.Error("expected key variable")
|
|
||||||
} else if stmt.Key.Value != tt.keyName {
|
|
||||||
t.Errorf("expected key %s, got %s", tt.keyName, stmt.Key.Value)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if stmt.Key != nil {
|
|
||||||
t.Error("unexpected key variable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if stmt.Value == nil {
|
|
||||||
t.Error("expected value variable")
|
|
||||||
} else if stmt.Value.Value != tt.valueName {
|
|
||||||
t.Errorf("expected value %s, got %s", tt.valueName, stmt.Value.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stmt.Iterable == nil {
|
|
||||||
t.Error("expected iterable expression")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(stmt.Body) == 0 {
|
|
||||||
t.Error("expected non-empty body")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNestedForLoops(t *testing.T) {
|
|
||||||
input := `for i = 1, 3 do
|
|
||||||
for j = 1, 2 do
|
|
||||||
echo i + j
|
|
||||||
end
|
|
||||||
for k, v in table do
|
|
||||||
echo v
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
outerLoop, ok := program.Statements[0].(*parser.ForStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected ForStatement, got %T", program.Statements[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(outerLoop.Body) != 2 {
|
|
||||||
t.Fatalf("expected 2 body statements, got %d", len(outerLoop.Body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// First nested loop: numeric
|
|
||||||
innerNumeric, ok := outerLoop.Body[0].(*parser.ForStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected nested ForStatement, got %T", outerLoop.Body[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if innerNumeric.Variable.Value != "j" {
|
|
||||||
t.Errorf("expected variable j, got %s", innerNumeric.Variable.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second nested loop: for-in
|
|
||||||
innerForIn, ok := outerLoop.Body[1].(*parser.ForInStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected nested ForInStatement, got %T", outerLoop.Body[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
if innerForIn.Key.Value != "k" || innerForIn.Value.Value != "v" {
|
|
||||||
t.Error("expected key k and value v in nested for-in loop")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForLoopWithComplexExpressions(t *testing.T) {
|
|
||||||
input := `for i = start, table.size, step + 1 do
|
|
||||||
for k, v in data[index] do
|
|
||||||
result[k] = v
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt, ok := program.Statements[0].(*parser.ForStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected ForStatement, got %T", program.Statements[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that complex expressions are parsed as expressions
|
|
||||||
if stmt.Start == nil || stmt.End == nil || stmt.Step == nil {
|
|
||||||
t.Error("expected all expressions to be non-nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test nested for-in with complex iterable
|
|
||||||
if len(stmt.Body) != 1 {
|
|
||||||
t.Fatalf("expected 1 body statement, got %d", len(stmt.Body))
|
|
||||||
}
|
|
||||||
|
|
||||||
nestedStmt, ok := stmt.Body[0].(*parser.ForInStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected nested ForInStatement, got %T", stmt.Body[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if nestedStmt.Iterable == nil {
|
|
||||||
t.Error("expected iterable expression")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForLoopErrors(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expectedError string
|
|
||||||
desc string
|
|
||||||
}{
|
|
||||||
{"for do end", "expected identifier after 'for'", "missing variable"},
|
|
||||||
{"for i do end", "expected '=', ',' or 'in' after for loop variable", "missing assignment or in"},
|
|
||||||
{"for i = do end", "expected start expression in for loop", "missing start expression"},
|
|
||||||
{"for i = 1 do end", "expected ',' after start expression in for loop", "missing comma"},
|
|
||||||
{"for i = 1, do end", "expected end expression in for loop", "missing end expression"},
|
|
||||||
{"for i = 1, 10 end", "expected 'do' after for loop header", "missing do"},
|
|
||||||
{"for i = 1, 10 do", "expected 'end' to close for loop", "missing end"},
|
|
||||||
{"for i, do end", "expected identifier after ',' in for loop", "missing second variable"},
|
|
||||||
{"for i, j do end", "expected 'in' in for loop", "missing in keyword"},
|
|
||||||
{"for i, j in do end", "expected expression after 'in' in for loop", "missing iterable"},
|
|
||||||
{"for i in arr end", "expected 'do' after for loop header", "missing do in for-in"},
|
|
||||||
{"for i in arr do", "expected 'end' to close for loop", "missing end in for-in"},
|
|
||||||
}
|
|
||||||
|
|
||||||
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 TestForLoopStringRepresentation(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
contains []string
|
|
||||||
desc string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"for i = 1, 10 do\necho i\nend",
|
|
||||||
[]string{"for i = 1.00, 10.00 do", "echo i", "end"},
|
|
||||||
"numeric for loop",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"for i = 1, 10, 2 do\nx = i\nend",
|
|
||||||
[]string{"for i = 1.00, 10.00, 2.00 do", "x = i", "end"},
|
|
||||||
"numeric for loop with step",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"for v in arr do\necho v\nend",
|
|
||||||
[]string{"for v in arr do", "echo v", "end"},
|
|
||||||
"single variable for-in",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"for k, v in table do\necho k\nend",
|
|
||||||
[]string{"for k, v in table do", "echo k", "end"},
|
|
||||||
"key-value for-in",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
programStr := program.String()
|
|
||||||
for _, contain := range tt.contains {
|
|
||||||
if !strings.Contains(programStr, contain) {
|
|
||||||
t.Errorf("expected string representation to contain %q, got:\n%s", contain, programStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForLoopInMixedProgram(t *testing.T) {
|
|
||||||
input := `arr = {1, 2, 3}
|
|
||||||
total = 0
|
|
||||||
for i = 1, 3 do
|
|
||||||
total = total + arr[i]
|
|
||||||
end
|
|
||||||
echo total
|
|
||||||
|
|
||||||
for k, v in arr do
|
|
||||||
echo v
|
|
||||||
end`
|
|
||||||
|
|
||||||
l := parser.NewLexer(input)
|
|
||||||
p := parser.NewParser(l)
|
|
||||||
program := p.ParseProgram()
|
|
||||||
checkParserErrors(t, p)
|
|
||||||
|
|
||||||
if len(program.Statements) != 5 {
|
|
||||||
t.Fatalf("expected 5 statements, got %d", len(program.Statements))
|
|
||||||
}
|
|
||||||
|
|
||||||
// First: table assignment
|
|
||||||
_, ok := program.Statements[0].(*parser.AssignStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("statement 0: expected AssignStatement, got %T", program.Statements[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second: variable assignment
|
|
||||||
_, ok = program.Statements[1].(*parser.AssignStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("statement 1: expected AssignStatement, got %T", program.Statements[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Third: numeric for loop
|
|
||||||
forStmt, ok := program.Statements[2].(*parser.ForStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("statement 2: expected ForStatement, got %T", program.Statements[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(forStmt.Body) != 1 {
|
|
||||||
t.Errorf("expected 1 body statement in for loop, got %d", len(forStmt.Body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fourth: echo statement
|
|
||||||
_, ok = program.Statements[3].(*parser.EchoStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("statement 3: expected EchoStatement, got %T", program.Statements[3])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fifth: for-in loop
|
|
||||||
forInStmt, ok := program.Statements[4].(*parser.ForInStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("statement 4: expected ForInStatement, got %T", program.Statements[4])
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(forInStmt.Body) != 1 {
|
|
||||||
t.Errorf("expected 1 body statement in for-in loop, got %d", len(forInStmt.Body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that body contains echo statement
|
|
||||||
_, ok = forInStmt.Body[0].(*parser.EchoStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("for-in body: expected EchoStatement, got %T", forInStmt.Body[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForLoopWithMemberAccess(t *testing.T) {
|
|
||||||
input := `for i = obj.start, obj.end, obj.step do
|
|
||||||
data[i] = result
|
|
||||||
result.items[i] = data[i]
|
|
||||||
end
|
|
||||||
|
|
||||||
for key, val in obj.items do
|
|
||||||
val.count = val.count + 1
|
|
||||||
end`
|
|
||||||
|
|
||||||
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: numeric for with member access in bounds
|
|
||||||
forStmt, ok := program.Statements[0].(*parser.ForStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("statement 0: expected ForStatement, got %T", program.Statements[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check bounds are dot expressions
|
|
||||||
_, ok = forStmt.Start.(*parser.DotExpression)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected DotExpression for start, got %T", forStmt.Start)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok = forStmt.End.(*parser.DotExpression)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected DotExpression for end, got %T", forStmt.End)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok = forStmt.Step.(*parser.DotExpression)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected DotExpression for step, got %T", forStmt.Step)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second: for-in with member access
|
|
||||||
forInStmt, ok := program.Statements[1].(*parser.ForInStatement)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("statement 1: expected ForInStatement, got %T", program.Statements[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok = forInStmt.Iterable.(*parser.DotExpression)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected DotExpression for iterable, got %T", forInStmt.Iterable)
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,14 +20,6 @@ const (
|
|||||||
SLASH // /
|
SLASH // /
|
||||||
DOT // .
|
DOT // .
|
||||||
|
|
||||||
// Comparison operators
|
|
||||||
EQ // ==
|
|
||||||
NOT_EQ // !=
|
|
||||||
LT // <
|
|
||||||
GT // >
|
|
||||||
LT_EQ // <=
|
|
||||||
GT_EQ // >=
|
|
||||||
|
|
||||||
// Delimiters
|
// Delimiters
|
||||||
LPAREN // (
|
LPAREN // (
|
||||||
RPAREN // )
|
RPAREN // )
|
||||||
@ -45,9 +37,6 @@ const (
|
|||||||
ELSE
|
ELSE
|
||||||
END
|
END
|
||||||
ECHO
|
ECHO
|
||||||
FOR
|
|
||||||
IN
|
|
||||||
DO
|
|
||||||
|
|
||||||
// Special
|
// Special
|
||||||
EOF
|
EOF
|
||||||
@ -68,23 +57,15 @@ type Precedence int
|
|||||||
const (
|
const (
|
||||||
_ Precedence = iota
|
_ Precedence = iota
|
||||||
LOWEST
|
LOWEST
|
||||||
EQUALS // ==, !=
|
SUM // +
|
||||||
LESSGREATER // >, <, >=, <=
|
PRODUCT // *
|
||||||
SUM // +, -
|
|
||||||
PRODUCT // *, /
|
|
||||||
PREFIX // -x, !x
|
|
||||||
MEMBER // table[key], table.key
|
MEMBER // table[key], table.key
|
||||||
|
PREFIX // -x, !x
|
||||||
CALL // function()
|
CALL // function()
|
||||||
)
|
)
|
||||||
|
|
||||||
// precedences maps token types to their precedence levels
|
// precedences maps token types to their precedence levels
|
||||||
var precedences = map[TokenType]Precedence{
|
var precedences = map[TokenType]Precedence{
|
||||||
EQ: EQUALS,
|
|
||||||
NOT_EQ: EQUALS,
|
|
||||||
LT: LESSGREATER,
|
|
||||||
GT: LESSGREATER,
|
|
||||||
LT_EQ: LESSGREATER,
|
|
||||||
GT_EQ: LESSGREATER,
|
|
||||||
PLUS: SUM,
|
PLUS: SUM,
|
||||||
MINUS: SUM,
|
MINUS: SUM,
|
||||||
SLASH: PRODUCT,
|
SLASH: PRODUCT,
|
||||||
@ -106,9 +87,6 @@ func lookupIdent(ident string) TokenType {
|
|||||||
"else": ELSE,
|
"else": ELSE,
|
||||||
"end": END,
|
"end": END,
|
||||||
"echo": ECHO,
|
"echo": ECHO,
|
||||||
"for": FOR,
|
|
||||||
"in": IN,
|
|
||||||
"do": DO,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tok, ok := keywords[ident]; ok {
|
if tok, ok := keywords[ident]; ok {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user