Step 7: Add integration tests for function definitions - define+call, multi-param, cross-function, nested, errors
diff --git a/integration_test.go b/integration_test.go
index 31ddd3f..b928d00 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -5,6 +5,7 @@
"strings"
"testing"
+ "matheval/ast"
"matheval/evaluator"
"matheval/lexer"
"matheval/parser"
@@ -43,6 +44,76 @@
return err
}
+// evalLine is a helper that uses ParseLine + Evaluator for a multi-line session.
+// It processes lines sequentially on a shared evaluator and returns the result
+// of the last expression statement.
+func evalLines(t *testing.T, ev *evaluator.Evaluator, lines ...string) float64 {
+ t.Helper()
+ var lastResult float64
+ for _, line := range lines {
+ tokens, err := lexer.Tokenize(line)
+ if err != nil {
+ t.Fatalf("lexer error for %q: %v", line, err)
+ }
+ stmt, err := parser.ParseLine(tokens)
+ if err != nil {
+ t.Fatalf("parser error for %q: %v", line, err)
+ }
+ switch s := stmt.(type) {
+ case *ast.FuncDef:
+ if err := ev.Define(s); err != nil {
+ t.Fatalf("define error for %q: %v", line, err)
+ }
+ case *ast.ExprStmt:
+ result, err := ev.Eval(s.Expr, nil)
+ if err != nil {
+ t.Fatalf("eval error for %q: %v", line, err)
+ }
+ lastResult = result
+ }
+ }
+ return lastResult
+}
+
+// evalLinesErr processes lines and expects the last one to produce an error.
+func evalLinesErr(t *testing.T, ev *evaluator.Evaluator, lines ...string) error {
+ t.Helper()
+ for i, line := range lines {
+ tokens, err := lexer.Tokenize(line)
+ if err != nil {
+ if i == len(lines)-1 {
+ return err
+ }
+ t.Fatalf("lexer error for %q: %v", line, err)
+ }
+ stmt, err := parser.ParseLine(tokens)
+ if err != nil {
+ if i == len(lines)-1 {
+ return err
+ }
+ t.Fatalf("parser error for %q: %v", line, err)
+ }
+ switch s := stmt.(type) {
+ case *ast.FuncDef:
+ if err := ev.Define(s); err != nil {
+ if i == len(lines)-1 {
+ return err
+ }
+ t.Fatalf("define error for %q: %v", line, err)
+ }
+ case *ast.ExprStmt:
+ _, err := ev.Eval(s.Expr, nil)
+ if err != nil {
+ if i == len(lines)-1 {
+ return err
+ }
+ t.Fatalf("eval error for %q: %v", line, err)
+ }
+ }
+ }
+ return nil
+}
+
func assertApprox(t *testing.T, input string, expected, got float64) {
t.Helper()
if math.Abs(expected-got) > 1e-9 {
@@ -259,3 +330,108 @@
t.Fatal("expected error for consecutive numbers without operator")
}
}
+
+// --- Function definitions (full pipeline) ---
+
+func TestIntegration_DefineAndCallSingleParam(t *testing.T) {
+ ev := evaluator.New()
+ result := evalLines(t, ev, "f(x) = x + 1", "f(5)")
+ assertApprox(t, "f(5)", 6, result)
+}
+
+func TestIntegration_DefineAndCallMultiParam(t *testing.T) {
+ ev := evaluator.New()
+ result := evalLines(t, ev, "add(x, y) = x + y", "add(3, 4)")
+ assertApprox(t, "add(3, 4)", 7, result)
+}
+
+func TestIntegration_CrossFunctionCalls(t *testing.T) {
+ ev := evaluator.New()
+ result := evalLines(t, ev,
+ "double(x) = x * 2",
+ "quad(x) = double(double(x))",
+ "quad(3)",
+ )
+ assertApprox(t, "quad(3)", 12, result)
+}
+
+func TestIntegration_NestedFuncCallsInExpr(t *testing.T) {
+ ev := evaluator.New()
+ result := evalLines(t, ev,
+ "f(x) = x + 1",
+ "f(f(f(1)))",
+ )
+ // f(1)=2, f(2)=3, f(3)=4
+ assertApprox(t, "f(f(f(1)))", 4, result)
+}
+
+func TestIntegration_FuncCallInBinaryExpr(t *testing.T) {
+ ev := evaluator.New()
+ result := evalLines(t, ev,
+ "f(x) = x * 2",
+ "f(3) + f(4)",
+ )
+ // f(3)=6, f(4)=8, 6+8=14
+ assertApprox(t, "f(3)+f(4)", 14, result)
+}
+
+func TestIntegration_FuncWithExprBody(t *testing.T) {
+ ev := evaluator.New()
+ result := evalLines(t, ev,
+ "area(w, h) = w * h",
+ "area(3, 5) + 1",
+ )
+ assertApprox(t, "area(3,5)+1", 16, result)
+}
+
+func TestIntegration_FuncNoParams(t *testing.T) {
+ ev := evaluator.New()
+ result := evalLines(t, ev, "pi() = 3", "pi() + 1")
+ assertApprox(t, "pi()+1", 4, result)
+}
+
+// --- Function error cases (full pipeline) ---
+
+func TestIntegration_UndefinedFunction(t *testing.T) {
+ ev := evaluator.New()
+ err := evalLinesErr(t, ev, "f(1)")
+ if err == nil {
+ t.Fatal("expected error for undefined function")
+ }
+ if !strings.Contains(err.Error(), "undefined function") {
+ t.Errorf("expected 'undefined function' in error, got: %v", err)
+ }
+}
+
+func TestIntegration_WrongArgCount(t *testing.T) {
+ ev := evaluator.New()
+ err := evalLinesErr(t, ev, "f(x) = x", "f(1, 2)")
+ if err == nil {
+ t.Fatal("expected error for wrong argument count")
+ }
+ if !strings.Contains(err.Error(), "expects 1 arguments, got 2") {
+ t.Errorf("expected arg count error, got: %v", err)
+ }
+}
+
+func TestIntegration_FunctionRedefinition(t *testing.T) {
+ ev := evaluator.New()
+ err := evalLinesErr(t, ev, "f(x) = x", "f(x) = x + 1")
+ if err == nil {
+ t.Fatal("expected error for function redefinition")
+ }
+ if !strings.Contains(err.Error(), "already defined") {
+ t.Errorf("expected 'already defined' in error, got: %v", err)
+ }
+}
+
+func TestIntegration_UndefinedVariable(t *testing.T) {
+ ev := evaluator.New()
+ err := evalLinesErr(t, ev, "f(x) = x + y", "f(1)")
+ if err == nil {
+ t.Fatal("expected error for undefined variable")
+ }
+ if !strings.Contains(err.Error(), "undefined variable") {
+ t.Errorf("expected 'undefined variable' in error, got: %v", err)
+ }
+}