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/__init__.py +84 -0
- math2tex/ast_nodes.py +205 -0
- math2tex/cli.py +60 -0
- math2tex/config.py +213 -0
- math2tex/formatter.py +504 -0
- math2tex/lexer.py +160 -0
- math2tex/macros.py +408 -0
- math2tex/parser.py +967 -0
- math2tex/tests_legacy.py +195 -0
- math2tex-1.0.0.dist-info/METADATA +135 -0
- math2tex-1.0.0.dist-info/RECORD +14 -0
- math2tex-1.0.0.dist-info/WHEEL +4 -0
- math2tex-1.0.0.dist-info/entry_points.txt +2 -0
- math2tex-1.0.0.dist-info/licenses/LICENSE +21 -0
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)
|