blob: b928d000c26342fa185dd6b5fcead9f6d09e8db0 [file] [log] [blame]
Sketch🕴️afb61d02026-02-28 19:34:37 +04001package matheval_test
2
3import (
4 "math"
5 "strings"
6 "testing"
7
Sketch🕴️02790132026-02-28 21:02:09 +04008 "matheval/ast"
Sketch🕴️afb61d02026-02-28 19:34:37 +04009 "matheval/evaluator"
10 "matheval/lexer"
11 "matheval/parser"
12)
13
14// eval is a helper that runs the full pipeline: lexer → parser → evaluator.
15func 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.
33func 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🕴️02790132026-02-28 21:02:09 +040047// 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.
50func 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.
79func 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🕴️afb61d02026-02-28 19:34:37 +0400117func 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
126func TestIntegration_SingleNumber(t *testing.T) {
127 assertApprox(t, "42", 42, eval(t, "42"))
128}
129
130func TestIntegration_DecimalNumber(t *testing.T) {
131 assertApprox(t, "3.14", 3.14, eval(t, "3.14"))
132}
133
134func TestIntegration_LeadingDot(t *testing.T) {
135 assertApprox(t, ".5", 0.5, eval(t, ".5"))
136}
137
138func TestIntegration_Addition(t *testing.T) {
139 assertApprox(t, "1 + 2", 3, eval(t, "1 + 2"))
140}
141
142func TestIntegration_Subtraction(t *testing.T) {
143 assertApprox(t, "10 - 4", 6, eval(t, "10 - 4"))
144}
145
146func TestIntegration_Multiplication(t *testing.T) {
147 assertApprox(t, "3 * 7", 21, eval(t, "3 * 7"))
148}
149
150func TestIntegration_Division(t *testing.T) {
151 assertApprox(t, "10 / 4", 2.5, eval(t, "10 / 4"))
152}
153
154// --- Precedence and associativity ---
155
156func 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
161func 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
166func 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
171func 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
178func TestIntegration_ParensOverridePrecedence(t *testing.T) {
179 // (2 + 3) * 4 = 20
180 assertApprox(t, "(2 + 3) * 4", 20, eval(t, "(2 + 3) * 4"))
181}
182
183func TestIntegration_NestedParens(t *testing.T) {
184 // ((1 + 2)) = 3
185 assertApprox(t, "((1 + 2))", 3, eval(t, "((1 + 2))"))
186}
187
188func TestIntegration_DeeplyNestedParens(t *testing.T) {
189 // ((((((1 + 2)))))) = 3
190 assertApprox(t, "((((((1 + 2))))))", 3, eval(t, "((((((1 + 2))))))"))
191}
192
193func 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
198func 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
205func 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
210func 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
215func 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
222func 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
227func TestIntegration_FloatDivision(t *testing.T) {
228 // 7 / 2 = 3.5
229 assertApprox(t, "7 / 2", 3.5, eval(t, "7 / 2"))
230}
231
232func 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
239func TestIntegration_NoSpaces(t *testing.T) {
240 assertApprox(t, "1+2*3", 7, eval(t, "1+2*3"))
241}
242
243func TestIntegration_ExtraSpaces(t *testing.T) {
244 assertApprox(t, " 1 + 2 ", 3, eval(t, " 1 + 2 "))
245}
246
247func TestIntegration_TabsAndSpaces(t *testing.T) {
248 assertApprox(t, "1\t+\t2", 3, eval(t, "1\t+\t2"))
249}
250
251// --- Error cases ---
252
253func 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
263func 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
270func 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
277func 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
284func 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
291func TestIntegration_EmptyParens(t *testing.T) {
292 err := evalErr(t, "()")
293 if err == nil {
294 t.Fatal("expected error for empty parentheses")
295 }
296}
297
298func 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
305func 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
312func 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
319func 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
327func 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🕴️02790132026-02-28 21:02:09 +0400333
334// --- Function definitions (full pipeline) ---
335
336func 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
342func 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
348func 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
358func 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
368func 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
378func 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
387func 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
395func 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
406func 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
417func 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
428func 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}