mathai 2.4.0__tar.gz → 2.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {mathai-2.4.0 → mathai-2.4.2}/PKG-INFO +1 -1
  2. {mathai-2.4.0 → mathai-2.4.2}/mathai/base.py +50 -41
  3. mathai-2.4.2/mathai/benchmark.py +25 -0
  4. {mathai-2.4.0 → mathai-2.4.2}/mathai/diff.py +2 -1
  5. {mathai-2.4.0 → mathai-2.4.2}/mathai/formula_compiler.py +82 -120
  6. {mathai-2.4.0 → mathai-2.4.2}/mathai/integrate.py +72 -39
  7. {mathai-2.4.0 → mathai-2.4.2}/mathai/limit.py +3 -2
  8. mathai-2.4.2/mathai/ncert_data.txt +53 -0
  9. {mathai-2.4.0 → mathai-2.4.2}/mathai/qm.py +3 -3
  10. {mathai-2.4.0 → mathai-2.4.2}/mathai/univariate_inequality.py +58 -43
  11. {mathai-2.4.0 → mathai-2.4.2}/mathai.egg-info/PKG-INFO +1 -1
  12. {mathai-2.4.0 → mathai-2.4.2}/setup.py +1 -1
  13. mathai-2.4.0/mathai/benchmark.py +0 -18
  14. mathai-2.4.0/mathai/ncert_data.txt +0 -61
  15. {mathai-2.4.0 → mathai-2.4.2}/README.md +0 -0
  16. {mathai-2.4.0 → mathai-2.4.2}/mathai/__init__.py +0 -0
  17. {mathai-2.4.0 → mathai-2.4.2}/mathai/apart.py +0 -0
  18. {mathai-2.4.0 → mathai-2.4.2}/mathai/decision.py +0 -0
  19. {mathai-2.4.0 → mathai-2.4.2}/mathai/electronic.py +0 -0
  20. {mathai-2.4.0 → mathai-2.4.2}/mathai/expand.py +0 -0
  21. {mathai-2.4.0 → mathai-2.4.2}/mathai/factor.py +0 -0
  22. {mathai-2.4.0 → mathai-2.4.2}/mathai/formula_applier.py +0 -0
  23. {mathai-2.4.0 → mathai-2.4.2}/mathai/fraction.py +0 -0
  24. {mathai-2.4.0 → mathai-2.4.2}/mathai/graphics.py +0 -0
  25. {mathai-2.4.0 → mathai-2.4.2}/mathai/inverse.py +0 -0
  26. {mathai-2.4.0 → mathai-2.4.2}/mathai/lambda_calculus.py +0 -0
  27. {mathai-2.4.0 → mathai-2.4.2}/mathai/linear.py +0 -0
  28. {mathai-2.4.0 → mathai-2.4.2}/mathai/logic.py +0 -0
  29. {mathai-2.4.0 → mathai-2.4.2}/mathai/matrix.py +0 -0
  30. {mathai-2.4.0 → mathai-2.4.2}/mathai/ml.py +0 -0
  31. {mathai-2.4.0 → mathai-2.4.2}/mathai/mlscalar.py +0 -0
  32. {mathai-2.4.0 → mathai-2.4.2}/mathai/ode.py +0 -0
  33. {mathai-2.4.0 → mathai-2.4.2}/mathai/parser.py +0 -0
  34. {mathai-2.4.0 → mathai-2.4.2}/mathai/pde.py +0 -0
  35. {mathai-2.4.0 → mathai-2.4.2}/mathai/printeq.py +0 -0
  36. {mathai-2.4.0 → mathai-2.4.2}/mathai/simplify.py +0 -0
  37. {mathai-2.4.0 → mathai-2.4.2}/mathai/statistics.py +0 -0
  38. {mathai-2.4.0 → mathai-2.4.2}/mathai/test.py +0 -0
  39. {mathai-2.4.0 → mathai-2.4.2}/mathai/tool.py +0 -0
  40. {mathai-2.4.0 → mathai-2.4.2}/mathai/trig.py +0 -0
  41. {mathai-2.4.0 → mathai-2.4.2}/mathai.egg-info/SOURCES.txt +0 -0
  42. {mathai-2.4.0 → mathai-2.4.2}/mathai.egg-info/dependency_links.txt +0 -0
  43. {mathai-2.4.0 → mathai-2.4.2}/mathai.egg-info/top_level.txt +0 -0
  44. {mathai-2.4.0 → mathai-2.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mathai
3
- Version: 2.4.0
3
+ Version: 2.4.2
4
4
  Summary: Mathematics solving Ai tailored to NCERT
5
5
  Home-page: https://github.com/infinity390/mathai
6
6
  Requires-Python: >=3.7
@@ -379,6 +379,7 @@ def factor_generation(eq, prime=False):
379
379
  else:
380
380
  output.append(child)
381
381
  return output
382
+
382
383
  def compute(eq):
383
384
  if not isinstance(eq, TreeNode):
384
385
  return eq
@@ -388,52 +389,60 @@ def compute(eq):
388
389
  elif eq.name == "s_pi":
389
390
  return math.pi
390
391
  elif eq.name.startswith("d_"):
391
- return float(eq.name[2:])
392
+ try:
393
+ return float(eq.name[2:])
394
+ except ValueError:
395
+ return None
392
396
  else:
393
397
  return None
398
+
394
399
  values = [compute(child) for child in eq.children]
395
400
  if None in values:
396
401
  return None
397
- if eq.name == "f_floor":
398
- return math.floor(values[0])
399
- elif eq.name == "f_ceil":
400
- return math.ceil(values[0])
401
- elif eq.name == "f_add":
402
- return sum(values)
403
- elif eq.name == "f_mod":
404
- return values[0] % values[1]
405
- elif eq.name == "f_abs":
406
- return math.fabs(values[0])
407
- elif eq.name == "f_sub":
408
- return values[0] - values[1]
409
- elif eq.name == "f_rad":
410
- return values[0] * math.pi / 180
411
- elif eq.name in ["f_wmul", "f_mul"]:
412
- result = 1.0
413
- for v in values:
414
- result *= v
415
- return result
416
- elif eq.name == "f_neg":
417
- return -values[0]
418
- elif eq.name == "f_div":
419
- return values[0] / values[1]
420
- elif eq.name == "f_pow":
421
- return math.pow(values[0],values[1])
422
- elif eq.name == "f_sin":
423
- return math.sin(values[0])
424
- elif eq.name == "f_cos":
425
- return math.cos(values[0])
426
- elif eq.name == "f_tan":
427
- return math.tan(values[0])
428
- elif eq.name == "f_arcsin":
429
- return math.asin(values[0])
430
- elif eq.name == "f_arccos":
431
- return math.acos(values[0])
432
- elif eq.name == "f_arctan":
433
- return math.atan(values[0])
434
- elif eq.name == "f_log":
435
- return math.log(values[0])
436
- else:
402
+
403
+ try:
404
+ if eq.name == "f_floor":
405
+ return math.floor(values[0])
406
+ elif eq.name == "f_ceil":
407
+ return math.ceil(values[0])
408
+ elif eq.name == "f_add":
409
+ return sum(values)
410
+ elif eq.name == "f_mod":
411
+ return values[0] % values[1] if values[1] != 0 else None
412
+ elif eq.name == "f_abs":
413
+ return math.fabs(values[0])
414
+ elif eq.name == "f_sub":
415
+ return values[0] - values[1]
416
+ elif eq.name == "f_rad":
417
+ return values[0] * math.pi / 180
418
+ elif eq.name in ["f_wmul", "f_mul"]:
419
+ result = 1.0
420
+ for v in values:
421
+ result *= v
422
+ return result
423
+ elif eq.name == "f_neg":
424
+ return -values[0]
425
+ elif eq.name == "f_div":
426
+ return values[0] / values[1] if values[1] != 0 else None
427
+ elif eq.name == "f_pow":
428
+ return math.pow(values[0], values[1])
429
+ elif eq.name == "f_sin":
430
+ return math.sin(values[0])
431
+ elif eq.name == "f_cos":
432
+ return math.cos(values[0])
433
+ elif eq.name == "f_tan":
434
+ return math.tan(values[0])
435
+ elif eq.name == "f_arcsin":
436
+ return math.asin(values[0])
437
+ elif eq.name == "f_arccos":
438
+ return math.acos(values[0])
439
+ elif eq.name == "f_arctan":
440
+ return math.atan(values[0])
441
+ elif eq.name == "f_log":
442
+ return math.log(values[0]) if values[0] > 0 else None
443
+ else:
444
+ return None
445
+ except (ValueError, ZeroDivisionError, IndexError):
437
446
  return None
438
447
  def num_dem(equation):
439
448
  num = []
@@ -0,0 +1,25 @@
1
+ import random
2
+ from .simplify import simplify
3
+ from .expand import expand
4
+ from .parser import parse
5
+ from .fraction import fraction
6
+ from .apart import apart
7
+ from .integrate import integrate_formula, integrate_trig_formula, integrate_fraction
8
+ from .integrate import integrate_subs_main as integrate_subs
9
+ from .integrate import solve_integrate as integrate_clean
10
+ from .factor import factor2
11
+ from .trig import trig0, trig1, trig4
12
+ fx_list = [simplify, expand, fraction, integrate_formula, integrate_subs, integrate_trig_formula, integrate_clean, apart, trig0, trig1, trig4, integrate_fraction, factor2]
13
+ name_list = "simplify, expand, fraction, integrate_formula, integrate_subs, integrate_trig_formula, integrate_clean, apart, trig0, trig1, trig4, integrate_fraction, factor2".split(", ")
14
+ def benchmark_ncert():
15
+ with open("mathai/ncert_data.txt", "r", encoding="utf-8") as file:
16
+ content = file.read()
17
+ content = content.strip().split("\n")
18
+ count = 0
19
+ for item in content:
20
+ count += 1
21
+ item = item.strip()
22
+ eq = simplify(parse(item.split(" ")[0]))
23
+ for item2 in item.split(" ")[1:]:
24
+ eq = fx_list[name_list.index(item2)](eq)
25
+ print(f"{count}. {item.split(" ")[0]} => {eq}")
@@ -10,6 +10,7 @@ pdif(cos(a),x) -sin(a)*pdif(a,x) [] x _ _ _
10
10
  pdif(arcsin(a),x) (1/sqrt(1-a^2))*pdif(a,x) [] x _ _ _
11
11
  pdif(arccos(a),x) (-1/sqrt(1-a^2))*pdif(a,x) [] x _ _ _
12
12
  pdif(arctan(a),x) (1/(1+a^2))*pdif(a,x) [] x _ _ _
13
+ pdif(log(a),x) (1/a)*pdif(a,x) [] x _ _ _
13
14
  pdif(k,x) 0 [] x k _ _
14
15
  pdif(x,x) 1 [] x _ _ _
15
16
  pdif(product(a,b),x) pdif(a,x)*b+a*pdif(b,x) [] x _ _ _
@@ -20,5 +21,5 @@ diff2 = formula_list_compiler(diff2_s)
20
21
 
21
22
  def diff(equation, var=None):
22
23
  var = var if var else "v_0"
23
- out = diff2(simplify(TreeNode("f_pdif", [equation, tree_form(var)])))
24
+ out = simplify(diff2(simplify(TreeNode("f_pdif", [equation, tree_form(var)]))))
24
25
  return out
@@ -1,14 +1,22 @@
1
1
  import copy
2
2
  import itertools
3
3
  import textwrap
4
+ import warnings
4
5
  from .base import *
5
6
  from .simplify import simplify
6
7
  from .parser import parse
7
8
  from .formula_applier import (
8
9
  extract_polynomial_coefficients, convert_string, _as_pairs, _replace_in,
9
- _as_exprs, get_name, en_w_addmul, formula_applier
10
+ _as_exprs, get_name, en_w_addmul, formula_applier,
10
11
  )
11
12
 
13
+ try:
14
+ from .formula_applier import _split_rules
15
+ except ImportError:
16
+ def _split_rules(s):
17
+ return tuple(line for line in s.strip().split("\n") if line.strip())
18
+
19
+
12
20
  # --- CONSTANTS & CONFIGURATION ---
13
21
 
14
22
  _W_NAMES = {
@@ -18,29 +26,29 @@ _W_NAMES = {
18
26
  "f_wadd": "f_waddw",
19
27
  "f_wmul": "f_wmulw",
20
28
  }
21
- _COMMUTATIVE = frozenset(("f_addw", "f_mulw", "f_hadamardw"))
29
+ _COMMUTATIVE = frozenset(("f_addw", "f_mulw", "f_hadamardw", "f_waddw"))
22
30
  _ORDERED_ASSOC = frozenset(("f_wmulw",))
23
31
  _LABEL_PREFIXES = frozenset(("f_", "s_", "d_"))
24
32
  _SPECIAL_PATTERN = frozenset(("f_poly", "f_sum", "f_wsum", "f_product", "f_hprod", "f_mprod"))
25
- _PATTERN_OPS = {"f_sum": "f_addw", "f_product": "f_mulw",
26
- "f_hprod": "f_hadamardw", "f_wsum": "f_waddw"}
33
+ _PATTERN_OPS = {
34
+ "f_sum": "f_addw",
35
+ "f_product": "f_mulw",
36
+ "f_hprod": "f_hadamardw",
37
+ "f_wsum": "f_waddw",
38
+ }
27
39
  _ORDERED_PATTERN_OPS = {"f_mprod": "f_wmulw"}
28
40
  _AGG_NAMES = frozenset((
29
41
  "f_addw", "f_add", "f_mulw", "f_mul", "f_hadamardw", "f_hadamard",
30
- "f_product", "f_sum", "f_hprod", "f_mprod", "f_poly", "f_wmulw", "f_wmul",
42
+ "f_product", "f_sum", "f_wsum", "f_hprod", "f_mprod", "f_poly", "f_wmulw", "f_wmul",
31
43
  ))
32
44
  _EXTRACT_NAMES = frozenset((
33
45
  "f_add", "f_wadd", "f_waddw", "f_wmul", "f_wmulw",
34
- "f_mul", "f_product", "f_sum", "f_hprod", "f_mprod", "f_poly", "f_wsum", "f_wsumw"
46
+ "f_mul", "f_product", "f_sum", "f_wsum", "f_hprod", "f_mprod", "f_poly",
35
47
  ))
36
- _DE_W_NAMES = frozenset(("f_addw", "f_mulw", "f_waddw", "f_hadamardw", "f_wmulw", "f_wsumw"))
48
+ _DE_W_NAMES = frozenset(("f_addw", "f_mulw", "f_waddw", "f_hadamardw", "f_wmulw"))
37
49
 
38
50
 
39
51
  # --- TREE NODE CLASS ---
40
- # FIX 1: `from .base import *` may already provide TreeNode (print_code2 emits
41
- # `a * b`, `~a`, `.fx(...)`, which only work on that class). Redefining it here
42
- # shadowed it, giving two incompatible TreeNode classes with no __eq__/__hash__.
43
- # Keep base's class if it exists; otherwise use this complete fallback.
44
52
 
45
53
  if "TreeNode" not in globals():
46
54
  class TreeNode:
@@ -55,7 +63,6 @@ if "TreeNode" not in globals():
55
63
  return repr(self.name)
56
64
  return f"TreeNode({self.name!r}, {self.children!r})"
57
65
 
58
- # structural equality (generated code relies on `==` / `!=`)
59
66
  def __eq__(self, other):
60
67
  if not (hasattr(other, "name") and hasattr(other, "children")):
61
68
  return NotImplemented
@@ -69,7 +76,6 @@ if "TreeNode" not in globals():
69
76
  op = "f_" + op
70
77
  return TreeNode(op, [self])
71
78
 
72
- # operators emitted by print_code2_with_varlist
73
79
  def __add__(self, o): return TreeNode("f_add", [self, o])
74
80
  def __radd__(self, o): return TreeNode("f_add", [o, self])
75
81
  def __mul__(self, o): return TreeNode("f_mul", [self, o])
@@ -92,7 +98,6 @@ def generate_tree_permutations(node):
92
98
  if node.name in _COMMUTATIVE:
93
99
  seen = set()
94
100
  for perm in itertools.permutations(combined_children):
95
- # FIX 2: dedupe on a structural signature (identity hashing never matched)
96
101
  sig = tuple(repr(c) for c in perm)
97
102
  if sig not in seen:
98
103
  seen.add(sig)
@@ -114,8 +119,6 @@ def de_w_addmul(eq):
114
119
 
115
120
 
116
121
  def print_condition(eq, printer=None):
117
- # FIX 3: take the printer as a parameter (print_code2 used to fall into
118
- # print_code_h) and don't IndexError when there is no else-branch.
119
122
  printer = printer or print_code_h
120
123
  assert eq.name == "f_condition"
121
124
 
@@ -152,8 +155,6 @@ def print_code_h(eq):
152
155
  if eq.name == "f_index":
153
156
  return f"{print_code_h(eq.children[0])}[{eq.children[1]}]"
154
157
  if eq.name == "f_any":
155
- # NOTE: `any(A B)` is not valid Python; left as-is because the intended
156
- # translation isn't clear from this file.
157
158
  return f"any({print_code_h(eq.children[0])} {print_code_h(eq.children[1])})"
158
159
  if eq.name == "f_list":
159
160
  return "[" + ", ".join(print_code_h(c) for c in eq.children) + "]"
@@ -169,9 +170,6 @@ def print_code2_with_varlist(eq, varlist):
169
170
  return "None"
170
171
  if not eq.children and eq.name in varlist:
171
172
  return varlist[eq.name]
172
- # FIX 4: the unreachable s_true / s_false branches that followed this
173
- # (shadowed by this check) were removed: in a tree-building context they
174
- # must stay tree nodes, which is what this line produces.
175
173
  if eq.name.startswith(("d_", "v_", "s_")):
176
174
  return f"tree_form('{eq.name}')"
177
175
 
@@ -198,15 +196,12 @@ def print_code2_with_varlist(eq, varlist):
198
196
 
199
197
 
200
198
  def print_code(eq):
201
- # FIX 5: dropped the blind str.replace loop (de_w_addmul already did the
202
- # renaming; the replace could corrupt longer identifiers).
203
199
  return print_code_h(de_w_addmul(eq))
204
200
 
205
201
 
206
202
  # --- SPECIAL PATTERN MATCHERS ---
207
203
 
208
204
  def _split_const(children, resolved_ignore):
209
- """Split into terms free of the 'variables of interest' and terms that use them."""
210
205
  const_t, non_t = [], []
211
206
  for ch in children:
212
207
  (non_t if set(vlist(ch)) & resolved_ignore else const_t).append(ch)
@@ -218,37 +213,45 @@ def _op(op_name, items):
218
213
 
219
214
 
220
215
  def _is_zero(node):
221
- # TreeNode == 0 is False; zero is represented as the node "d_0"
222
216
  return node is not None and (getattr(node, "name", None) == "d_0" or node == 0)
223
217
 
224
218
 
225
219
  def special_pattern_poly(eq, f, binding):
226
- # FIX 18: poly(x, a, b) = a*x + b has 2 coefficients -> degree 1. Ask the
227
- # extractor for exactly that degree (it returns degree+1 ascending
228
- # coefficients, or None if eq has a higher degree) and require a nonzero
229
- # leading coefficient. The old code asked for degree 2, then stripped leading
230
- # zeros with `out[0] == 0`, which is False for TreeNodes.
231
220
  n_coeffs = len(f.children) - 1
232
221
  if n_coeffs < 1:
233
222
  return False
223
+ var_name = f.children[0].name
224
+
225
+ if var_name in binding:
226
+ bound = binding[var_name]
227
+ if getattr(bound, "children", None) or not str(getattr(bound, "name", "")).startswith("v_"):
228
+ return False
229
+ candidates = [bound.name]
230
+ else:
231
+ candidates = vlist(eq)
232
+
234
233
  eq_plain = de_w_addmul(eq)
235
- for item in vlist(eq):
234
+ for item in candidates:
236
235
  out = extract_polynomial_coefficients(eq_plain, item, n_coeffs - 1)
237
236
  if out is None or len(out) != n_coeffs or _is_zero(out[-1]):
238
237
  continue
239
- out = out[::-1] # highest degree first
238
+ out = out[::-1]
240
239
  local_binding = binding.copy()
241
- for i in range(1, len(f.children)):
242
- local_binding[f.children[i].name] = out[i - 1]
243
- local_binding[f.children[0].name] = TreeNode(item)
240
+ consistent = True
241
+ for c, val in zip(f.children[1:], out):
242
+ if c.name in binding and binding[c.name] != val:
243
+ consistent = False
244
+ break
245
+ local_binding[c.name] = val
246
+ if not consistent:
247
+ continue
248
+ local_binding[var_name] = TreeNode(item)
244
249
  binding.update(local_binding)
245
250
  return True
246
251
  return False
247
252
 
248
253
 
249
254
  def special_pattern(eq, f, binding, const_1_vars, resolved_ignore):
250
- # FIX 6: the old code had two near-identical branches, accepted patterns with
251
- # >2 children but only bound two of them, and used a strict-subset test.
252
255
  if eq is None or f is None:
253
256
  return False
254
257
  if f.name == "f_poly":
@@ -272,8 +275,8 @@ def special_pattern(eq, f, binding, const_1_vars, resolved_ignore):
272
275
  if not const_t:
273
276
  return False
274
277
  if not non_t:
275
- binding[c0] = const_t[0]
276
- binding[c1] = _op(op_name, const_t[1:])
278
+ binding[const_name] = const_t[0]
279
+ binding[other_name] = _op(op_name, const_t[1:])
277
280
  else:
278
281
  binding[const_name] = _op(op_name, const_t)
279
282
  binding[other_name] = _op(op_name, non_t)
@@ -316,15 +319,17 @@ def special_pattern_router(fname, eq, f=None, binding=None, const_1_vars=None, r
316
319
  return special_pattern_ordered(eq, f, binding, const_1_vars, resolved_ignore) if f else False
317
320
  if fname in _PATTERN_OPS:
318
321
  return special_pattern(eq, f, binding, const_1_vars, resolved_ignore) if f else False
322
+ if fname in _SPECIAL_PATTERN:
323
+ raise ValueError(
324
+ f"special pattern {fname!r} is not registered in _PATTERN_OPS / _ORDERED_PATTERN_OPS"
325
+ )
319
326
  return True
320
327
 
321
328
 
322
329
  # --- TWO-STEP COMPILER ---
323
330
 
324
331
  def _consumed(form_node):
325
- """How many children of the matched node this pattern node uses
326
- (an ordered-assoc child consumes two)."""
327
- return sum(2 if c.name in _ORDERED_ASSOC else 1 for c in form_node.children)
332
+ return len(form_node.children)
328
333
 
329
334
 
330
335
  def _node_src(node):
@@ -340,9 +345,6 @@ def compile_permutation_matcher_two_step(
340
345
  negative=None,
341
346
  forbidden_list=None,
342
347
  ):
343
- """Returns the *body* of a function of `eq`: it returns the rewritten tree
344
- on a match and None otherwise. Conditions are built as plain Python
345
- expression strings joined with short-circuiting `and`."""
346
348
  ignore_list = ignore_list or []
347
349
  const_1 = const_1 or []
348
350
  positive = positive or []
@@ -350,24 +352,20 @@ def compile_permutation_matcher_two_step(
350
352
  forbidden_list = forbidden_list or []
351
353
 
352
354
  ignore_set = {get_name(i) for i in ignore_list}
353
- # FIX 7: const_1 holds TreeNodes; the old `key in const_1` compared a str to
354
- # TreeNodes and was never true.
355
355
  const_1_set = {get_name(i) for i in const_1}
356
356
 
357
- varlist = {} # pattern var name -> python expression that yields the bound subtree
358
- specials = [] # (expr, pattern_node), checked after the structure matches
359
- late_eq = [] # equalities involving special-bound vars; only valid after the router ran
357
+ varlist = {}
358
+ specials = []
359
+ late_eq = []
360
+ struct_expr = {}
360
361
 
361
362
  def helper(expr, form_node):
362
363
  name = form_node.name
363
364
 
364
365
  if name in _SPECIAL_PATTERN:
365
- # FIX 8: special patterns were emitted as router calls with no pattern,
366
- # no binding dict -> always False and never bound anything.
367
366
  specials.append((expr, form_node))
368
367
  for c in form_node.children:
369
368
  if c.name in varlist:
370
- # FIX 16: var already bound structurally -> the router's binding must agree
371
369
  late_eq.append(f"{varlist[c.name]} == _b[{c.name!r}]")
372
370
  else:
373
371
  varlist[c.name] = f"_b[{c.name!r}]"
@@ -376,37 +374,22 @@ def compile_permutation_matcher_two_step(
376
374
  if name.startswith("v_"):
377
375
  if name in varlist:
378
376
  if varlist[name].startswith("_b["):
379
- # FIX 17: `_b[...]` doesn't exist until the router has run, so this
380
- # comparison must come after it (it used to be inline -> KeyError)
381
377
  late_eq.append(f"{varlist[name]} == {expr}")
378
+ struct_expr.setdefault(name, expr)
382
379
  return "True"
383
380
  return f"{varlist[name]} == {expr}"
384
381
  varlist[name] = expr
382
+ struct_expr[name] = expr
385
383
  return "True"
386
384
 
387
385
  n = _consumed(form_node)
388
- access = [f"{expr}.children[{i}]" for i in range(n)]
389
- child_exprs, pos = [], 0
390
- for child in form_node.children:
391
- if child.name in _ORDERED_ASSOC:
392
- # FIX 9: wrap with the *child's* name (was the parent's name)
393
- child_exprs.append(f"TreeNode({child.name!r}, [{access[pos]}, {access[pos + 1]}])")
394
- pos += 2
395
- else:
396
- child_exprs.append(access[pos])
397
- pos += 1
398
-
399
- # FIX 10: length check uses the number of children actually consumed
400
386
  checks = [f"{expr}.name == {name!r}", f"len({expr}.children) == {n}"]
401
- for child, ce in zip(form_node.children, child_exprs):
402
- sub = helper(ce, child)
387
+ for i, child in enumerate(form_node.children):
388
+ sub = helper(f"{expr}.children[{i}]", child)
403
389
  if sub != "True":
404
390
  checks.append(sub)
405
391
  return "(" + " and ".join(checks) + ")"
406
392
 
407
- # FIX 11: the old `form_node.count` permutation hack enumerated n! orderings per
408
- # node just to pick the last one. Every pattern ordering is already produced by
409
- # generate_tree_permutations, so matching children positionally is enough.
410
393
  match = helper("eq", tree_node)
411
394
 
412
395
  special_bound = {c.name for _, f in specials for c in f.children}
@@ -415,15 +398,16 @@ def compile_permutation_matcher_two_step(
415
398
 
416
399
  conds = [match]
417
400
 
418
- # ignore-vars must be bare variables
419
401
  for e in struct_ignore:
420
402
  conds.append(f"({e}.name.startswith('v_') and not {e}.children)")
421
403
 
422
- # FIX 12: `ignore` used to collect *every* v_ variable (and mid-traversal); it is
423
- # now the ignore_list variables only, and special patterns get real variable names.
424
404
  c1_src = "set([" + ", ".join(repr(k) for k in sorted(const_1_set)) + "])"
425
405
  ig_src = "set([" + ", ".join(f"{e}.name" for e in struct_ignore) + "])"
426
406
  for expr, fnode in specials:
407
+ seed = {c.name: struct_expr[c.name] for c in fnode.children if c.name in struct_expr}
408
+ if seed:
409
+ items = ", ".join(f"{k!r}: {v}" for k, v in seed.items())
410
+ conds.append(f"(_b.update({{{items}}}) or True)")
427
411
  conds.append(
428
412
  f"special_pattern_router({fnode.name!r}, {expr}, {_node_src(fnode)}, _b, {c1_src}, {ig_src})"
429
413
  )
@@ -439,7 +423,6 @@ def compile_permutation_matcher_two_step(
439
423
  if k in varlist:
440
424
  conds.append(f"all(not contain({varlist[k]}, item) for item in {ig_list})")
441
425
 
442
- # FIX 13: forbidden values were interpolated with repr() -> `x != 'd_0'`
443
426
  for lhs, val in forbidden_list:
444
427
  if lhs.name in varlist:
445
428
  conds.append(f"{varlist[lhs.name]} != {print_code2_with_varlist(val, varlist)}")
@@ -468,7 +451,7 @@ def indent_block(code_str, spaces=4):
468
451
  return textwrap.indent(code_str, " " * spaces)
469
452
 
470
453
 
471
- # --- CONTAINER (sub-multiset / sub-sequence) MATCH BLOCKS ---
454
+ # --- CONTAINER MATCH BLOCKS ---
472
455
 
473
456
  def _commutative_block(op, k, fn):
474
457
  plain = op[:-1]
@@ -503,7 +486,6 @@ def _ordered_block(op, k, fn):
503
486
 
504
487
 
505
488
  def formula_compiler(s):
506
- """Compile a rule string into a transformation function."""
507
489
  item = s.split(" ")
508
490
  a, b = (simplify(parse(x)) for x in item[:2])
509
491
 
@@ -559,10 +541,6 @@ def formula_compiler(s):
559
541
  negative=_as_exprs(neg),
560
542
  forbidden_list=_as_pairs(forb),
561
543
  )
562
- # FIX 14: every matcher is its own top-level function with a parameter
563
- # named `eq` (the generated code refers to `eq`, but the old wrapper
564
- # called its parameter `sub_eq`), and indentation is done with
565
- # textwrap instead of multi-line f-string interpolation.
566
544
  fn = f"_m{len(fn_defs)}"
567
545
  fn_defs.append(f"def {fn}(eq):\n" + indent_block(body, 4))
568
546
 
@@ -573,10 +551,12 @@ def formula_compiler(s):
573
551
  else:
574
552
  blocks.append(f"r = {fn}(eq)\nif r is not None:\n return r\n")
575
553
 
554
+ if not fn_defs:
555
+ warnings.warn(f"rule produced no matchers (simplify returned None?): {s!r}")
556
+
576
557
  src = "\n\n".join(fn_defs) + "\n\n"
577
558
  src += "def transform(eq_orig):\n eq = copy.deepcopy(eq_orig)\n"
578
559
  for blk in blocks:
579
- # FIX 15: blocks used to be appended at column 0, i.e. outside the def
580
560
  src += indent_block(blk, 4) + "\n"
581
561
  src += " return eq_orig\n"
582
562
 
@@ -604,38 +584,27 @@ def formula_compiler(s):
604
584
  def apply(x):
605
585
  w = en_w_addmul(x)
606
586
  out = env["transform"](w)
607
- if out is w: # no rule matched: hand back the very same object
587
+ if out is w:
608
588
  return x
609
- return None if out is None else de_w_addmul(out) # simplify() can return None
589
+ return None if out is None else de_w_addmul(out)
610
590
 
611
- apply.source = src # handy for debugging
591
+ apply.source = src
612
592
  return apply
613
593
 
614
594
 
615
- # --- RULE LISTS ---
595
+ formula_compiler_from_rule_string = formula_compiler
616
596
 
617
- try:
618
- from .formula_applier import _split_rules
619
- except ImportError:
620
- def _split_rules(s):
621
- return tuple(line for line in s.strip().split("\n") if line.strip())
622
597
 
598
+ # --- RULE LISTS ---
623
599
 
624
600
  def transform_dfs_changed(root, func, arg=None, key=None):
625
- """Iterative post-order transform that
626
- * only rebuilds a node if one of its children actually changed,
627
- * re-normalises a node's replacement when func changes it (so no outer
628
- dowhile is needed), and
629
- * never processes the same subtree twice: every subtree that has reached a
630
- fixed point is cached by structural key and reused.
631
- `func(node, *arg)` may return None or the same node to mean 'unchanged'."""
632
601
  if root is None:
633
602
  return None
634
603
  arg = () if arg is None else arg
635
604
  key = key or str_form
636
- stable = {} # key(subtree) -> fully normalised subtree
637
- in_progress = set() # keys being normalised (guards against rewrite cycles)
638
- values = [] # results of finished 'visit's, in visiting order
605
+ stable = {}
606
+ in_progress = set()
607
+ values = []
639
608
  stack = [("visit", root, None)]
640
609
 
641
610
  def finish(sig, cur, out):
@@ -652,7 +621,7 @@ def transform_dfs_changed(root, func, arg=None, key=None):
652
621
  if sig in stable:
653
622
  values.append(stable[sig])
654
623
  continue
655
- if sig in in_progress: # A -> B -> A: stop, keep as is
624
+ if sig in in_progress:
656
625
  values.append(node)
657
626
  continue
658
627
  in_progress.add(sig)
@@ -664,44 +633,37 @@ def transform_dfs_changed(root, func, arg=None, key=None):
664
633
  kids = values[len(values) - n:]
665
634
  del values[len(values) - n:]
666
635
  if all(a is b for a, b in zip(kids, node.children)):
667
- cur = node # nothing below changed: reuse
636
+ cur = node
668
637
  else:
669
638
  cur = TreeNode(node.name, kids)
670
639
  res = func(cur, *arg)
671
640
  if res is None or res is cur or key(res) == key(cur):
672
- finish(sig, cur, cur) # unchanged
641
+ finish(sig, cur, cur)
673
642
  else:
674
643
  stack.append(("record", cur, sig))
675
- stack.append(("visit", res, None)) # changed: normalise the new subtree
676
- else: # record
644
+ stack.append(("visit", res, None))
645
+ else:
677
646
  finish(sig, node, values.pop())
678
647
  return values[-1]
679
-
648
+
649
+
680
650
  def formula_list_compiler(s, mode="fixpoint"):
681
- """Compile a multi-line rule string (one rule per line) into one function.
682
- mode="fixpoint": transform_dfs_changed (skips unchanged / already-stable nodes)
683
- mode="dowhile" : whole-tree dowhile(transform_dfs(...)) loop
684
- A line starting with "@" is always handled by formula_applier (the "@" is stripped);
685
- other lines go there only if _needs_applier() says so."""
686
-
687
651
  rules = []
688
652
  for line in _split_rules(s):
689
653
  line = line.strip()
690
- forced = line.startswith("@") # "@rule ..." = always use formula_applier
691
- if forced:
654
+ if line.startswith("@"):
692
655
  line = line[1:].strip()
693
- if forced:
694
656
  rules.append(lambda x, line=line: formula_applier(x, line))
695
657
  else:
696
658
  rules.append(formula_compiler(line))
697
-
659
+
698
660
  def node_fn(node):
699
661
  for rule in rules:
700
662
  r = rule(node)
701
663
  if r is not None:
702
664
  node = r
703
665
  return node
704
-
666
+
705
667
  if mode == "dowhile":
706
668
  def apply(eq):
707
669
  if eq is None:
@@ -712,7 +674,7 @@ def formula_list_compiler(s, mode="fixpoint"):
712
674
  return None if eq is None else transform_dfs_changed(eq, node_fn)
713
675
  else:
714
676
  raise ValueError("mode must be 'fixpoint' or 'dowhile'")
715
-
677
+
716
678
  apply.rules = rules
717
679
  apply.node_fn = node_fn
718
680
  apply.sources = [getattr(r, "source", None) for r in rules]