enhance errors
This commit is contained in:
parent
1fdd6ed28c
commit
a744c12baf
153
parser/parser.go
153
parser/parser.go
@ -5,6 +5,19 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ParseError represents a parsing error with location information
|
||||||
|
type ParseError struct {
|
||||||
|
Message string
|
||||||
|
Line int
|
||||||
|
Column int
|
||||||
|
Token Token
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe ParseError) Error() string {
|
||||||
|
return fmt.Sprintf("Parse error at line %d, column %d: %s (near '%s')",
|
||||||
|
pe.Line, pe.Column, pe.Message, pe.Token.Literal)
|
||||||
|
}
|
||||||
|
|
||||||
// Parser implements a recursive descent Pratt parser
|
// Parser implements a recursive descent Pratt parser
|
||||||
type Parser struct {
|
type Parser struct {
|
||||||
lexer *Lexer
|
lexer *Lexer
|
||||||
@ -15,14 +28,14 @@ type Parser struct {
|
|||||||
prefixParseFns map[TokenType]func() Expression
|
prefixParseFns map[TokenType]func() Expression
|
||||||
infixParseFns map[TokenType]func(Expression) Expression
|
infixParseFns map[TokenType]func(Expression) Expression
|
||||||
|
|
||||||
errors []string
|
errors []ParseError
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewParser creates a new parser instance
|
// NewParser creates a new parser instance
|
||||||
func NewParser(lexer *Lexer) *Parser {
|
func NewParser(lexer *Lexer) *Parser {
|
||||||
p := &Parser{
|
p := &Parser{
|
||||||
lexer: lexer,
|
lexer: lexer,
|
||||||
errors: []string{},
|
errors: []ParseError{},
|
||||||
}
|
}
|
||||||
|
|
||||||
p.prefixParseFns = make(map[TokenType]func() Expression)
|
p.prefixParseFns = make(map[TokenType]func() Expression)
|
||||||
@ -81,12 +94,25 @@ func (p *Parser) ParseProgram() *Program {
|
|||||||
|
|
||||||
// parseStatement parses a statement
|
// parseStatement parses a statement
|
||||||
func (p *Parser) parseStatement() Statement {
|
func (p *Parser) parseStatement() Statement {
|
||||||
if p.curTokenIs(IDENT) && p.peekTokenIs(ASSIGN) {
|
switch p.curToken.Type {
|
||||||
|
case IDENT:
|
||||||
|
if p.peekTokenIs(ASSIGN) {
|
||||||
return p.parseAssignStatement()
|
return p.parseAssignStatement()
|
||||||
}
|
}
|
||||||
|
p.addError("unexpected identifier, expected assignment or declaration")
|
||||||
// Skip unknown statements for now
|
|
||||||
return nil
|
return nil
|
||||||
|
case ASSIGN:
|
||||||
|
p.addError("assignment operator '=' without left-hand side identifier")
|
||||||
|
return nil
|
||||||
|
case ILLEGAL:
|
||||||
|
p.addError(fmt.Sprintf("unexpected token '%s'", p.curToken.Literal))
|
||||||
|
return nil
|
||||||
|
case EOF:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
p.addError(fmt.Sprintf("unexpected token '%s', expected statement", p.curToken.Literal))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseAssignStatement parses variable assignment
|
// parseAssignStatement parses variable assignment
|
||||||
@ -94,6 +120,7 @@ func (p *Parser) parseAssignStatement() *AssignStatement {
|
|||||||
stmt := &AssignStatement{}
|
stmt := &AssignStatement{}
|
||||||
|
|
||||||
if !p.curTokenIs(IDENT) {
|
if !p.curTokenIs(IDENT) {
|
||||||
|
p.addError("expected identifier for assignment")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +133,10 @@ func (p *Parser) parseAssignStatement() *AssignStatement {
|
|||||||
p.nextToken()
|
p.nextToken()
|
||||||
|
|
||||||
stmt.Value = p.parseExpression(LOWEST)
|
stmt.Value = p.parseExpression(LOWEST)
|
||||||
|
if stmt.Value == nil {
|
||||||
|
p.addError("expected expression after assignment operator")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return stmt
|
return stmt
|
||||||
}
|
}
|
||||||
@ -119,6 +150,9 @@ func (p *Parser) parseExpression(precedence Precedence) Expression {
|
|||||||
}
|
}
|
||||||
|
|
||||||
leftExp := prefix()
|
leftExp := prefix()
|
||||||
|
if leftExp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for !p.peekTokenIs(EOF) && precedence < p.peekPrecedence() {
|
for !p.peekTokenIs(EOF) && precedence < p.peekPrecedence() {
|
||||||
infix := p.infixParseFns[p.peekToken.Type]
|
infix := p.infixParseFns[p.peekToken.Type]
|
||||||
@ -128,6 +162,9 @@ func (p *Parser) parseExpression(precedence Precedence) Expression {
|
|||||||
|
|
||||||
p.nextToken()
|
p.nextToken()
|
||||||
leftExp = infix(leftExp)
|
leftExp = infix(leftExp)
|
||||||
|
if leftExp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return leftExp
|
return leftExp
|
||||||
@ -143,8 +180,7 @@ func (p *Parser) parseNumberLiteral() Expression {
|
|||||||
|
|
||||||
value, err := strconv.ParseFloat(p.curToken.Literal, 64)
|
value, err := strconv.ParseFloat(p.curToken.Literal, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("could not parse %q as float", p.curToken.Literal)
|
p.addError(fmt.Sprintf("could not parse '%s' as number", p.curToken.Literal))
|
||||||
p.errors = append(p.errors, msg)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +204,9 @@ func (p *Parser) parseGroupedExpression() Expression {
|
|||||||
p.nextToken()
|
p.nextToken()
|
||||||
|
|
||||||
exp := p.parseExpression(LOWEST)
|
exp := p.parseExpression(LOWEST)
|
||||||
|
if exp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if !p.expectPeek(RPAREN) {
|
if !p.expectPeek(RPAREN) {
|
||||||
return nil
|
return nil
|
||||||
@ -186,6 +225,11 @@ func (p *Parser) parseInfixExpression(left Expression) Expression {
|
|||||||
p.nextToken()
|
p.nextToken()
|
||||||
expression.Right = p.parseExpression(precedence)
|
expression.Right = p.parseExpression(precedence)
|
||||||
|
|
||||||
|
if expression.Right == nil {
|
||||||
|
p.addError(fmt.Sprintf("expected expression after operator '%s'", expression.Operator))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return expression
|
return expression
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,21 +246,48 @@ func (p *Parser) expectPeek(t TokenType) bool {
|
|||||||
if p.peekTokenIs(t) {
|
if p.peekTokenIs(t) {
|
||||||
p.nextToken()
|
p.nextToken()
|
||||||
return true
|
return true
|
||||||
} else {
|
}
|
||||||
p.peekError(t)
|
p.peekError(t)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error handling methods
|
||||||
|
func (p *Parser) addError(message string) {
|
||||||
|
p.errors = append(p.errors, ParseError{
|
||||||
|
Message: message,
|
||||||
|
Line: p.curToken.Line,
|
||||||
|
Column: p.curToken.Column,
|
||||||
|
Token: p.curToken,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) peekError(t TokenType) {
|
func (p *Parser) peekError(t TokenType) {
|
||||||
msg := fmt.Sprintf("expected next token to be %v, got %v instead",
|
message := fmt.Sprintf("expected next token to be %s, got %s instead",
|
||||||
t, p.peekToken.Type)
|
tokenTypeString(t), tokenTypeString(p.peekToken.Type))
|
||||||
p.errors = append(p.errors, msg)
|
p.errors = append(p.errors, ParseError{
|
||||||
|
Message: message,
|
||||||
|
Line: p.peekToken.Line,
|
||||||
|
Column: p.peekToken.Column,
|
||||||
|
Token: p.peekToken,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) noPrefixParseFnError(t TokenType) {
|
func (p *Parser) noPrefixParseFnError(t TokenType) {
|
||||||
msg := fmt.Sprintf("no prefix parse function for %v found", t)
|
var message string
|
||||||
p.errors = append(p.errors, msg)
|
switch t {
|
||||||
|
case ASSIGN:
|
||||||
|
message = "unexpected assignment operator, missing left-hand side identifier"
|
||||||
|
case PLUS, MINUS, STAR, SLASH:
|
||||||
|
message = fmt.Sprintf("unexpected operator '%s', missing left operand", tokenTypeString(t))
|
||||||
|
case RPAREN:
|
||||||
|
message = "unexpected closing parenthesis"
|
||||||
|
case EOF:
|
||||||
|
message = "unexpected end of input"
|
||||||
|
default:
|
||||||
|
message = fmt.Sprintf("unexpected token '%s'", tokenTypeString(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
p.addError(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) peekPrecedence() Precedence {
|
func (p *Parser) peekPrecedence() Precedence {
|
||||||
@ -234,6 +305,58 @@ func (p *Parser) curPrecedence() Precedence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Errors returns all parsing errors
|
// Errors returns all parsing errors
|
||||||
func (p *Parser) Errors() []string {
|
func (p *Parser) Errors() []ParseError {
|
||||||
return p.errors
|
return p.errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasErrors returns true if there are any parsing errors
|
||||||
|
func (p *Parser) HasErrors() bool {
|
||||||
|
return len(p.errors) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorStrings returns error messages as strings for backward compatibility
|
||||||
|
func (p *Parser) ErrorStrings() []string {
|
||||||
|
result := make([]string, len(p.errors))
|
||||||
|
for i, err := range p.errors {
|
||||||
|
result[i] = err.Error()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenTypeString returns a human-readable string for token types
|
||||||
|
func tokenTypeString(t TokenType) string {
|
||||||
|
switch t {
|
||||||
|
case IDENT:
|
||||||
|
return "identifier"
|
||||||
|
case NUMBER:
|
||||||
|
return "number"
|
||||||
|
case STRING:
|
||||||
|
return "string"
|
||||||
|
case TRUE, FALSE:
|
||||||
|
return "boolean"
|
||||||
|
case NIL:
|
||||||
|
return "nil"
|
||||||
|
case ASSIGN:
|
||||||
|
return "="
|
||||||
|
case PLUS:
|
||||||
|
return "+"
|
||||||
|
case MINUS:
|
||||||
|
return "-"
|
||||||
|
case STAR:
|
||||||
|
return "*"
|
||||||
|
case SLASH:
|
||||||
|
return "/"
|
||||||
|
case LPAREN:
|
||||||
|
return "("
|
||||||
|
case RPAREN:
|
||||||
|
return ")"
|
||||||
|
case VAR:
|
||||||
|
return "var"
|
||||||
|
case EOF:
|
||||||
|
return "end of file"
|
||||||
|
case ILLEGAL:
|
||||||
|
return "illegal token"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,19 +20,11 @@ func TestLiterals(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.input, func(t *testing.T) {
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
// Parse as expression directly - literals are not valid statements
|
||||||
l := NewLexer(tt.input)
|
l := NewLexer(tt.input)
|
||||||
p := NewParser(l)
|
p := NewParser(l)
|
||||||
program := p.ParseProgram()
|
|
||||||
checkParserErrors(t, p)
|
|
||||||
|
|
||||||
if len(program.Statements) != 0 {
|
|
||||||
t.Fatalf("expected 0 statements for literal, got %d", len(program.Statements))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse as expression
|
|
||||||
l = NewLexer(tt.input)
|
|
||||||
p = NewParser(l)
|
|
||||||
expr := p.parseExpression(LOWEST)
|
expr := p.parseExpression(LOWEST)
|
||||||
|
checkParserErrors(t, p)
|
||||||
|
|
||||||
switch expected := tt.expected.(type) {
|
switch expected := tt.expected.(type) {
|
||||||
case float64:
|
case float64:
|
||||||
@ -159,10 +152,16 @@ func TestParsingErrors(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
expectedError string
|
expectedError string
|
||||||
|
line int
|
||||||
|
column int
|
||||||
}{
|
}{
|
||||||
{"x =", "no prefix parse function"},
|
{"= 5", "assignment operator '=' without left-hand side identifier", 1, 1},
|
||||||
{"= 5", "no prefix parse function"},
|
{"x =", "expected expression after assignment operator", 1, 3},
|
||||||
{"(1 + 2", "expected next token to be"},
|
{"(1 + 2", "expected next token to be )", 1, 7},
|
||||||
|
{"+ 5", "unexpected operator '+'", 1, 1},
|
||||||
|
{"1 +", "expected expression after operator '+'", 1, 3},
|
||||||
|
{"@", "unexpected token '@'", 1, 1},
|
||||||
|
{"invalid@", "unexpected identifier", 1, 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -170,11 +169,14 @@ func TestParsingErrors(t *testing.T) {
|
|||||||
l := NewLexer(tt.input)
|
l := NewLexer(tt.input)
|
||||||
p := NewParser(l)
|
p := NewParser(l)
|
||||||
|
|
||||||
if tt.input == "x =" {
|
// Decide parsing strategy based on the type of error we're testing
|
||||||
p.ParseProgram()
|
switch tt.input {
|
||||||
} else {
|
case "(1 + 2", "+ 5", "1 +":
|
||||||
// Parse as expression to catch syntax errors
|
// These are expression-level errors
|
||||||
p.parseExpression(LOWEST)
|
p.parseExpression(LOWEST)
|
||||||
|
default:
|
||||||
|
// These are statement-level errors
|
||||||
|
p.ParseProgram()
|
||||||
}
|
}
|
||||||
|
|
||||||
errors := p.Errors()
|
errors := p.Errors()
|
||||||
@ -184,19 +186,69 @@ func TestParsingErrors(t *testing.T) {
|
|||||||
|
|
||||||
found := false
|
found := false
|
||||||
for _, err := range errors {
|
for _, err := range errors {
|
||||||
if containsSubstring(err, tt.expectedError) {
|
if strings.Contains(err.Message, tt.expectedError) {
|
||||||
found = true
|
found = true
|
||||||
|
if err.Line != tt.line {
|
||||||
|
t.Errorf("expected error at line %d, got line %d", tt.line, err.Line)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Errorf("expected error containing %q, got %v", tt.expectedError, errors)
|
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 TestErrorRecovery(t *testing.T) {
|
||||||
|
input := `x = 42
|
||||||
|
= 5
|
||||||
|
y = "hello"`
|
||||||
|
|
||||||
|
l := NewLexer(input)
|
||||||
|
p := NewParser(l)
|
||||||
|
program := p.ParseProgram()
|
||||||
|
|
||||||
|
// Should have errors but still parse valid statements
|
||||||
|
if !p.HasErrors() {
|
||||||
|
t.Fatal("expected parsing errors")
|
||||||
|
}
|
||||||
|
|
||||||
|
errors := p.Errors()
|
||||||
|
found := false
|
||||||
|
for _, err := range errors {
|
||||||
|
if strings.Contains(err.Message, "assignment operator '=' without left-hand side identifier") {
|
||||||
|
found = true
|
||||||
|
if err.Line != 2 {
|
||||||
|
t.Errorf("expected error at line 2, got line %d", err.Line)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Error("expected specific assignment error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should still have parsed the valid statements
|
||||||
|
validStatements := 0
|
||||||
|
for _, stmt := range program.Statements {
|
||||||
|
if stmt != nil {
|
||||||
|
validStatements++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if validStatements < 2 {
|
||||||
|
t.Errorf("expected at least 2 valid statements, got %d", validStatements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProgram(t *testing.T) {
|
func TestProgram(t *testing.T) {
|
||||||
input := `x = 42
|
input := `x = 42
|
||||||
y = "hello"
|
y = "hello"
|
||||||
@ -224,6 +276,39 @@ z = true + false`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestErrorMessages(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expectedMessage string
|
||||||
|
}{
|
||||||
|
{"= 5", "Parse error at line 1, column 1: assignment operator '=' without left-hand side identifier (near '=')"},
|
||||||
|
{"x =", "Parse error at line 1, column 3: expected expression after assignment operator (near '')"},
|
||||||
|
{"(", "Parse error at line 1, column 1: unexpected end of input (near '')"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
l := NewLexer(tt.input)
|
||||||
|
p := 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions for testing specific node types
|
// Helper functions for testing specific node types
|
||||||
func testNumberLiteral(t *testing.T, expr Expression, expected float64) {
|
func testNumberLiteral(t *testing.T, expr Expression, expected float64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@ -315,17 +400,8 @@ func checkParserErrors(t *testing.T, p *Parser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Errorf("parser has %d errors", len(errors))
|
t.Errorf("parser has %d errors", len(errors))
|
||||||
for _, msg := range errors {
|
for _, err := range errors {
|
||||||
t.Errorf("parser error: %q", msg)
|
t.Errorf("parser error: %s", err.Error())
|
||||||
}
|
}
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsSubstring(s, substr string) bool {
|
|
||||||
for i := 0; i <= len(s)-len(substr); i++ {
|
|
||||||
if s[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user