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/parser.py
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
"""
|
|
2
|
+
math2tex.parser — Recursive-descent parser converting tokens into an AST.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, fields as dataclass_fields
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from .config import (
|
|
9
|
+
ALL_FUNC_NAMES, BIG_OPS, BUILTIN_LATEX_FUNCS, GREEK_LETTERS,
|
|
10
|
+
IMPLICIT_MULT_STOP, MATRIX_FUNCS, MAX_DEPTH, SPECIAL_CONSTANTS,
|
|
11
|
+
WORD_ADD_OPS, WORD_MULT_OPS, WORD_RELATIONS,
|
|
12
|
+
levenshtein_distance,
|
|
13
|
+
)
|
|
14
|
+
from .ast_nodes import (
|
|
15
|
+
Abs, BigOp, BinOp, Derivative, Factorial, Fraction, Func, Group,
|
|
16
|
+
InnerProduct, Integral, IntervalNode, MatrixNode, Node, Norm, Num,
|
|
17
|
+
PiecewiseNode, Power, Prime, SetBuilder, Subscript, TextLiteral,
|
|
18
|
+
UnOp, Var,
|
|
19
|
+
)
|
|
20
|
+
from .lexer import ParseError, Token, _Backtrack
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# Parser State
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class _ParserState:
|
|
28
|
+
pos: int = 0
|
|
29
|
+
in_abs: int = 0
|
|
30
|
+
in_set_builder: bool = False
|
|
31
|
+
in_bound: bool = False
|
|
32
|
+
in_superscript: bool = False
|
|
33
|
+
depth: int = 0
|
|
34
|
+
|
|
35
|
+
_STATE_FIELDS = tuple(f.name for f in dataclass_fields(_ParserState))
|
|
36
|
+
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# Depth Guard Decorator
|
|
39
|
+
# =============================================================================
|
|
40
|
+
|
|
41
|
+
def depth_guarded(func):
|
|
42
|
+
def wrapper(self, *args, **kwargs):
|
|
43
|
+
self.depth += 1
|
|
44
|
+
if self.depth > MAX_DEPTH:
|
|
45
|
+
raise ParseError('Expression too deeply nested')
|
|
46
|
+
try:
|
|
47
|
+
return func(self, *args, **kwargs)
|
|
48
|
+
finally:
|
|
49
|
+
self.depth -= 1
|
|
50
|
+
return wrapper
|
|
51
|
+
|
|
52
|
+
# =============================================================================
|
|
53
|
+
# Parser
|
|
54
|
+
# =============================================================================
|
|
55
|
+
|
|
56
|
+
class Parser:
|
|
57
|
+
def __init__(self, tokens: List[Token],
|
|
58
|
+
text_map: Optional[dict] = None,
|
|
59
|
+
macro_env: Optional[dict] = None):
|
|
60
|
+
# Immutable configuration
|
|
61
|
+
self.text_map = text_map or {}
|
|
62
|
+
self.macro_env = macro_env or {}
|
|
63
|
+
self.tokens = tokens
|
|
64
|
+
|
|
65
|
+
_defaults = _ParserState()
|
|
66
|
+
for fname in _STATE_FIELDS:
|
|
67
|
+
setattr(self, fname, getattr(_defaults, fname))
|
|
68
|
+
|
|
69
|
+
# --- Derivative token splitting ---
|
|
70
|
+
valid_words = (
|
|
71
|
+
ALL_FUNC_NAMES | BIG_OPS | GREEK_LETTERS | set(SPECIAL_CONSTANTS)
|
|
72
|
+
| set(WORD_RELATIONS) | set(WORD_MULT_OPS)
|
|
73
|
+
| set(WORD_ADD_OPS) | set(MATRIX_FUNCS.keys())
|
|
74
|
+
| {'if', 'otherwise', 'else', 'choose', 'from', 'to', 'of', 'as'}
|
|
75
|
+
| set(self.macro_env.keys())
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
new_tokens: List[Token] = []
|
|
79
|
+
for t in tokens:
|
|
80
|
+
val = t.value
|
|
81
|
+
is_deriv = False
|
|
82
|
+
if t.type == 'ID':
|
|
83
|
+
if (val.startswith('d') and len(val) > 1
|
|
84
|
+
and (val[1:] in valid_words
|
|
85
|
+
or (len(val[1:]) == 1 and val[1:].isalpha()))):
|
|
86
|
+
is_deriv = True
|
|
87
|
+
elif (val.startswith('partial') and len(val) > 7
|
|
88
|
+
and (val[7:] in valid_words
|
|
89
|
+
or (len(val[7:]) == 1 and val[7:].isalpha()))):
|
|
90
|
+
is_deriv = True
|
|
91
|
+
|
|
92
|
+
new_tokens.append(t)
|
|
93
|
+
|
|
94
|
+
self.tokens = new_tokens
|
|
95
|
+
self._valid_words = valid_words
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------
|
|
98
|
+
# Diagnostic fuzzy matching
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def _suggest_correction(self, token_value: str) -> str:
|
|
102
|
+
if len(token_value) < 4:
|
|
103
|
+
return ''
|
|
104
|
+
best_match = None
|
|
105
|
+
best_dist = 3
|
|
106
|
+
for word in self._valid_words:
|
|
107
|
+
dist = levenshtein_distance(token_value, word)
|
|
108
|
+
if dist <= 2 and dist < best_dist:
|
|
109
|
+
best_match = word
|
|
110
|
+
best_dist = dist
|
|
111
|
+
if best_match:
|
|
112
|
+
return f" (Did you mean '{best_match}'?)"
|
|
113
|
+
return ''
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
# Token access
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def current(self) -> Token:
|
|
121
|
+
return self.tokens[self.pos]
|
|
122
|
+
|
|
123
|
+
def consume(self, expected_type: str = None) -> Token:
|
|
124
|
+
tok = self.current
|
|
125
|
+
if expected_type and tok.type != expected_type:
|
|
126
|
+
raise SyntaxError(
|
|
127
|
+
f"Expected {expected_type}, got {tok.type} '{tok.value}'"
|
|
128
|
+
)
|
|
129
|
+
self.pos += 1
|
|
130
|
+
return tok
|
|
131
|
+
|
|
132
|
+
def match(self, ttype: str, val: str = None) -> bool:
|
|
133
|
+
if (self.current.type == ttype
|
|
134
|
+
and (val is None or self.current.value == val)):
|
|
135
|
+
self.pos += 1
|
|
136
|
+
return True
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def peek(self, offset: int = 1) -> Token:
|
|
140
|
+
idx = self.pos + offset
|
|
141
|
+
if idx >= len(self.tokens):
|
|
142
|
+
return self.tokens[-1]
|
|
143
|
+
return self.tokens[idx]
|
|
144
|
+
|
|
145
|
+
def rewind(self, n: int = 1) -> None:
|
|
146
|
+
self.pos -= n
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
# State snapshot / restore
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
def _snapshot_state(self) -> dict:
|
|
153
|
+
return {f: getattr(self, f) for f in _STATE_FIELDS}
|
|
154
|
+
|
|
155
|
+
def _restore_state(self, snapshot: dict) -> None:
|
|
156
|
+
for f, v in snapshot.items():
|
|
157
|
+
setattr(self, f, v)
|
|
158
|
+
|
|
159
|
+
def try_parse(self, parse_func):
|
|
160
|
+
snapshot = self._snapshot_state()
|
|
161
|
+
try:
|
|
162
|
+
return parse_func()
|
|
163
|
+
except _Backtrack:
|
|
164
|
+
self._restore_state(snapshot)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
# Entry
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def parse(self) -> Node:
|
|
172
|
+
if self.current.type == 'EOF':
|
|
173
|
+
return Var('')
|
|
174
|
+
expr = self.parse_expression()
|
|
175
|
+
if self.current.type != 'EOF':
|
|
176
|
+
tok = self.current
|
|
177
|
+
hint = self._suggest_correction(tok.value) if tok.type == 'ID' else ''
|
|
178
|
+
raise ParseError(
|
|
179
|
+
f"Unexpected token at end: {tok}{hint}\n"
|
|
180
|
+
f"% Input: {' '.join(t.value for t in self.tokens)}"
|
|
181
|
+
)
|
|
182
|
+
return expr
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
# Precedence Levels
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
@depth_guarded
|
|
189
|
+
def parse_expression(self) -> Node:
|
|
190
|
+
left = self.parse_relational()
|
|
191
|
+
if self.current.type == 'ID' and self.current.value == 'if':
|
|
192
|
+
self.consume('ID')
|
|
193
|
+
right = self.parse_relational()
|
|
194
|
+
return BinOp('if', left, right)
|
|
195
|
+
if self.current.type == 'ID' and self.current.value == 'otherwise':
|
|
196
|
+
self.consume('ID')
|
|
197
|
+
return BinOp('otherwise', left, Var(''))
|
|
198
|
+
return left
|
|
199
|
+
|
|
200
|
+
@depth_guarded
|
|
201
|
+
def parse_relational(self) -> Node:
|
|
202
|
+
left = self.parse_choose()
|
|
203
|
+
while True:
|
|
204
|
+
if self.current.type in ('REL', 'ARROW', 'COLON'):
|
|
205
|
+
op = self.consume().value
|
|
206
|
+
right = self.parse_choose()
|
|
207
|
+
left = BinOp(op, left, right)
|
|
208
|
+
elif (self.current.type == 'ID'
|
|
209
|
+
and self.current.value in WORD_RELATIONS):
|
|
210
|
+
op = self.consume().value
|
|
211
|
+
right = self.parse_choose()
|
|
212
|
+
left = BinOp(op, left, right)
|
|
213
|
+
else:
|
|
214
|
+
break
|
|
215
|
+
return left
|
|
216
|
+
|
|
217
|
+
@depth_guarded
|
|
218
|
+
def parse_choose(self) -> Node:
|
|
219
|
+
left = self.parse_additive()
|
|
220
|
+
while self.match('ID', 'choose'):
|
|
221
|
+
right = self.parse_additive()
|
|
222
|
+
left = BinOp('choose', left, right)
|
|
223
|
+
return left
|
|
224
|
+
|
|
225
|
+
@depth_guarded
|
|
226
|
+
def parse_additive(self) -> Node:
|
|
227
|
+
left = self.parse_multiplicative()
|
|
228
|
+
while True:
|
|
229
|
+
if (self.current.type == 'OP'
|
|
230
|
+
and self.current.value in ('+', '-')):
|
|
231
|
+
op = self.consume().value
|
|
232
|
+
right = self.parse_multiplicative()
|
|
233
|
+
left = BinOp(op, left, right)
|
|
234
|
+
elif self.current.type == 'PM':
|
|
235
|
+
op = self.consume().value
|
|
236
|
+
right = self.parse_multiplicative()
|
|
237
|
+
left = BinOp(op, left, right)
|
|
238
|
+
elif (self.current.type == 'ID'
|
|
239
|
+
and self.current.value in WORD_ADD_OPS):
|
|
240
|
+
op = self.consume().value
|
|
241
|
+
right = self.parse_multiplicative()
|
|
242
|
+
left = BinOp(op, left, right)
|
|
243
|
+
else:
|
|
244
|
+
break
|
|
245
|
+
return left
|
|
246
|
+
|
|
247
|
+
@depth_guarded
|
|
248
|
+
def parse_multiplicative(self) -> Node:
|
|
249
|
+
left = self.parse_implicit()
|
|
250
|
+
while True:
|
|
251
|
+
if self.current.type == 'OP' and self.current.value == '*':
|
|
252
|
+
self.consume()
|
|
253
|
+
right = self.parse_implicit()
|
|
254
|
+
left = BinOp('*', left, right)
|
|
255
|
+
elif self.current.type == 'OP' and self.current.value == '/':
|
|
256
|
+
self.consume()
|
|
257
|
+
right = self.parse_postfix()
|
|
258
|
+
left = Fraction(left, right)
|
|
259
|
+
while (self.current.type in ('ID', 'NUMBER', 'LPAREN',
|
|
260
|
+
'LBRACK', 'LBRACE', 'ELLIPSIS')
|
|
261
|
+
or (self.current.type in ('BAR', 'DBLBAR')
|
|
262
|
+
and self.in_abs == 0
|
|
263
|
+
and not self.in_set_builder)):
|
|
264
|
+
if (self.current.type == 'ID'
|
|
265
|
+
and self.current.value in IMPLICIT_MULT_STOP):
|
|
266
|
+
break
|
|
267
|
+
if (self.current.type == 'ID'
|
|
268
|
+
and self.current.value in BIG_OPS):
|
|
269
|
+
break
|
|
270
|
+
right = self.parse_postfix()
|
|
271
|
+
left = BinOp(' ', left, right)
|
|
272
|
+
elif (self.current.type == 'ID'
|
|
273
|
+
and self.current.value in WORD_MULT_OPS):
|
|
274
|
+
op = self.consume().value
|
|
275
|
+
right = self.parse_implicit()
|
|
276
|
+
left = BinOp(op, left, right)
|
|
277
|
+
else:
|
|
278
|
+
break
|
|
279
|
+
return left
|
|
280
|
+
|
|
281
|
+
@depth_guarded
|
|
282
|
+
def parse_implicit(self) -> Node:
|
|
283
|
+
# Check for big operators first
|
|
284
|
+
if (self.current.type == 'ID'
|
|
285
|
+
and self.current.value in BIG_OPS):
|
|
286
|
+
if self.current.value == 'inf':
|
|
287
|
+
if self.pos + 1 < len(self.tokens) and self.tokens[self.pos + 1].value == '_':
|
|
288
|
+
return self.parse_bigop()
|
|
289
|
+
else:
|
|
290
|
+
return self.parse_bigop()
|
|
291
|
+
|
|
292
|
+
left = self.parse_postfix()
|
|
293
|
+
|
|
294
|
+
# Implicit multiplication lookahead
|
|
295
|
+
while (self.current.type in ('ID', 'NUMBER', 'LPAREN',
|
|
296
|
+
'LBRACK', 'LBRACE', 'ELLIPSIS')
|
|
297
|
+
or (self.current.type in ('BAR', 'DBLBAR')
|
|
298
|
+
and self.in_abs == 0
|
|
299
|
+
and not self.in_set_builder)):
|
|
300
|
+
if (self.current.type == 'ID'
|
|
301
|
+
and self.current.value in IMPLICIT_MULT_STOP):
|
|
302
|
+
break
|
|
303
|
+
if (self.current.type == 'ID'
|
|
304
|
+
and self.current.value in BIG_OPS):
|
|
305
|
+
break
|
|
306
|
+
right = self.parse_postfix()
|
|
307
|
+
left = BinOp(' ', left, right)
|
|
308
|
+
return left
|
|
309
|
+
|
|
310
|
+
@depth_guarded
|
|
311
|
+
def parse_postfix(self) -> Node:
|
|
312
|
+
node = self.parse_power()
|
|
313
|
+
while True:
|
|
314
|
+
if self.current.type == 'OP' and self.current.value == '!':
|
|
315
|
+
self.consume()
|
|
316
|
+
node = Factorial(node)
|
|
317
|
+
elif self.current.type == 'PRIME':
|
|
318
|
+
count = len(self.consume().value)
|
|
319
|
+
node = Prime(node, count)
|
|
320
|
+
elif (self.current.type == 'OP'
|
|
321
|
+
and self.current.value in ('^', '_')):
|
|
322
|
+
if self.current.value == '_' and self.in_superscript:
|
|
323
|
+
break
|
|
324
|
+
op = self.consume().value
|
|
325
|
+
if op == '^':
|
|
326
|
+
old_sup = self.in_superscript
|
|
327
|
+
self.in_superscript = True
|
|
328
|
+
node = Power(node, self.parse_power())
|
|
329
|
+
self.in_superscript = old_sup
|
|
330
|
+
else:
|
|
331
|
+
node = Subscript(node, self.parse_subscript())
|
|
332
|
+
else:
|
|
333
|
+
break
|
|
334
|
+
return node
|
|
335
|
+
|
|
336
|
+
@depth_guarded
|
|
337
|
+
def parse_subscript(self) -> Node:
|
|
338
|
+
node = self.parse_unary()
|
|
339
|
+
if self.current.type == 'OP' and self.current.value == '_':
|
|
340
|
+
self.consume()
|
|
341
|
+
node = Subscript(node, self.parse_subscript())
|
|
342
|
+
return node
|
|
343
|
+
|
|
344
|
+
@depth_guarded
|
|
345
|
+
def parse_power(self) -> Node:
|
|
346
|
+
node = self.parse_unary()
|
|
347
|
+
while (self.current.type == 'OP'
|
|
348
|
+
and self.current.value in ('^', '_')):
|
|
349
|
+
if self.current.value == '_' and self.in_superscript:
|
|
350
|
+
break
|
|
351
|
+
|
|
352
|
+
op = self.consume().value
|
|
353
|
+
if op == '^':
|
|
354
|
+
old_sup = self.in_superscript
|
|
355
|
+
self.in_superscript = True
|
|
356
|
+
node = Power(node, self.parse_power()) # right-associative power
|
|
357
|
+
self.in_superscript = old_sup
|
|
358
|
+
else:
|
|
359
|
+
# Subscripts are right-associative among themselves, but ^ binds left
|
|
360
|
+
node = Subscript(node, self.parse_subscript())
|
|
361
|
+
return node
|
|
362
|
+
|
|
363
|
+
@depth_guarded
|
|
364
|
+
def parse_unary(self) -> Node:
|
|
365
|
+
if self.current.type == 'OP' and self.current.value in ('+', '-'):
|
|
366
|
+
return UnOp(self.consume().value, self.parse_unary())
|
|
367
|
+
if self.current.type == 'PM':
|
|
368
|
+
op = self.consume().value
|
|
369
|
+
expr = self.parse_unary()
|
|
370
|
+
return BinOp(op, Var(''), expr)
|
|
371
|
+
return self.parse_primary()
|
|
372
|
+
|
|
373
|
+
@depth_guarded
|
|
374
|
+
def parse_primary(self) -> Node:
|
|
375
|
+
deriv = self.try_parse(self._parse_derivative)
|
|
376
|
+
if deriv is not None:
|
|
377
|
+
return deriv
|
|
378
|
+
|
|
379
|
+
inner = self.try_parse(self._parse_inner_product)
|
|
380
|
+
if inner is not None:
|
|
381
|
+
return inner
|
|
382
|
+
|
|
383
|
+
tok = self.current
|
|
384
|
+
|
|
385
|
+
if tok.type == 'NUMBER':
|
|
386
|
+
return Num(self.consume().value)
|
|
387
|
+
|
|
388
|
+
elif tok.type == 'ELLIPSIS':
|
|
389
|
+
self.consume()
|
|
390
|
+
return Var('...')
|
|
391
|
+
|
|
392
|
+
elif tok.type == 'ID':
|
|
393
|
+
val = self.consume().value
|
|
394
|
+
|
|
395
|
+
if val in self.text_map:
|
|
396
|
+
return TextLiteral(self.text_map[val])
|
|
397
|
+
|
|
398
|
+
if val in MATRIX_FUNCS and self.current.type == 'LBRACK':
|
|
399
|
+
self.consume('LBRACK')
|
|
400
|
+
matrix_node = self.parse_matrix_bracket()
|
|
401
|
+
if isinstance(matrix_node, MatrixNode):
|
|
402
|
+
return MatrixNode(matrix_node.rows,
|
|
403
|
+
kind=MATRIX_FUNCS[val])
|
|
404
|
+
elif isinstance(matrix_node, IntervalNode):
|
|
405
|
+
return MatrixNode(
|
|
406
|
+
[[matrix_node.left_expr, matrix_node.right_expr]],
|
|
407
|
+
kind=MATRIX_FUNCS[val])
|
|
408
|
+
return matrix_node
|
|
409
|
+
|
|
410
|
+
if val in BIG_OPS:
|
|
411
|
+
if val == 'inf':
|
|
412
|
+
if self.current.type == 'OP' and self.current.value == '_':
|
|
413
|
+
self.rewind(1)
|
|
414
|
+
return self.parse_bigop()
|
|
415
|
+
else:
|
|
416
|
+
self.rewind(1)
|
|
417
|
+
return self.parse_bigop()
|
|
418
|
+
|
|
419
|
+
snapshot = self._snapshot_state()
|
|
420
|
+
prime_count = 0
|
|
421
|
+
if self.current.type == 'PRIME':
|
|
422
|
+
prime_count = len(self.consume().value)
|
|
423
|
+
|
|
424
|
+
sub = None
|
|
425
|
+
power = None
|
|
426
|
+
if not self.in_bound:
|
|
427
|
+
while (self.current.type == 'OP'
|
|
428
|
+
and self.current.value in ('_', '^')):
|
|
429
|
+
op = self.consume().value
|
|
430
|
+
old = self.in_bound
|
|
431
|
+
self.in_bound = True
|
|
432
|
+
if op == '_':
|
|
433
|
+
sub = self.parse_primary()
|
|
434
|
+
elif op == '^':
|
|
435
|
+
old_sup = self.in_superscript
|
|
436
|
+
self.in_superscript = True
|
|
437
|
+
power = self.parse_postfix()
|
|
438
|
+
self.in_superscript = old_sup
|
|
439
|
+
self.in_bound = old
|
|
440
|
+
|
|
441
|
+
is_known = (val in ALL_FUNC_NAMES or val in self.macro_env
|
|
442
|
+
or val in self.text_map)
|
|
443
|
+
|
|
444
|
+
is_func = is_known or (self.current.type == 'LPAREN') or (val in ALL_FUNC_NAMES and self.current.type == 'ID' and self.current.value == 'of')
|
|
445
|
+
|
|
446
|
+
if not is_func:
|
|
447
|
+
self._restore_state(snapshot)
|
|
448
|
+
return Var(val)
|
|
449
|
+
|
|
450
|
+
if (self.current.type == 'LPAREN'
|
|
451
|
+
and (not self.in_bound or is_known)):
|
|
452
|
+
self.consume('LPAREN')
|
|
453
|
+
args = []
|
|
454
|
+
if self.current.type != 'RPAREN':
|
|
455
|
+
args.append(self.parse_relational())
|
|
456
|
+
while self.match('COMMA'):
|
|
457
|
+
args.append(self.parse_relational())
|
|
458
|
+
self.consume('RPAREN')
|
|
459
|
+
|
|
460
|
+
if val in self.macro_env:
|
|
461
|
+
from .macros import substitute
|
|
462
|
+
macro = self.macro_env[val]
|
|
463
|
+
if len(args) != len(macro.args):
|
|
464
|
+
raise ParseError(
|
|
465
|
+
f"Macro '{val}' expects {len(macro.args)} "
|
|
466
|
+
f"arguments, got {len(args)}."
|
|
467
|
+
)
|
|
468
|
+
bindings = dict(zip(macro.args, args))
|
|
469
|
+
node = substitute(macro.body, bindings)
|
|
470
|
+
if prime_count > 0:
|
|
471
|
+
node = Prime(node, prime_count)
|
|
472
|
+
if sub is not None:
|
|
473
|
+
node = Subscript(node, sub)
|
|
474
|
+
if power is not None:
|
|
475
|
+
node = Power(node, power)
|
|
476
|
+
return node
|
|
477
|
+
|
|
478
|
+
func_node = Func(val, args, sub, power, prime_count)
|
|
479
|
+
|
|
480
|
+
from .config import ACCENT_FUNCTIONS, FONT_FUNCTIONS
|
|
481
|
+
if (val in ACCENT_FUNCTIONS or val in FONT_FUNCTIONS) and self.current.type == 'LPAREN':
|
|
482
|
+
self.consume('LPAREN')
|
|
483
|
+
call_args = []
|
|
484
|
+
if self.current.type != 'RPAREN':
|
|
485
|
+
call_args.append(self.parse_relational())
|
|
486
|
+
while self.match('COMMA'):
|
|
487
|
+
call_args.append(self.parse_relational())
|
|
488
|
+
self.consume('RPAREN')
|
|
489
|
+
|
|
490
|
+
return Func(val, args, sub, power, prime_count, call_args=call_args)
|
|
491
|
+
return func_node
|
|
492
|
+
|
|
493
|
+
elif val in ALL_FUNC_NAMES and self.match('ID', 'of'):
|
|
494
|
+
arg = self.parse_relational()
|
|
495
|
+
return Func(val, [arg], sub, power, prime_count)
|
|
496
|
+
elif val in self.macro_env and self.match('ID', 'of'):
|
|
497
|
+
from .macros import substitute
|
|
498
|
+
arg = self.parse_relational()
|
|
499
|
+
macro = self.macro_env[val]
|
|
500
|
+
if len(macro.args) != 1:
|
|
501
|
+
raise ParseError(
|
|
502
|
+
f"Macro '{val}' expects {len(macro.args)} "
|
|
503
|
+
f"arguments, got 1."
|
|
504
|
+
)
|
|
505
|
+
node = substitute(macro.body, {macro.args[0]: arg})
|
|
506
|
+
if prime_count > 0:
|
|
507
|
+
node = Prime(node, prime_count)
|
|
508
|
+
if sub is not None:
|
|
509
|
+
node = Subscript(node, sub)
|
|
510
|
+
if power is not None:
|
|
511
|
+
node = Power(node, power)
|
|
512
|
+
return node
|
|
513
|
+
|
|
514
|
+
if val in self.macro_env:
|
|
515
|
+
from .macros import substitute
|
|
516
|
+
macro = self.macro_env[val]
|
|
517
|
+
if len(macro.args) > 0:
|
|
518
|
+
raise ParseError(
|
|
519
|
+
f"Macro '{val}' expects {len(macro.args)} "
|
|
520
|
+
f"arguments, but none were provided."
|
|
521
|
+
)
|
|
522
|
+
node = substitute(macro.body, {})
|
|
523
|
+
else:
|
|
524
|
+
node = Var(val)
|
|
525
|
+
|
|
526
|
+
if prime_count > 0:
|
|
527
|
+
node = Prime(node, prime_count)
|
|
528
|
+
if sub is not None:
|
|
529
|
+
node = Subscript(node, sub)
|
|
530
|
+
if power is not None:
|
|
531
|
+
node = Power(node, power)
|
|
532
|
+
return node
|
|
533
|
+
|
|
534
|
+
elif tok.type == 'LPAREN':
|
|
535
|
+
self.consume('LPAREN')
|
|
536
|
+
if self.current.type == 'RPAREN':
|
|
537
|
+
raise ParseError(
|
|
538
|
+
"Empty parentheses '()' are not allowed "
|
|
539
|
+
"outside of function calls."
|
|
540
|
+
)
|
|
541
|
+
expr = self.parse_relational()
|
|
542
|
+
if self.match('COMMA'):
|
|
543
|
+
expr2 = self.parse_relational()
|
|
544
|
+
if self.current.type == 'RPAREN':
|
|
545
|
+
self.consume('RPAREN')
|
|
546
|
+
return IntervalNode('(', expr, expr2, ')')
|
|
547
|
+
elif self.current.type == 'RBRACK':
|
|
548
|
+
self.consume('RBRACK')
|
|
549
|
+
return IntervalNode('(', expr, expr2, ']')
|
|
550
|
+
else:
|
|
551
|
+
raise ParseError("Expected ')' or ']' for interval")
|
|
552
|
+
self.consume('RPAREN')
|
|
553
|
+
return expr
|
|
554
|
+
|
|
555
|
+
elif tok.type == 'BAR':
|
|
556
|
+
return self._parse_abs_or_norm('abs', 'BAR', 'BAR')
|
|
557
|
+
|
|
558
|
+
elif tok.type == 'DBLBAR':
|
|
559
|
+
return self._parse_abs_or_norm('norm', 'DBLBAR', 'DBLBAR')
|
|
560
|
+
|
|
561
|
+
elif tok.type == 'LBRACK':
|
|
562
|
+
self.consume('LBRACK')
|
|
563
|
+
|
|
564
|
+
if self.current.type == 'LBRACK':
|
|
565
|
+
rows = []
|
|
566
|
+
while (self.current.type != 'RBRACK'
|
|
567
|
+
and self.current.type != 'EOF'):
|
|
568
|
+
self.consume('LBRACK')
|
|
569
|
+
row = []
|
|
570
|
+
if self.current.type != 'RBRACK':
|
|
571
|
+
row.append(self.parse_relational())
|
|
572
|
+
while self.match('COMMA'):
|
|
573
|
+
row.append(self.parse_relational())
|
|
574
|
+
self.consume('RBRACK')
|
|
575
|
+
rows.append(row)
|
|
576
|
+
self.match('COMMA')
|
|
577
|
+
self.consume('RBRACK')
|
|
578
|
+
return MatrixNode(rows)
|
|
579
|
+
else:
|
|
580
|
+
expr = self.parse_relational()
|
|
581
|
+
if self.match('COMMA'):
|
|
582
|
+
expr2 = self.parse_relational()
|
|
583
|
+
if self.current.type == 'RPAREN':
|
|
584
|
+
self.consume('RPAREN')
|
|
585
|
+
return IntervalNode('[', expr, expr2, ')')
|
|
586
|
+
elif self.current.type == 'RBRACK':
|
|
587
|
+
self.consume('RBRACK')
|
|
588
|
+
return IntervalNode('[', expr, expr2, ']')
|
|
589
|
+
else:
|
|
590
|
+
row = [expr, expr2]
|
|
591
|
+
rows: list = []
|
|
592
|
+
while True:
|
|
593
|
+
if self.match('COMMA'):
|
|
594
|
+
row.append(self.parse_relational())
|
|
595
|
+
elif self.match('SEMI'):
|
|
596
|
+
rows.append(row)
|
|
597
|
+
row = [self.parse_relational()]
|
|
598
|
+
else:
|
|
599
|
+
break
|
|
600
|
+
if row:
|
|
601
|
+
rows.append(row)
|
|
602
|
+
self.consume('RBRACK')
|
|
603
|
+
return MatrixNode(rows)
|
|
604
|
+
elif self.match('SEMI'):
|
|
605
|
+
row = [expr]
|
|
606
|
+
rows = []
|
|
607
|
+
while True:
|
|
608
|
+
expr = self.parse_relational()
|
|
609
|
+
row.append(expr)
|
|
610
|
+
if self.match('COMMA'):
|
|
611
|
+
pass
|
|
612
|
+
elif self.match('SEMI'):
|
|
613
|
+
rows.append(row)
|
|
614
|
+
row = []
|
|
615
|
+
else:
|
|
616
|
+
break
|
|
617
|
+
if row:
|
|
618
|
+
rows.append(row)
|
|
619
|
+
self.consume('RBRACK')
|
|
620
|
+
return MatrixNode(rows)
|
|
621
|
+
else:
|
|
622
|
+
self.consume('RBRACK')
|
|
623
|
+
return MatrixNode([[expr]])
|
|
624
|
+
|
|
625
|
+
elif tok.type == 'LBRACE':
|
|
626
|
+
return self.parse_brace_structure()
|
|
627
|
+
|
|
628
|
+
raise ParseError(f"Unexpected token: {tok}")
|
|
629
|
+
|
|
630
|
+
# ------------------------------------------------------------------
|
|
631
|
+
# Specialised Parsers
|
|
632
|
+
# ------------------------------------------------------------------
|
|
633
|
+
|
|
634
|
+
def _parse_derivative(self) -> Derivative:
|
|
635
|
+
val = self.current.value
|
|
636
|
+
is_attached = False
|
|
637
|
+
if self.current.type != 'ID':
|
|
638
|
+
raise _Backtrack()
|
|
639
|
+
|
|
640
|
+
if val in ('d', 'partial'):
|
|
641
|
+
pass
|
|
642
|
+
elif (val.startswith('d') and len(val) >= 2
|
|
643
|
+
and (val[1:] in GREEK_LETTERS
|
|
644
|
+
or (len(val[1:]) == 1 and val[1:].isalpha()))):
|
|
645
|
+
is_attached = True
|
|
646
|
+
elif (val.startswith('partial') and len(val) > 7
|
|
647
|
+
and (val[7:] in GREEK_LETTERS
|
|
648
|
+
or (len(val[7:]) == 1 and val[7:].isalpha()))):
|
|
649
|
+
is_attached = True
|
|
650
|
+
else:
|
|
651
|
+
raise _Backtrack()
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
tok = self.consume('ID')
|
|
655
|
+
is_partial = tok.value.startswith('partial')
|
|
656
|
+
|
|
657
|
+
order = None
|
|
658
|
+
if self.match('OP', '^'):
|
|
659
|
+
order = self.parse_unary()
|
|
660
|
+
|
|
661
|
+
target = None
|
|
662
|
+
if is_attached:
|
|
663
|
+
if is_partial:
|
|
664
|
+
target = Var(tok.value[7:])
|
|
665
|
+
else:
|
|
666
|
+
target = Var(tok.value[1:])
|
|
667
|
+
else:
|
|
668
|
+
if not (self.current.type == 'OP'
|
|
669
|
+
and self.current.value == '/'):
|
|
670
|
+
target = self.parse_implicit()
|
|
671
|
+
|
|
672
|
+
if not self.match('OP', '/'):
|
|
673
|
+
raise _Backtrack()
|
|
674
|
+
|
|
675
|
+
den_tok = self.consume('ID')
|
|
676
|
+
var2 = None
|
|
677
|
+
if is_partial:
|
|
678
|
+
if (den_tok.value != 'partial'
|
|
679
|
+
and not den_tok.value.startswith('partial')):
|
|
680
|
+
raise _Backtrack()
|
|
681
|
+
|
|
682
|
+
if den_tok.value == 'partial':
|
|
683
|
+
var = self.consume('ID').value
|
|
684
|
+
else:
|
|
685
|
+
var = den_tok.value[7:]
|
|
686
|
+
|
|
687
|
+
if order and self.match('OP', '^'):
|
|
688
|
+
self.parse_unary()
|
|
689
|
+
|
|
690
|
+
if (self.current.type == 'ID'
|
|
691
|
+
and self.current.value.startswith('partial')):
|
|
692
|
+
tok2 = self.consume('ID')
|
|
693
|
+
if tok2.value == 'partial':
|
|
694
|
+
var2 = self.consume('ID').value
|
|
695
|
+
else:
|
|
696
|
+
var2 = tok2.value[7:]
|
|
697
|
+
|
|
698
|
+
if order and self.match('OP', '^'):
|
|
699
|
+
self.parse_unary()
|
|
700
|
+
else:
|
|
701
|
+
if den_tok.value == 'd':
|
|
702
|
+
var = self.consume('ID').value
|
|
703
|
+
elif (den_tok.value.startswith('d')
|
|
704
|
+
and len(den_tok.value) > 1):
|
|
705
|
+
var = den_tok.value[1:]
|
|
706
|
+
else:
|
|
707
|
+
raise _Backtrack()
|
|
708
|
+
|
|
709
|
+
if order and self.match('OP', '^'):
|
|
710
|
+
self.parse_unary()
|
|
711
|
+
|
|
712
|
+
if target:
|
|
713
|
+
expr = target
|
|
714
|
+
else:
|
|
715
|
+
if self.current.type != 'EOF':
|
|
716
|
+
expr = self.parse_implicit()
|
|
717
|
+
else:
|
|
718
|
+
expr = Var('')
|
|
719
|
+
|
|
720
|
+
return Derivative(expr, var, order, is_partial, var2)
|
|
721
|
+
except ParseError:
|
|
722
|
+
raise _Backtrack()
|
|
723
|
+
|
|
724
|
+
def _parse_bound(self) -> Node:
|
|
725
|
+
"""Parse a single integral bound (handles fractions like pi/2)."""
|
|
726
|
+
node = self.parse_postfix()
|
|
727
|
+
if self.match('OP', '/'):
|
|
728
|
+
den = self.parse_postfix()
|
|
729
|
+
node = Fraction(node, den)
|
|
730
|
+
return node
|
|
731
|
+
|
|
732
|
+
@depth_guarded
|
|
733
|
+
def parse_bigop(self) -> Node:
|
|
734
|
+
op_name = self.consume('ID').value
|
|
735
|
+
if op_name == 'integrate':
|
|
736
|
+
op_name = 'integral'
|
|
737
|
+
|
|
738
|
+
self.match('ID', 'of')
|
|
739
|
+
|
|
740
|
+
if op_name in ('integral', 'int', 'iint', 'iiint',
|
|
741
|
+
'oint', 'oiint'):
|
|
742
|
+
lower, upper = None, None
|
|
743
|
+
|
|
744
|
+
if self.match('ID', 'from'):
|
|
745
|
+
lower = self._parse_bound()
|
|
746
|
+
if self.match('ID', 'to'):
|
|
747
|
+
upper = self._parse_bound()
|
|
748
|
+
else:
|
|
749
|
+
while (self.current.type == 'OP'
|
|
750
|
+
and self.current.value in ('_', '^')):
|
|
751
|
+
op = self.consume().value
|
|
752
|
+
old = self.in_bound
|
|
753
|
+
self.in_bound = True
|
|
754
|
+
if op == '_':
|
|
755
|
+
lower = self.parse_unary()
|
|
756
|
+
else:
|
|
757
|
+
upper = self.parse_unary()
|
|
758
|
+
self.in_bound = old
|
|
759
|
+
|
|
760
|
+
try:
|
|
761
|
+
expr = self.parse_additive()
|
|
762
|
+
except ParseError:
|
|
763
|
+
if self.current.type == 'EOF':
|
|
764
|
+
expr = Var('')
|
|
765
|
+
else:
|
|
766
|
+
raise
|
|
767
|
+
|
|
768
|
+
if lower is None and upper is None:
|
|
769
|
+
if self.match('ID', 'from'):
|
|
770
|
+
lower = self._parse_bound()
|
|
771
|
+
if self.match('ID', 'to'):
|
|
772
|
+
upper = self._parse_bound()
|
|
773
|
+
|
|
774
|
+
kind_map = {
|
|
775
|
+
'integral': 'single', 'int': 'single',
|
|
776
|
+
'iint': 'double', 'iiint': 'triple',
|
|
777
|
+
'oint': 'contour', 'oiint': 'double_contour',
|
|
778
|
+
}
|
|
779
|
+
return Integral(expr, lower, upper,
|
|
780
|
+
kind_map.get(op_name, 'single'))
|
|
781
|
+
|
|
782
|
+
elif op_name in ('sum', 'prod'):
|
|
783
|
+
lower, upper = None, None
|
|
784
|
+
|
|
785
|
+
while (self.current.type == 'OP'
|
|
786
|
+
and self.current.value in ('_', '^')):
|
|
787
|
+
op = self.consume().value
|
|
788
|
+
old = self.in_bound
|
|
789
|
+
self.in_bound = True
|
|
790
|
+
if op == '_':
|
|
791
|
+
lower = self.parse_unary()
|
|
792
|
+
else:
|
|
793
|
+
upper = self.parse_unary()
|
|
794
|
+
self.in_bound = old
|
|
795
|
+
|
|
796
|
+
if lower is not None or upper is not None:
|
|
797
|
+
try:
|
|
798
|
+
expr = self.parse_additive()
|
|
799
|
+
except ParseError:
|
|
800
|
+
if self.current.type == 'EOF':
|
|
801
|
+
expr = Var('')
|
|
802
|
+
else:
|
|
803
|
+
raise
|
|
804
|
+
return BigOp(op_name, lower, upper, expr)
|
|
805
|
+
|
|
806
|
+
try:
|
|
807
|
+
expr = self.parse_additive()
|
|
808
|
+
except ParseError:
|
|
809
|
+
if self.current.type == 'EOF':
|
|
810
|
+
expr = Var('')
|
|
811
|
+
else:
|
|
812
|
+
raise
|
|
813
|
+
|
|
814
|
+
if self.match('COMMA'):
|
|
815
|
+
lower = self.parse_relational()
|
|
816
|
+
if self.match('COMMA'):
|
|
817
|
+
upper = self.parse_relational()
|
|
818
|
+
elif self.match('ID', 'to'):
|
|
819
|
+
upper = self.parse_relational()
|
|
820
|
+
elif self.match('ID', 'from'):
|
|
821
|
+
lower = self.parse_relational()
|
|
822
|
+
if self.match('ID', 'to'):
|
|
823
|
+
upper = self.parse_relational()
|
|
824
|
+
|
|
825
|
+
return BigOp(op_name, lower, upper, expr)
|
|
826
|
+
|
|
827
|
+
elif op_name == 'lim':
|
|
828
|
+
lower = None
|
|
829
|
+
|
|
830
|
+
if self.match('OP', '_'):
|
|
831
|
+
lower = self.parse_unary()
|
|
832
|
+
expr = self.parse_multiplicative()
|
|
833
|
+
return BigOp(op_name, lower, None, expr)
|
|
834
|
+
|
|
835
|
+
try:
|
|
836
|
+
expr = self.parse_multiplicative()
|
|
837
|
+
except ParseError:
|
|
838
|
+
expr = Var('')
|
|
839
|
+
|
|
840
|
+
if self.match('ID', 'as'):
|
|
841
|
+
lower = self.parse_relational()
|
|
842
|
+
|
|
843
|
+
return BigOp(op_name, lower, None, expr)
|
|
844
|
+
|
|
845
|
+
lower, upper = None, None
|
|
846
|
+
while (self.current.type == 'OP'
|
|
847
|
+
and self.current.value in ('_', '^')):
|
|
848
|
+
op = self.consume().value
|
|
849
|
+
old = self.in_bound
|
|
850
|
+
self.in_bound = True
|
|
851
|
+
if op == '_':
|
|
852
|
+
lower = self.parse_unary()
|
|
853
|
+
else:
|
|
854
|
+
upper = self.parse_unary()
|
|
855
|
+
self.in_bound = old
|
|
856
|
+
|
|
857
|
+
try:
|
|
858
|
+
expr = self.parse_multiplicative()
|
|
859
|
+
except ParseError:
|
|
860
|
+
if self.current.type == 'EOF':
|
|
861
|
+
expr = Var('')
|
|
862
|
+
else:
|
|
863
|
+
raise
|
|
864
|
+
return BigOp(op_name, lower, upper, expr)
|
|
865
|
+
|
|
866
|
+
def _parse_abs_or_norm(self, kind: str,
|
|
867
|
+
open_tok: str, close_tok: str) -> Node:
|
|
868
|
+
self.consume(open_tok)
|
|
869
|
+
self.in_abs += 1
|
|
870
|
+
expr = self.parse_relational()
|
|
871
|
+
self.in_abs -= 1
|
|
872
|
+
self.consume(close_tok)
|
|
873
|
+
if kind == 'abs':
|
|
874
|
+
return Abs(expr)
|
|
875
|
+
return Norm(expr)
|
|
876
|
+
|
|
877
|
+
def _parse_inner_product(self) -> InnerProduct:
|
|
878
|
+
if not self.match('REL', '<'):
|
|
879
|
+
raise _Backtrack()
|
|
880
|
+
left = self.parse_additive()
|
|
881
|
+
if not self.match('COMMA'):
|
|
882
|
+
raise _Backtrack()
|
|
883
|
+
right = self.parse_additive()
|
|
884
|
+
if not self.match('REL', '>'):
|
|
885
|
+
raise _Backtrack()
|
|
886
|
+
return InnerProduct(left, right)
|
|
887
|
+
|
|
888
|
+
def parse_matrix_bracket(self) -> Node:
|
|
889
|
+
rows: list = []
|
|
890
|
+
if self.match('LBRACK'):
|
|
891
|
+
# Nested format: [[1,2],[3,4]]
|
|
892
|
+
while True:
|
|
893
|
+
row = [self.parse_relational()]
|
|
894
|
+
while self.match('COMMA'):
|
|
895
|
+
row.append(self.parse_relational())
|
|
896
|
+
self.consume('RBRACK')
|
|
897
|
+
rows.append(row)
|
|
898
|
+
if not self.match('COMMA'):
|
|
899
|
+
break
|
|
900
|
+
self.consume('LBRACK')
|
|
901
|
+
self.consume('RBRACK')
|
|
902
|
+
return MatrixNode(rows)
|
|
903
|
+
|
|
904
|
+
else:
|
|
905
|
+
# Flat list: [a, b] or [1,2; 3,4]
|
|
906
|
+
row = [self.parse_relational()]
|
|
907
|
+
while True:
|
|
908
|
+
if self.match('COMMA'):
|
|
909
|
+
row.append(self.parse_relational())
|
|
910
|
+
elif self.match('SEMI'):
|
|
911
|
+
rows.append(row)
|
|
912
|
+
row = [self.parse_relational()]
|
|
913
|
+
else:
|
|
914
|
+
break
|
|
915
|
+
if row:
|
|
916
|
+
rows.append(row)
|
|
917
|
+
self.consume('RBRACK')
|
|
918
|
+
|
|
919
|
+
if len(rows) == 1 and len(rows[0]) == 2:
|
|
920
|
+
return IntervalNode('[', rows[0][0], rows[0][1], ']')
|
|
921
|
+
return MatrixNode(rows)
|
|
922
|
+
|
|
923
|
+
def parse_brace_structure(self) -> Node:
|
|
924
|
+
self.consume('LBRACE')
|
|
925
|
+
|
|
926
|
+
old_in_set = self.in_set_builder
|
|
927
|
+
self.in_set_builder = True
|
|
928
|
+
first = self.parse_relational()
|
|
929
|
+
self.in_set_builder = old_in_set
|
|
930
|
+
|
|
931
|
+
# Set builder: {expr | condition}
|
|
932
|
+
if self.match('BAR'):
|
|
933
|
+
cond = self.parse_relational()
|
|
934
|
+
self.consume('RBRACE')
|
|
935
|
+
return SetBuilder(first, cond)
|
|
936
|
+
|
|
937
|
+
# Piecewise: {expr if cond, expr2 if cond2, ...}
|
|
938
|
+
conditions = []
|
|
939
|
+
is_piecewise = False
|
|
940
|
+
cond = None
|
|
941
|
+
|
|
942
|
+
if self.match('ID', 'if'):
|
|
943
|
+
cond = self.parse_relational()
|
|
944
|
+
is_piecewise = True
|
|
945
|
+
elif (self.current.type == 'ID'
|
|
946
|
+
and self.current.value in ('otherwise', 'else')):
|
|
947
|
+
self.consume('ID')
|
|
948
|
+
cond = Var(r'\text{otherwise}')
|
|
949
|
+
is_piecewise = True
|
|
950
|
+
|
|
951
|
+
if is_piecewise:
|
|
952
|
+
conditions.append((first, cond))
|
|
953
|
+
while self.match('COMMA'):
|
|
954
|
+
expr = self.parse_relational()
|
|
955
|
+
cond = None
|
|
956
|
+
if self.match('ID', 'if'):
|
|
957
|
+
cond = self.parse_relational()
|
|
958
|
+
elif (self.current.type == 'ID'
|
|
959
|
+
and self.current.value in ('otherwise', 'else')):
|
|
960
|
+
self.consume('ID')
|
|
961
|
+
cond = Var(r'\text{otherwise}')
|
|
962
|
+
conditions.append((expr, cond))
|
|
963
|
+
self.consume('RBRACE')
|
|
964
|
+
return PiecewiseNode(conditions)
|
|
965
|
+
|
|
966
|
+
self.consume('RBRACE')
|
|
967
|
+
return first
|