Skip to content

Commit c5a9227

Browse files
authored
Improving precedence comparison. (#1154)
This PR includes some fixes and improvings for handling operator precedence. In particular, as in WMA, avoids wrapping in parenthesis the second element of `Rule[pat_, Function[...]]`.
1 parent e4bb914 commit c5a9227

File tree

8 files changed

+78
-57
lines changed

8 files changed

+78
-57
lines changed

mathics/builtin/atomic/symbols.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ def lhs(expr):
527527
return Expression(SymbolFormat, expr, Symbol(format))
528528

529529
def rhs(expr):
530-
if expr.has_formf(SymbolInfix, None):
530+
if expr.has_form(SymbolInfix, None):
531531
expr = Expression(
532532
Expression(SymbolHoldForm, expr.head), *expr.elements
533533
)

mathics/builtin/layout.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from mathics.builtin.makeboxes import MakeBoxes
1717
from mathics.builtin.options import options_to_rules
1818
from mathics.core.atoms import Real, String
19-
from mathics.core.builtin import BinaryOperator, Builtin, Operator
19+
from mathics.core.builtin import Builtin, Operator, PostfixOperator, PrefixOperator
2020
from mathics.core.expression import Evaluation, Expression
2121
from mathics.core.list import ListExpression
2222
from mathics.core.symbols import Symbol
@@ -214,7 +214,7 @@ class NonAssociative(Builtin):
214214
summary_text = "non-associative operator"
215215

216216

217-
class Postfix(BinaryOperator):
217+
class Postfix(PostfixOperator):
218218
"""
219219
<url>:WMA link:https://reference.wolfram.com/language/ref/Postfix.html</url>
220220
@@ -294,7 +294,7 @@ class PrecedenceForm(Builtin):
294294
summary_text = "parenthesize with a precedence"
295295

296296

297-
class Prefix(BinaryOperator):
297+
class Prefix(PrefixOperator):
298298
"""
299299
<url>:WMA link:https://reference.wolfram.com/language/ref/Prefix.html</url>
300300

mathics/builtin/numbers/diffeqns.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ class DSolve(Builtin):
3131
= {{y[x] -> C[1] E ^ (-x) + C[2] E ^ x}}
3232
3333
>> DSolve[y''[x] == y[x], y, x]
34-
= {{y -> (Function[{x}, C[1] E ^ (-x) + C[2] E ^ x])}}
34+
= {{y -> Function[{x}, C[1] E ^ (-x) + C[2] E ^ x]}}
3535
3636
DSolve can also solve basic PDE
3737
>> DSolve[D[f[x, y], x] / f[x, y] + 3 D[f[x, y], y] / f[x, y] == 2, f, {x, y}]
38-
= {{f -> (Function[{x, y}, E ^ (x / 5 + 3 y / 5) C[1][3 x - y]])}}
38+
= {{f -> Function[{x, y}, E ^ (x / 5 + 3 y / 5) C[1][3 x - y]]}}
3939
4040
>> DSolve[D[f[x, y], x] x + D[f[x, y], y] y == 2, f[x, y], {x, y}]
4141
= {{f[x, y] -> 2 Log[x] + C[1][y / x]}}

mathics/builtin/patterns/rules.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,11 +503,11 @@ class Rule_(BinaryOperator):
503503
= a
504504
"""
505505

506-
name = "Rule"
507-
operator = "->"
508506
attributes = A_SEQUENCE_HOLD | A_PROTECTED
509507
grouping = "Right"
508+
name = "Rule"
510509
needs_verbatim = True
510+
operator = "->"
511511
summary_text = "a replacement rule"
512512

513513
def eval_rule(self, elems, evaluation):

mathics/builtin/recurrence.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,18 @@ class RSolve(Builtin):
4040
4141
No boundary conditions gives two general parameters:
4242
>> RSolve[{a[n + 2] == a[n]}, a, n]
43-
= {{a -> (Function[{n}, C[0] + C[1] (-1) ^ n])}}
43+
= {{a -> Function[{n}, C[0] + C[1] (-1) ^ n]}}
4444
4545
Include one boundary condition:
4646
>> RSolve[{a[n + 2] == a[n], a[0] == 1}, a, n]
4747
= ...
4848
## Order of terms depends on interpreter:
49-
## PyPy: {{a -> (Function[{n}, 1 - C[1] + C[1] -1 ^ n])}}
50-
## CPython: {{a -> (Function[{n}, 1 + C[1] -1 ^ n - C[1]])}
49+
## PyPy: {{a -> Function[{n}, 1 - C[1] + C[1] -1 ^ n]}}
50+
## CPython: {{a -> Function[{n}, 1 + C[1] -1 ^ n - C[1]]}
5151
52-
Geta "pure function" solution for a with two boundary conditions:
52+
Get a "pure function" solution for a with two boundary conditions:
5353
>> RSolve[{a[n + 2] == a[n], a[0] == 1, a[1] == 4}, a, n]
54-
= {{a -> (Function[{n}, 5 / 2 - 3 (-1) ^ n / 2])}}
54+
= {{a -> Function[{n}, 5 / 2 - 3 (-1) ^ n / 2]}}
5555
"""
5656

5757
messages = {

mathics/core/builtin.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,22 +1151,29 @@ def __init__(self, *args, **kwargs):
11511151

11521152
# Prevent pattern matching symbols from gaining meaning here using
11531153
# Verbatim
1154-
name = f"Verbatim[{name}]"
1154+
verbatim_name = f"Verbatim[{name}]"
11551155

11561156
# For compatibility, allow grouping symbols in builtins to be
11571157
# specified without System`.
11581158
self.grouping = ensure_context(self.grouping)
11591159

11601160
if self.grouping in ("System`None", "System`NonAssociative"):
1161-
op_pattern = f"{name}[items__]"
1161+
op_pattern = f"{verbatim_name}[items__]"
11621162
replace_items = "items"
11631163
else:
1164-
op_pattern = f"{name}[x_, y_]"
1164+
op_pattern = f"{verbatim_name}[x_, y_]"
11651165
replace_items = "x, y"
11661166

11671167
operator = ascii_operator_to_symbol.get(self.operator, self.__class__.__name__)
11681168

11691169
if self.default_formats:
1170+
if name not in ("Rule", "RuleDelayed"):
1171+
formats = {
1172+
op_pattern: "HoldForm[Infix[{%s}, %s, %d, %s]]"
1173+
% (replace_items, operator, self.precedence, self.grouping)
1174+
}
1175+
formats.update(self.formats)
1176+
self.formats = formats
11701177
formatted = "MakeBoxes[Infix[{%s}, %s, %d,%s], form]" % (
11711178
replace_items,
11721179
operator,

mathics/eval/makeboxes.py

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
"""
77

88

9-
import typing
10-
from typing import Any, Callable, Dict, Optional, Type
9+
from typing import Any, Callable, Dict, List, Optional, Type
1110

1211
from mathics.core.atoms import Complex, Integer, Rational, Real, String, SymbolI
1312
from mathics.core.convert.expression import to_expression_with_specialization
@@ -70,6 +69,35 @@ def _boxed_string(string: str, **options):
7069
return StyleBox(String(string), **options)
7170

7271

72+
def compare_precedence(
73+
element: BaseElement, precedence: Optional[int] = None
74+
) -> Optional[int]:
75+
"""
76+
compare the precedence of the element regarding a precedence value.
77+
If both precedences are equal, return 0. If precedence of the
78+
first element is higher, return 1, otherwise -1.
79+
If precedences cannot be compared, return None.
80+
"""
81+
while element.has_form("HoldForm", 1):
82+
element = element.elements[0]
83+
84+
if precedence is None:
85+
return None
86+
if element.has_form(("Infix", "Prefix", "Postfix"), 3, None):
87+
element_prec = element.elements[2].value
88+
elif element.has_form("PrecedenceForm", 2):
89+
element_prec = element.elements[1].value
90+
# For negative values, ensure that the element_precedence is at least the precedence. (Fixes #332)
91+
elif isinstance(element, (Integer, Real)) and element.value < 0:
92+
element_prec = precedence
93+
else:
94+
element_prec = builtins_precedence.get(element.get_head_name())
95+
96+
if element_prec is None:
97+
return None
98+
return 0 if element_prec == precedence else (1 if element_prec > precedence else -1)
99+
100+
73101
# 640 = sys.int_info.str_digits_check_threshold.
74102
# Someday when 3.11 is the minimum version of Python supported,
75103
# we can replace the magic value 640 below with sys.int.str_digits_check_threshold.
@@ -211,7 +239,6 @@ def do_format_element(
211239
Applies formats associated to the expression and removes
212240
superfluous enclosing formats.
213241
"""
214-
215242
from mathics.core.definitions import OutputForms
216243

217244
evaluation.inc_recursion_depth()
@@ -234,6 +261,7 @@ def do_format_element(
234261
if include_form:
235262
expr = Expression(form, expr)
236263
return expr
264+
237265
# Repeated and RepeatedNull confuse the formatter,
238266
# so we need to hardlink their format rules:
239267
if head is SymbolRepeated:
@@ -279,8 +307,8 @@ def format_expr(expr):
279307

280308
formatted = format_expr(expr) if isinstance(expr, EvalMixin) else None
281309
if formatted is not None:
282-
do_format = element_formatters.get(type(formatted), do_format_element)
283-
result = do_format(formatted, evaluation, form)
310+
do_format_fn = element_formatters.get(type(formatted), do_format_element)
311+
result = do_format_fn(formatted, evaluation, form)
284312
if include_form and result is not None:
285313
result = Expression(form, result)
286314
return result
@@ -297,8 +325,8 @@ def format_expr(expr):
297325
# just return it as it is.
298326
if len(expr.get_elements()) != 1:
299327
return expr
300-
do_format = element_formatters.get(type(element), do_format_element)
301-
result = do_format(expr, evaluation, form)
328+
do_format_fn = element_formatters.get(type(element), do_format_element)
329+
result = do_format_fn(expr, evaluation, form)
302330
if isinstance(result, Expression):
303331
expr = result
304332

@@ -307,13 +335,14 @@ def format_expr(expr):
307335
and not isinstance(expr, (Atom, BoxElementMixin))
308336
and head not in (SymbolGraphics, SymbolGraphics3D)
309337
):
310-
# print("Not inside graphics or numberform, and not is atom")
311-
new_elements = [
312-
element_formatters.get(type(element), do_format_element)(
313-
element, evaluation, form
338+
new_elements = tuple(
339+
(
340+
element_formatters.get(type(element), do_format_element)(
341+
element, evaluation, form
342+
)
343+
for element in expr.elements
314344
)
315-
for element in expr.elements
316-
]
345+
)
317346
expr_head = expr.head
318347
do_format = element_formatters.get(type(expr_head), do_format_element)
319348
head = do_format(expr_head, evaluation, form)
@@ -367,7 +396,7 @@ def do_format_complex(
367396
form,
368397
)
369398

370-
parts: typing.List[Any] = []
399+
parts: List[Any] = []
371400
if element.is_machine_precision() or not element.real.is_zero:
372401
parts.append(element.real)
373402
if element.imag.sameQ(Integer(1)):
@@ -418,27 +447,12 @@ def parenthesize(
418447
If when_equal is True, parentheses will be added if the two
419448
precedence values are equal.
420449
"""
421-
while element.has_form("HoldForm", 1):
422-
element = element.elements[0]
423-
424-
if element.has_form(("Infix", "Prefix", "Postfix"), 3, None):
425-
element_prec = element.elements[2].value
426-
elif element.has_form("PrecedenceForm", 2):
427-
element_prec = element.elements[1].value
428-
# If "element" is a negative number, we need to parenthesize the number. (Fixes #332)
429-
elif isinstance(element, (Integer, Real)) and element.value < 0:
430-
# Force parenthesis by adjusting the surrounding context's precedence value,
431-
# We can't change the precedence for the number since it, doesn't
432-
# have a precedence value.
433-
element_prec = precedence
434-
else:
435-
element_prec = builtins_precedence.get(element.get_head())
436-
if precedence is not None and element_prec is not None:
437-
if precedence > element_prec or (precedence == element_prec and when_equal):
438-
return Expression(
439-
SymbolRowBox,
440-
ListExpression(StringLParen, element_boxes, StringRParen),
441-
)
450+
cmp = compare_precedence(element, precedence)
451+
if cmp is not None and (cmp == -1 or cmp == 0 and when_equal):
452+
return Expression(
453+
SymbolRowBox,
454+
ListExpression(String("("), element_boxes, String(")")),
455+
)
442456
return element_boxes
443457

444458

test/builtin/numbers/test_diffeqns.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,19 @@
4545
(
4646
"DSolve[f'[x] == f[x], f, x] /. {C[1] -> 1}",
4747
None,
48-
"{{f -> (Function[{x}, 1 E ^ x])}}",
48+
"{{f -> Function[{x}, 1 E ^ x]}}",
4949
None,
5050
),
5151
(
5252
"DSolve[f'[x] == f[x], f, x] /. {C -> D}",
5353
None,
54-
"{{f -> (Function[{x}, D[1] E ^ x])}}",
54+
"{{f -> Function[{x}, D[1] E ^ x]}}",
5555
None,
5656
),
5757
(
5858
"DSolve[f'[x] == f[x], f, x] /. {C[1] -> C[0]}",
5959
None,
60-
"{{f -> (Function[{x}, C[0] E ^ x])}}",
60+
"{{f -> Function[{x}, C[0] E ^ x]}}",
6161
None,
6262
),
6363
(
@@ -66,11 +66,11 @@
6666
"DSolve[f[x] == 0, f, {}]",
6767
None,
6868
),
69-
## Order of arguments shoudn't matter
69+
# # Order of arguments shoudn't matter
7070
(
7171
"DSolve[D[f[x, y], x] == D[f[x, y], y], f, {x, y}]",
7272
None,
73-
"{{f -> (Function[{x, y}, C[1][-x - y]])}}",
73+
"{{f -> Function[{x, y}, C[1][-x - y]]}}",
7474
None,
7575
),
7676
(
@@ -88,7 +88,7 @@
8888
(
8989
"DSolve[\\[Gamma]'[x] == 0, \\[Gamma], x]",
9090
None,
91-
"{{γ -> (Function[{x}, C[1]])}}",
91+
"{{γ -> Function[{x}, C[1]]}}",
9292
"sympy #11669 test",
9393
),
9494
],

0 commit comments

Comments
 (0)