math2tex 1.0.0__py3-none-any.whl

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.
math2tex/macros.py ADDED
@@ -0,0 +1,408 @@
1
+ """
2
+ math2tex.macros — Macro definition, storage, and hygienic substitution.
3
+ """
4
+
5
+ import copy
6
+ import re
7
+ import uuid
8
+ from dataclasses import dataclass
9
+ from typing import Dict, List, Optional, Set, Tuple
10
+
11
+ from .config import (
12
+ ALL_FUNC_NAMES, BUILTIN_LATEX_FUNCS, GREEK_LETTERS, SPECIAL_CONSTANTS,
13
+ )
14
+ from .ast_nodes import (
15
+ Abs, BigOp, BinOp, Derivative, Factorial, Fraction, Func,
16
+ InnerProduct, Integral, IntervalNode, MatrixNode, Node, Norm, Num,
17
+ PiecewiseNode, Power, Prime, SetBuilder, Subscript, TextLiteral,
18
+ UnOp, Var,
19
+ )
20
+ from .lexer import ParseError
21
+
22
+ # =============================================================================
23
+ # Macro Dataclass
24
+ # =============================================================================
25
+
26
+ @dataclass(frozen=True)
27
+ class Macro:
28
+ name: str
29
+ args: List[str]
30
+ body: Node
31
+
32
+
33
+ # =============================================================================
34
+ # Maximum Expansion Depth
35
+ # =============================================================================
36
+
37
+ MAX_MACRO_DEPTH = 64
38
+
39
+ # =============================================================================
40
+ # Unique Symbol Generation
41
+ # =============================================================================
42
+
43
+ def _gensym(prefix: str = 'v') -> str:
44
+ return f'_m{prefix}_{uuid.uuid4().hex[:8]}'
45
+
46
+ # =============================================================================
47
+ # AST Inspection Helpers
48
+ # =============================================================================
49
+
50
+ def _iter_children(node: Node):
51
+ if isinstance(node, (Num, Var, TextLiteral)):
52
+ return
53
+ if isinstance(node, BinOp):
54
+ yield node.left; yield node.right
55
+ elif isinstance(node, UnOp):
56
+ yield node.expr
57
+ elif isinstance(node, Fraction):
58
+ yield node.num; yield node.den
59
+ elif isinstance(node, Power):
60
+ yield node.base; yield node.exp
61
+ elif isinstance(node, Subscript):
62
+ yield node.base; yield node.sub
63
+ elif isinstance(node, Func):
64
+ yield from node.args
65
+ if node.sub: yield node.sub
66
+ if node.power: yield node.power
67
+ elif isinstance(node, Factorial):
68
+ yield node.expr
69
+ elif isinstance(node, Prime):
70
+ yield node.expr
71
+ elif isinstance(node, Derivative):
72
+ yield node.expr
73
+ if node.order: yield node.order
74
+ elif isinstance(node, Integral):
75
+ yield node.expr
76
+ if node.lower: yield node.lower
77
+ if node.upper: yield node.upper
78
+ elif isinstance(node, BigOp):
79
+ if node.lower: yield node.lower
80
+ if node.upper: yield node.upper
81
+ yield node.expr
82
+ elif isinstance(node, Abs):
83
+ yield node.expr
84
+ elif isinstance(node, Norm):
85
+ yield node.expr
86
+ elif isinstance(node, InnerProduct):
87
+ yield node.left; yield node.right
88
+ elif isinstance(node, SetBuilder):
89
+ yield node.expr; yield node.condition
90
+ elif isinstance(node, MatrixNode):
91
+ for row in node.rows:
92
+ yield from row
93
+ elif isinstance(node, IntervalNode):
94
+ yield node.left_expr; yield node.right_expr
95
+ elif isinstance(node, PiecewiseNode):
96
+ for expr, cond in node.conditions:
97
+ yield expr
98
+ if cond: yield cond
99
+
100
+
101
+ def free_vars(node: Node) -> Set[str]:
102
+ free: Set[str] = set()
103
+ bound: Set[str] = set()
104
+
105
+ def walk(n: Node, locally_bound: Set[str]) -> None:
106
+ if isinstance(n, Var):
107
+ if n.name and n.name not in locally_bound:
108
+ free.add(n.name)
109
+ return
110
+ if isinstance(n, (Num, TextLiteral)):
111
+ return
112
+
113
+ new_bound = set(locally_bound)
114
+ if isinstance(n, BigOp) and n.lower is not None:
115
+ bv = _extract_binding_name(n.lower)
116
+ if bv:
117
+ new_bound.add(bv)
118
+ if isinstance(n, Derivative):
119
+ new_bound.add(n.var)
120
+ if n.var2:
121
+ new_bound.add(n.var2)
122
+
123
+ for child in _iter_children(n):
124
+ walk(child, new_bound)
125
+
126
+ walk(node, set())
127
+ return free
128
+
129
+
130
+ def _extract_binding_name(lower_node: Node) -> Optional[str]:
131
+ if isinstance(lower_node, BinOp) and lower_node.op in ('=', '->'):
132
+ if isinstance(lower_node.left, Var):
133
+ return lower_node.left.name
134
+ return None
135
+
136
+
137
+ def find_bound_vars(node: Node) -> Set[str]:
138
+ names: Set[str] = set()
139
+
140
+ def walk(n: Node) -> None:
141
+ if isinstance(n, BigOp) and n.lower is not None:
142
+ bv = _extract_binding_name(n.lower)
143
+ if bv:
144
+ names.add(bv)
145
+ if isinstance(n, Derivative):
146
+ names.add(n.var)
147
+ if n.var2:
148
+ names.add(n.var2)
149
+ for child in _iter_children(n):
150
+ walk(child)
151
+
152
+ walk(node)
153
+ return names
154
+
155
+ # =============================================================================
156
+ # Alpha-Renaming
157
+ # =============================================================================
158
+
159
+ def _rename_var(node: Node, old_name: str, new_name: str) -> Node:
160
+ if isinstance(node, Var):
161
+ return Var(new_name) if node.name == old_name else node
162
+ if isinstance(node, Num):
163
+ return node
164
+ if isinstance(node, TextLiteral):
165
+ return node
166
+ if isinstance(node, BinOp):
167
+ return BinOp(node.op,
168
+ _rename_var(node.left, old_name, new_name),
169
+ _rename_var(node.right, old_name, new_name))
170
+ if isinstance(node, UnOp):
171
+ return UnOp(node.op, _rename_var(node.expr, old_name, new_name))
172
+ if isinstance(node, Fraction):
173
+ return Fraction(_rename_var(node.num, old_name, new_name),
174
+ _rename_var(node.den, old_name, new_name))
175
+ if isinstance(node, Power):
176
+ return Power(_rename_var(node.base, old_name, new_name),
177
+ _rename_var(node.exp, old_name, new_name))
178
+ if isinstance(node, Subscript):
179
+ return Subscript(_rename_var(node.base, old_name, new_name),
180
+ _rename_var(node.sub, old_name, new_name))
181
+ if isinstance(node, Func):
182
+ return Func(
183
+ node.name,
184
+ [_rename_var(a, old_name, new_name) for a in node.args],
185
+ _rename_var(node.sub, old_name, new_name) if node.sub else None,
186
+ _rename_var(node.power, old_name, new_name) if node.power else None,
187
+ node.primes,
188
+ )
189
+ if isinstance(node, Factorial):
190
+ return Factorial(_rename_var(node.expr, old_name, new_name))
191
+ if isinstance(node, Prime):
192
+ return Prime(_rename_var(node.expr, old_name, new_name), node.order)
193
+ if isinstance(node, Derivative):
194
+ var = new_name if node.var == old_name else node.var
195
+ var2 = new_name if node.var2 == old_name else node.var2
196
+ return Derivative(
197
+ _rename_var(node.expr, old_name, new_name),
198
+ var,
199
+ _rename_var(node.order, old_name, new_name) if node.order else None,
200
+ node.is_partial,
201
+ var2,
202
+ )
203
+ if isinstance(node, Integral):
204
+ return Integral(
205
+ _rename_var(node.expr, old_name, new_name),
206
+ _rename_var(node.lower, old_name, new_name) if node.lower else None,
207
+ _rename_var(node.upper, old_name, new_name) if node.upper else None,
208
+ node.kind,
209
+ )
210
+ if isinstance(node, BigOp):
211
+ return BigOp(
212
+ node.op_name,
213
+ _rename_var(node.lower, old_name, new_name) if node.lower else None,
214
+ _rename_var(node.upper, old_name, new_name) if node.upper else None,
215
+ _rename_var(node.expr, old_name, new_name),
216
+ )
217
+ if isinstance(node, Abs):
218
+ return Abs(_rename_var(node.expr, old_name, new_name))
219
+ if isinstance(node, Norm):
220
+ return Norm(_rename_var(node.expr, old_name, new_name))
221
+ if isinstance(node, InnerProduct):
222
+ return InnerProduct(_rename_var(node.left, old_name, new_name),
223
+ _rename_var(node.right, old_name, new_name))
224
+ if isinstance(node, SetBuilder):
225
+ return SetBuilder(_rename_var(node.expr, old_name, new_name),
226
+ _rename_var(node.condition, old_name, new_name))
227
+ if isinstance(node, MatrixNode):
228
+ return MatrixNode(
229
+ [[_rename_var(c, old_name, new_name) for c in row]
230
+ for row in node.rows],
231
+ node.kind,
232
+ )
233
+ if isinstance(node, IntervalNode):
234
+ return IntervalNode(
235
+ node.left_delim,
236
+ _rename_var(node.left_expr, old_name, new_name),
237
+ _rename_var(node.right_expr, old_name, new_name),
238
+ node.right_delim,
239
+ )
240
+ if isinstance(node, PiecewiseNode):
241
+ return PiecewiseNode([
242
+ (_rename_var(e, old_name, new_name),
243
+ _rename_var(c, old_name, new_name) if c else None)
244
+ for e, c in node.conditions
245
+ ])
246
+
247
+ raise ValueError(f"Cannot rename in node of type {type(node)}")
248
+
249
+ # =============================================================================
250
+ # Clean Substitution
251
+ # =============================================================================
252
+
253
+ def substitute(node: Node, bindings: Dict[str, Node],
254
+ _depth: int = 0) -> Node:
255
+ if _depth > MAX_MACRO_DEPTH:
256
+ raise ParseError(
257
+ f"Maximum macro expansion depth ({MAX_MACRO_DEPTH}) exceeded "
258
+ f"— possible infinite recursion in macro definitions."
259
+ )
260
+
261
+ if not bindings:
262
+ return copy.deepcopy(node)
263
+
264
+ arg_free: Set[str] = set()
265
+ for arg_node in bindings.values():
266
+ arg_free |= free_vars(arg_node)
267
+
268
+ body_bound = find_bound_vars(node)
269
+
270
+ collisions = body_bound & (set(bindings.keys()) | arg_free)
271
+ renamed_node = node
272
+ for bv in collisions:
273
+ new_name = _gensym(bv)
274
+ renamed_node = _rename_var(renamed_node, bv, new_name)
275
+
276
+ return _do_substitute(renamed_node, bindings, _depth)
277
+
278
+
279
+ def _do_substitute(node: Node, bindings: Dict[str, Node],
280
+ _depth: int) -> Node:
281
+ if isinstance(node, Var):
282
+ if node.name in bindings:
283
+ return copy.deepcopy(bindings[node.name])
284
+ return node
285
+ if isinstance(node, Num):
286
+ return node
287
+ if isinstance(node, TextLiteral):
288
+ return node
289
+ if isinstance(node, BinOp):
290
+ return BinOp(node.op,
291
+ _do_substitute(node.left, bindings, _depth),
292
+ _do_substitute(node.right, bindings, _depth))
293
+ if isinstance(node, UnOp):
294
+ return UnOp(node.op, _do_substitute(node.expr, bindings, _depth))
295
+ if isinstance(node, Fraction):
296
+ return Fraction(_do_substitute(node.num, bindings, _depth),
297
+ _do_substitute(node.den, bindings, _depth))
298
+ if isinstance(node, Power):
299
+ return Power(_do_substitute(node.base, bindings, _depth),
300
+ _do_substitute(node.exp, bindings, _depth))
301
+ if isinstance(node, Subscript):
302
+ return Subscript(_do_substitute(node.base, bindings, _depth),
303
+ _do_substitute(node.sub, bindings, _depth))
304
+ if isinstance(node, Func):
305
+ return Func(
306
+ node.name,
307
+ [_do_substitute(a, bindings, _depth) for a in node.args],
308
+ _do_substitute(node.sub, bindings, _depth) if node.sub else None,
309
+ _do_substitute(node.power, bindings, _depth) if node.power else None,
310
+ node.primes,
311
+ )
312
+ if isinstance(node, Factorial):
313
+ return Factorial(_do_substitute(node.expr, bindings, _depth))
314
+ if isinstance(node, Prime):
315
+ return Prime(_do_substitute(node.expr, bindings, _depth), node.order)
316
+ if isinstance(node, Derivative):
317
+ return Derivative(
318
+ _do_substitute(node.expr, bindings, _depth),
319
+ node.var,
320
+ _do_substitute(node.order, bindings, _depth) if node.order else None,
321
+ node.is_partial,
322
+ node.var2,
323
+ )
324
+ if isinstance(node, Integral):
325
+ return Integral(
326
+ _do_substitute(node.expr, bindings, _depth),
327
+ _do_substitute(node.lower, bindings, _depth) if node.lower else None,
328
+ _do_substitute(node.upper, bindings, _depth) if node.upper else None,
329
+ node.kind,
330
+ )
331
+ if isinstance(node, BigOp):
332
+ return BigOp(
333
+ node.op_name,
334
+ _do_substitute(node.lower, bindings, _depth) if node.lower else None,
335
+ _do_substitute(node.upper, bindings, _depth) if node.upper else None,
336
+ _do_substitute(node.expr, bindings, _depth),
337
+ )
338
+ if isinstance(node, Abs):
339
+ return Abs(_do_substitute(node.expr, bindings, _depth))
340
+ if isinstance(node, Norm):
341
+ return Norm(_do_substitute(node.expr, bindings, _depth))
342
+ if isinstance(node, InnerProduct):
343
+ return InnerProduct(_do_substitute(node.left, bindings, _depth),
344
+ _do_substitute(node.right, bindings, _depth))
345
+ if isinstance(node, SetBuilder):
346
+ return SetBuilder(_do_substitute(node.expr, bindings, _depth),
347
+ _do_substitute(node.condition, bindings, _depth))
348
+ if isinstance(node, MatrixNode):
349
+ return MatrixNode(
350
+ [[_do_substitute(c, bindings, _depth) for c in row]
351
+ for row in node.rows],
352
+ node.kind,
353
+ )
354
+ if isinstance(node, IntervalNode):
355
+ return IntervalNode(
356
+ node.left_delim,
357
+ _do_substitute(node.left_expr, bindings, _depth),
358
+ _do_substitute(node.right_expr, bindings, _depth),
359
+ node.right_delim,
360
+ )
361
+ if isinstance(node, PiecewiseNode):
362
+ return PiecewiseNode([
363
+ (_do_substitute(e, bindings, _depth),
364
+ _do_substitute(c, bindings, _depth) if c else None)
365
+ for e, c in node.conditions
366
+ ])
367
+
368
+ raise ValueError(f"Cannot substitute node of type {type(node)}")
369
+
370
+ # =============================================================================
371
+ # Macro Definition Parser
372
+ # =============================================================================
373
+
374
+ def parse_macro_def(line: str) -> Tuple[str, Macro]:
375
+ from .parser import Parser
376
+ from .lexer import _preprocess, tokenize
377
+
378
+ line = line.strip()[len("define "):].strip()
379
+ if '=' not in line:
380
+ raise ValueError("Macro definition must contain '='")
381
+ left, right = line.split('=', 1)
382
+ left, right = left.strip(), right.strip()
383
+
384
+ match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)(?:\((.*?)\))?$', left)
385
+ if not match:
386
+ raise ValueError("Invalid macro signature.")
387
+
388
+ name = match.group(1)
389
+ if (name in ALL_FUNC_NAMES or name in GREEK_LETTERS
390
+ or name in SPECIAL_CONSTANTS or name in BUILTIN_LATEX_FUNCS):
391
+ raise ValueError(
392
+ f"Cannot overwrite built-in function or constant '{name}'."
393
+ )
394
+
395
+ args_str = match.group(2)
396
+ args: List[str] = []
397
+ if args_str:
398
+ args = [a.strip() for a in args_str.split(',')]
399
+ for a in args:
400
+ if not re.match(r'^[a-zA-Z]+$', a):
401
+ raise ValueError(f"Invalid argument name '{a}' in macro.")
402
+
403
+ preprocessed, text_map = _preprocess(right)
404
+ tokens = tokenize(preprocessed)
405
+ parser = Parser(tokens, text_map=text_map)
406
+ body_ast = parser.parse()
407
+
408
+ return name, Macro(name, args, body_ast)