Step 4: Add parser tests for Ident, FuncCall, ParseLine, and error cases
diff --git a/parser/parser.go b/parser/parser.go
index c7be53b..15b4cea 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -23,6 +23,125 @@
return node, nil
}
+// ParseLine parses a full REPL line, returning either a function definition
+// or an expression statement.
+func ParseLine(tokens []token.Token) (ast.Statement, error) {
+ if len(tokens) == 0 || tokens[0].Type == token.EOF {
+ return nil, fmt.Errorf("empty input")
+ }
+
+ // Detect function definition: look for Equals token.
+ // A function definition has the form: Ident LParen params RParen Equals body
+ if isFuncDef(tokens) {
+ return parseFuncDef(tokens)
+ }
+
+ // Otherwise, parse as expression.
+ node, err := Parse(tokens)
+ if err != nil {
+ return nil, err
+ }
+ return &ast.ExprStmt{Expr: node}, nil
+}
+
+// isFuncDef checks if the token stream looks like a function definition.
+// Pattern: Ident LParen ... RParen Equals ...
+func isFuncDef(tokens []token.Token) bool {
+ if len(tokens) < 5 {
+ return false
+ }
+ if tokens[0].Type != token.Ident {
+ return false
+ }
+ if tokens[1].Type != token.LParen {
+ return false
+ }
+ // Find matching RParen, then check for Equals.
+ depth := 0
+ for i := 1; i < len(tokens); i++ {
+ switch tokens[i].Type {
+ case token.LParen:
+ depth++
+ case token.RParen:
+ depth--
+ if depth == 0 {
+ // Next token must be Equals for this to be a func def.
+ if i+1 < len(tokens) && tokens[i+1].Type == token.Equals {
+ return true
+ }
+ return false
+ }
+ case token.EOF:
+ return false
+ }
+ }
+ return false
+}
+
+// parseFuncDef parses: Ident LParen param1, param2, ... RParen Equals body
+func parseFuncDef(tokens []token.Token) (*ast.FuncDef, error) {
+ p := &parser{tokens: tokens}
+
+ // Function name.
+ nameTok, err := p.expect(token.Ident)
+ if err != nil {
+ return nil, fmt.Errorf("expected function name: %w", err)
+ }
+ name := nameTok.Literal
+
+ // Opening paren.
+ if _, err := p.expect(token.LParen); err != nil {
+ return nil, fmt.Errorf("expected '(' after function name: %w", err)
+ }
+
+ // Parameters: comma-separated identifiers.
+ var params []string
+ if p.current().Type != token.RParen {
+ paramTok, err := p.expect(token.Ident)
+ if err != nil {
+ return nil, fmt.Errorf("expected parameter name: %w", err)
+ }
+ params = append(params, paramTok.Literal)
+
+ for p.current().Type == token.Comma {
+ p.advance() // consume comma
+ paramTok, err := p.expect(token.Ident)
+ if err != nil {
+ return nil, fmt.Errorf("expected parameter name after ',': %w", err)
+ }
+ params = append(params, paramTok.Literal)
+ }
+ }
+
+ // Closing paren.
+ if _, err := p.expect(token.RParen); err != nil {
+ return nil, fmt.Errorf("expected ')' after parameters: %w", err)
+ }
+
+ // Equals sign.
+ if _, err := p.expect(token.Equals); err != nil {
+ return nil, fmt.Errorf("expected '=' in function definition: %w", err)
+ }
+
+ // Body expression.
+ body, err := p.expr()
+ if err != nil {
+ return nil, fmt.Errorf("error in function body: %w", err)
+ }
+
+ // Ensure all tokens consumed.
+ if p.current().Type != token.EOF {
+ tok := p.current()
+ return nil, fmt.Errorf("unexpected token %v at position %d after function body", tok.Type, tok.Pos)
+ }
+
+ return &ast.FuncDef{
+ Name: name,
+ Params: params,
+ Body: body,
+ }, nil
+}
+
// parser holds the state for a single parse operation.
type parser struct {
tokens []token.Token
@@ -96,7 +215,7 @@
return left, nil
}
-// factor → NUMBER | '(' expr ')'
+// factor → NUMBER | IDENT | IDENT '(' args ')' | '(' expr ')'
func (p *parser) factor() (ast.Node, error) {
tok := p.current()
switch tok.Type {
@@ -108,6 +227,35 @@
}
return &ast.NumberLit{Value: val}, nil
+ case token.Ident:
+ p.advance()
+ // If followed by '(', this is a function call.
+ if p.current().Type == token.LParen {
+ p.advance() // consume '('
+ var args []ast.Node
+ if p.current().Type != token.RParen {
+ arg, err := p.expr()
+ if err != nil {
+ return nil, err
+ }
+ args = append(args, arg)
+ for p.current().Type == token.Comma {
+ p.advance() // consume ','
+ arg, err := p.expr()
+ if err != nil {
+ return nil, err
+ }
+ args = append(args, arg)
+ }
+ }
+ if _, err := p.expect(token.RParen); err != nil {
+ return nil, fmt.Errorf("expected ')' after function arguments at position %d", p.current().Pos)
+ }
+ return &ast.FuncCall{Name: tok.Literal, Args: args}, nil
+ }
+ // Otherwise, it's a variable reference.
+ return &ast.Ident{Name: tok.Literal}, nil
+
case token.LParen:
p.advance() // consume '('
node, err := p.expr()