| package matheval_test |
| |
| import ( |
| "math" |
| "strings" |
| "testing" |
| |
| "matheval/ast" |
| "matheval/evaluator" |
| "matheval/lexer" |
| "matheval/parser" |
| ) |
| |
| // eval is a helper that runs the full pipeline: lexer → parser → evaluator. |
| func eval(t *testing.T, input string) float64 { |
| t.Helper() |
| tokens, err := lexer.Tokenize(input) |
| if err != nil { |
| t.Fatalf("lexer error for %q: %v", input, err) |
| } |
| tree, err := parser.Parse(tokens) |
| if err != nil { |
| t.Fatalf("parser error for %q: %v", input, err) |
| } |
| result, err := evaluator.Eval(tree) |
| if err != nil { |
| t.Fatalf("evaluator error for %q: %v", input, err) |
| } |
| return result |
| } |
| |
| // evalErr is a helper that expects the full pipeline to return an error. |
| func evalErr(t *testing.T, input string) error { |
| t.Helper() |
| tokens, err := lexer.Tokenize(input) |
| if err != nil { |
| return err |
| } |
| tree, err := parser.Parse(tokens) |
| if err != nil { |
| return err |
| } |
| _, err = evaluator.Eval(tree) |
| 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 { |
| t.Errorf("%q: expected %v, got %v", input, expected, got) |
| } |
| } |
| |
| // --- Basic arithmetic --- |
| |
| func TestIntegration_SingleNumber(t *testing.T) { |
| assertApprox(t, "42", 42, eval(t, "42")) |
| } |
| |
| func TestIntegration_DecimalNumber(t *testing.T) { |
| assertApprox(t, "3.14", 3.14, eval(t, "3.14")) |
| } |
| |
| func TestIntegration_LeadingDot(t *testing.T) { |
| assertApprox(t, ".5", 0.5, eval(t, ".5")) |
| } |
| |
| func TestIntegration_Addition(t *testing.T) { |
| assertApprox(t, "1 + 2", 3, eval(t, "1 + 2")) |
| } |
| |
| func TestIntegration_Subtraction(t *testing.T) { |
| assertApprox(t, "10 - 4", 6, eval(t, "10 - 4")) |
| } |
| |
| func TestIntegration_Multiplication(t *testing.T) { |
| assertApprox(t, "3 * 7", 21, eval(t, "3 * 7")) |
| } |
| |
| func TestIntegration_Division(t *testing.T) { |
| assertApprox(t, "10 / 4", 2.5, eval(t, "10 / 4")) |
| } |
| |
| // --- Precedence and associativity --- |
| |
| func TestIntegration_PrecedenceMulOverAdd(t *testing.T) { |
| // 2 + 3 * 4 = 2 + 12 = 14 |
| assertApprox(t, "2 + 3 * 4", 14, eval(t, "2 + 3 * 4")) |
| } |
| |
| func TestIntegration_PrecedenceDivOverSub(t *testing.T) { |
| // 10 - 6 / 3 = 10 - 2 = 8 |
| assertApprox(t, "10 - 6 / 3", 8, eval(t, "10 - 6 / 3")) |
| } |
| |
| func TestIntegration_LeftAssociativitySub(t *testing.T) { |
| // 10 - 3 - 2 = (10 - 3) - 2 = 5 |
| assertApprox(t, "10 - 3 - 2", 5, eval(t, "10 - 3 - 2")) |
| } |
| |
| func TestIntegration_LeftAssociativityDiv(t *testing.T) { |
| // 24 / 4 / 3 = (24 / 4) / 3 = 2 |
| assertApprox(t, "24 / 4 / 3", 2, eval(t, "24 / 4 / 3")) |
| } |
| |
| // --- Parentheses --- |
| |
| func TestIntegration_ParensOverridePrecedence(t *testing.T) { |
| // (2 + 3) * 4 = 20 |
| assertApprox(t, "(2 + 3) * 4", 20, eval(t, "(2 + 3) * 4")) |
| } |
| |
| func TestIntegration_NestedParens(t *testing.T) { |
| // ((1 + 2)) = 3 |
| assertApprox(t, "((1 + 2))", 3, eval(t, "((1 + 2))")) |
| } |
| |
| func TestIntegration_DeeplyNestedParens(t *testing.T) { |
| // ((((((1 + 2)))))) = 3 |
| assertApprox(t, "((((((1 + 2))))))", 3, eval(t, "((((((1 + 2))))))")) |
| } |
| |
| func TestIntegration_ParensOnBothSides(t *testing.T) { |
| // (1 + 2) * (3 + 4) = 3 * 7 = 21 |
| assertApprox(t, "(1 + 2) * (3 + 4)", 21, eval(t, "(1 + 2) * (3 + 4)")) |
| } |
| |
| func TestIntegration_ParensNestedComplex(t *testing.T) { |
| // ((2 + 3) * (4 - 1)) / 5 = (5 * 3) / 5 = 3 |
| assertApprox(t, "((2 + 3) * (4 - 1)) / 5", 3, eval(t, "((2 + 3) * (4 - 1)) / 5")) |
| } |
| |
| // --- Complex expressions --- |
| |
| func TestIntegration_AllOperators(t *testing.T) { |
| // 1 + 2 * 3 - 4 / 2 = 1 + 6 - 2 = 5 |
| assertApprox(t, "1 + 2 * 3 - 4 / 2", 5, eval(t, "1 + 2 * 3 - 4 / 2")) |
| } |
| |
| func TestIntegration_LongChainedAddition(t *testing.T) { |
| // 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55 |
| assertApprox(t, "1+2+3+4+5+6+7+8+9+10", 55, eval(t, "1+2+3+4+5+6+7+8+9+10")) |
| } |
| |
| func TestIntegration_LongChainedMixed(t *testing.T) { |
| // 2 * 3 + 4 * 5 - 6 / 2 + 1 = 6 + 20 - 3 + 1 = 24 |
| assertApprox(t, "2 * 3 + 4 * 5 - 6 / 2 + 1", 24, eval(t, "2 * 3 + 4 * 5 - 6 / 2 + 1")) |
| } |
| |
| // --- Floating point --- |
| |
| func TestIntegration_FloatArithmetic(t *testing.T) { |
| // 1.5 + 2.5 = 4.0 |
| assertApprox(t, "1.5 + 2.5", 4.0, eval(t, "1.5 + 2.5")) |
| } |
| |
| func TestIntegration_FloatDivision(t *testing.T) { |
| // 7 / 2 = 3.5 |
| assertApprox(t, "7 / 2", 3.5, eval(t, "7 / 2")) |
| } |
| |
| func TestIntegration_FloatPrecision(t *testing.T) { |
| // 0.1 + 0.2 ≈ 0.3 (within tolerance) |
| assertApprox(t, "0.1 + 0.2", 0.3, eval(t, "0.1 + 0.2")) |
| } |
| |
| // --- Whitespace variations --- |
| |
| func TestIntegration_NoSpaces(t *testing.T) { |
| assertApprox(t, "1+2*3", 7, eval(t, "1+2*3")) |
| } |
| |
| func TestIntegration_ExtraSpaces(t *testing.T) { |
| assertApprox(t, " 1 + 2 ", 3, eval(t, " 1 + 2 ")) |
| } |
| |
| func TestIntegration_TabsAndSpaces(t *testing.T) { |
| assertApprox(t, "1\t+\t2", 3, eval(t, "1\t+\t2")) |
| } |
| |
| // --- Error cases --- |
| |
| func TestIntegration_DivisionByZero(t *testing.T) { |
| err := evalErr(t, "1 / 0") |
| if err == nil { |
| t.Fatal("expected division by zero error") |
| } |
| if !strings.Contains(err.Error(), "division by zero") { |
| t.Errorf("expected 'division by zero' in error, got: %v", err) |
| } |
| } |
| |
| func TestIntegration_DivisionByZeroInSubExpr(t *testing.T) { |
| err := evalErr(t, "1 + 2 / 0") |
| if err == nil { |
| t.Fatal("expected division by zero error") |
| } |
| } |
| |
| func TestIntegration_InvalidCharacter(t *testing.T) { |
| err := evalErr(t, "1 @ 2") |
| if err == nil { |
| t.Fatal("expected error for invalid character") |
| } |
| } |
| |
| func TestIntegration_MismatchedParenLeft(t *testing.T) { |
| err := evalErr(t, "(1 + 2") |
| if err == nil { |
| t.Fatal("expected error for missing closing paren") |
| } |
| } |
| |
| func TestIntegration_MismatchedParenRight(t *testing.T) { |
| err := evalErr(t, "1 + 2)") |
| if err == nil { |
| t.Fatal("expected error for unexpected closing paren") |
| } |
| } |
| |
| func TestIntegration_EmptyParens(t *testing.T) { |
| err := evalErr(t, "()") |
| if err == nil { |
| t.Fatal("expected error for empty parentheses") |
| } |
| } |
| |
| func TestIntegration_TrailingOperator(t *testing.T) { |
| err := evalErr(t, "1 +") |
| if err == nil { |
| t.Fatal("expected error for trailing operator") |
| } |
| } |
| |
| func TestIntegration_LeadingOperator(t *testing.T) { |
| err := evalErr(t, "* 1") |
| if err == nil { |
| t.Fatal("expected error for leading operator") |
| } |
| } |
| |
| func TestIntegration_ConsecutiveOperators(t *testing.T) { |
| err := evalErr(t, "1 + * 2") |
| if err == nil { |
| t.Fatal("expected error for consecutive operators") |
| } |
| } |
| |
| func TestIntegration_EmptyInput(t *testing.T) { |
| // Empty string should produce only EOF, parser should error |
| err := evalErr(t, "") |
| if err == nil { |
| t.Fatal("expected error for empty input") |
| } |
| } |
| |
| func TestIntegration_ConsecutiveNumbers(t *testing.T) { |
| err := evalErr(t, "1 2") |
| if err == nil { |
| 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) |
| } |
| } |