Implementation Plan: Function Definitions

Overview

Bottom-up implementation through the stack: token → ast → lexer → parser → evaluator → repl → integration tests. Each step maintains backward compatibility and follows TDD.

Steps

Step 1: Token layer (token/token.go)

  • Add Ident, Comma, Equals constants to Type enum
  • Update String() for new types
  • No tests needed — pure data types

Step 2: AST layer (ast/ast.go)

  • Add Ident struct: Name string; implements Node
  • Add FuncCall struct: Name string, Args []Node; implements Node
  • Add Statement interface with sealed stmt() marker
  • Add ExprStmt struct: Expr Node; implements Statement
  • Add FuncDef struct: Name string, Params []string, Body Node; implements Statement
  • No tests needed — pure data types

Step 3: Lexer (lexer/lexer.go)

  • Add isLetter(ch byte) bool helper
  • Before the single-char switch, add branch: if isLetter(ch), scan identifier (letter then letters/digits), emit Ident token
  • Add ','Comma and '='Equals to single-char switch
  • Tests: identifiers (x, foo, f1), comma, equals, full definition f(x) = x + 1, call f(1, 2), mixed with numbers

Step 4: Parser (parser/parser.go)

  • Extend factor():
    • Ident followed by LParen → parse FuncCall: consume (, parse args as comma-separated exprs, consume )
    • Ident not followed by LParen → return &ast.Ident{Name}
  • Add parseFuncDef(): expects Ident( params ) = expr
  • Add ParseLine(tokens) (Statement, error):
    • Scan for Equals token (not inside parens)
    • If found → parseFuncDef()*ast.FuncDef
    • If not → expr()*ast.ExprStmt{Expr}
  • Keep Parse() unchanged for backward compat
  • Tests: ParseLine for defs and exprs, factor for ident and func call, error cases

Step 5: Evaluator (evaluator/evaluator.go)

  • Add Evaluator struct with funcs map[string]*ast.FuncDef
  • New() *Evaluator
  • Define(def *ast.FuncDef) error — error on redefinition
  • Eval(node ast.Node, env map[string]float64) (float64, error):
    • *ast.NumberLit → return value
    • *ast.BinaryExpr → recurse left/right with same env
    • *ast.Ident → lookup in env, error if not found
    • *ast.FuncCall → lookup func, eval args in caller env, bind params, eval body in new env
  • Keep package-level Eval(node) (float64, error) as backward-compat wrapper
  • Tests: all existing tests still pass, new tests for Ident, FuncCall, Define, errors

Step 6: REPL (repl/repl.go)

  • In Run(): create evaluator.New() before loop
  • Replace evalLine() with inline logic using ParseLine()
  • *ast.FuncDefev.Define(def), print "defined <name>"
  • *ast.ExprStmtev.Eval(stmt.Expr, nil), print result
  • Tests: define + call across lines, redefine error, undefined func error

Step 7: Integration tests (integration_test.go)

  • Update eval()/evalErr() helpers to use Evaluator struct
  • Add tests:
    • Define and call single-param function
    • Define and call multi-param function
    • Cross-function calls
    • Nested function calls in expressions
    • Error: undefined function
    • Error: wrong argument count
    • Error: function redefinition
    • Error: undefined variable

Backward Compatibility

  • Parse() remains unchanged — returns ast.Node
  • Package-level Eval() remains — wraps New().Eval(node, nil)
  • Existing tests must continue to pass at every step