| Sketch🕴️ | afb61d0 | 2026-02-28 19:34:37 +0400 | [diff] [blame] | 1 | package matheval_test |
| 2 | |
| 3 | import ( |
| 4 | "math" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| Sketch🕴️ | 0279013 | 2026-02-28 21:02:09 +0400 | [diff] [blame^] | 8 | "matheval/ast" |
| Sketch🕴️ | afb61d0 | 2026-02-28 19:34:37 +0400 | [diff] [blame] | 9 | "matheval/evaluator" |
| 10 | "matheval/lexer" |
| 11 | "matheval/parser" |
| 12 | ) |
| 13 | |
| 14 | // eval is a helper that runs the full pipeline: lexer → parser → evaluator. |
| 15 | func eval(t *testing.T, input string) float64 { |
| 16 | t.Helper() |
| 17 | tokens, err := lexer.Tokenize(input) |
| 18 | if err != nil { |
| 19 | t.Fatalf("lexer error for %q: %v", input, err) |
| 20 | } |
| 21 | tree, err := parser.Parse(tokens) |
| 22 | if err != nil { |
| 23 | t.Fatalf("parser error for %q: %v", input, err) |
| 24 | } |
| 25 | result, err := evaluator.Eval(tree) |
| 26 | if err != nil { |
| 27 | t.Fatalf("evaluator error for %q: %v", input, err) |
| 28 | } |
| 29 | return result |
| 30 | } |
| 31 | |
| 32 | // evalErr is a helper that expects the full pipeline to return an error. |
| 33 | func evalErr(t *testing.T, input string) error { |
| 34 | t.Helper() |
| 35 | tokens, err := lexer.Tokenize(input) |
| 36 | if err != nil { |
| 37 | return err |
| 38 | } |
| 39 | tree, err := parser.Parse(tokens) |
| 40 | if err != nil { |
| 41 | return err |
| 42 | } |
| 43 | _, err = evaluator.Eval(tree) |
| 44 | return err |
| 45 | } |
| 46 | |
| Sketch🕴️ | 0279013 | 2026-02-28 21:02:09 +0400 | [diff] [blame^] | 47 | // evalLine is a helper that uses ParseLine + Evaluator for a multi-line session. |
| 48 | // It processes lines sequentially on a shared evaluator and returns the result |
| 49 | // of the last expression statement. |
| 50 | func evalLines(t *testing.T, ev *evaluator.Evaluator, lines ...string) float64 { |
| 51 | t.Helper() |
| 52 | var lastResult float64 |
| 53 | for _, line := range lines { |
| 54 | tokens, err := lexer.Tokenize(line) |
| 55 | if err != nil { |
| 56 | t.Fatalf("lexer error for %q: %v", line, err) |
| 57 | } |
| 58 | stmt, err := parser.ParseLine(tokens) |
| 59 | if err != nil { |
| 60 | t.Fatalf("parser error for %q: %v", line, err) |
| 61 | } |
| 62 | switch s := stmt.(type) { |
| 63 | case *ast.FuncDef: |
| 64 | if err := ev.Define(s); err != nil { |
| 65 | t.Fatalf("define error for %q: %v", line, err) |
| 66 | } |
| 67 | case *ast.ExprStmt: |
| 68 | result, err := ev.Eval(s.Expr, nil) |
| 69 | if err != nil { |
| 70 | t.Fatalf("eval error for %q: %v", line, err) |
| 71 | } |
| 72 | lastResult = result |
| 73 | } |
| 74 | } |
| 75 | return lastResult |
| 76 | } |
| 77 | |
| 78 | // evalLinesErr processes lines and expects the last one to produce an error. |
| 79 | func evalLinesErr(t *testing.T, ev *evaluator.Evaluator, lines ...string) error { |
| 80 | t.Helper() |
| 81 | for i, line := range lines { |
| 82 | tokens, err := lexer.Tokenize(line) |
| 83 | if err != nil { |
| 84 | if i == len(lines)-1 { |
| 85 | return err |
| 86 | } |
| 87 | t.Fatalf("lexer error for %q: %v", line, err) |
| 88 | } |
| 89 | stmt, err := parser.ParseLine(tokens) |
| 90 | if err != nil { |
| 91 | if i == len(lines)-1 { |
| 92 | return err |
| 93 | } |
| 94 | t.Fatalf("parser error for %q: %v", line, err) |
| 95 | } |
| 96 | switch s := stmt.(type) { |
| 97 | case *ast.FuncDef: |
| 98 | if err := ev.Define(s); err != nil { |
| 99 | if i == len(lines)-1 { |
| 100 | return err |
| 101 | } |
| 102 | t.Fatalf("define error for %q: %v", line, err) |
| 103 | } |
| 104 | case *ast.ExprStmt: |
| 105 | _, err := ev.Eval(s.Expr, nil) |
| 106 | if err != nil { |
| 107 | if i == len(lines)-1 { |
| 108 | return err |
| 109 | } |
| 110 | t.Fatalf("eval error for %q: %v", line, err) |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | return nil |
| 115 | } |
| 116 | |
| Sketch🕴️ | afb61d0 | 2026-02-28 19:34:37 +0400 | [diff] [blame] | 117 | func assertApprox(t *testing.T, input string, expected, got float64) { |
| 118 | t.Helper() |
| 119 | if math.Abs(expected-got) > 1e-9 { |
| 120 | t.Errorf("%q: expected %v, got %v", input, expected, got) |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | // --- Basic arithmetic --- |
| 125 | |
| 126 | func TestIntegration_SingleNumber(t *testing.T) { |
| 127 | assertApprox(t, "42", 42, eval(t, "42")) |
| 128 | } |
| 129 | |
| 130 | func TestIntegration_DecimalNumber(t *testing.T) { |
| 131 | assertApprox(t, "3.14", 3.14, eval(t, "3.14")) |
| 132 | } |
| 133 | |
| 134 | func TestIntegration_LeadingDot(t *testing.T) { |
| 135 | assertApprox(t, ".5", 0.5, eval(t, ".5")) |
| 136 | } |
| 137 | |
| 138 | func TestIntegration_Addition(t *testing.T) { |
| 139 | assertApprox(t, "1 + 2", 3, eval(t, "1 + 2")) |
| 140 | } |
| 141 | |
| 142 | func TestIntegration_Subtraction(t *testing.T) { |
| 143 | assertApprox(t, "10 - 4", 6, eval(t, "10 - 4")) |
| 144 | } |
| 145 | |
| 146 | func TestIntegration_Multiplication(t *testing.T) { |
| 147 | assertApprox(t, "3 * 7", 21, eval(t, "3 * 7")) |
| 148 | } |
| 149 | |
| 150 | func TestIntegration_Division(t *testing.T) { |
| 151 | assertApprox(t, "10 / 4", 2.5, eval(t, "10 / 4")) |
| 152 | } |
| 153 | |
| 154 | // --- Precedence and associativity --- |
| 155 | |
| 156 | func TestIntegration_PrecedenceMulOverAdd(t *testing.T) { |
| 157 | // 2 + 3 * 4 = 2 + 12 = 14 |
| 158 | assertApprox(t, "2 + 3 * 4", 14, eval(t, "2 + 3 * 4")) |
| 159 | } |
| 160 | |
| 161 | func TestIntegration_PrecedenceDivOverSub(t *testing.T) { |
| 162 | // 10 - 6 / 3 = 10 - 2 = 8 |
| 163 | assertApprox(t, "10 - 6 / 3", 8, eval(t, "10 - 6 / 3")) |
| 164 | } |
| 165 | |
| 166 | func TestIntegration_LeftAssociativitySub(t *testing.T) { |
| 167 | // 10 - 3 - 2 = (10 - 3) - 2 = 5 |
| 168 | assertApprox(t, "10 - 3 - 2", 5, eval(t, "10 - 3 - 2")) |
| 169 | } |
| 170 | |
| 171 | func TestIntegration_LeftAssociativityDiv(t *testing.T) { |
| 172 | // 24 / 4 / 3 = (24 / 4) / 3 = 2 |
| 173 | assertApprox(t, "24 / 4 / 3", 2, eval(t, "24 / 4 / 3")) |
| 174 | } |
| 175 | |
| 176 | // --- Parentheses --- |
| 177 | |
| 178 | func TestIntegration_ParensOverridePrecedence(t *testing.T) { |
| 179 | // (2 + 3) * 4 = 20 |
| 180 | assertApprox(t, "(2 + 3) * 4", 20, eval(t, "(2 + 3) * 4")) |
| 181 | } |
| 182 | |
| 183 | func TestIntegration_NestedParens(t *testing.T) { |
| 184 | // ((1 + 2)) = 3 |
| 185 | assertApprox(t, "((1 + 2))", 3, eval(t, "((1 + 2))")) |
| 186 | } |
| 187 | |
| 188 | func TestIntegration_DeeplyNestedParens(t *testing.T) { |
| 189 | // ((((((1 + 2)))))) = 3 |
| 190 | assertApprox(t, "((((((1 + 2))))))", 3, eval(t, "((((((1 + 2))))))")) |
| 191 | } |
| 192 | |
| 193 | func TestIntegration_ParensOnBothSides(t *testing.T) { |
| 194 | // (1 + 2) * (3 + 4) = 3 * 7 = 21 |
| 195 | assertApprox(t, "(1 + 2) * (3 + 4)", 21, eval(t, "(1 + 2) * (3 + 4)")) |
| 196 | } |
| 197 | |
| 198 | func TestIntegration_ParensNestedComplex(t *testing.T) { |
| 199 | // ((2 + 3) * (4 - 1)) / 5 = (5 * 3) / 5 = 3 |
| 200 | assertApprox(t, "((2 + 3) * (4 - 1)) / 5", 3, eval(t, "((2 + 3) * (4 - 1)) / 5")) |
| 201 | } |
| 202 | |
| 203 | // --- Complex expressions --- |
| 204 | |
| 205 | func TestIntegration_AllOperators(t *testing.T) { |
| 206 | // 1 + 2 * 3 - 4 / 2 = 1 + 6 - 2 = 5 |
| 207 | assertApprox(t, "1 + 2 * 3 - 4 / 2", 5, eval(t, "1 + 2 * 3 - 4 / 2")) |
| 208 | } |
| 209 | |
| 210 | func TestIntegration_LongChainedAddition(t *testing.T) { |
| 211 | // 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55 |
| 212 | 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")) |
| 213 | } |
| 214 | |
| 215 | func TestIntegration_LongChainedMixed(t *testing.T) { |
| 216 | // 2 * 3 + 4 * 5 - 6 / 2 + 1 = 6 + 20 - 3 + 1 = 24 |
| 217 | assertApprox(t, "2 * 3 + 4 * 5 - 6 / 2 + 1", 24, eval(t, "2 * 3 + 4 * 5 - 6 / 2 + 1")) |
| 218 | } |
| 219 | |
| 220 | // --- Floating point --- |
| 221 | |
| 222 | func TestIntegration_FloatArithmetic(t *testing.T) { |
| 223 | // 1.5 + 2.5 = 4.0 |
| 224 | assertApprox(t, "1.5 + 2.5", 4.0, eval(t, "1.5 + 2.5")) |
| 225 | } |
| 226 | |
| 227 | func TestIntegration_FloatDivision(t *testing.T) { |
| 228 | // 7 / 2 = 3.5 |
| 229 | assertApprox(t, "7 / 2", 3.5, eval(t, "7 / 2")) |
| 230 | } |
| 231 | |
| 232 | func TestIntegration_FloatPrecision(t *testing.T) { |
| 233 | // 0.1 + 0.2 ≈ 0.3 (within tolerance) |
| 234 | assertApprox(t, "0.1 + 0.2", 0.3, eval(t, "0.1 + 0.2")) |
| 235 | } |
| 236 | |
| 237 | // --- Whitespace variations --- |
| 238 | |
| 239 | func TestIntegration_NoSpaces(t *testing.T) { |
| 240 | assertApprox(t, "1+2*3", 7, eval(t, "1+2*3")) |
| 241 | } |
| 242 | |
| 243 | func TestIntegration_ExtraSpaces(t *testing.T) { |
| 244 | assertApprox(t, " 1 + 2 ", 3, eval(t, " 1 + 2 ")) |
| 245 | } |
| 246 | |
| 247 | func TestIntegration_TabsAndSpaces(t *testing.T) { |
| 248 | assertApprox(t, "1\t+\t2", 3, eval(t, "1\t+\t2")) |
| 249 | } |
| 250 | |
| 251 | // --- Error cases --- |
| 252 | |
| 253 | func TestIntegration_DivisionByZero(t *testing.T) { |
| 254 | err := evalErr(t, "1 / 0") |
| 255 | if err == nil { |
| 256 | t.Fatal("expected division by zero error") |
| 257 | } |
| 258 | if !strings.Contains(err.Error(), "division by zero") { |
| 259 | t.Errorf("expected 'division by zero' in error, got: %v", err) |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | func TestIntegration_DivisionByZeroInSubExpr(t *testing.T) { |
| 264 | err := evalErr(t, "1 + 2 / 0") |
| 265 | if err == nil { |
| 266 | t.Fatal("expected division by zero error") |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | func TestIntegration_InvalidCharacter(t *testing.T) { |
| 271 | err := evalErr(t, "1 @ 2") |
| 272 | if err == nil { |
| 273 | t.Fatal("expected error for invalid character") |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | func TestIntegration_MismatchedParenLeft(t *testing.T) { |
| 278 | err := evalErr(t, "(1 + 2") |
| 279 | if err == nil { |
| 280 | t.Fatal("expected error for missing closing paren") |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | func TestIntegration_MismatchedParenRight(t *testing.T) { |
| 285 | err := evalErr(t, "1 + 2)") |
| 286 | if err == nil { |
| 287 | t.Fatal("expected error for unexpected closing paren") |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | func TestIntegration_EmptyParens(t *testing.T) { |
| 292 | err := evalErr(t, "()") |
| 293 | if err == nil { |
| 294 | t.Fatal("expected error for empty parentheses") |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | func TestIntegration_TrailingOperator(t *testing.T) { |
| 299 | err := evalErr(t, "1 +") |
| 300 | if err == nil { |
| 301 | t.Fatal("expected error for trailing operator") |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | func TestIntegration_LeadingOperator(t *testing.T) { |
| 306 | err := evalErr(t, "* 1") |
| 307 | if err == nil { |
| 308 | t.Fatal("expected error for leading operator") |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | func TestIntegration_ConsecutiveOperators(t *testing.T) { |
| 313 | err := evalErr(t, "1 + * 2") |
| 314 | if err == nil { |
| 315 | t.Fatal("expected error for consecutive operators") |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | func TestIntegration_EmptyInput(t *testing.T) { |
| 320 | // Empty string should produce only EOF, parser should error |
| 321 | err := evalErr(t, "") |
| 322 | if err == nil { |
| 323 | t.Fatal("expected error for empty input") |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | func TestIntegration_ConsecutiveNumbers(t *testing.T) { |
| 328 | err := evalErr(t, "1 2") |
| 329 | if err == nil { |
| 330 | t.Fatal("expected error for consecutive numbers without operator") |
| 331 | } |
| 332 | } |
| Sketch🕴️ | 0279013 | 2026-02-28 21:02:09 +0400 | [diff] [blame^] | 333 | |
| 334 | // --- Function definitions (full pipeline) --- |
| 335 | |
| 336 | func TestIntegration_DefineAndCallSingleParam(t *testing.T) { |
| 337 | ev := evaluator.New() |
| 338 | result := evalLines(t, ev, "f(x) = x + 1", "f(5)") |
| 339 | assertApprox(t, "f(5)", 6, result) |
| 340 | } |
| 341 | |
| 342 | func TestIntegration_DefineAndCallMultiParam(t *testing.T) { |
| 343 | ev := evaluator.New() |
| 344 | result := evalLines(t, ev, "add(x, y) = x + y", "add(3, 4)") |
| 345 | assertApprox(t, "add(3, 4)", 7, result) |
| 346 | } |
| 347 | |
| 348 | func TestIntegration_CrossFunctionCalls(t *testing.T) { |
| 349 | ev := evaluator.New() |
| 350 | result := evalLines(t, ev, |
| 351 | "double(x) = x * 2", |
| 352 | "quad(x) = double(double(x))", |
| 353 | "quad(3)", |
| 354 | ) |
| 355 | assertApprox(t, "quad(3)", 12, result) |
| 356 | } |
| 357 | |
| 358 | func TestIntegration_NestedFuncCallsInExpr(t *testing.T) { |
| 359 | ev := evaluator.New() |
| 360 | result := evalLines(t, ev, |
| 361 | "f(x) = x + 1", |
| 362 | "f(f(f(1)))", |
| 363 | ) |
| 364 | // f(1)=2, f(2)=3, f(3)=4 |
| 365 | assertApprox(t, "f(f(f(1)))", 4, result) |
| 366 | } |
| 367 | |
| 368 | func TestIntegration_FuncCallInBinaryExpr(t *testing.T) { |
| 369 | ev := evaluator.New() |
| 370 | result := evalLines(t, ev, |
| 371 | "f(x) = x * 2", |
| 372 | "f(3) + f(4)", |
| 373 | ) |
| 374 | // f(3)=6, f(4)=8, 6+8=14 |
| 375 | assertApprox(t, "f(3)+f(4)", 14, result) |
| 376 | } |
| 377 | |
| 378 | func TestIntegration_FuncWithExprBody(t *testing.T) { |
| 379 | ev := evaluator.New() |
| 380 | result := evalLines(t, ev, |
| 381 | "area(w, h) = w * h", |
| 382 | "area(3, 5) + 1", |
| 383 | ) |
| 384 | assertApprox(t, "area(3,5)+1", 16, result) |
| 385 | } |
| 386 | |
| 387 | func TestIntegration_FuncNoParams(t *testing.T) { |
| 388 | ev := evaluator.New() |
| 389 | result := evalLines(t, ev, "pi() = 3", "pi() + 1") |
| 390 | assertApprox(t, "pi()+1", 4, result) |
| 391 | } |
| 392 | |
| 393 | // --- Function error cases (full pipeline) --- |
| 394 | |
| 395 | func TestIntegration_UndefinedFunction(t *testing.T) { |
| 396 | ev := evaluator.New() |
| 397 | err := evalLinesErr(t, ev, "f(1)") |
| 398 | if err == nil { |
| 399 | t.Fatal("expected error for undefined function") |
| 400 | } |
| 401 | if !strings.Contains(err.Error(), "undefined function") { |
| 402 | t.Errorf("expected 'undefined function' in error, got: %v", err) |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | func TestIntegration_WrongArgCount(t *testing.T) { |
| 407 | ev := evaluator.New() |
| 408 | err := evalLinesErr(t, ev, "f(x) = x", "f(1, 2)") |
| 409 | if err == nil { |
| 410 | t.Fatal("expected error for wrong argument count") |
| 411 | } |
| 412 | if !strings.Contains(err.Error(), "expects 1 arguments, got 2") { |
| 413 | t.Errorf("expected arg count error, got: %v", err) |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | func TestIntegration_FunctionRedefinition(t *testing.T) { |
| 418 | ev := evaluator.New() |
| 419 | err := evalLinesErr(t, ev, "f(x) = x", "f(x) = x + 1") |
| 420 | if err == nil { |
| 421 | t.Fatal("expected error for function redefinition") |
| 422 | } |
| 423 | if !strings.Contains(err.Error(), "already defined") { |
| 424 | t.Errorf("expected 'already defined' in error, got: %v", err) |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | func TestIntegration_UndefinedVariable(t *testing.T) { |
| 429 | ev := evaluator.New() |
| 430 | err := evalLinesErr(t, ev, "f(x) = x + y", "f(1)") |
| 431 | if err == nil { |
| 432 | t.Fatal("expected error for undefined variable") |
| 433 | } |
| 434 | if !strings.Contains(err.Error(), "undefined variable") { |
| 435 | t.Errorf("expected 'undefined variable' in error, got: %v", err) |
| 436 | } |
| 437 | } |