From 3c084b2168b9053cb0a8dce94b3e53fee31c4876 Mon Sep 17 00:00:00 2001 From: Stanislav Date: Fri, 16 Oct 2026 16:43:44 +0400 Subject: [PATCH] Infix, Prefix, Postfix: do not format their first argument as a whole Format[p[t__]] := Infix[p[t], "~"] made printing p[a, b] hit $RecursionLimit. Before boxing, do_format_element formats every subexpression of the result of a format rule, so it formatted p[a, b] inside the Infix again, which gave the same Infix, and so on. Infix, Prefix and Postfix display only the elements of their first argument, never the argument itself, so only those elements are formatted now. This is how WMA handles such definitions, which are the usual way to give an expression an operator form. Co-Authored-By: Claude Opus 5.5 --- mathics/format/box/formatvalues.py | 33 ++++++++++++++++++++++++++---- test/format/test_format.py | 27 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/mathics/format/box/formatvalues.py b/mathics/format/box/formatvalues.py index a52521bd02..262679cc41 100644 --- a/mathics/format/box/formatvalues.py +++ b/mathics/format/box/formatvalues.py @@ -25,7 +25,19 @@ SymbolPlus, SymbolTimes, ) -from mathics.core.systemsymbols import SymbolInputForm, SymbolMinus, SymbolOutputForm +from mathics.core.systemsymbols import ( + SymbolInfix, + SymbolInputForm, + SymbolMinus, + SymbolOutputForm, + SymbolPostfix, + SymbolPrefix, +) + +# Operator forms that display only the elements of their first argument, +# not the argument itself: in Format[f[x__]] := Infix[f[x], "~"], f[x] must +# not be formatted again. +OPERATOR_FORMS = (SymbolInfix, SymbolPostfix, SymbolPrefix) # These Strings are used in Boxing output StringElipsis = String("...") @@ -134,12 +146,25 @@ def format_expr(expr): and isinstance(expr, Expression) and head not in (SymbolGraphics, SymbolGraphics3D) ): + + def format_element(element): + return _element_formatters.get(type(element), do_format_element)( + element, evaluation, form + ) + new_elements = tuple( ( - _element_formatters.get(type(element), do_format_element)( - element, evaluation, form + ( + to_expression_with_specialization( + element.head, + *(format_element(e) for e in element.elements), + ) + if index == 0 + and head in OPERATOR_FORMS + and isinstance(element, Expression) + else format_element(element) ) - for element in expr.elements + for index, element in enumerate(expr.elements) ) ) expr_head = expr.head diff --git a/test/format/test_format.py b/test/format/test_format.py index cad1345fd1..551b498eb4 100644 --- a/test/format/test_format.py +++ b/test/format/test_format.py @@ -268,6 +268,33 @@ def test_format_private_doctests(str_expr, str_expected, msg): None, ), ("r[1]", None, "ab", None), + # Infix, Prefix and Postfix display the elements of their first + # argument, so a format may wrap the expression it formats. + ( + 'Format[pInfix[t__]] := Infix[pInfix[t], "~"]; pInfix[a, b]', + (), + "a ~ b", + "Infix of the formatted expression itself", + ), + ("pInfix[a, pInfix[b, c]] + 1", (), "1 + a ~ b ~ c", "Nested, inside Plus"), + ( + 'Format[pLeft[t__]] := Infix[pLeft[t], "~", 300, Left]; pLeft[pLeft[a, b], c]', + (), + "a ~ b ~ c", + "With precedence and grouping", + ), + ( + 'Format[pPre[x_]] := Prefix[pPre[x], "!"]; pPre[a]', + (), + "!a", + "Prefix of the formatted expression itself", + ), + ( + 'Format[pPost[x_]] := Postfix[pPost[x], "!!"]; pPost[a]', + (), + "a!!", + "Postfix of the formatted expression itself", + ), (None, None, None, None), ], )