From a35983e8dea62594993c397c77db70088adb9424 Mon Sep 17 00:00:00 2001 From: mmatera Date: Sun, 10 Nov 2024 20:11:31 -0300 Subject: [PATCH 01/49] formatting OutputForm outputform and fullform following the WMA formatting steps --- mathics/builtin/box/layout.py | 27 + mathics/builtin/forms/output.py | 6 + mathics/core/atoms.py | 5 +- mathics/eval/makeboxes/__init__.py | 9 +- mathics/eval/makeboxes/makeboxes.py | 35 +- mathics/format/latex.py | 12 + mathics/format/mathml.py | 12 + mathics/format/outputform.py | 779 ++++++++++++++++++++++++++++ mathics/format/text.py | 26 +- 9 files changed, 900 insertions(+), 11 deletions(-) create mode 100644 mathics/format/outputform.py diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index 94fb41c85..e26d45447 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -188,6 +188,33 @@ def eval_display(boxexpr, evaluation): return boxexpr.elements[0] +class PaneBox(BoxExpression): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/InterpretationBox.html + +
+
'PaneBox[expr]' +
is a low-level box construct, used in OutputForm. +
+ + """ + + attributes = A_HOLD_ALL_COMPLETE | A_PROTECTED | A_READ_PROTECTED + summary_text = "box associated to panel" + + def apply_display_form(boxexpr, form, evaluation, expression): + """ToExpression[boxexpr_PaneBox, form_]""" + return Expression(expression.head, boxexpr.elements[0], form).evaluate( + evaluation + ) + + def apply_display(boxexpr, evaluation): + """DisplayForm[boxexpr_PaneBox]""" + return boxexpr.elements[0] + + class RowBox(BoxExpression): """ diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index 7ec5d1d48..a6ea6ea37 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -37,6 +37,7 @@ StringLParen, StringRParen, eval_baseform, + eval_makeboxes_outputform, eval_mathmlform, eval_tableform, eval_texform, @@ -490,8 +491,13 @@ class OutputForm(FormBaseClass): = -Graphics- """ + formats = {"OutputForm[s_String]": "s"} summary_text = "plain-text output format" + def eval_makeboxes(self, expr, form, evaluation): + """MakeBoxes[OutputForm[expr_], form_]""" + return eval_makeboxes_outputform(expr, evaluation, form) + class PythonForm(FormBaseClass): """ diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py index d6e542a60..e2b37e4c9 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -1016,10 +1016,13 @@ def __str__(self) -> str: return '"%s"' % self.value def atom_to_boxes(self, f, evaluation): + return self.make_boxes(f.get_name()) + + def make_boxes(self, f): from mathics.eval.makeboxes import _boxed_string inner = str(self.value) - if f in SYSTEM_SYMBOLS_INPUT_OR_FULL_FORM: + if f in ("System`InputForm", "System`FullForm"): inner = '"' + inner.replace("\\", "\\\\") + '"' return _boxed_string(inner, **{"System`ShowStringCharacters": SymbolTrue}) return String('"' + inner + '"') diff --git a/mathics/eval/makeboxes/__init__.py b/mathics/eval/makeboxes/__init__.py index a6cb2a6e7..ec20abf22 100644 --- a/mathics/eval/makeboxes/__init__.py +++ b/mathics/eval/makeboxes/__init__.py @@ -7,6 +7,7 @@ _boxed_string, eval_generic_makeboxes, eval_makeboxes, + eval_makeboxes_outputform, format_element, int_to_string_shorter_repr, to_boxes, @@ -18,7 +19,11 @@ eval_tableform, eval_texform, ) -from mathics.eval.makeboxes.precedence import builtins_precedence, parenthesize +from mathics.eval.makeboxes.precedence import ( + builtins_precedence, + compare_precedence, + parenthesize, +) __all__ = [ "NumberForm_to_String", @@ -26,11 +31,13 @@ "StringRParen", "_boxed_string", "builtins_precedence", + "compare_precedence", "do_format", "eval_baseform", "eval_generic_makeboxes", "eval_infix", "eval_makeboxes", + "eval_makeboxes_outputform", "eval_mathmlform", "eval_postprefix", "eval_tableform", diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 2d7275a2e..66b8c6b4d 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -13,7 +13,7 @@ from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import Atom, Symbol, SymbolFullForm, SymbolMakeBoxes -from mathics.core.systemsymbols import SymbolStandardForm +from mathics.core.systemsymbols import SymbolOutputForm, SymbolStandardForm from mathics.eval.makeboxes.formatvalues import do_format from mathics.eval.makeboxes.precedence import parenthesize @@ -126,8 +126,21 @@ def int_to_string_shorter_repr(value: int, form: Symbol, max_digits=640): return String(value_str) +def eval_makeboxes_outputform(expr, evaluation, form): + """ + Build a 2D text representation of the expression. + """ + from mathics.builtin.box.layout import InterpretationBox, PaneBox + from mathics.format.outputform import expression_to_outputform_text + + text_outputform = str(expression_to_outputform_text(expr, evaluation, form)) + elem1 = PaneBox(String(text_outputform)) + elem2 = Expression(SymbolOutputForm, expr) + return InterpretationBox(elem1, elem2) + + def eval_fullform_makeboxes( - self, expr, evaluation: Evaluation, form=SymbolStandardForm + expr, evaluation: Evaluation, form=SymbolStandardForm ) -> Optional[BaseElement]: """ This function takes the definitions provided by the evaluation @@ -220,9 +233,27 @@ def format_element( Applies formats associated to the expression, and then calls Makeboxes """ evaluation.is_boxing = True + while element.get_head() is form: + element = element.elements[0] + + if element.has_form("FullForm", 1): + return eval_fullform_makeboxes(element.elements[0], evaluation) + + # In order to work like in WMA, `format_element` + # should evaluate `MakeBoxes[element//form, StandardForm]` + # Then, MakeBoxes[expr_, StandardForm], for any expr, + # should apply Format[...] rules, and then + # MakeBoxes[...] rules. These rules should be stored + # as FormatValues[...] + # As a first step in that direction, let's mimic this behaviour + # just for the case of OutputForm: + if element.has_form("OutputForm", 1): + return eval_makeboxes_outputform(element.elements[0], evaluation, form) + expr = do_format(element, evaluation, form) if expr is None: return None + result = Expression(SymbolMakeBoxes, expr, form) result_box = result.evaluate(evaluation) if isinstance(result_box, String): diff --git a/mathics/format/latex.py b/mathics/format/latex.py index 707e6ec41..eda0ca037 100644 --- a/mathics/format/latex.py +++ b/mathics/format/latex.py @@ -19,6 +19,8 @@ from mathics.builtin.box.layout import ( FractionBox, GridBox, + InterpretationBox, + PaneBox, RowBox, SqrtBox, StyleBox, @@ -133,6 +135,16 @@ def render(format, string, in_text=False): add_conversion_fn(String, string) +def interpretation_panebox(self, **options): + return lookup_conversion_method(self.elements[0], "latex")( + self.elements[0], **options + ) + + +add_conversion_fn(InterpretationBox, interpretation_panebox) +add_conversion_fn(PaneBox, interpretation_panebox) + + def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) diff --git a/mathics/format/mathml.py b/mathics/format/mathml.py index 7bbdd65da..d8ac94922 100644 --- a/mathics/format/mathml.py +++ b/mathics/format/mathml.py @@ -15,6 +15,8 @@ from mathics.builtin.box.layout import ( FractionBox, GridBox, + InterpretationBox, + PaneBox, RowBox, SqrtBox, StyleBox, @@ -110,6 +112,16 @@ def render(format, string): add_conversion_fn(String, string) +def interpretation_panebox(self, **options): + return lookup_conversion_method(self.elements[0], "latex")( + self.elements[0], **options + ) + + +add_conversion_fn(InterpretationBox, interpretation_panebox) +add_conversion_fn(PaneBox, interpretation_panebox) + + def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) diff --git a/mathics/format/outputform.py b/mathics/format/outputform.py new file mode 100644 index 000000000..0fc8309ec --- /dev/null +++ b/mathics/format/outputform.py @@ -0,0 +1,779 @@ +""" +This module builts the 2D string associated to the OutputForm +""" + +from typing import Callable, Dict, List, Union + +from mathics.core.atoms import ( + Integer, + Integer1, + Integer2, + IntegerM1, + Rational, + Real, + String, +) +from mathics.core.element import BaseElement +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Atom, Symbol, SymbolTimes +from mathics.core.systemsymbols import ( + SymbolDerivative, + SymbolInfix, + SymbolNone, + SymbolOutputForm, + SymbolPower, + SymbolStandardForm, + SymbolTraditionalForm, +) +from mathics.eval.makeboxes import compare_precedence, do_format # , format_element + +SymbolNonAssociative = Symbol("System`NonAssociative") +SymbolPostfix = Symbol("System`Postfix") +SymbolPrefix = Symbol("System`Prefix") +SymbolRight = Symbol("System`Right") +SymbolLeft = Symbol("System`Left") + + +expr_to_outputform_text_map: Dict[str, Callable] = {} + + +# This Exception if the expression should +# be processed by the default routine +class _WrongFormattedExpression(Exception): + pass + + +class IsNotGrid(Exception): + pass + + +class IsNot2DArray(Exception): + pass + + +def parenthesize(expr_str: str) -> str: + """wrap with parenthesis""" + return f"({expr_str})" + + +def bracket(expr_str: str) -> str: + """wrap with square brackets""" + return f"[{expr_str}]" + + +def grid(expr): + raise NotImplementedError + + +def expression_to_outputform_text( + expr: BaseElement, evaluation: Evaluation, form=SymbolStandardForm, **kwargs +): + """ + Build a 2d text from an `Expression` + """ + ## TODO: format the expression + format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore + + # Strip HoldForm + while format_expr.has_form("HoldForm", 1): # type: ignore + format_expr = format_expr.elements[0] + + lookup_name = format_expr.get_head().get_lookup_name() + try: + result = expr_to_outputform_text_map[lookup_name]( + format_expr, evaluation, form, **kwargs + ) + return result + except _WrongFormattedExpression: + # If the key is not present, or the execution fails for any reason, use + # the default + pass + except KeyError: + pass + return _default_expression_to_outputform_text( + format_expr, evaluation, form, **kwargs + ) + + +def _default_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + """ + Default representation of a function + """ + expr_head = expr.head + head = expression_to_outputform_text(expr_head, evaluation, form, **kwargs) + comma = ", " + elements = [ + expression_to_outputform_text(elem, evaluation) for elem in expr.elements + ] + result = elements.pop(0) if elements else " " + while elements: + result = result + comma + elements.pop(0) + + if form is SymbolTraditionalForm: + return head + parenthesize(result) + return head + bracket(result) + + +def _divide(num, den, evaluation, form, **kwargs): + infix_form = Expression( + SymbolInfix, ListExpression(num, den), String("/"), Integer(400), SymbolLeft + ) + return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) + + +def _strip_1_parm_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + if len(expr.elements) != 1: + raise _WrongFormattedExpression + return expression_to_outputform_text(expr.elements[0], evaluation, form, **kwargs) + + +expr_to_outputform_text_map[ + "System`HoldForm" +] = _strip_1_parm_expression_to_outputform_text +expr_to_outputform_text_map[ + "System`InputForm" +] = _strip_1_parm_expression_to_outputform_text + + +def derivative_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + """Derivative operator""" + head = expr.get_head() + if head is SymbolDerivative: + return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + super_head = head.get_head() + if super_head is SymbolDerivative: + expr_elements = expr.elements + if len(expr_elements) != 1: + return _default_expression_to_outputform_text( + expr, evaluation, form, **kwargs + ) + function_head = expression_to_outputform_text( + expr_elements[0], evaluation, form, **kwargs + ) + derivatives = head.elements + if len(derivatives) == 1: + order_iv = derivatives[0] + if order_iv == Integer1: + return function_head + "'" + elif order_iv == Integer2: + return function_head + "''" + + return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + + # Full Function with arguments: delegate to the default conversion. + # It will call us again with the head + return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + + +expr_to_outputform_text_map[ + "System`Derivative" +] = derivative_expression_to_outputform_text + + +def divide_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + if len(expr.elements) != 2: + raise _WrongFormattedExpression + num, den = expr.elements + return _divide(num, den, evaluation, form, **kwargs) + + +expr_to_outputform_text_map["System`Divide"] = divide_expression_to_outputform_text + + +def graphics(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: + return "-Graphics-" + + +expr_to_outputform_text_map["System`Graphics"] = graphics + + +def graphics3d(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: + return "-Graphics3D-" + + +expr_to_outputform_text_map["System`Graphics3D"] = graphics3d + + +def grid_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + if len(expr.elements) == 0: + raise IsNotGrid + if len(expr.elements) > 1 and not expr.elements[1].has_form( + ["Rule", "RuleDelayed"], 2 + ): + raise IsNotGrid + if not expr.elements[0].has_form("List", None): + raise IsNotGrid + + elements = expr.elements[0].elements + rows = [] + for idx, item in enumerate(elements): + if item.has_form("List", None): + rows.append( + [ + expression_to_outputform_text(item_elem, evaluation, form, **kwargs) + for item_elem in item.elements + ] + ) + else: + rows.append(expression_to_outputform_text(item, evaluation, form, **kwargs)) + + return grid(rows) + + +expr_to_outputform_text_map["System`Grid"] = grid_expression_to_outputform_text + + +def integer_expression_to_outputform_text( + n: Integer, evaluation: Evaluation, form: Symbol, **kwargs +): + return str(n.value) + + +expr_to_outputform_text_map["System`Integer"] = integer_expression_to_outputform_text + + +def list_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + result, *rest_elems = ( + expression_to_outputform_text(elem, evaluation, form, **kwargs) + for elem in expr.elements + ) + comma_tb = ", " + for next_elem in rest_elems: + result = result + comma_tb + next_elem + return "{" + result + "}" + + +expr_to_outputform_text_map["System`List"] = list_expression_to_outputform_text + + +def mathmlform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + # boxes = format_element(expr.elements[0], evaluation, form) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + ).evaluate(evaluation) + return boxes.boxes_to_mathml() # type: ignore[union-attr] + + +expr_to_outputform_text_map[ + "System`MathMLForm" +] = mathmlform_expression_to_outputform_text + + +def matrixform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + # return parenthesize(tableform_expression_to_outputform_text(expr, evaluation, form, **kwargs)) + return tableform_expression_to_outputform_text(expr, evaluation, form, **kwargs) + + +expr_to_outputform_text_map[ + "System`MatrixForm" +] = matrixform_expression_to_outputform_text + + +def plus_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + elements = expr.elements + result = "" + for i, elem in enumerate(elements): + if elem.has_form("Times", None): + # If the first element is -1, remove it and use + # a minus sign. Otherwise, if negative, do not add a sign. + first = elem.elements[0] + if isinstance(first, Integer): + if first.value == -1: + result = ( + result + + " - " + + expression_to_outputform_text( + Expression(SymbolTimes, *elem.elements[1:]), + evaluation, + form, + **kwargs, + ) + ) + continue + elif first.value < 0: + result = ( + result + + " " + + expression_to_outputform_text( + elem, evaluation, form, **kwargs + ) + ) + continue + elif isinstance(first, Real): + if first.value < 0: + result = ( + result + + " " + + expression_to_outputform_text( + elem, evaluation, form, **kwargs + ) + ) + continue + result = ( + result + + " + " + + expression_to_outputform_text(elem, evaluation, form, **kwargs) + ) + ## TODO: handle complex numbers? + else: + elem_txt = expression_to_outputform_text(elem, evaluation, form, **kwargs) + if (compare_precedence(elem, 310) or -1) < 0: + elem_txt = parenthesize(elem_txt) + result = result + " + " + elem_txt + elif i == 0 or ( + (isinstance(elem, Integer) and elem.value < 0) + or (isinstance(elem, Real) and elem.value < 0) + ): + result = result + elem_txt + else: + result = ( + result + + " + " + + expression_to_outputform_text(elem, evaluation, form, **kwargs) + ) + return result + + +expr_to_outputform_text_map["System`Plus"] = plus_expression_to_outputform_text + + +def power_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +): + if len(expr.elements) != 2: + raise _WrongFormattedExpression + + infix_form = Expression( + SymbolInfix, + ListExpression(*(expr.elements)), + String("^"), + Integer(590), + SymbolRight, + ) + return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) + + +expr_to_outputform_text_map["System`Power"] = power_expression_to_outputform_text + + +def pre_pos_fix_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + elements = expr.elements + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + group = None + precedence = 670 + # Processing the first argument: + head = expr.get_head() + target = expr.elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + operands = list(target.elements) + if len(operands) != 1: + raise _WrongFormattedExpression + + # Processing the second argument, if it is there: + if len(elements) > 1: + ops = elements[1] + ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolPrefix: + default_symb = " @ " + ops_txt = ( + expression_to_outputform_text(head, evaluation, form, **kwargs) + + default_symb + ) + elif head is SymbolPostfix: + default_symb = " // " + ops_txt = default_symb + expression_to_outputform_text( + head, evaluation, form, **kwargs + ) + + # Processing the third argument, if it is there: + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: + raise _WrongFormattedExpression + + # Processing the forth argument, if it is there: + if len(elements) > 3: + group = elements[3] + if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): + raise _WrongFormattedExpression + if group is SymbolNone: + group = None + + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + + return ops_txt[0] + target_txt if head is SymbolPrefix else target_txt + ops_txt[0] + + +expr_to_outputform_text_map["System`Prefix"] = pre_pos_fix_expression_to_outputform_text +expr_to_outputform_text_map[ + "System`Postfix" +] = pre_pos_fix_expression_to_outputform_text + + +def infix_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + elements = expr.elements + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + group = None + precedence = 670 + # Processing the first argument: + head = expr.get_head() + target = expr.elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + operands = list(target.elements) + + if len(operands) < 2: + raise _WrongFormattedExpression + + # Processing the second argument, if it is there: + if len(elements) > 1: + ops = elements[1] + if head is SymbolInfix: + # This is not the WMA behaviour, but the Mathics current implementation requires it: + num_ops = 1 + if ops.has_form("List", None): + num_ops = len(ops.elements) + ops_lst = [ + expression_to_outputform_text(op, evaluation, form, **kwargs) + for op in ops.elements + ] + else: + ops_lst = [ + expression_to_outputform_text(ops, evaluation, form, **kwargs) + ] + elif head in (SymbolPrefix, SymbolPostfix): + ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolInfix: + num_ops = 1 + default_symb = " ~ " + ops_lst = [ + default_symb + + expression_to_outputform_text(head, evaluation, form, **kwargs) + + default_symb + ] + elif head is SymbolPrefix: + default_symb = " @ " + ops_txt = ( + expression_to_outputform_text(head, evaluation, form, **kwargs) + + default_symb + ) + elif head is SymbolPostfix: + default_symb = " // " + ops_txt = default_symb + expression_to_outputform_text( + head, evaluation, form, **kwargs + ) + + # Processing the third argument, if it is there: + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: + raise _WrongFormattedExpression + + # Processing the forth argument, if it is there: + if len(elements) > 3: + group = elements[3] + if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): + raise _WrongFormattedExpression + if group is SymbolNone: + group = None + + if head is SymbolPrefix: + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + return ops_txt[0] + target_txt + if head is SymbolPostfix: + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + return target_txt + ops_txt[0] + else: # Infix + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + for index, operand in enumerate(operands): + operand_txt = str( + expression_to_outputform_text(operand, evaluation, form, **kwargs) + ) + cmp_precedence = compare_precedence(operand, precedence) + if cmp_precedence is not None and ( + cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) + ): + operand_txt = parenthesize(operand_txt) + + if index == 0: + result = operand_txt + # After the first element, for lateral + # associativity, parenthesized is flipped: + if group in (SymbolLeft, SymbolRight): + parenthesized = not parenthesized + else: + space = " " + result_lst: List[str] + if str(ops_lst[index % num_ops]) != " ": + result_lst = [ + result, + space, + str(ops_lst[index % num_ops]), + space, + operand_txt, + ] + else: + result_lst = [result, space, operand_txt] + + return "".join(result_lst) + + +expr_to_outputform_text_map["System`Infix"] = infix_expression_to_outputform_text + + +def precedenceform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + if len(expr.elements) == 2: + return expression_to_outputform_text( + expr.elements[0], evaluation, form, **kwargs + ) + raise _WrongFormattedExpression + + +expr_to_outputform_text_map[ + "System`PrecedenceForm" +] = precedenceform_expression_to_outputform_text + + +def rational_expression_to_outputform_text( + n: Union[Rational, Expression], evaluation: Evaluation, form: Symbol, **kwargs +): + if n.has_form("Rational", 2): + num, den = n.elements # type: ignore[union-attr] + else: + num, den = n.numerator(), n.denominator() # type: ignore[union-attr] + return _divide(num, den, evaluation, form, **kwargs) + + +expr_to_outputform_text_map["System`Rational"] = rational_expression_to_outputform_text + + +def real_expression_to_outputform_text( + n: Real, evaluation: Evaluation, form: Symbol, **kwargs +): + str_n = n.make_boxes("System`OutputForm").boxes_to_text() # type: ignore[attr-defined] + return str(str_n) + + +expr_to_outputform_text_map["System`Real"] = real_expression_to_outputform_text + + +def string_expression_to_outputform_text( + expr: String, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + lines = expr.value.split("\n") + max_len = max([len(line) for line in lines]) + lines = [line + (max_len - len(line)) * " " for line in lines] + return "\n".join(lines) + + +expr_to_outputform_text_map["System`String"] = string_expression_to_outputform_text + + +def stringform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + strform = expr.elements[0] + if not isinstance(strform, String): + raise _WrongFormattedExpression + + items = list( + expression_to_outputform_text(item, evaluation, form, **kwargs) + for item in expr.elements[1:] + ) + + curr_indx = 0 + parts = strform.value.split("`") + result = str(parts[0]) + if len(parts) == 1: + return result + + quote_open = True + remaining = len(parts) - 1 + + for part in parts[1:]: + remaining -= 1 + if quote_open: + if remaining == 0: + result = result + "`" + part + quote_open = False + continue + if len(part) == 0: + result = result + items[curr_indx] + continue + try: + idx = int(part) + except ValueError: + idx = None + if idx is not None and str(idx) == part: + curr_indx = idx - 1 + result = result + items[curr_indx] + quote_open = False + continue + else: + result = result + "`" + part + "`" + quote_open = False + continue + else: + result = result + part + quote_open = True + + return result + + +expr_to_outputform_text_map[ + "System`StringForm" +] = stringform_expression_to_outputform_text + + +def symbol_expression_to_outputform_text( + symb: Symbol, evaluation: Evaluation, form: Symbol, **kwargs +): + return evaluation.definitions.shorten_name(symb.name) + + +expr_to_outputform_text_map["System`Symbol"] = symbol_expression_to_outputform_text + + +def tableform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + return grid_expression_to_outputform_text(expr, evaluation, form) + + +expr_to_outputform_text_map[ + "System`TableForm" +] = tableform_expression_to_outputform_text + + +def texform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + # boxes = format_element(expr.elements[0], evaluation, form) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + ).evaluate(evaluation) + return boxes.boxes_to_tex() # type: ignore + + +expr_to_outputform_text_map["System`TeXForm"] = texform_expression_to_outputform_text + + +def times_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + elements = expr.elements + num: List[BaseElement] = [] + den: List[BaseElement] = [] + # First, split factors with integer, negative powers: + for elem in elements: + if elem.has_form("Power", 2): + base, exponent = elem.elements + if isinstance(exponent, Integer): + if exponent.value == -1: + den.append(base) + continue + elif exponent.value < 0: + den.append(Expression(SymbolPower, base, Integer(-exponent.value))) + continue + elif isinstance(elem, Rational): + num.append(elem.numerator()) + den.append(elem.denominator()) + continue + elif elem.has_form("Rational", 2): + elem_elements = elem.elements + num.append(elem_elements[0]) + den.append(elem_elements[1]) + continue + + num.append(elem) + + # If there are integer, negative powers, process as a fraction: + if den: + den_expr = den[0] if len(den) == 1 else Expression(SymbolTimes, *den) + num_expr = ( + Expression(SymbolTimes, *num) + if len(num) > 1 + else num[0] + if len(num) == 1 + else Integer1 + ) + return _divide(num_expr, den_expr, evaluation, form, **kwargs) + + # there are no integer negative powers: + if len(num) == 1: + return expression_to_outputform_text(num[0], evaluation, form, **kwargs) + + prefactor = 1 + result: str = "" + for i, elem in enumerate(num): + if elem is IntegerM1: + prefactor *= -1 + continue + if isinstance(elem, Integer): + prefactor *= -1 + elem = Integer(-elem.value) + + elem_txt = expression_to_outputform_text(elem, evaluation, form, **kwargs) + if compare_precedence(elem, 400): + elem_txt = parenthesize(elem_txt) + if i == 0: + result = elem_txt + else: + result = result + " " + elem_txt + if result == "": + result = "1" + if prefactor == -1: + result = "-" + result + return result + + +expr_to_outputform_text_map["System`Times"] = times_expression_to_outputform_text diff --git a/mathics/format/text.py b/mathics/format/text.py index 422ce940a..681017848 100644 --- a/mathics/format/text.py +++ b/mathics/format/text.py @@ -9,6 +9,8 @@ from mathics.builtin.box.layout import ( FractionBox, GridBox, + InterpretationBox, + PaneBox, RowBox, SqrtBox, StyleBox, @@ -40,6 +42,14 @@ def string(self, **options) -> str: add_conversion_fn(String, string) +def interpretation_panebox(self, **options): + return boxes_to_text(self.elements[0], **options) + + +add_conversion_fn(InterpretationBox, interpretation_panebox) +add_conversion_fn(PaneBox, interpretation_panebox) + + def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) @@ -72,13 +82,15 @@ def gridbox(self, elements=None, **box_options) -> str: widths = [0] cells = [ - [ - # TODO: check if this evaluation is necessary. - boxes_to_text(item, **box_options).splitlines() - for item in row - ] - if isinstance(row, tuple) - else [boxes_to_text(row, **box_options).splitlines()] + ( + [ + # TODO: check if this evaluation is necessary. + boxes_to_text(item, **box_options).splitlines() + for item in row + ] + if isinstance(row, tuple) + else [boxes_to_text(row, **box_options).splitlines()] + ) for row in items ] From 1388dd5b164caf91fc8aae56afbe9cde73c152ad Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 26 Dec 2025 10:58:59 -0300 Subject: [PATCH 02/49] fix typo --- mathics/eval/makeboxes/makeboxes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index af362594b..17d16e17f 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -149,6 +149,7 @@ def eval_makeboxes_outputform(expr, evaluation, form): elem2 = Expression(SymbolOutputForm, expr) return InterpretationBox(elem1, elem2) + # TODO: evaluation is needed because `atom_to_boxes` uses it. Can we remove this # argument? def eval_makeboxes_fullform( @@ -283,7 +284,7 @@ def format_element( element = element.elements[0] if element.has_form("FullForm", 1): - return eval_fullform_makeboxes(element.elements[0], evaluation) + return eval_makeboxes_fullform(element.elements[0], evaluation) # In order to work like in WMA, `format_element` # should evaluate `MakeBoxes[element//form, StandardForm]` From 89678aabc904ed4b2656e189bc20d8e0d38a9ea4 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 26 Dec 2025 11:29:33 -0300 Subject: [PATCH 03/49] merge --- mathics/eval/makeboxes/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/eval/makeboxes/__init__.py b/mathics/eval/makeboxes/__init__.py index a7f9d3dc9..43a714a9d 100644 --- a/mathics/eval/makeboxes/__init__.py +++ b/mathics/eval/makeboxes/__init__.py @@ -7,8 +7,8 @@ _boxed_string, eval_generic_makeboxes, eval_makeboxes, - eval_makeboxes_outputform, eval_makeboxes_fullform, + eval_makeboxes_outputform, format_element, int_to_string_shorter_repr, to_boxes, From 777b4180d4e75a429f0e7bbc028bf2425fbc2cf0 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 27 Dec 2025 13:05:33 -0300 Subject: [PATCH 04/49] add Pane and PaneBox --- mathics/builtin/box/layout.py | 19 +- mathics/builtin/forms/output.py | 6 - mathics/builtin/layout.py | 47 +- mathics/eval/makeboxes/__init__.py | 9 +- mathics/eval/makeboxes/makeboxes.py | 39 +- mathics/format/latex.py | 46 +- mathics/format/mathml.py | 46 +- mathics/format/outputform.py | 779 ---------------------------- mathics/format/text.py | 12 +- 9 files changed, 157 insertions(+), 846 deletions(-) delete mode 100644 mathics/format/outputform.py diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index 079f84551..b9a0233ae 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -20,6 +20,7 @@ from mathics.core.list import ListExpression from mathics.core.symbols import Symbol from mathics.core.systemsymbols import ( + SymbolAutomatic, SymbolFractionBox, SymbolRowBox, SymbolSqrtBox, @@ -192,7 +193,7 @@ class PaneBox(BoxExpression): """ :WMA link: - https://reference.wolfram.com/language/ref/InterpretationBox.html + https://reference.wolfram.com/language/ref/Pane.html
'PaneBox[expr]' @@ -202,15 +203,23 @@ class PaneBox(BoxExpression): """ attributes = A_HOLD_ALL_COMPLETE | A_PROTECTED | A_READ_PROTECTED - summary_text = "box associated to panel" + summary_text = "box associated to pane" + options = {"ImageSize": "Automatic"} + + def init(self, expr, **options): + self.box_options = options - def apply_display_form(boxexpr, form, evaluation, expression): - """ToExpression[boxexpr_PaneBox, form_]""" + def eval_panebox1(self, expr, evaluation, options): + "PaneBox[expr_, OptionsPattern[]]" + return PaneBox(expr, **options) + + def eval_display_form(boxexpr, form, evaluation, expression, options): + """ToExpression[boxexpr_PaneBox, form_, OptionsPattern[]]""" return Expression(expression.head, boxexpr.elements[0], form).evaluate( evaluation ) - def apply_display(boxexpr, evaluation): + def eval_display(boxexpr, evaluation): """DisplayForm[boxexpr_PaneBox]""" return boxexpr.elements[0] diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index 6c48f04d9..021440fc4 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -37,7 +37,6 @@ StringLParen, StringRParen, eval_baseform, - eval_makeboxes_outputform, eval_mathmlform, eval_tableform, eval_texform, @@ -491,13 +490,8 @@ class OutputForm(FormBaseClass): = -Graphics- """ - formats = {"OutputForm[s_String]": "s"} summary_text = "plain-text output format" - def eval_makeboxes(self, expr, form, evaluation): - """MakeBoxes[OutputForm[expr_], form_]""" - return eval_makeboxes_outputform(expr, evaluation, form) - class PythonForm(FormBaseClass): """ diff --git a/mathics/builtin/layout.py b/mathics/builtin/layout.py index c5b4eb6cc..e8ba10e91 100644 --- a/mathics/builtin/layout.py +++ b/mathics/builtin/layout.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - """ Layout @@ -12,7 +11,7 @@ """ -from mathics.builtin.box.layout import GridBox, RowBox, to_boxes +from mathics.builtin.box.layout import GridBox, PaneBox, RowBox, to_boxes from mathics.builtin.makeboxes import MakeBoxes from mathics.builtin.options import options_to_rules from mathics.core.atoms import Real, String @@ -211,6 +210,50 @@ class NonAssociative(Builtin): summary_text = "non-associative operator" +class Pane(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/Pane.html + +
+
'Pane[$expr$, $width$]' +
display $expr$ inside a pane. +
+ + >> Pane[37!] + = 13763753091226345046315979581580902400000000 + + In TeXForm, $Pane$ produce minipage environments: + >> {{Pane[a,3], Pane[expt, 3]}}//TableForm//TeXForm + = ... + In MathMLForm, $Pane$ wraps the elements in
...
tags: + >> {{Pane[a,3], Pane[expt, 3]}}//TableForm//MathMLForm + = ... + + """ + + summary_text = "put expressions inside a pane" + options = { + "ImageSize": "Automatic", + } + + def eval_makeboxes(self, expr, f, evaluation, options): + """MakeBoxes[Pane[expr_, OptionsPattern[Pane]], f_]""" + box_expr = Expression(SymbolMakeBoxes, expr, f).evaluate(evaluation) + return PaneBox(box_expr, **options) + + def eval_makeboxes2(self, expr, width, f, evaluation, options): + """MakeBoxes[Pane[expr_, width_, OptionsPattern[Pane]], f_]""" + box_expr = Expression(SymbolMakeBoxes, expr, f).evaluate(evaluation) + options["System`ImageSize"] = width + return PaneBox(box_expr, **options) + + def eval_makeboxes3(self, expr, width, height, f, evaluation, options): + """MakeBoxes[Pane[expr_, width_, height_, OptionsPattern[Pane]], f_]""" + box_expr = Expression(SymbolMakeBoxes, expr, f).evaluate(evaluation) + options["System`ImageSize"] = ListExpression(width, height) + return PaneBox(box_expr, **options) + + class Postfix(PostfixOperator): """ :WMA link:https://reference.wolfram.com/language/ref/Postfix.html diff --git a/mathics/eval/makeboxes/__init__.py b/mathics/eval/makeboxes/__init__.py index 43a714a9d..b3ca977f6 100644 --- a/mathics/eval/makeboxes/__init__.py +++ b/mathics/eval/makeboxes/__init__.py @@ -8,7 +8,6 @@ eval_generic_makeboxes, eval_makeboxes, eval_makeboxes_fullform, - eval_makeboxes_outputform, format_element, int_to_string_shorter_repr, to_boxes, @@ -20,11 +19,7 @@ eval_tableform, eval_texform, ) -from mathics.eval.makeboxes.precedence import ( - builtins_precedence, - compare_precedence, - parenthesize, -) +from mathics.eval.makeboxes.precedence import builtins_precedence, parenthesize __all__ = [ "NumberForm_to_String", @@ -32,14 +27,12 @@ "StringRParen", "_boxed_string", "builtins_precedence", - "compare_precedence", "do_format", "eval_baseform", "eval_generic_makeboxes", "eval_infix", "eval_makeboxes", "eval_makeboxes_fullform", - "eval_makeboxes_outputform", "eval_mathmlform", "eval_postprefix", "eval_tableform", diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 17d16e17f..e67b4c14a 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -21,7 +21,6 @@ ) from mathics.core.systemsymbols import ( # SymbolRule, SymbolRuleDelayed, SymbolComplex, - SymbolOutputForm, SymbolRational, SymbolStandardForm, ) @@ -137,19 +136,6 @@ def int_to_string_shorter_repr(value: int, form: Symbol, max_digits=640): return String(value_str) -def eval_makeboxes_outputform(expr, evaluation, form): - """ - Build a 2D text representation of the expression. - """ - from mathics.builtin.box.layout import InterpretationBox, PaneBox - from mathics.format.outputform import expression_to_outputform_text - - text_outputform = str(expression_to_outputform_text(expr, evaluation, form)) - elem1 = PaneBox(String(text_outputform)) - elem2 = Expression(SymbolOutputForm, expr) - return InterpretationBox(elem1, elem2) - - # TODO: evaluation is needed because `atom_to_boxes` uses it. Can we remove this # argument? def eval_makeboxes_fullform( @@ -280,29 +266,10 @@ def format_element( Applies formats associated to the expression, and then calls Makeboxes """ evaluation.is_boxing = True - while element.get_head() is form: - element = element.elements[0] - - if element.has_form("FullForm", 1): - return eval_makeboxes_fullform(element.elements[0], evaluation) - - # In order to work like in WMA, `format_element` - # should evaluate `MakeBoxes[element//form, StandardForm]` - # Then, MakeBoxes[expr_, StandardForm], for any expr, - # should apply Format[...] rules, and then - # MakeBoxes[...] rules. These rules should be stored - # as FormatValues[...] - # As a first step in that direction, let's mimic this behaviour - # just for the case of OutputForm: - if element.has_form("OutputForm", 1): - return eval_makeboxes_outputform(element.elements[0], evaluation, form) - - expr = do_format(element, evaluation, form) - if expr is None: + formatted_expr = do_format(element, evaluation, form) + if formatted_expr is None: return None - - result = Expression(SymbolMakeBoxes, expr, form) - result_box = result.evaluate(evaluation) + result_box = eval_makeboxes(formatted_expr, evaluation, form) if isinstance(result_box, String): return result_box if isinstance(result_box, BoxElementMixin): diff --git a/mathics/format/latex.py b/mathics/format/latex.py index e1cf20444..047ac6179 100644 --- a/mathics/format/latex.py +++ b/mathics/format/latex.py @@ -37,6 +37,7 @@ lookup_method as lookup_conversion_method, ) from mathics.core.symbols import SymbolTrue +from mathics.core.systemsymbols import SymbolAutomatic from mathics.format.asy_fns import asy_color, asy_create_pens, asy_number # mathics_scanner does not generates this table in a way that we can load it here. @@ -135,14 +136,53 @@ def render(format, string, in_text=False): add_conversion_fn(String, string) -def interpretation_panebox(self, **options): +def interpretation_box(self, **options): return lookup_conversion_method(self.elements[0], "latex")( self.elements[0], **options ) -add_conversion_fn(InterpretationBox, interpretation_panebox) -add_conversion_fn(PaneBox, interpretation_panebox) +add_conversion_fn(InterpretationBox, interpretation_box) + + +def pane_box(self, **options): + content = lookup_conversion_method(self.elements[0], "latex")( + self.elements[0], **options + ) + options = self.box_options + size = options.get("System`ImageSize", SymbolAutomatic).to_python() + if size is SymbolAutomatic: + width = "\\textwidth" + height = "" + elif isinstance(size, int): + width = f"{size}pt" + height = "" + elif isinstance(size, tuple) and len(size) == 2: + width_val, height_val = size[0], size[1] + if isinstance(width_val, int): + width = f"{width_val}pt" + else: + width = "\\textwidth" + if isinstance(height_val, int): + height = f"[{height_val}pt]" + else: + height = "" + else: + width = "\\textwidth" + height = "" + + return ( + "\\begin{minipage}{" + + width + + "}" + + height + + "\n" + + content + + "\n\\end{minipage}" + ) + + +add_conversion_fn(PaneBox, pane_box) def fractionbox(self, **options) -> str: diff --git a/mathics/format/mathml.py b/mathics/format/mathml.py index 93db96f8a..a358ac26c 100644 --- a/mathics/format/mathml.py +++ b/mathics/format/mathml.py @@ -33,6 +33,7 @@ ) from mathics.core.load_builtin import display_operators_set as operators from mathics.core.symbols import SymbolFalse, SymbolTrue +from mathics.core.systemsymbols import SymbolAutomatic def encode_mathml(text: str) -> str: @@ -114,14 +115,51 @@ def render(format, string): add_conversion_fn(String, string) -def interpretation_panebox(self, **options): - return lookup_conversion_method(self.elements[0], "latex")( +def interpretation_box(self, **options): + return lookup_conversion_method(self.elements[0], "mathml")( self.elements[0], **options ) -add_conversion_fn(InterpretationBox, interpretation_panebox) -add_conversion_fn(PaneBox, interpretation_panebox) +add_conversion_fn(InterpretationBox, interpretation_box) + + +def pane_box(self, **options): + content = lookup_conversion_method(self.elements[0], "latex")( + self.elements[0], **options + ) + options = self.box_options + size = options.get("System`ImageSize", SymbolAutomatic).to_python() + if size is SymbolAutomatic: + width = "" + height = "" + elif isinstance(size, int): + width = f"{size}pt" + height = "" + elif isinstance(size, tuple) and len(size) == 2: + width_val, height_val = size[0], size[1] + if isinstance(width_val, int): + width = f"{width_val}pt" + else: + width = "" + if isinstance(height_val, int): + height = f"[{height_val}pt]" + else: + height = "" + else: + width = "" + height = "" + + dims = f"width:{width};" if width else "" + if height: + dims += f"height:{width};" + if dims: + dims = f' style="{dims}" ' + + return f"\n" + content + "\n" + + +add_conversion_fn(PaneBox, pane_box) def fractionbox(self, **options) -> str: diff --git a/mathics/format/outputform.py b/mathics/format/outputform.py deleted file mode 100644 index 0fc8309ec..000000000 --- a/mathics/format/outputform.py +++ /dev/null @@ -1,779 +0,0 @@ -""" -This module builts the 2D string associated to the OutputForm -""" - -from typing import Callable, Dict, List, Union - -from mathics.core.atoms import ( - Integer, - Integer1, - Integer2, - IntegerM1, - Rational, - Real, - String, -) -from mathics.core.element import BaseElement -from mathics.core.evaluation import Evaluation -from mathics.core.expression import Expression -from mathics.core.list import ListExpression -from mathics.core.symbols import Atom, Symbol, SymbolTimes -from mathics.core.systemsymbols import ( - SymbolDerivative, - SymbolInfix, - SymbolNone, - SymbolOutputForm, - SymbolPower, - SymbolStandardForm, - SymbolTraditionalForm, -) -from mathics.eval.makeboxes import compare_precedence, do_format # , format_element - -SymbolNonAssociative = Symbol("System`NonAssociative") -SymbolPostfix = Symbol("System`Postfix") -SymbolPrefix = Symbol("System`Prefix") -SymbolRight = Symbol("System`Right") -SymbolLeft = Symbol("System`Left") - - -expr_to_outputform_text_map: Dict[str, Callable] = {} - - -# This Exception if the expression should -# be processed by the default routine -class _WrongFormattedExpression(Exception): - pass - - -class IsNotGrid(Exception): - pass - - -class IsNot2DArray(Exception): - pass - - -def parenthesize(expr_str: str) -> str: - """wrap with parenthesis""" - return f"({expr_str})" - - -def bracket(expr_str: str) -> str: - """wrap with square brackets""" - return f"[{expr_str}]" - - -def grid(expr): - raise NotImplementedError - - -def expression_to_outputform_text( - expr: BaseElement, evaluation: Evaluation, form=SymbolStandardForm, **kwargs -): - """ - Build a 2d text from an `Expression` - """ - ## TODO: format the expression - format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore - - # Strip HoldForm - while format_expr.has_form("HoldForm", 1): # type: ignore - format_expr = format_expr.elements[0] - - lookup_name = format_expr.get_head().get_lookup_name() - try: - result = expr_to_outputform_text_map[lookup_name]( - format_expr, evaluation, form, **kwargs - ) - return result - except _WrongFormattedExpression: - # If the key is not present, or the execution fails for any reason, use - # the default - pass - except KeyError: - pass - return _default_expression_to_outputform_text( - format_expr, evaluation, form, **kwargs - ) - - -def _default_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - """ - Default representation of a function - """ - expr_head = expr.head - head = expression_to_outputform_text(expr_head, evaluation, form, **kwargs) - comma = ", " - elements = [ - expression_to_outputform_text(elem, evaluation) for elem in expr.elements - ] - result = elements.pop(0) if elements else " " - while elements: - result = result + comma + elements.pop(0) - - if form is SymbolTraditionalForm: - return head + parenthesize(result) - return head + bracket(result) - - -def _divide(num, den, evaluation, form, **kwargs): - infix_form = Expression( - SymbolInfix, ListExpression(num, den), String("/"), Integer(400), SymbolLeft - ) - return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) - - -def _strip_1_parm_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - if len(expr.elements) != 1: - raise _WrongFormattedExpression - return expression_to_outputform_text(expr.elements[0], evaluation, form, **kwargs) - - -expr_to_outputform_text_map[ - "System`HoldForm" -] = _strip_1_parm_expression_to_outputform_text -expr_to_outputform_text_map[ - "System`InputForm" -] = _strip_1_parm_expression_to_outputform_text - - -def derivative_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - """Derivative operator""" - head = expr.get_head() - if head is SymbolDerivative: - return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) - super_head = head.get_head() - if super_head is SymbolDerivative: - expr_elements = expr.elements - if len(expr_elements) != 1: - return _default_expression_to_outputform_text( - expr, evaluation, form, **kwargs - ) - function_head = expression_to_outputform_text( - expr_elements[0], evaluation, form, **kwargs - ) - derivatives = head.elements - if len(derivatives) == 1: - order_iv = derivatives[0] - if order_iv == Integer1: - return function_head + "'" - elif order_iv == Integer2: - return function_head + "''" - - return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) - - # Full Function with arguments: delegate to the default conversion. - # It will call us again with the head - return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) - - -expr_to_outputform_text_map[ - "System`Derivative" -] = derivative_expression_to_outputform_text - - -def divide_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - if len(expr.elements) != 2: - raise _WrongFormattedExpression - num, den = expr.elements - return _divide(num, den, evaluation, form, **kwargs) - - -expr_to_outputform_text_map["System`Divide"] = divide_expression_to_outputform_text - - -def graphics(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: - return "-Graphics-" - - -expr_to_outputform_text_map["System`Graphics"] = graphics - - -def graphics3d(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: - return "-Graphics3D-" - - -expr_to_outputform_text_map["System`Graphics3D"] = graphics3d - - -def grid_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - if len(expr.elements) == 0: - raise IsNotGrid - if len(expr.elements) > 1 and not expr.elements[1].has_form( - ["Rule", "RuleDelayed"], 2 - ): - raise IsNotGrid - if not expr.elements[0].has_form("List", None): - raise IsNotGrid - - elements = expr.elements[0].elements - rows = [] - for idx, item in enumerate(elements): - if item.has_form("List", None): - rows.append( - [ - expression_to_outputform_text(item_elem, evaluation, form, **kwargs) - for item_elem in item.elements - ] - ) - else: - rows.append(expression_to_outputform_text(item, evaluation, form, **kwargs)) - - return grid(rows) - - -expr_to_outputform_text_map["System`Grid"] = grid_expression_to_outputform_text - - -def integer_expression_to_outputform_text( - n: Integer, evaluation: Evaluation, form: Symbol, **kwargs -): - return str(n.value) - - -expr_to_outputform_text_map["System`Integer"] = integer_expression_to_outputform_text - - -def list_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - result, *rest_elems = ( - expression_to_outputform_text(elem, evaluation, form, **kwargs) - for elem in expr.elements - ) - comma_tb = ", " - for next_elem in rest_elems: - result = result + comma_tb + next_elem - return "{" + result + "}" - - -expr_to_outputform_text_map["System`List"] = list_expression_to_outputform_text - - -def mathmlform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - # boxes = format_element(expr.elements[0], evaluation, form) - boxes = Expression( - Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm - ).evaluate(evaluation) - return boxes.boxes_to_mathml() # type: ignore[union-attr] - - -expr_to_outputform_text_map[ - "System`MathMLForm" -] = mathmlform_expression_to_outputform_text - - -def matrixform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - # return parenthesize(tableform_expression_to_outputform_text(expr, evaluation, form, **kwargs)) - return tableform_expression_to_outputform_text(expr, evaluation, form, **kwargs) - - -expr_to_outputform_text_map[ - "System`MatrixForm" -] = matrixform_expression_to_outputform_text - - -def plus_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - elements = expr.elements - result = "" - for i, elem in enumerate(elements): - if elem.has_form("Times", None): - # If the first element is -1, remove it and use - # a minus sign. Otherwise, if negative, do not add a sign. - first = elem.elements[0] - if isinstance(first, Integer): - if first.value == -1: - result = ( - result - + " - " - + expression_to_outputform_text( - Expression(SymbolTimes, *elem.elements[1:]), - evaluation, - form, - **kwargs, - ) - ) - continue - elif first.value < 0: - result = ( - result - + " " - + expression_to_outputform_text( - elem, evaluation, form, **kwargs - ) - ) - continue - elif isinstance(first, Real): - if first.value < 0: - result = ( - result - + " " - + expression_to_outputform_text( - elem, evaluation, form, **kwargs - ) - ) - continue - result = ( - result - + " + " - + expression_to_outputform_text(elem, evaluation, form, **kwargs) - ) - ## TODO: handle complex numbers? - else: - elem_txt = expression_to_outputform_text(elem, evaluation, form, **kwargs) - if (compare_precedence(elem, 310) or -1) < 0: - elem_txt = parenthesize(elem_txt) - result = result + " + " + elem_txt - elif i == 0 or ( - (isinstance(elem, Integer) and elem.value < 0) - or (isinstance(elem, Real) and elem.value < 0) - ): - result = result + elem_txt - else: - result = ( - result - + " + " - + expression_to_outputform_text(elem, evaluation, form, **kwargs) - ) - return result - - -expr_to_outputform_text_map["System`Plus"] = plus_expression_to_outputform_text - - -def power_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -): - if len(expr.elements) != 2: - raise _WrongFormattedExpression - - infix_form = Expression( - SymbolInfix, - ListExpression(*(expr.elements)), - String("^"), - Integer(590), - SymbolRight, - ) - return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) - - -expr_to_outputform_text_map["System`Power"] = power_expression_to_outputform_text - - -def pre_pos_fix_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - elements = expr.elements - if not (0 <= len(elements) <= 4): - raise _WrongFormattedExpression - - group = None - precedence = 670 - # Processing the first argument: - head = expr.get_head() - target = expr.elements[0] - if isinstance(target, Atom): - raise _WrongFormattedExpression - - operands = list(target.elements) - if len(operands) != 1: - raise _WrongFormattedExpression - - # Processing the second argument, if it is there: - if len(elements) > 1: - ops = elements[1] - ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] - else: - if head is SymbolPrefix: - default_symb = " @ " - ops_txt = ( - expression_to_outputform_text(head, evaluation, form, **kwargs) - + default_symb - ) - elif head is SymbolPostfix: - default_symb = " // " - ops_txt = default_symb + expression_to_outputform_text( - head, evaluation, form, **kwargs - ) - - # Processing the third argument, if it is there: - if len(elements) > 2: - if isinstance(elements[2], Integer): - precedence = elements[2].value - else: - raise _WrongFormattedExpression - - # Processing the forth argument, if it is there: - if len(elements) > 3: - group = elements[3] - if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): - raise _WrongFormattedExpression - if group is SymbolNone: - group = None - - operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: - target_txt = parenthesize(target_txt) - - return ops_txt[0] + target_txt if head is SymbolPrefix else target_txt + ops_txt[0] - - -expr_to_outputform_text_map["System`Prefix"] = pre_pos_fix_expression_to_outputform_text -expr_to_outputform_text_map[ - "System`Postfix" -] = pre_pos_fix_expression_to_outputform_text - - -def infix_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - elements = expr.elements - if not (0 <= len(elements) <= 4): - raise _WrongFormattedExpression - - group = None - precedence = 670 - # Processing the first argument: - head = expr.get_head() - target = expr.elements[0] - if isinstance(target, Atom): - raise _WrongFormattedExpression - - operands = list(target.elements) - - if len(operands) < 2: - raise _WrongFormattedExpression - - # Processing the second argument, if it is there: - if len(elements) > 1: - ops = elements[1] - if head is SymbolInfix: - # This is not the WMA behaviour, but the Mathics current implementation requires it: - num_ops = 1 - if ops.has_form("List", None): - num_ops = len(ops.elements) - ops_lst = [ - expression_to_outputform_text(op, evaluation, form, **kwargs) - for op in ops.elements - ] - else: - ops_lst = [ - expression_to_outputform_text(ops, evaluation, form, **kwargs) - ] - elif head in (SymbolPrefix, SymbolPostfix): - ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] - else: - if head is SymbolInfix: - num_ops = 1 - default_symb = " ~ " - ops_lst = [ - default_symb - + expression_to_outputform_text(head, evaluation, form, **kwargs) - + default_symb - ] - elif head is SymbolPrefix: - default_symb = " @ " - ops_txt = ( - expression_to_outputform_text(head, evaluation, form, **kwargs) - + default_symb - ) - elif head is SymbolPostfix: - default_symb = " // " - ops_txt = default_symb + expression_to_outputform_text( - head, evaluation, form, **kwargs - ) - - # Processing the third argument, if it is there: - if len(elements) > 2: - if isinstance(elements[2], Integer): - precedence = elements[2].value - else: - raise _WrongFormattedExpression - - # Processing the forth argument, if it is there: - if len(elements) > 3: - group = elements[3] - if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): - raise _WrongFormattedExpression - if group is SymbolNone: - group = None - - if head is SymbolPrefix: - operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: - target_txt = parenthesize(target_txt) - return ops_txt[0] + target_txt - if head is SymbolPostfix: - operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: - target_txt = parenthesize(target_txt) - return target_txt + ops_txt[0] - else: # Infix - parenthesized = group in (None, SymbolRight, SymbolNonAssociative) - for index, operand in enumerate(operands): - operand_txt = str( - expression_to_outputform_text(operand, evaluation, form, **kwargs) - ) - cmp_precedence = compare_precedence(operand, precedence) - if cmp_precedence is not None and ( - cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) - ): - operand_txt = parenthesize(operand_txt) - - if index == 0: - result = operand_txt - # After the first element, for lateral - # associativity, parenthesized is flipped: - if group in (SymbolLeft, SymbolRight): - parenthesized = not parenthesized - else: - space = " " - result_lst: List[str] - if str(ops_lst[index % num_ops]) != " ": - result_lst = [ - result, - space, - str(ops_lst[index % num_ops]), - space, - operand_txt, - ] - else: - result_lst = [result, space, operand_txt] - - return "".join(result_lst) - - -expr_to_outputform_text_map["System`Infix"] = infix_expression_to_outputform_text - - -def precedenceform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - if len(expr.elements) == 2: - return expression_to_outputform_text( - expr.elements[0], evaluation, form, **kwargs - ) - raise _WrongFormattedExpression - - -expr_to_outputform_text_map[ - "System`PrecedenceForm" -] = precedenceform_expression_to_outputform_text - - -def rational_expression_to_outputform_text( - n: Union[Rational, Expression], evaluation: Evaluation, form: Symbol, **kwargs -): - if n.has_form("Rational", 2): - num, den = n.elements # type: ignore[union-attr] - else: - num, den = n.numerator(), n.denominator() # type: ignore[union-attr] - return _divide(num, den, evaluation, form, **kwargs) - - -expr_to_outputform_text_map["System`Rational"] = rational_expression_to_outputform_text - - -def real_expression_to_outputform_text( - n: Real, evaluation: Evaluation, form: Symbol, **kwargs -): - str_n = n.make_boxes("System`OutputForm").boxes_to_text() # type: ignore[attr-defined] - return str(str_n) - - -expr_to_outputform_text_map["System`Real"] = real_expression_to_outputform_text - - -def string_expression_to_outputform_text( - expr: String, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - lines = expr.value.split("\n") - max_len = max([len(line) for line in lines]) - lines = [line + (max_len - len(line)) * " " for line in lines] - return "\n".join(lines) - - -expr_to_outputform_text_map["System`String"] = string_expression_to_outputform_text - - -def stringform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - strform = expr.elements[0] - if not isinstance(strform, String): - raise _WrongFormattedExpression - - items = list( - expression_to_outputform_text(item, evaluation, form, **kwargs) - for item in expr.elements[1:] - ) - - curr_indx = 0 - parts = strform.value.split("`") - result = str(parts[0]) - if len(parts) == 1: - return result - - quote_open = True - remaining = len(parts) - 1 - - for part in parts[1:]: - remaining -= 1 - if quote_open: - if remaining == 0: - result = result + "`" + part - quote_open = False - continue - if len(part) == 0: - result = result + items[curr_indx] - continue - try: - idx = int(part) - except ValueError: - idx = None - if idx is not None and str(idx) == part: - curr_indx = idx - 1 - result = result + items[curr_indx] - quote_open = False - continue - else: - result = result + "`" + part + "`" - quote_open = False - continue - else: - result = result + part - quote_open = True - - return result - - -expr_to_outputform_text_map[ - "System`StringForm" -] = stringform_expression_to_outputform_text - - -def symbol_expression_to_outputform_text( - symb: Symbol, evaluation: Evaluation, form: Symbol, **kwargs -): - return evaluation.definitions.shorten_name(symb.name) - - -expr_to_outputform_text_map["System`Symbol"] = symbol_expression_to_outputform_text - - -def tableform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - return grid_expression_to_outputform_text(expr, evaluation, form) - - -expr_to_outputform_text_map[ - "System`TableForm" -] = tableform_expression_to_outputform_text - - -def texform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - # boxes = format_element(expr.elements[0], evaluation, form) - boxes = Expression( - Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm - ).evaluate(evaluation) - return boxes.boxes_to_tex() # type: ignore - - -expr_to_outputform_text_map["System`TeXForm"] = texform_expression_to_outputform_text - - -def times_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - elements = expr.elements - num: List[BaseElement] = [] - den: List[BaseElement] = [] - # First, split factors with integer, negative powers: - for elem in elements: - if elem.has_form("Power", 2): - base, exponent = elem.elements - if isinstance(exponent, Integer): - if exponent.value == -1: - den.append(base) - continue - elif exponent.value < 0: - den.append(Expression(SymbolPower, base, Integer(-exponent.value))) - continue - elif isinstance(elem, Rational): - num.append(elem.numerator()) - den.append(elem.denominator()) - continue - elif elem.has_form("Rational", 2): - elem_elements = elem.elements - num.append(elem_elements[0]) - den.append(elem_elements[1]) - continue - - num.append(elem) - - # If there are integer, negative powers, process as a fraction: - if den: - den_expr = den[0] if len(den) == 1 else Expression(SymbolTimes, *den) - num_expr = ( - Expression(SymbolTimes, *num) - if len(num) > 1 - else num[0] - if len(num) == 1 - else Integer1 - ) - return _divide(num_expr, den_expr, evaluation, form, **kwargs) - - # there are no integer negative powers: - if len(num) == 1: - return expression_to_outputform_text(num[0], evaluation, form, **kwargs) - - prefactor = 1 - result: str = "" - for i, elem in enumerate(num): - if elem is IntegerM1: - prefactor *= -1 - continue - if isinstance(elem, Integer): - prefactor *= -1 - elem = Integer(-elem.value) - - elem_txt = expression_to_outputform_text(elem, evaluation, form, **kwargs) - if compare_precedence(elem, 400): - elem_txt = parenthesize(elem_txt) - if i == 0: - result = elem_txt - else: - result = result + " " + elem_txt - if result == "": - result = "1" - if prefactor == -1: - result = "-" + result - return result - - -expr_to_outputform_text_map["System`Times"] = times_expression_to_outputform_text diff --git a/mathics/format/text.py b/mathics/format/text.py index 681017848..184ee24c9 100644 --- a/mathics/format/text.py +++ b/mathics/format/text.py @@ -42,12 +42,18 @@ def string(self, **options) -> str: add_conversion_fn(String, string) -def interpretation_panebox(self, **options): +def interpretation_box(self, **options): return boxes_to_text(self.elements[0], **options) -add_conversion_fn(InterpretationBox, interpretation_panebox) -add_conversion_fn(PaneBox, interpretation_panebox) +add_conversion_fn(InterpretationBox, interpretation_box) + + +def pane_box(self, **options): + return boxes_to_text(self.elements[0], **options) + + +add_conversion_fn(PaneBox, pane_box) def fractionbox(self, **options) -> str: From 9bd5cad05c759dcd37f697b0e945c36869768b86 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 27 Dec 2025 14:02:12 -0300 Subject: [PATCH 05/49] improve Pane in mathml --- mathics/format/mathml.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/mathics/format/mathml.py b/mathics/format/mathml.py index a358ac26c..556ad85ae 100644 --- a/mathics/format/mathml.py +++ b/mathics/format/mathml.py @@ -125,7 +125,7 @@ def interpretation_box(self, **options): def pane_box(self, **options): - content = lookup_conversion_method(self.elements[0], "latex")( + content = lookup_conversion_method(self.elements[0], "mathml")( self.elements[0], **options ) options = self.box_options @@ -134,16 +134,16 @@ def pane_box(self, **options): width = "" height = "" elif isinstance(size, int): - width = f"{size}pt" + width = f"{size}px" height = "" elif isinstance(size, tuple) and len(size) == 2: width_val, height_val = size[0], size[1] if isinstance(width_val, int): - width = f"{width_val}pt" + width = f"{width_val}px" else: width = "" if isinstance(height_val, int): - height = f"[{height_val}pt]" + height = f"{height_val}px" else: height = "" else: @@ -152,11 +152,13 @@ def pane_box(self, **options): dims = f"width:{width};" if width else "" if height: - dims += f"height:{width};" + dims += f"height:{height};" if dims: + dims += "overflow:hidden;" dims = f' style="{dims}" ' - - return f"\n" + content + "\n" + if dims: + return f"\n{content}\n" + return content add_conversion_fn(PaneBox, pane_box) From 088e657ee3ed89cc31aa20e1576888ba1f2b0fe6 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 27 Dec 2025 14:33:33 -0300 Subject: [PATCH 06/49] improve compatibility in OutputForm --- mathics/builtin/forms/output.py | 6 + mathics/eval/makeboxes/__init__.py | 9 +- mathics/eval/makeboxes/makeboxes.py | 39 +- mathics/format/outputform.py | 779 ++++++++++++++++++++++++++++ 4 files changed, 829 insertions(+), 4 deletions(-) create mode 100644 mathics/format/outputform.py diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index 021440fc4..6c48f04d9 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -37,6 +37,7 @@ StringLParen, StringRParen, eval_baseform, + eval_makeboxes_outputform, eval_mathmlform, eval_tableform, eval_texform, @@ -490,8 +491,13 @@ class OutputForm(FormBaseClass): = -Graphics- """ + formats = {"OutputForm[s_String]": "s"} summary_text = "plain-text output format" + def eval_makeboxes(self, expr, form, evaluation): + """MakeBoxes[OutputForm[expr_], form_]""" + return eval_makeboxes_outputform(expr, evaluation, form) + class PythonForm(FormBaseClass): """ diff --git a/mathics/eval/makeboxes/__init__.py b/mathics/eval/makeboxes/__init__.py index b3ca977f6..43a714a9d 100644 --- a/mathics/eval/makeboxes/__init__.py +++ b/mathics/eval/makeboxes/__init__.py @@ -8,6 +8,7 @@ eval_generic_makeboxes, eval_makeboxes, eval_makeboxes_fullform, + eval_makeboxes_outputform, format_element, int_to_string_shorter_repr, to_boxes, @@ -19,7 +20,11 @@ eval_tableform, eval_texform, ) -from mathics.eval.makeboxes.precedence import builtins_precedence, parenthesize +from mathics.eval.makeboxes.precedence import ( + builtins_precedence, + compare_precedence, + parenthesize, +) __all__ = [ "NumberForm_to_String", @@ -27,12 +32,14 @@ "StringRParen", "_boxed_string", "builtins_precedence", + "compare_precedence", "do_format", "eval_baseform", "eval_generic_makeboxes", "eval_infix", "eval_makeboxes", "eval_makeboxes_fullform", + "eval_makeboxes_outputform", "eval_mathmlform", "eval_postprefix", "eval_tableform", diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index e67b4c14a..17d16e17f 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -21,6 +21,7 @@ ) from mathics.core.systemsymbols import ( # SymbolRule, SymbolRuleDelayed, SymbolComplex, + SymbolOutputForm, SymbolRational, SymbolStandardForm, ) @@ -136,6 +137,19 @@ def int_to_string_shorter_repr(value: int, form: Symbol, max_digits=640): return String(value_str) +def eval_makeboxes_outputform(expr, evaluation, form): + """ + Build a 2D text representation of the expression. + """ + from mathics.builtin.box.layout import InterpretationBox, PaneBox + from mathics.format.outputform import expression_to_outputform_text + + text_outputform = str(expression_to_outputform_text(expr, evaluation, form)) + elem1 = PaneBox(String(text_outputform)) + elem2 = Expression(SymbolOutputForm, expr) + return InterpretationBox(elem1, elem2) + + # TODO: evaluation is needed because `atom_to_boxes` uses it. Can we remove this # argument? def eval_makeboxes_fullform( @@ -266,10 +280,29 @@ def format_element( Applies formats associated to the expression, and then calls Makeboxes """ evaluation.is_boxing = True - formatted_expr = do_format(element, evaluation, form) - if formatted_expr is None: + while element.get_head() is form: + element = element.elements[0] + + if element.has_form("FullForm", 1): + return eval_makeboxes_fullform(element.elements[0], evaluation) + + # In order to work like in WMA, `format_element` + # should evaluate `MakeBoxes[element//form, StandardForm]` + # Then, MakeBoxes[expr_, StandardForm], for any expr, + # should apply Format[...] rules, and then + # MakeBoxes[...] rules. These rules should be stored + # as FormatValues[...] + # As a first step in that direction, let's mimic this behaviour + # just for the case of OutputForm: + if element.has_form("OutputForm", 1): + return eval_makeboxes_outputform(element.elements[0], evaluation, form) + + expr = do_format(element, evaluation, form) + if expr is None: return None - result_box = eval_makeboxes(formatted_expr, evaluation, form) + + result = Expression(SymbolMakeBoxes, expr, form) + result_box = result.evaluate(evaluation) if isinstance(result_box, String): return result_box if isinstance(result_box, BoxElementMixin): diff --git a/mathics/format/outputform.py b/mathics/format/outputform.py new file mode 100644 index 000000000..0fc8309ec --- /dev/null +++ b/mathics/format/outputform.py @@ -0,0 +1,779 @@ +""" +This module builts the 2D string associated to the OutputForm +""" + +from typing import Callable, Dict, List, Union + +from mathics.core.atoms import ( + Integer, + Integer1, + Integer2, + IntegerM1, + Rational, + Real, + String, +) +from mathics.core.element import BaseElement +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Atom, Symbol, SymbolTimes +from mathics.core.systemsymbols import ( + SymbolDerivative, + SymbolInfix, + SymbolNone, + SymbolOutputForm, + SymbolPower, + SymbolStandardForm, + SymbolTraditionalForm, +) +from mathics.eval.makeboxes import compare_precedence, do_format # , format_element + +SymbolNonAssociative = Symbol("System`NonAssociative") +SymbolPostfix = Symbol("System`Postfix") +SymbolPrefix = Symbol("System`Prefix") +SymbolRight = Symbol("System`Right") +SymbolLeft = Symbol("System`Left") + + +expr_to_outputform_text_map: Dict[str, Callable] = {} + + +# This Exception if the expression should +# be processed by the default routine +class _WrongFormattedExpression(Exception): + pass + + +class IsNotGrid(Exception): + pass + + +class IsNot2DArray(Exception): + pass + + +def parenthesize(expr_str: str) -> str: + """wrap with parenthesis""" + return f"({expr_str})" + + +def bracket(expr_str: str) -> str: + """wrap with square brackets""" + return f"[{expr_str}]" + + +def grid(expr): + raise NotImplementedError + + +def expression_to_outputform_text( + expr: BaseElement, evaluation: Evaluation, form=SymbolStandardForm, **kwargs +): + """ + Build a 2d text from an `Expression` + """ + ## TODO: format the expression + format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore + + # Strip HoldForm + while format_expr.has_form("HoldForm", 1): # type: ignore + format_expr = format_expr.elements[0] + + lookup_name = format_expr.get_head().get_lookup_name() + try: + result = expr_to_outputform_text_map[lookup_name]( + format_expr, evaluation, form, **kwargs + ) + return result + except _WrongFormattedExpression: + # If the key is not present, or the execution fails for any reason, use + # the default + pass + except KeyError: + pass + return _default_expression_to_outputform_text( + format_expr, evaluation, form, **kwargs + ) + + +def _default_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + """ + Default representation of a function + """ + expr_head = expr.head + head = expression_to_outputform_text(expr_head, evaluation, form, **kwargs) + comma = ", " + elements = [ + expression_to_outputform_text(elem, evaluation) for elem in expr.elements + ] + result = elements.pop(0) if elements else " " + while elements: + result = result + comma + elements.pop(0) + + if form is SymbolTraditionalForm: + return head + parenthesize(result) + return head + bracket(result) + + +def _divide(num, den, evaluation, form, **kwargs): + infix_form = Expression( + SymbolInfix, ListExpression(num, den), String("/"), Integer(400), SymbolLeft + ) + return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) + + +def _strip_1_parm_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + if len(expr.elements) != 1: + raise _WrongFormattedExpression + return expression_to_outputform_text(expr.elements[0], evaluation, form, **kwargs) + + +expr_to_outputform_text_map[ + "System`HoldForm" +] = _strip_1_parm_expression_to_outputform_text +expr_to_outputform_text_map[ + "System`InputForm" +] = _strip_1_parm_expression_to_outputform_text + + +def derivative_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + """Derivative operator""" + head = expr.get_head() + if head is SymbolDerivative: + return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + super_head = head.get_head() + if super_head is SymbolDerivative: + expr_elements = expr.elements + if len(expr_elements) != 1: + return _default_expression_to_outputform_text( + expr, evaluation, form, **kwargs + ) + function_head = expression_to_outputform_text( + expr_elements[0], evaluation, form, **kwargs + ) + derivatives = head.elements + if len(derivatives) == 1: + order_iv = derivatives[0] + if order_iv == Integer1: + return function_head + "'" + elif order_iv == Integer2: + return function_head + "''" + + return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + + # Full Function with arguments: delegate to the default conversion. + # It will call us again with the head + return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + + +expr_to_outputform_text_map[ + "System`Derivative" +] = derivative_expression_to_outputform_text + + +def divide_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + if len(expr.elements) != 2: + raise _WrongFormattedExpression + num, den = expr.elements + return _divide(num, den, evaluation, form, **kwargs) + + +expr_to_outputform_text_map["System`Divide"] = divide_expression_to_outputform_text + + +def graphics(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: + return "-Graphics-" + + +expr_to_outputform_text_map["System`Graphics"] = graphics + + +def graphics3d(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: + return "-Graphics3D-" + + +expr_to_outputform_text_map["System`Graphics3D"] = graphics3d + + +def grid_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + if len(expr.elements) == 0: + raise IsNotGrid + if len(expr.elements) > 1 and not expr.elements[1].has_form( + ["Rule", "RuleDelayed"], 2 + ): + raise IsNotGrid + if not expr.elements[0].has_form("List", None): + raise IsNotGrid + + elements = expr.elements[0].elements + rows = [] + for idx, item in enumerate(elements): + if item.has_form("List", None): + rows.append( + [ + expression_to_outputform_text(item_elem, evaluation, form, **kwargs) + for item_elem in item.elements + ] + ) + else: + rows.append(expression_to_outputform_text(item, evaluation, form, **kwargs)) + + return grid(rows) + + +expr_to_outputform_text_map["System`Grid"] = grid_expression_to_outputform_text + + +def integer_expression_to_outputform_text( + n: Integer, evaluation: Evaluation, form: Symbol, **kwargs +): + return str(n.value) + + +expr_to_outputform_text_map["System`Integer"] = integer_expression_to_outputform_text + + +def list_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + result, *rest_elems = ( + expression_to_outputform_text(elem, evaluation, form, **kwargs) + for elem in expr.elements + ) + comma_tb = ", " + for next_elem in rest_elems: + result = result + comma_tb + next_elem + return "{" + result + "}" + + +expr_to_outputform_text_map["System`List"] = list_expression_to_outputform_text + + +def mathmlform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + # boxes = format_element(expr.elements[0], evaluation, form) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + ).evaluate(evaluation) + return boxes.boxes_to_mathml() # type: ignore[union-attr] + + +expr_to_outputform_text_map[ + "System`MathMLForm" +] = mathmlform_expression_to_outputform_text + + +def matrixform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + # return parenthesize(tableform_expression_to_outputform_text(expr, evaluation, form, **kwargs)) + return tableform_expression_to_outputform_text(expr, evaluation, form, **kwargs) + + +expr_to_outputform_text_map[ + "System`MatrixForm" +] = matrixform_expression_to_outputform_text + + +def plus_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + elements = expr.elements + result = "" + for i, elem in enumerate(elements): + if elem.has_form("Times", None): + # If the first element is -1, remove it and use + # a minus sign. Otherwise, if negative, do not add a sign. + first = elem.elements[0] + if isinstance(first, Integer): + if first.value == -1: + result = ( + result + + " - " + + expression_to_outputform_text( + Expression(SymbolTimes, *elem.elements[1:]), + evaluation, + form, + **kwargs, + ) + ) + continue + elif first.value < 0: + result = ( + result + + " " + + expression_to_outputform_text( + elem, evaluation, form, **kwargs + ) + ) + continue + elif isinstance(first, Real): + if first.value < 0: + result = ( + result + + " " + + expression_to_outputform_text( + elem, evaluation, form, **kwargs + ) + ) + continue + result = ( + result + + " + " + + expression_to_outputform_text(elem, evaluation, form, **kwargs) + ) + ## TODO: handle complex numbers? + else: + elem_txt = expression_to_outputform_text(elem, evaluation, form, **kwargs) + if (compare_precedence(elem, 310) or -1) < 0: + elem_txt = parenthesize(elem_txt) + result = result + " + " + elem_txt + elif i == 0 or ( + (isinstance(elem, Integer) and elem.value < 0) + or (isinstance(elem, Real) and elem.value < 0) + ): + result = result + elem_txt + else: + result = ( + result + + " + " + + expression_to_outputform_text(elem, evaluation, form, **kwargs) + ) + return result + + +expr_to_outputform_text_map["System`Plus"] = plus_expression_to_outputform_text + + +def power_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +): + if len(expr.elements) != 2: + raise _WrongFormattedExpression + + infix_form = Expression( + SymbolInfix, + ListExpression(*(expr.elements)), + String("^"), + Integer(590), + SymbolRight, + ) + return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) + + +expr_to_outputform_text_map["System`Power"] = power_expression_to_outputform_text + + +def pre_pos_fix_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + elements = expr.elements + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + group = None + precedence = 670 + # Processing the first argument: + head = expr.get_head() + target = expr.elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + operands = list(target.elements) + if len(operands) != 1: + raise _WrongFormattedExpression + + # Processing the second argument, if it is there: + if len(elements) > 1: + ops = elements[1] + ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolPrefix: + default_symb = " @ " + ops_txt = ( + expression_to_outputform_text(head, evaluation, form, **kwargs) + + default_symb + ) + elif head is SymbolPostfix: + default_symb = " // " + ops_txt = default_symb + expression_to_outputform_text( + head, evaluation, form, **kwargs + ) + + # Processing the third argument, if it is there: + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: + raise _WrongFormattedExpression + + # Processing the forth argument, if it is there: + if len(elements) > 3: + group = elements[3] + if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): + raise _WrongFormattedExpression + if group is SymbolNone: + group = None + + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + + return ops_txt[0] + target_txt if head is SymbolPrefix else target_txt + ops_txt[0] + + +expr_to_outputform_text_map["System`Prefix"] = pre_pos_fix_expression_to_outputform_text +expr_to_outputform_text_map[ + "System`Postfix" +] = pre_pos_fix_expression_to_outputform_text + + +def infix_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + elements = expr.elements + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + group = None + precedence = 670 + # Processing the first argument: + head = expr.get_head() + target = expr.elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + operands = list(target.elements) + + if len(operands) < 2: + raise _WrongFormattedExpression + + # Processing the second argument, if it is there: + if len(elements) > 1: + ops = elements[1] + if head is SymbolInfix: + # This is not the WMA behaviour, but the Mathics current implementation requires it: + num_ops = 1 + if ops.has_form("List", None): + num_ops = len(ops.elements) + ops_lst = [ + expression_to_outputform_text(op, evaluation, form, **kwargs) + for op in ops.elements + ] + else: + ops_lst = [ + expression_to_outputform_text(ops, evaluation, form, **kwargs) + ] + elif head in (SymbolPrefix, SymbolPostfix): + ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolInfix: + num_ops = 1 + default_symb = " ~ " + ops_lst = [ + default_symb + + expression_to_outputform_text(head, evaluation, form, **kwargs) + + default_symb + ] + elif head is SymbolPrefix: + default_symb = " @ " + ops_txt = ( + expression_to_outputform_text(head, evaluation, form, **kwargs) + + default_symb + ) + elif head is SymbolPostfix: + default_symb = " // " + ops_txt = default_symb + expression_to_outputform_text( + head, evaluation, form, **kwargs + ) + + # Processing the third argument, if it is there: + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: + raise _WrongFormattedExpression + + # Processing the forth argument, if it is there: + if len(elements) > 3: + group = elements[3] + if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): + raise _WrongFormattedExpression + if group is SymbolNone: + group = None + + if head is SymbolPrefix: + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + return ops_txt[0] + target_txt + if head is SymbolPostfix: + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + return target_txt + ops_txt[0] + else: # Infix + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + for index, operand in enumerate(operands): + operand_txt = str( + expression_to_outputform_text(operand, evaluation, form, **kwargs) + ) + cmp_precedence = compare_precedence(operand, precedence) + if cmp_precedence is not None and ( + cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) + ): + operand_txt = parenthesize(operand_txt) + + if index == 0: + result = operand_txt + # After the first element, for lateral + # associativity, parenthesized is flipped: + if group in (SymbolLeft, SymbolRight): + parenthesized = not parenthesized + else: + space = " " + result_lst: List[str] + if str(ops_lst[index % num_ops]) != " ": + result_lst = [ + result, + space, + str(ops_lst[index % num_ops]), + space, + operand_txt, + ] + else: + result_lst = [result, space, operand_txt] + + return "".join(result_lst) + + +expr_to_outputform_text_map["System`Infix"] = infix_expression_to_outputform_text + + +def precedenceform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + if len(expr.elements) == 2: + return expression_to_outputform_text( + expr.elements[0], evaluation, form, **kwargs + ) + raise _WrongFormattedExpression + + +expr_to_outputform_text_map[ + "System`PrecedenceForm" +] = precedenceform_expression_to_outputform_text + + +def rational_expression_to_outputform_text( + n: Union[Rational, Expression], evaluation: Evaluation, form: Symbol, **kwargs +): + if n.has_form("Rational", 2): + num, den = n.elements # type: ignore[union-attr] + else: + num, den = n.numerator(), n.denominator() # type: ignore[union-attr] + return _divide(num, den, evaluation, form, **kwargs) + + +expr_to_outputform_text_map["System`Rational"] = rational_expression_to_outputform_text + + +def real_expression_to_outputform_text( + n: Real, evaluation: Evaluation, form: Symbol, **kwargs +): + str_n = n.make_boxes("System`OutputForm").boxes_to_text() # type: ignore[attr-defined] + return str(str_n) + + +expr_to_outputform_text_map["System`Real"] = real_expression_to_outputform_text + + +def string_expression_to_outputform_text( + expr: String, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + lines = expr.value.split("\n") + max_len = max([len(line) for line in lines]) + lines = [line + (max_len - len(line)) * " " for line in lines] + return "\n".join(lines) + + +expr_to_outputform_text_map["System`String"] = string_expression_to_outputform_text + + +def stringform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + strform = expr.elements[0] + if not isinstance(strform, String): + raise _WrongFormattedExpression + + items = list( + expression_to_outputform_text(item, evaluation, form, **kwargs) + for item in expr.elements[1:] + ) + + curr_indx = 0 + parts = strform.value.split("`") + result = str(parts[0]) + if len(parts) == 1: + return result + + quote_open = True + remaining = len(parts) - 1 + + for part in parts[1:]: + remaining -= 1 + if quote_open: + if remaining == 0: + result = result + "`" + part + quote_open = False + continue + if len(part) == 0: + result = result + items[curr_indx] + continue + try: + idx = int(part) + except ValueError: + idx = None + if idx is not None and str(idx) == part: + curr_indx = idx - 1 + result = result + items[curr_indx] + quote_open = False + continue + else: + result = result + "`" + part + "`" + quote_open = False + continue + else: + result = result + part + quote_open = True + + return result + + +expr_to_outputform_text_map[ + "System`StringForm" +] = stringform_expression_to_outputform_text + + +def symbol_expression_to_outputform_text( + symb: Symbol, evaluation: Evaluation, form: Symbol, **kwargs +): + return evaluation.definitions.shorten_name(symb.name) + + +expr_to_outputform_text_map["System`Symbol"] = symbol_expression_to_outputform_text + + +def tableform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + return grid_expression_to_outputform_text(expr, evaluation, form) + + +expr_to_outputform_text_map[ + "System`TableForm" +] = tableform_expression_to_outputform_text + + +def texform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + # boxes = format_element(expr.elements[0], evaluation, form) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + ).evaluate(evaluation) + return boxes.boxes_to_tex() # type: ignore + + +expr_to_outputform_text_map["System`TeXForm"] = texform_expression_to_outputform_text + + +def times_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + elements = expr.elements + num: List[BaseElement] = [] + den: List[BaseElement] = [] + # First, split factors with integer, negative powers: + for elem in elements: + if elem.has_form("Power", 2): + base, exponent = elem.elements + if isinstance(exponent, Integer): + if exponent.value == -1: + den.append(base) + continue + elif exponent.value < 0: + den.append(Expression(SymbolPower, base, Integer(-exponent.value))) + continue + elif isinstance(elem, Rational): + num.append(elem.numerator()) + den.append(elem.denominator()) + continue + elif elem.has_form("Rational", 2): + elem_elements = elem.elements + num.append(elem_elements[0]) + den.append(elem_elements[1]) + continue + + num.append(elem) + + # If there are integer, negative powers, process as a fraction: + if den: + den_expr = den[0] if len(den) == 1 else Expression(SymbolTimes, *den) + num_expr = ( + Expression(SymbolTimes, *num) + if len(num) > 1 + else num[0] + if len(num) == 1 + else Integer1 + ) + return _divide(num_expr, den_expr, evaluation, form, **kwargs) + + # there are no integer negative powers: + if len(num) == 1: + return expression_to_outputform_text(num[0], evaluation, form, **kwargs) + + prefactor = 1 + result: str = "" + for i, elem in enumerate(num): + if elem is IntegerM1: + prefactor *= -1 + continue + if isinstance(elem, Integer): + prefactor *= -1 + elem = Integer(-elem.value) + + elem_txt = expression_to_outputform_text(elem, evaluation, form, **kwargs) + if compare_precedence(elem, 400): + elem_txt = parenthesize(elem_txt) + if i == 0: + result = elem_txt + else: + result = result + " " + elem_txt + if result == "": + result = "1" + if prefactor == -1: + result = "-" + result + return result + + +expr_to_outputform_text_map["System`Times"] = times_expression_to_outputform_text From 0e7a3a40dfa9787b36434b6af087b037a29dcdec Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 27 Dec 2025 14:40:11 -0300 Subject: [PATCH 07/49] reduce trivial changes --- mathics/eval/makeboxes/makeboxes.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 17d16e17f..84f7074e0 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -297,12 +297,11 @@ def format_element( if element.has_form("OutputForm", 1): return eval_makeboxes_outputform(element.elements[0], evaluation, form) - expr = do_format(element, evaluation, form) - if expr is None: + formatted_expr = do_format(element, evaluation, form) + if formatted_expr is None: return None - result = Expression(SymbolMakeBoxes, expr, form) - result_box = result.evaluate(evaluation) + result_box = eval_makeboxes(formatted_expr, evaluation, form) if isinstance(result_box, String): return result_box if isinstance(result_box, BoxElementMixin): From 4cd237bbf2a3ebbfe09b5144aefd355c0384a435 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 28 Dec 2025 10:28:46 -0300 Subject: [PATCH 08/49] Update mathics/eval/makeboxes/makeboxes.py Co-authored-by: R. Bernstein --- mathics/eval/makeboxes/makeboxes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 84f7074e0..54a3eca82 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -279,6 +279,7 @@ def format_element( """ Applies formats associated to the expression, and then calls Makeboxes """ + # Halt any potential evaluation tracing while performing boxing. evaluation.is_boxing = True while element.get_head() is form: element = element.elements[0] From 89f506361dbc74505df08f3cac3078363171f5f7 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 28 Dec 2025 10:28:55 -0300 Subject: [PATCH 09/49] Update mathics/eval/makeboxes/makeboxes.py Co-authored-by: R. Bernstein --- mathics/eval/makeboxes/makeboxes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 54a3eca82..967e53a3a 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -139,7 +139,7 @@ def int_to_string_shorter_repr(value: int, form: Symbol, max_digits=640): def eval_makeboxes_outputform(expr, evaluation, form): """ - Build a 2D text representation of the expression. + Build a 2D representation of the expression using only keyboard characters. """ from mathics.builtin.box.layout import InterpretationBox, PaneBox from mathics.format.outputform import expression_to_outputform_text From 72eb41a5dd7032fd2bb52c875d5799b3d1168e1a Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 28 Dec 2025 11:16:25 -0300 Subject: [PATCH 10/49] system symbols --- mathics/format/outputform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mathics/format/outputform.py b/mathics/format/outputform.py index 0fc8309ec..b322f0e49 100644 --- a/mathics/format/outputform.py +++ b/mathics/format/outputform.py @@ -21,9 +21,11 @@ from mathics.core.systemsymbols import ( SymbolDerivative, SymbolInfix, + SymbolLeft, SymbolNone, SymbolOutputForm, SymbolPower, + SymbolRight, SymbolStandardForm, SymbolTraditionalForm, ) @@ -32,8 +34,6 @@ SymbolNonAssociative = Symbol("System`NonAssociative") SymbolPostfix = Symbol("System`Postfix") SymbolPrefix = Symbol("System`Prefix") -SymbolRight = Symbol("System`Right") -SymbolLeft = Symbol("System`Left") expr_to_outputform_text_map: Dict[str, Callable] = {} From 4810a6c93b284e2eb8fbd5f2e71bff79a39a0d69 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 29 Dec 2025 23:07:29 -0300 Subject: [PATCH 11/49] tests for MakeBoxes --- test/builtin/test_makeboxes.py | 109 +++++++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/test/builtin/test_makeboxes.py b/test/builtin/test_makeboxes.py index c0a853332..85eac3052 100644 --- a/test/builtin/test_makeboxes.py +++ b/test/builtin/test_makeboxes.py @@ -9,9 +9,95 @@ DEBUGMAKEBOXES = int(os.environ.get("DEBUGMAKEBOXES", "0")) == 1 if DEBUGMAKEBOXES: - skip_or_fail = pytest.mark.xfail + + def skip_or_fail(x): + return x + else: - skip_or_fail = pytest.mark.skip + skip_or_fail = pytest.mark.xfail + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "fail_msg", "msgs"), + [ + ("MakeBoxes[x]", '"x"', "StandardForm"), + ( + "MakeBoxes[InputForm[x]]", + 'InterpretationBox[StyleBox["x", ShowStringCharacters -> True, NumberMarks -> True], InputForm[x], Editable -> True, AutoDelete -> True]', + "InputForm, expression", + ), + ( + "MakeBoxes[OutputForm[x]]", + 'InterpretationBox[PaneBox[""x""], OutputForm[x], Editable -> False]', + "OutputForm, expression", + ), + ( + "MakeBoxes[FullForm[x]]", + 'TagBox[StyleBox["x", ShowSpecialCharacters -> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]', + "MakeBoxes expression FullForm", + ), + ( + "MakeBoxes[TeXForm[x]]", + 'InterpretationBox[""x"", TeXForm[x], Editable -> True, AutoDelete -> True]', + "TeXForm, expression", + ), + # Basic Expressions + ("MakeBoxes[F[x]]", 'RowBox[{"F", "[", "x", "]"}]', "StandardForm"), + ( + "MakeBoxes[InputForm[F[x]]]", + 'InterpretationBox[StyleBox["F[x]", ShowStringCharacters -> True, NumberMarks -> True], InputForm[F[x]], Editable -> True, AutoDelete -> True]', + "InputForm, expression", + ), + ( + "MakeBoxes[OutputForm[F[x]]]", + 'InterpretationBox[PaneBox[""F[x]""], OutputForm[F[x]], Editable -> False]', + "OutputForm, expression", + ), + ( + "MakeBoxes[FullForm[F[x]]]", + 'TagBox[StyleBox[RowBox[{"F", "[", "x", "]"}], ShowSpecialCharacters -> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]', + "MakeBoxes expression FullForm", + ), + ( + "MakeBoxes[TeXForm[F[x]]]", + 'InterpretationBox[""F(x)"", TeXForm[F[x]], Editable -> True, AutoDelete -> True]', + "TeXForm, expression", + ), + # Arithmetic + ("MakeBoxes[a-b]", 'RowBox[{"a", "-", "b"}]', "difference, StandardForm"), + ( + "MakeBoxes[a-b//InputForm]", + 'InterpretationBox[StyleBox["a - b", ShowStringCharacters -> True, NumberMarks -> True], InputForm[a - b], Editable -> True, AutoDelete -> True]', + "difference, InputForm", + ), + ( + "MakeBoxes[a-b//OutputForm]", + 'InterpretationBox[PaneBox[""a - b""], OutputForm[a - b], Editable -> False]', + "difference, OutputForm", + ), + ( + "MakeBoxes[a-b//FullForm]", + 'TagBox[StyleBox[RowBox[{"Plus", "[", RowBox[{"a", ",", RowBox[{"Times", "[", RowBox[{RowBox[{"-", "1"}], ",", "b"}], "]"}]}], "]"}], ShowSpecialCharacters -> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]', + "Difference, FullForm", + ), + ( + "MakeBoxes[a-b//TeXForm]", + ' InterpretationBox[""a-b"", TeXForm[a-b], Editable -> True, AutoDelete -> True]', + "Difference, TeXForm", + ), + ], +) +@skip_or_fail +def test_makeboxes_basic_forms(str_expr, str_expected, fail_msg, msgs): + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=fail_msg, + expected_messages=msgs, + ) @pytest.mark.parametrize( @@ -79,8 +165,22 @@ def test_makeboxes_real(str_expr, str_expected, msg): @pytest.mark.parametrize( ("str_expr", "str_expected", "msg"), [ - (r"MakeBoxes[1.4]", r"1.4`", None), - (r"MakeBoxes[1.4`]", r"1.4`", None), + (r"MakeBoxes[1.4`]", r"1.4`", "StandardForm always shows a prec mark."), + ( + r"MakeBoxes[OutputForm[1.4]]", + r'InterpretationBox[PaneBox["1.4"], 1.4, Editable -> False]', + "MachineReal, OutputForm", + ), + ( + r"MakeBoxes[3.142`3]", + r"3.142`3", + "StandadForm with Prec real shows all the stored digits, and prec", + ), + ( + r"MakeBoxes[OutputForm[3.142`3]]", + r'InterpretationBox[PaneBox["3.14"], 3.14, Editable -> False]', + "OutputForm trims digits up to prec.", + ), (r"MakeBoxes[1.5`20]", r"1.5`20.", None), (r"MakeBoxes[1.4`20]", r"1.4`20.", None), (r"MakeBoxes[1.5``20]", r"1.5`20.1760912591", None), @@ -88,7 +188,6 @@ def test_makeboxes_real(str_expr, str_expected, msg): (r"MakeBoxes[34.*^3]", r"34000.`", None), (r"MakeBoxes[0`]", r"0.`", None), (r"MakeBoxes[0``30]", r"0.``30.", None), - (r"MakeBoxes[0.`]", r"0.`", None), (r"MakeBoxes[0.`3]", r"0.`", None), (r"MakeBoxes[0.``30]", r"0.``30.", None), (r"MakeBoxes[-14]", r"RowBox[{-, 14}]", None), From 794ca77ea16863e51b9ce90b49609110baf91119 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 29 Dec 2025 23:26:30 -0300 Subject: [PATCH 12/49] add tests for the structure --- test/builtin/test_forms.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/builtin/test_forms.py b/test/builtin/test_forms.py index 1bc31a0c5..267e9753c 100644 --- a/test/builtin/test_forms.py +++ b/test/builtin/test_forms.py @@ -3,11 +3,32 @@ Unit tests from mathics.builtin.forms. """ -from test.helper import check_evaluation +from test.helper import check_evaluation, session import pytest +@pytest.mark.parametrize( + ("expr", "form", "head", "subhead"), + [ + ("x", "InputForm", "InterpretationBox", "StyleBox"), + ("x", "OutputForm", "InterpretationBox", "PaneBox"), + ("x", "TeXForm", "InterpretationBox", "String"), + ("x", "StandardForm", "TagForm", "FormBox"), + ("x", "FullForm", "TagBox", "StyleBox"), + ], +) +@pytest.mark.xfail +def test_makeboxes_form(expr, form, head, subhead): + """ + Check the structure of the result of MakeBoxes + on expressions with different forms. + """ + expr = session.evaluate("MakeBoxes[{form}[{expr}]]") + assert expr.get_head_name() == f"System`{head}" + assert expr.elements[0].get_head_name() == f"System`{subhead}" + + @pytest.mark.parametrize( ("str_expr", "msgs", "str_expected", "fail_msg"), [ From 65a1656c64fcc755d6da62c834078612166c121c Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Tue, 30 Dec 2025 00:04:00 -0300 Subject: [PATCH 13/49] another round. --- mathics/builtin/forms/output.py | 8 ++++++-- mathics/builtin/makeboxes.py | 2 +- mathics/eval/makeboxes/makeboxes.py | 3 +-- test/builtin/box/test_custom_boxexpression.py | 7 +++++++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index 6c48f04d9..e8d0c3b8c 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -14,7 +14,7 @@ """ from typing import Optional -from mathics.builtin.box.layout import RowBox +from mathics.builtin.box.layout import InterpretationBox, RowBox from mathics.builtin.forms.base import FormBaseClass from mathics.core.atoms import Integer, Real, String, StringFromPython from mathics.core.builtin import Builtin @@ -28,6 +28,7 @@ SymbolInfinity, SymbolMakeBoxes, SymbolNumberForm, + SymbolOutputForm, SymbolRowBox, SymbolRuleDelayed, SymbolSuperscriptBox, @@ -496,7 +497,10 @@ class OutputForm(FormBaseClass): def eval_makeboxes(self, expr, form, evaluation): """MakeBoxes[OutputForm[expr_], form_]""" - return eval_makeboxes_outputform(expr, evaluation, form) + pane = eval_makeboxes_outputform(expr, evaluation, form) + return InterpretationBox( + pane, Expression(SymbolOutputForm, expr), **{"System`Editable": SymbolFalse} + ) class PythonForm(FormBaseClass): diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index 2cf811b3f..8a551e2db 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -96,7 +96,7 @@ class MakeBoxes(Builtin): 'MakeBoxes[Infix[head[elements], StringForm["~`1`~", head]], f]' ), "MakeBoxes[expr_]": "MakeBoxes[expr, StandardForm]", - "MakeBoxes[(form:StandardForm|TraditionalForm|OutputForm|TeXForm|" + "MakeBoxes[(form:StandardForm|TraditionalForm|TeXForm|" "MathMLForm)[expr_], StandardForm|TraditionalForm]": ("MakeBoxes[expr, form]"), "MakeBoxes[(form:StandardForm|OutputForm|MathMLForm|TeXForm)[expr_], OutputForm]": "MakeBoxes[expr, form]", "MakeBoxes[(form:FullForm|InputForm)[expr_], StandardForm|TraditionalForm|OutputForm]": "StyleBox[MakeBoxes[expr, form], ShowStringCharacters->True]", diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 8fd018a46..6a3848fde 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -146,8 +146,7 @@ def eval_makeboxes_outputform(expr, evaluation, form): text_outputform = str(expression_to_outputform_text(expr, evaluation, form)) elem1 = PaneBox(String(text_outputform)) - elem2 = Expression(SymbolOutputForm, expr) - return InterpretationBox(elem1, elem2) + return elem1 # TODO: evaluation is needed because `atom_to_boxes` uses it. Can we remove this diff --git a/test/builtin/box/test_custom_boxexpression.py b/test/builtin/box/test_custom_boxexpression.py index d7bcb8afe..61feae319 100644 --- a/test/builtin/box/test_custom_boxexpression.py +++ b/test/builtin/box/test_custom_boxexpression.py @@ -67,6 +67,13 @@ def eval_box(self, expr, evaluation: Evaluation, options: dict): instance = CustomGraphicsBox(*(expr.elements), evaluation=evaluation) return instance + def eval_box_outputForm(self, expr, evaluation: Evaluation, options: dict): + """System`MakeBoxes[System`OutputForm[System`Graphics[System`expr_, System`OptionsPattern[System`Graphics]]], + System`StandardForm|System`TraditionalForm]""" + print("MakeBoxes OutputForm") + instance = CustomGraphicsBox(*(expr.elements), evaluation=evaluation) + return instance + def boxes_to_text(self, elements=None, **options): if elements: self._elements = elements From a4963b67b467a4bc02f3dbc419ed4778864408c9 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Tue, 30 Dec 2025 00:08:19 -0300 Subject: [PATCH 14/49] fix parametrize --- test/builtin/test_makeboxes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/builtin/test_makeboxes.py b/test/builtin/test_makeboxes.py index 85eac3052..253817e41 100644 --- a/test/builtin/test_makeboxes.py +++ b/test/builtin/test_makeboxes.py @@ -18,7 +18,7 @@ def skip_or_fail(x): @pytest.mark.parametrize( - ("str_expr", "str_expected", "fail_msg", "msgs"), + ("str_expr", "str_expected", "fail_msg"), [ ("MakeBoxes[x]", '"x"', "StandardForm"), ( From e93512e347558633ce670ce5be55a31f3d35fc4f Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Wed, 31 Dec 2025 12:05:37 -0300 Subject: [PATCH 15/49] basic implementation for OutputForm grid --- mathics/format/outputform.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/mathics/format/outputform.py b/mathics/format/outputform.py index b322f0e49..f57f3a712 100644 --- a/mathics/format/outputform.py +++ b/mathics/format/outputform.py @@ -64,7 +64,20 @@ def bracket(expr_str: str) -> str: def grid(expr): - raise NotImplementedError + # Very basic implementation. + result = "Grid[" + for row in expr: + if result[-1] != "[": + result += "," + + result += "\n{" + for field in row: + if result[-1] != "}": + result += "," + result += field + result += "}" + result += "}]" + return result def expression_to_outputform_text( From 2948b449098d169e75e89b840509d19d7c640982 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Wed, 31 Dec 2025 12:08:43 -0300 Subject: [PATCH 16/49] fix typos --- test/builtin/test_makeboxes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/builtin/test_makeboxes.py b/test/builtin/test_makeboxes.py index 253817e41..76f2f62fd 100644 --- a/test/builtin/test_makeboxes.py +++ b/test/builtin/test_makeboxes.py @@ -165,7 +165,7 @@ def test_makeboxes_real(str_expr, str_expected, msg): @pytest.mark.parametrize( ("str_expr", "str_expected", "msg"), [ - (r"MakeBoxes[1.4`]", r"1.4`", "StandardForm always shows a prec mark."), + (r"MakeBoxes[1.4`]", r"1.4`", "StandardForm always shows a precision mark."), ( r"MakeBoxes[OutputForm[1.4]]", r'InterpretationBox[PaneBox["1.4"], 1.4, Editable -> False]', @@ -174,12 +174,12 @@ def test_makeboxes_real(str_expr, str_expected, msg): ( r"MakeBoxes[3.142`3]", r"3.142`3", - "StandadForm with Prec real shows all the stored digits, and prec", + "StandadForm with PrecisionReal shows all the stored digits, and precision", ), ( r"MakeBoxes[OutputForm[3.142`3]]", r'InterpretationBox[PaneBox["3.14"], 3.14, Editable -> False]', - "OutputForm trims digits up to prec.", + "OutputForm trims digits up to precision.", ), (r"MakeBoxes[1.5`20]", r"1.5`20.", None), (r"MakeBoxes[1.4`20]", r"1.4`20.", None), From f4a8d6065eef2718039a6c57d1b33216feb95656 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Wed, 31 Dec 2025 12:09:57 -0300 Subject: [PATCH 17/49] more typos --- test/builtin/test_makeboxes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/builtin/test_makeboxes.py b/test/builtin/test_makeboxes.py index 253817e41..79833e144 100644 --- a/test/builtin/test_makeboxes.py +++ b/test/builtin/test_makeboxes.py @@ -165,7 +165,7 @@ def test_makeboxes_real(str_expr, str_expected, msg): @pytest.mark.parametrize( ("str_expr", "str_expected", "msg"), [ - (r"MakeBoxes[1.4`]", r"1.4`", "StandardForm always shows a prec mark."), + (r"MakeBoxes[1.4`]", r"1.4`", "StandardForm always shows a precision mark."), ( r"MakeBoxes[OutputForm[1.4]]", r'InterpretationBox[PaneBox["1.4"], 1.4, Editable -> False]', @@ -174,12 +174,12 @@ def test_makeboxes_real(str_expr, str_expected, msg): ( r"MakeBoxes[3.142`3]", r"3.142`3", - "StandadForm with Prec real shows all the stored digits, and prec", + "StandadForm with PrecisionReal shows all the stored digits, and precision.", ), ( r"MakeBoxes[OutputForm[3.142`3]]", r'InterpretationBox[PaneBox["3.14"], 3.14, Editable -> False]', - "OutputForm trims digits up to prec.", + "OutputForm trims digits up to precision.", ), (r"MakeBoxes[1.5`20]", r"1.5`20.", None), (r"MakeBoxes[1.4`20]", r"1.4`20.", None), From b56e59af507f57d7e29ab0200a13999b332835f7 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 2 Jan 2026 18:06:50 -0300 Subject: [PATCH 18/49] move more tests to the yaml file --- mathics/builtin/box/expression.py | 2 +- mathics/builtin/forms/output.py | 24 +- mathics/builtin/makeboxes.py | 2 +- mathics/format/latex.py | 10 + mathics/format/mathml.py | 10 + mathics/format/text.py | 8 + test/format/convert_yaml2json.py | 25 ++ test/format/{test.m => format_test.m} | 0 test/format/makeboxes_tests.json | 1 + test/format/makeboxes_tests.m | 42 ++++ test/format/makeboxes_tests.yaml | 261 ++++++++++++++++++++ test/{builtin => format}/test_makeboxes.py | 272 ++++----------------- 12 files changed, 429 insertions(+), 228 deletions(-) create mode 100755 test/format/convert_yaml2json.py rename test/format/{test.m => format_test.m} (100%) create mode 100644 test/format/makeboxes_tests.json create mode 100755 test/format/makeboxes_tests.m create mode 100644 test/format/makeboxes_tests.yaml rename test/{builtin => format}/test_makeboxes.py (66%) diff --git a/mathics/builtin/box/expression.py b/mathics/builtin/box/expression.py index 142b1a533..ad6b3b32c 100644 --- a/mathics/builtin/box/expression.py +++ b/mathics/builtin/box/expression.py @@ -194,7 +194,7 @@ def replace_vars(self, vars, options=None, in_scoping=True, in_function=True): def sameQ(self, expr) -> bool: """Mathics SameQ""" - return expr.sameQ(self) + return expr.sameQ(self.to_expression()) def tex_block(self, tex, only_subsup=False): if len(tex) == 1: diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index 021440fc4..5225fabb3 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -14,7 +14,7 @@ """ from typing import Optional -from mathics.builtin.box.layout import RowBox +from mathics.builtin.box.layout import RowBox, StyleBox, TagBox from mathics.builtin.forms.base import FormBaseClass from mathics.core.atoms import Integer, Real, String, StringFromPython from mathics.core.builtin import Builtin @@ -22,7 +22,13 @@ from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.number import dps -from mathics.core.symbols import Symbol, SymbolFalse, SymbolNull, SymbolTrue +from mathics.core.symbols import ( + Symbol, + SymbolFalse, + SymbolFullForm, + SymbolNull, + SymbolTrue, +) from mathics.core.systemsymbols import ( SymbolAutomatic, SymbolInfinity, @@ -37,6 +43,7 @@ StringLParen, StringRParen, eval_baseform, + eval_makeboxes_fullform, eval_mathmlform, eval_tableform, eval_texform, @@ -124,6 +131,19 @@ class FullForm(FormBaseClass): in_printforms = True summary_text = "underlying M-Expression representation" + def eval_makeboxes(self, expr, fmt, evaluation): + """MakeBoxes[FullForm[expr_], fmt_]""" + fullform_box = eval_makeboxes_fullform(expr, evaluation) + style_box = StyleBox( + fullform_box, + **{ + "System`ShowSpecialCharacters": SymbolFalse, + "System`ShowStringCharacters": SymbolTrue, + "System`NumberMarks": SymbolTrue, + }, + ) + return TagBox(style_box, SymbolFullForm) + class MathMLForm(FormBaseClass): """ diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index 2cf811b3f..fd037ab6f 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -99,7 +99,7 @@ class MakeBoxes(Builtin): "MakeBoxes[(form:StandardForm|TraditionalForm|OutputForm|TeXForm|" "MathMLForm)[expr_], StandardForm|TraditionalForm]": ("MakeBoxes[expr, form]"), "MakeBoxes[(form:StandardForm|OutputForm|MathMLForm|TeXForm)[expr_], OutputForm]": "MakeBoxes[expr, form]", - "MakeBoxes[(form:FullForm|InputForm)[expr_], StandardForm|TraditionalForm|OutputForm]": "StyleBox[MakeBoxes[expr, form], ShowStringCharacters->True]", + "MakeBoxes[(form:InputForm)[expr_], StandardForm|TraditionalForm|OutputForm]": "StyleBox[MakeBoxes[expr, form], ShowStringCharacters->True]", "MakeBoxes[PrecedenceForm[expr_, prec_], f_]": "MakeBoxes[expr, f]", "MakeBoxes[Style[expr_, OptionsPattern[Style]], f_]": ( "StyleBox[MakeBoxes[expr, f], " diff --git a/mathics/format/latex.py b/mathics/format/latex.py index e324c92b2..e1eb4c482 100644 --- a/mathics/format/latex.py +++ b/mathics/format/latex.py @@ -27,6 +27,7 @@ SubscriptBox, SubsuperscriptBox, SuperscriptBox, + TagBox, ) from mathics.builtin.colors.color_directives import RGBColor from mathics.core.atoms import String @@ -629,3 +630,12 @@ def graphics3dbox(self, elements=None, **options) -> str: add_conversion_fn(Graphics3DBox, graphics3dbox) + + +def tag_box(self, **options): + return lookup_conversion_method(self.elements[0], "latex")( + self.elements[0], **options + ) + + +add_conversion_fn(TagBox, tag_box) diff --git a/mathics/format/mathml.py b/mathics/format/mathml.py index 975f3c05e..149d5fac8 100644 --- a/mathics/format/mathml.py +++ b/mathics/format/mathml.py @@ -23,6 +23,7 @@ SubscriptBox, SubsuperscriptBox, SuperscriptBox, + TagBox, ) from mathics.core.atoms import String from mathics.core.element import BoxElementMixin @@ -367,3 +368,12 @@ def graphics3dbox(self, elements=None, **options) -> str: add_conversion_fn(Graphics3DBox, graphics3dbox) + + +def tag_box(self, **options): + return lookup_conversion_method(self.elements[0], "mathml")( + self.elements[0], **options + ) + + +add_conversion_fn(TagBox, tag_box) diff --git a/mathics/format/text.py b/mathics/format/text.py index 664c14761..a81b0e270 100644 --- a/mathics/format/text.py +++ b/mathics/format/text.py @@ -17,6 +17,7 @@ SubscriptBox, SubsuperscriptBox, SuperscriptBox, + TagBox, ) from mathics.core.atoms import String from mathics.core.exceptions import BoxConstructError @@ -251,3 +252,10 @@ def graphics3dbox(self, elements=None, **options) -> str: add_conversion_fn(Graphics3DBox, graphics3dbox) + + +def tag_box(self, **options): + return boxes_to_text(self.elements[0], **options) + + +add_conversion_fn(TagBox, tag_box) diff --git a/test/format/convert_yaml2json.py b/test/format/convert_yaml2json.py new file mode 100755 index 000000000..ff5b74309 --- /dev/null +++ b/test/format/convert_yaml2json.py @@ -0,0 +1,25 @@ +#!/bin/env python +""" +Convert a YAML file into a JSON file. +""" + + +import json +import sys + +import yaml + + +def main(): + filename = sys.argv[1] + name, ext = filename.split(".") + assert ext.upper() == "YAML" + with open(filename, "r") as strm: + test_dict = yaml.safe_load(strm) + with open(f"{name}.json", "w") as strm: + json.dump(test_dict, strm) + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/test/format/test.m b/test/format/format_test.m similarity index 100% rename from test/format/test.m rename to test/format/format_test.m diff --git a/test/format/makeboxes_tests.json b/test/format/makeboxes_tests.json new file mode 100644 index 000000000..8c63b1a7c --- /dev/null +++ b/test/format/makeboxes_tests.json @@ -0,0 +1 @@ +{"Basic Forms": {"Arithmetic": {"FullForm": {"expect": "TagBox[StyleBox[RowBox[{\"Plus\", \"[\", RowBox[{\"a\", \",\", RowBox[{\"Times\", \"[\", RowBox[{RowBox[{\"-\", \"1\"}], \",\", \"b\"}], \"]\"}]}], \"]\"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[a-b//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"a - b\", ShowStringCharacters -> True, NumberMarks-> True], InputForm[a - b], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[a-b//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"a - b\\\"\"], OutputForm[a - b], Editable-> False]", "expr": "MakeBoxes[a-b//OutputForm]"}, "StandardForm": {"expect": "RowBox[{\"a\", \"-\", \"b\"}]", "expr": "MakeBoxes[a-b]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"a-b\\\"\", TeXForm[a-b], Editable -> True, AutoDelete-> True]", "expr": "MakeBoxes[a-b//TeXForm]"}}, "Expression": {"Format": {"expect": "TagBox[FormBox[RowBox[List[\"F\", \"[\", \"x\", \"]\"]], StandardForm], (Format[#1, StandardForm])&]", "expr": "MakeBoxes[Format[F[x], StandardForm]]"}, "TraditionalForm": {"expect": "TagBox[FormBox[RowBox[List[\"F\", \"(\", \"x\", \")\"]], TraditionalForm], TraditionalForm, Editable-> True]", "expr": "MakeBoxes[F[x]//TraditionalForm]"}, "FullForm": {"expect": "TagBox[StyleBox[RowBox[{\"F\", \"[\", \"x\", \"]\"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[F[x]//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"F[x]\", ShowStringCharacters -> True, NumberMarks-> True], InputForm[F[x]], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[F[x]//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"F[x]\\\"\"], OutputForm[F[x]], Editable ->False]", "expr": "MakeBoxes[F[x]//OutputForm]"}, "StandardForm": {"expect": "RowBox[{\"F\", \"[\", \"x\", \"]\"}]", "expr": "MakeBoxes[F[x]]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"F(x)\\\"\", TeXForm[F[x]], Editable -> True, AutoDelete-> True]", "expr": "MakeBoxes[F[x]//TeXForm]"}}, "Integer_negative": {"FullForm": {"expect": "TagBox[StyleBox[RowBox[{\"-\", \"14\"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[-14//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"-14\", ShowStringCharacters -> True, NumberMarks -> True], InputForm[-14], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[-14//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"-14\\\"\"], OutputForm[-14], Editable -> False]", "expr": "MakeBoxes[-14//OutputForm]"}, "StandardForm": {"expect": "RowBox[{\"-\", \"14\"}]", "expr": "MakeBoxes[-14]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"-14\\\"\", TeXForm[-14], Editable -> True, AutoDelete-> True]", "expr": "MakeBoxes[-14//TeXForm]"}}, "Integer_positive": {"FullForm": {"expect": "TagBox[StyleBox[\"14\", ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[14//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"14\", ShowStringCharacters -> True, NumberMarks-> True], InputForm[14], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[14//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"14\\\"\"], OutputForm[14], Editable -> False]", "expr": "MakeBoxes[14//OutputForm]"}, "StandardForm": {"expect": "14", "expr": "MakeBoxes[14]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"14\\\"\", TeXForm[14], Editable -> True, AutoDelete-> True]", "expr": "MakeBoxes[14//TeXForm]"}}, "PrecisionReal": {"FullForm": {"expect": "TagBox[StyleBox[RowBox[{\"-\", \"14.`3.\"}], ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[-14.`3//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"-14.`3\", ShowStringCharacters -> True, NumberMarks-> True], InputForm[-14.`3], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[-14.`3//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"-14.0\\\"\"], OutputForm[-14.0], Editable-> False]", "expr": "MakeBoxes[-14.0//OutputForm]"}, "StandardForm": {"expect": "RowBox[{\"-\",\"14.`3\"}]", "expr": "MakeBoxes[-14.`3]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"-14.`3\\\"\", TeXForm[-14.`3], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[14.`3//TeXForm]"}}, "Symbol": {"FullForm": {"expect": "TagBox[StyleBox[\"x\", ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[x//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"x\", ShowStringCharacters -> True, NumberMarks-> True], InputForm[x], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[x//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"x\\\"\"], OutputForm[x], Editable -> False]", "expr": "MakeBoxes[x//OutputForm]"}, "StandardForm": {"expect": "\"x\"", "expr": "MakeBoxes[x]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"x\\\"\", TeXForm[x], Editable -> True, AutoDelete-> True]", "expr": "MakeBoxes[x//TeXForm]"}}}, "Numbers": {"MachineReal, positive": {"StandardForm": {"expr": "MakeBoxes[1.4`]", "expect": "\"1.4`\"", "msg": "StandardForm always shows a precision mark."}, "OutputForm": {"expr": "MakeBoxes[1.4`//OutputForm]", "expect": "InterpretationBox[PaneBox[\"\\\"1.4\\\"\"], OutputForm[1.4], Editable -> False]", "msg": "MachineReal, OutputForm."}}, "MachineReal, negative": {"StandardForm": {"expr": "MakeBoxes[-1.4`]", "expect": "RowBox[{\"-\", \"1.4`\"}]", "msg": "StandardForm always shows a precision mark."}, "OutputForm": {"expr": "MakeBoxes[-1.4`//OutputForm]", "expect": "InterpretationBox[PaneBox[\"\\\"-1.4\\\"\"], OutputForm[-1.4], Editable -> False]", "msg": "MachineReal, negative, OutputForm."}}, "MachineReal, Large": {"StandardForm": {"expr": "MakeBoxes[34.*^3]", "expect": "\"34000.`\""}}, "MachineReal, Very Large": {"StandardForm": {"expr": "MakeBoxes[34.*^9]", "expect": "\"3.4`*^10\""}}, "PrecisionReal, Few Digits": {"StandardForm": {"expr": "MakeBoxes[3.142`3]", "expect": "\"3.142`3\"", "msg": "StandardForm with PrecisionReal shows all the stored digits, and precision."}, "OutputForm": {"expr": "MakeBoxes[OutputForm[3.142`3]]", "expect": "InterpretationBox[PaneBox[\"\\\"3.14\\\"\"], OutputForm[3.14], Editable -> False]", "msg": "OutputForm trims digits up to precision."}}, "PrecisionReal, Many Digits": {"StandardForm": {"expr": "MakeBoxes[1.4`20]", "expect": "\"1.4`20\""}}, "PrecisionReal, ManyAccuracy": {"StandardForm": {"expr": "MakeBoxes[1.5``20]", "expect": "\"1.5`20\""}}, "PrecisionReal, Zero_LowPrec": {"StandardForm": {"expr": "MakeBoxes[0.`3]", "expect": "\"0.`\""}}, "PrecisionReal, Zero_LargePrec": {"StandardForm": {"expr": "MakeBoxes[0.`30]", "expect": "\"0.`\""}}, "PrecisionReal, Zero_Accuracy": {"StandardForm": {"expr": "MakeBoxes[0.``30]", "expect": "\"0``30\""}}}, "Graphics": {"Disk": {"StandardForm": {"expr": "MakeBoxes[Graphics[{Disk[{0,0}, 1]}]]", "expect": "GraphicsBox[{DiskBox[{0, 0}, 1]}]"}, "OutputForm": {"expr": "MakeBoxes[Graphics[{Disk[{0,0}, 1]}]//OutputForm]", "expect": "InterpretationBox[PaneBox[\"\\\"-Graphics-\\\"\"], OutputForm[Graphics[{Disk[{0, 0}, 1]}]], Editable -> False]"}}, "Sphere": {"StandardForm": {"expr": "MakeBoxes[Graphics3D[{Sphere[{0,0,0}, 1]}]]", "expect": "Graphics3DBox[{SphereBox[{0, 0,0}, 1]}]"}, "OutputForm": {"expr": "MakeBoxes[Graphics3D[{Sphere[{0,0,0}, 1]}]//OutputForm]", "expect": "InterpretationBox[PaneBox[\"\\\"-Graphics3D-\\\"\"], OutputForm[Graphics3D[{Sphere[{0, 0, 0}, 1]}]], Editable -> False]"}}}, "Parsing string form": {"RowBox": {"StandardForm": {"expr": "\\( a, b \\)", "expect": "RowBox[{\"a\", \",\", \"b\"}]", "msg": "TODO: Parsing of special characters (like commas)."}}, "FractionBox": {"StandardForm": {"expr": "\\(x \\/ y + z\\)", "expect": "RowBox[{FractionBox[\"x\", \"y\"], \"+\", \"z\"}]"}}, "FractionBox bracket": {"StandardForm": {"expr": "\\(x \\/ (y + z)\\)", "expect": "FractionBox[\"x\", RowBox[{\"(\", RowBox[{\"y\", \"+\", \"z\"}], \")\"}]]"}}, "SqrtBox": {"StandardForm": {"expr": "\\( \\@ a + b \\)", "expect": "RowBox[{SqrtBox[\"a\"], \"+\", \"b\"}]"}}, "FormBox": {"StandardForm": {"expr": "\\(TraditionalForm \\` a + b\\)", "expect": "FormBox[RowBox[{\"a\", \"+\", \"b\"}], TraditionalForm]"}}}} diff --git a/test/format/makeboxes_tests.m b/test/format/makeboxes_tests.m new file mode 100755 index 000000000..026499710 --- /dev/null +++ b/test/format/makeboxes_tests.m @@ -0,0 +1,42 @@ +(* ::Package:: *) + +(************************************************************************************** +Run the format tests in WMA. + +Notice that some of the tests that produce meaninful outputs in Mathics, +fails miserably to produce an output in WMA. Also, the results of these tests +in the Notebook interface, the CLI (math) and wolframscript are not fully consistent. + +****************************************************************************************) + + +Print["Read json"]; +data = Import["makeboxes_tests.json"]; +Print["Found ", Length[data], " expressions to test."]; + + +Do[ +title=testsblock[[1]]; +Print["\n",title, "\n",StringJoin[Table["=",{StringLength[title]}]]]; + Do[ + case = tests[[1]]; + Print["\n ", case,"\n"," ",StringJoin[Table["=",{StringLength[case]}]]]; + Do[ + form = caseform[[1]]; + rul = caseform[[2]]; + expr = "expr"/.rul; + result = ToExpression[expr]; + expect = ToExpression["expect"/.rul]; + If[SameQ[result, expect], + Print[" ", form, " [OK]"], + Print[" ", form, " [Failed]"]; + Print[" expr = ", expr ]; + Print[" result = ", result]; + Print[" expected= ",expect]; + ], + {caseform, tests[[2]]} + ], + {tests, testsblock[[2]]}], + {testsblock, data} +]; +Print["Done"] diff --git a/test/format/makeboxes_tests.yaml b/test/format/makeboxes_tests.yaml new file mode 100644 index 000000000..a560f3f60 --- /dev/null +++ b/test/format/makeboxes_tests.yaml @@ -0,0 +1,261 @@ +# YAML file containing tests for basi MakeBoxes. +# Has three levels: +# 1. Group of tests +# 2. Kind of expression +# 3. Form +# +# +# ```MakeBoxes[expr, fmt_]``` +# +# Apply the format rules matching with `Format[expr, fmt]`, and then the MakeBoxes rules +# matching with the result. If `fmt` is ommited, it is assumed `StandardForm`. +# +# For example, `MakeBoxes[A+B, StandardForm]` applyies first the format rule +# `Format[Plus[x___], StandardForm] :> Infix[{x}, "+",...]` leadint to +# `Infix[{A,B},"+"]` +# and then the MakeBoxes rule `Infix[{x}, "+",...]:> RowBox[{ToString[x[[1]]],"+",ToString[x[[2]]],...}` +# leading to `RowBox[{"A","+","B"}]`. +# +# The behavior is different if `expr` is of the form `inner//Form`, with `Form` an `$OutputForm`. +# In that case, `Format[inner, Form]` is evaluated, then the MakeBoxes rules are applied, and finally, +# the result is wrapped into different structures depending on what is `Form`. +# +# Form is in `PrintForms` +# ======================= +# +# ```MakeBoxes[expr//`Form`,fmt_]``` +# with Form in $PrintForms (InputForm, OutputForm, TeXForm, etc) +# produce always an output of the form +# ```InterpretationBox[`formatted`, Form[expr], opts...]``` +# with `formatted` a string or a string inside a StyleBox. +# +# Form is `FullForm` or a form in `$BoxForm` +# ========================================== +# +# On the other hand, for FullForm, or forms in $BoxForm, +# the result is of the form +# +# ```TagBox[`formatted`,`Form`, `opts`]``` +# * For FullForm, `formatted` is a `StyleForm`, and TagBox does not have options. +# * For $BoxForm's, `opts` is `Editable->True`, and `formatted` is a `FormBox`, +# with the first element the boxed representation, and the second element keeping the +# value of `Form`. +# +# `expr` is a `Format` expression: +# ================================ +# +# Another possibility is to state explicitly `Format[expr, form]` as the argument of MakeBoxes. +# In that case, for forms in $PrintForms, the result is the same as if we evaluate over `form[expr]`. +# For forms in `$BoxForms`, the result is similar to the one obtained from `form[expr]` but +# * the result does not have the option `Editable->True` +# * the second element of the TagBox is `Format[#1, form]&` instead of `form`. +# +# `Format[expr, FullForm]` is not treated as `FullForm[expr]`, but as its literal expression: +# +# ```MakeBoxes[Format[F[x],FullForm]]``` +# produces +# ```RowBox[{"Format", "[", RowBox[{"F", "[","x","]"}], "]"}] +# +# +# To run the tests in the interpreter: +# ``` +# ./convert_yaml2json.py makeboxes_tests.yaml && $(WMAINTERPRETER) -f makeboxes_tests.m +# ``` +# +Basic Forms: + Arithmetic: + FullForm: + expect: TagBox[StyleBox[RowBox[{"Plus", "[", RowBox[{"a", ",", RowBox[{"Times", "[", RowBox[{RowBox[{"-", "1"}], ",", "b"}], "]"}]}], "]"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm] + expr: MakeBoxes[a-b//FullForm] + InputForm: + expect: InterpretationBox[StyleBox["a - b", ShowStringCharacters -> True, NumberMarks-> True], InputForm[a - b], Editable -> True, AutoDelete -> True] + expr: MakeBoxes[a-b//InputForm] + OutputForm: + expect: InterpretationBox[PaneBox["\"a - b\""], OutputForm[a - b], Editable-> False] + expr: MakeBoxes[a-b//OutputForm] + StandardForm: + expect: RowBox[{"a", "-", "b"}] + expr: MakeBoxes[a-b] + TeXForm: + expect: InterpretationBox["\"a-b\"", TeXForm[a-b], Editable -> True, AutoDelete-> True] + expr: MakeBoxes[a-b//TeXForm] + Expression: + Format: + expect: TagBox[FormBox[RowBox[List["F", "[", "x", "]"]], StandardForm], (Format[#1, StandardForm])&] + expr: MakeBoxes[Format[F[x], StandardForm]] + TraditionalForm: + expect: TagBox[FormBox[RowBox[List["F", "(", "x", ")"]], TraditionalForm], TraditionalForm, Editable-> True] + expr: MakeBoxes[F[x]//TraditionalForm] + FullForm: + expect: TagBox[StyleBox[RowBox[{"F", "[", "x", "]"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm] + expr: MakeBoxes[F[x]//FullForm] + InputForm: + expect: InterpretationBox[StyleBox["F[x]", ShowStringCharacters -> True, NumberMarks-> True], InputForm[F[x]], Editable -> True, AutoDelete -> True] + expr: MakeBoxes[F[x]//InputForm] + OutputForm: + expect: InterpretationBox[PaneBox["\"F[x]\""], OutputForm[F[x]], Editable ->False] + expr: MakeBoxes[F[x]//OutputForm] + StandardForm: + expect: RowBox[{"F", "[", "x", "]"}] + expr: MakeBoxes[F[x]] + TeXForm: + expect: InterpretationBox["\"F(x)\"", TeXForm[F[x]], Editable -> True, AutoDelete-> True] + expr: MakeBoxes[F[x]//TeXForm] + Integer_negative: + FullForm: + expect: TagBox[StyleBox[RowBox[{"-", "14"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm] + expr: MakeBoxes[-14//FullForm] + InputForm: + expect: InterpretationBox[StyleBox["-14", ShowStringCharacters -> True, NumberMarks -> True], InputForm[-14], Editable -> True, AutoDelete -> True] + expr: MakeBoxes[-14//InputForm] + OutputForm: + expect: InterpretationBox[PaneBox["\"-14\""], OutputForm[-14], Editable -> False] + expr: MakeBoxes[-14//OutputForm] + StandardForm: + expect: RowBox[{"-", "14"}] + expr: MakeBoxes[-14] + TeXForm: + expect: 'InterpretationBox["\"-14\"", TeXForm[-14], Editable -> True, AutoDelete-> True]' + expr: MakeBoxes[-14//TeXForm] + Integer_positive: + FullForm: + expect: TagBox[StyleBox["14", ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm] + expr: MakeBoxes[14//FullForm] + InputForm: + expect: InterpretationBox[StyleBox["14", ShowStringCharacters -> True, NumberMarks-> True], InputForm[14], Editable -> True, AutoDelete -> True] + expr: MakeBoxes[14//InputForm] + OutputForm: + expect: InterpretationBox[PaneBox["\"14\""], OutputForm[14], Editable -> False] + expr: MakeBoxes[14//OutputForm] + StandardForm: + expect: "14" + expr: MakeBoxes[14] + TeXForm: + expect: 'InterpretationBox["\"14\"", TeXForm[14], Editable -> True, AutoDelete-> True]' + expr: MakeBoxes[14//TeXForm] + PrecisionReal: + FullForm: + expect: TagBox[StyleBox[RowBox[{"-", "14.`3."}], ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm] + expr: MakeBoxes[-14.`3//FullForm] + InputForm: + expect: InterpretationBox[StyleBox["-14.`3", ShowStringCharacters -> True, NumberMarks-> True], InputForm[-14.`3], Editable -> True, AutoDelete -> True] + expr: MakeBoxes[-14.`3//InputForm] + OutputForm: + expect: InterpretationBox[PaneBox["\"-14.0\""], OutputForm[-14.0], Editable-> False] + expr: MakeBoxes[-14.0//OutputForm] + StandardForm: + expect: RowBox[{"-","14.`3"}] + expr: MakeBoxes[-14.`3] + TeXForm: + expect: InterpretationBox["\"-14.`3\"", TeXForm[-14.`3], Editable -> True, AutoDelete + -> True] + expr: MakeBoxes[14.`3//TeXForm] + Symbol: + FullForm: + expect: TagBox[StyleBox["x", ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm] + expr: MakeBoxes[x//FullForm] + InputForm: + expect: InterpretationBox[StyleBox["x", ShowStringCharacters -> True, NumberMarks-> True], InputForm[x], Editable -> True, AutoDelete -> True] + expr: MakeBoxes[x//InputForm] + OutputForm: + expect: InterpretationBox[PaneBox["\"x\""], OutputForm[x], Editable -> False] + expr: MakeBoxes[x//OutputForm] + StandardForm: + expect: "\"x\"" + expr: MakeBoxes[x] + TeXForm: + expect: InterpretationBox["\"x\"", TeXForm[x], Editable -> True, AutoDelete-> True] + expr: MakeBoxes[x//TeXForm] +Numbers: + "MachineReal, positive": + StandardForm: + expr: MakeBoxes[1.4`] + expect: "\"1.4`\"" + msg: "StandardForm always shows a precision mark." + OutputForm: + expr: MakeBoxes[1.4`//OutputForm] + expect: InterpretationBox[PaneBox["\"1.4\""], OutputForm[1.4], Editable -> False] + msg: "MachineReal, OutputForm." + "MachineReal, negative": + StandardForm: + expr: MakeBoxes[-1.4`] + expect: RowBox[{"-", "1.4`"}] + msg: "StandardForm always shows a precision mark." + OutputForm: + expr: MakeBoxes[-1.4`//OutputForm] + expect: InterpretationBox[PaneBox["\"-1.4\""], OutputForm[-1.4], Editable -> False] + msg: "MachineReal, negative, OutputForm." + "MachineReal, Large": + StandardForm: + expr: MakeBoxes[34.*^3] + expect: "\"34000.`\"" + "MachineReal, Very Large": + StandardForm: + expr: MakeBoxes[34.*^9] + expect: "\"3.4`*^10\"" + "PrecisionReal, Few Digits": + StandardForm: + expr: MakeBoxes[3.142`3] + expect: "\"3.142`3\"" + msg: "StandardForm with PrecisionReal shows all the stored digits, and precision." + OutputForm: + expr: MakeBoxes[OutputForm[3.142`3]] + expect: InterpretationBox[PaneBox["\"3.14\""], OutputForm[3.14], Editable -> False] + msg: "OutputForm trims digits up to precision." + "PrecisionReal, Many Digits": + StandardForm: + expr: MakeBoxes[1.4`20] + expect: "\"1.4`20\"" + "PrecisionReal, ManyAccuracy": + StandardForm: + expr: MakeBoxes[1.5``20] + expect: "\"1.5`20\"" + "PrecisionReal, Zero_LowPrec": + StandardForm: + expr: MakeBoxes[0.`3] + expect: "\"0.`\"" + "PrecisionReal, Zero_LargePrec": + StandardForm: + expr: MakeBoxes[0.`30] + expect: "\"0.`\"" + "PrecisionReal, Zero_Accuracy": + StandardForm: + expr: MakeBoxes[0.``30] + expect: "\"0``30\"" +Graphics: + "Disk": + StandardForm: + expr: "MakeBoxes[Graphics[{Disk[{0,0}, 1]}]]" + expect: "GraphicsBox[{DiskBox[{0, 0}, 1]}]" + OutputForm: + expr: "MakeBoxes[Graphics[{Disk[{0,0}, 1]}]//OutputForm]" + expect: InterpretationBox[PaneBox["\"-Graphics-\""], OutputForm[Graphics[{Disk[{0, 0}, 1]}]], Editable -> False] + "Sphere": + StandardForm: + expr: "MakeBoxes[Graphics3D[{Sphere[{0,0,0}, 1]}]]" + expect: "Graphics3DBox[{SphereBox[{0, 0,0}, 1]}]" + OutputForm: + expr: "MakeBoxes[Graphics3D[{Sphere[{0,0,0}, 1]}]//OutputForm]" + expect: InterpretationBox[PaneBox["\"-Graphics3D-\""], OutputForm[Graphics3D[{Sphere[{0, 0, 0}, 1]}]], Editable -> False] +"Parsing string form": + "RowBox": + StandardForm: + expr: "\\( a, b \\)" + expect: RowBox[{"a", ",", "b"}] + msg: "TODO: Parsing of special characters (like commas)." + "FractionBox": + StandardForm: + expr: "\\(x \\/ y + z\\)" + expect: RowBox[{FractionBox["x", "y"], "+", "z"}] + "FractionBox bracket": + StandardForm: + expr: "\\(x \\/ (y + z)\\)" + expect: FractionBox["x", RowBox[{"(", RowBox[{"y", "+", "z"}], ")"}]] + "SqrtBox": + StandardForm: + expr: "\\( \\@ a + b \\)" + expect: RowBox[{SqrtBox["a"], "+", "b"}] + "FormBox": + StandardForm: + expr: "\\(TraditionalForm \\` a + b\\)" + expect: FormBox[RowBox[{"a", "+", "b"}], TraditionalForm] diff --git a/test/builtin/test_makeboxes.py b/test/format/test_makeboxes.py similarity index 66% rename from test/builtin/test_makeboxes.py rename to test/format/test_makeboxes.py index 79833e144..e6c8c7a95 100644 --- a/test/builtin/test_makeboxes.py +++ b/test/format/test_makeboxes.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- import os +import test from test.helper import check_evaluation import pytest +import yaml # To check the progress in the improvement of formatting routines, set this variable to 1. # Otherwise, the tests are going to be skipped. @@ -17,184 +19,42 @@ def skip_or_fail(x): skip_or_fail = pytest.mark.xfail -@pytest.mark.parametrize( - ("str_expr", "str_expected", "fail_msg"), - [ - ("MakeBoxes[x]", '"x"', "StandardForm"), - ( - "MakeBoxes[InputForm[x]]", - 'InterpretationBox[StyleBox["x", ShowStringCharacters -> True, NumberMarks -> True], InputForm[x], Editable -> True, AutoDelete -> True]', - "InputForm, expression", - ), - ( - "MakeBoxes[OutputForm[x]]", - 'InterpretationBox[PaneBox[""x""], OutputForm[x], Editable -> False]', - "OutputForm, expression", - ), - ( - "MakeBoxes[FullForm[x]]", - 'TagBox[StyleBox["x", ShowSpecialCharacters -> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]', - "MakeBoxes expression FullForm", - ), - ( - "MakeBoxes[TeXForm[x]]", - 'InterpretationBox[""x"", TeXForm[x], Editable -> True, AutoDelete -> True]', - "TeXForm, expression", - ), - # Basic Expressions - ("MakeBoxes[F[x]]", 'RowBox[{"F", "[", "x", "]"}]', "StandardForm"), - ( - "MakeBoxes[InputForm[F[x]]]", - 'InterpretationBox[StyleBox["F[x]", ShowStringCharacters -> True, NumberMarks -> True], InputForm[F[x]], Editable -> True, AutoDelete -> True]', - "InputForm, expression", - ), - ( - "MakeBoxes[OutputForm[F[x]]]", - 'InterpretationBox[PaneBox[""F[x]""], OutputForm[F[x]], Editable -> False]', - "OutputForm, expression", - ), - ( - "MakeBoxes[FullForm[F[x]]]", - 'TagBox[StyleBox[RowBox[{"F", "[", "x", "]"}], ShowSpecialCharacters -> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]', - "MakeBoxes expression FullForm", - ), - ( - "MakeBoxes[TeXForm[F[x]]]", - 'InterpretationBox[""F(x)"", TeXForm[F[x]], Editable -> True, AutoDelete -> True]', - "TeXForm, expression", - ), - # Arithmetic - ("MakeBoxes[a-b]", 'RowBox[{"a", "-", "b"}]', "difference, StandardForm"), - ( - "MakeBoxes[a-b//InputForm]", - 'InterpretationBox[StyleBox["a - b", ShowStringCharacters -> True, NumberMarks -> True], InputForm[a - b], Editable -> True, AutoDelete -> True]', - "difference, InputForm", - ), - ( - "MakeBoxes[a-b//OutputForm]", - 'InterpretationBox[PaneBox[""a - b""], OutputForm[a - b], Editable -> False]', - "difference, OutputForm", - ), - ( - "MakeBoxes[a-b//FullForm]", - 'TagBox[StyleBox[RowBox[{"Plus", "[", RowBox[{"a", ",", RowBox[{"Times", "[", RowBox[{RowBox[{"-", "1"}], ",", "b"}], "]"}]}], "]"}], ShowSpecialCharacters -> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]', - "Difference, FullForm", - ), - ( - "MakeBoxes[a-b//TeXForm]", - ' InterpretationBox[""a-b"", TeXForm[a-b], Editable -> True, AutoDelete -> True]', - "Difference, TeXForm", - ), - ], -) -@skip_or_fail -def test_makeboxes_basic_forms(str_expr, str_expected, fail_msg, msgs): - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=fail_msg, - expected_messages=msgs, - ) +path = os.path.dirname(__file__) + os.path.sep +with open(path + "makeboxes_tests.yaml", "r") as src: + MAKEBOXES_TESTS = yaml.safe_load(src) -@pytest.mark.parametrize( - ("str_expr", "str_expected", "fail_msg", "msgs"), - [ - ('rb=RowBox[{"a", "b"}]; rb[[1]]', "{a, b}", None, []), - ("rb[[0]]", "RowBox", None, []), - ( - "rb[[2]]", - "RowBox[{a, b}][[2]]", - None, - ["Part 2 of RowBox[{a, b}] does not exist."], - ), - ('fb=FractionBox["1", "2"]; fb[[0]]', "FractionBox", None, []), - ("fb[[1]]", "1", None, []), - ('sb=StyleBox["string", "Section"]; sb[[0]]', "StyleBox", None, []), - ("sb[[1]]", "string", None, []), - # FIXME: <> - # ('rb[[All, 1]]', "{a, b}", "\"a\"", []), - # ('fb[[All]][[1]]','1', None, []), - # ('sb[[All]][[1]]','string', None, []), - ], -) -@skip_or_fail -def test_part_boxes(str_expr, str_expected, fail_msg, msgs): - """ - This unit test checks that certain typical box structures - work together with `Part`. In the current master, - these expressions crashes the interpreter. - """ - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=fail_msg, - expected_messages=msgs, - ) + +def makeboxes_basic_forms_iterator(block): + for key, tests in MAKEBOXES_TESTS[block].items(): + for form, entry in tests.items(): + msg = f"{key}, {form}" + expr = entry["expr"] + expect = entry["expect"] + yield expr, expect, msg -# 15 tests @pytest.mark.parametrize( - ("str_expr", "str_expected", "msg"), - [ - (r"MakeBoxes[0`3]", r"0", None), - (r"MakeBoxes[14]", r"14", None), - ], + ("str_expr", "str_expected", "fail_msg"), + list(makeboxes_basic_forms_iterator("Basic Forms")), ) -def test_makeboxes_real(str_expr, str_expected, msg): - """ - # Constructing boxes from Real - """ +@skip_or_fail +def test_makeboxes_basic_forms(str_expr, str_expected, fail_msg): check_evaluation( str_expr, str_expected, to_string_expr=True, to_string_expected=True, hold_expected=True, - failure_message=msg, + failure_message=fail_msg, ) -# 15 tests @pytest.mark.parametrize( - ("str_expr", "str_expected", "msg"), - [ - (r"MakeBoxes[1.4`]", r"1.4`", "StandardForm always shows a precision mark."), - ( - r"MakeBoxes[OutputForm[1.4]]", - r'InterpretationBox[PaneBox["1.4"], 1.4, Editable -> False]', - "MachineReal, OutputForm", - ), - ( - r"MakeBoxes[3.142`3]", - r"3.142`3", - "StandadForm with PrecisionReal shows all the stored digits, and precision.", - ), - ( - r"MakeBoxes[OutputForm[3.142`3]]", - r'InterpretationBox[PaneBox["3.14"], 3.14, Editable -> False]', - "OutputForm trims digits up to precision.", - ), - (r"MakeBoxes[1.5`20]", r"1.5`20.", None), - (r"MakeBoxes[1.4`20]", r"1.4`20.", None), - (r"MakeBoxes[1.5``20]", r"1.5`20.1760912591", None), - (r"MakeBoxes[-1.4]", r"RowBox[{-, 1.4`}]", None), - (r"MakeBoxes[34.*^3]", r"34000.`", None), - (r"MakeBoxes[0`]", r"0.`", None), - (r"MakeBoxes[0``30]", r"0.``30.", None), - (r"MakeBoxes[0.`3]", r"0.`", None), - (r"MakeBoxes[0.``30]", r"0.``30.", None), - (r"MakeBoxes[-14]", r"RowBox[{-, 14}]", None), - ], + ("str_expr", "str_expected", "msg"), list(makeboxes_basic_forms_iterator("Numbers")) ) @skip_or_fail -def test_makeboxes_real_fail(str_expr, str_expected, msg): +def test_makeboxes_real(str_expr, str_expected, msg): """ # TODO: Constructing boxes from Real which are currently failing """ @@ -208,13 +68,9 @@ def test_makeboxes_real_fail(str_expr, str_expected, msg): ) -# 3 tests @pytest.mark.parametrize( ("str_expr", "str_expected", "msg"), - [ - (r"\(x \/ y + z\)", r"RowBox[{FractionBox[x, y], +, z}]", None), - (r"\( \@ a + b \)", r"RowBox[{SqrtBox[a], +, b}]", None), - ], + list(makeboxes_basic_forms_iterator("Parsing string form")), ) def test_makeboxes_precedence(str_expr, str_expected, msg): """Test precedence in string-like boxes""" @@ -228,40 +84,15 @@ def test_makeboxes_precedence(str_expr, str_expected, msg): ) -# 2 tests @pytest.mark.parametrize( ("str_expr", "str_expected", "msg"), - [ - ( - r"\(x \/ (y + z)\)", - r"FractionBox[x, RowBox[{(, RowBox[{y, +, z}], )}]]", - None, - ), - ], + list(makeboxes_basic_forms_iterator("Graphics")), ) @skip_or_fail -def test_makeboxes_precedence_fail(str_expr, str_expected, msg): - """TODO: fix the parsing for testing precedence in string-like boxes (""" - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=msg, - ) - - -# 3 tests -# TODO: Convert operators to appropriate representations e.g. 'Plus' to '+' -@pytest.mark.parametrize( - ("str_expr", "str_expected", "msg"), - [ - (r"\(a + b\)", r"RowBox[{a, +, b}]", None), - (r"\(x \/ \(y + z\)\)", r"FractionBox[x, RowBox[{y, +, z}]]", None), - ], -) -def test_makeboxes_representation(str_expr, str_expected, msg): +def test_makeboxes_graphics(str_expr, str_expected, msg): + """ + # TODO: Constructing boxes from Real which are currently failing + """ check_evaluation( str_expr, str_expected, @@ -272,49 +103,42 @@ def test_makeboxes_representation(str_expr, str_expected, msg): ) -# 3 tests -# TODO: Convert operators to appropriate representations e.g. 'Plus' to '+' @pytest.mark.parametrize( - ("str_expr", "str_expected", "msg"), + ("str_expr", "str_expected", "fail_msg", "msgs"), [ + ('rb=RowBox[{"a", "b"}]; rb[[1]]', "{a, b}", None, []), + ("rb[[0]]", "RowBox", None, []), ( - r"\(TraditionalForm \` a + b\)", - r"FormBox[RowBox[{a, +, b}], TraditionalForm]", + "rb[[2]]", + "RowBox[{a, b}][[2]]", None, + ["Part 2 of RowBox[{a, b}] does not exist."], ), + ('fb=FractionBox["1", "2"]; fb[[0]]', "FractionBox", None, []), + ("fb[[1]]", "1", None, []), + ('sb=StyleBox["string", "Section"]; sb[[0]]', "StyleBox", None, []), + ("sb[[1]]", "string", None, []), + # FIXME: <> + ("rb[[All, 1]]", "{a, b}", '"a"', []), + ("fb[[All]][[1]]", "1", None, []), + ("sb[[All]][[1]]", "string", None, []), ], ) @skip_or_fail -def test_makeboxes_representation_fail(str_expr, str_expected, msg): - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=msg, - ) - - -# 5 tests -@pytest.mark.parametrize( - ("str_expr", "str_expected", "msg"), - [ - ( - r"\( a, b \)", - r"RowBox[{a, ,, b}]", - "TODO: Parsing of special characters (like commas)", - ), - ], -) -def test_makeboxes_others(str_expr, str_expected, msg): +def test_part_boxes(str_expr, str_expected, fail_msg, msgs): + """ + This unit test checks that certain typical box structures + work together with `Part`. In the current master, + these expressions crashes the interpreter. + """ check_evaluation( str_expr, str_expected, to_string_expr=True, to_string_expected=True, hold_expected=True, - failure_message=msg, + failure_message=fail_msg, + expected_messages=msgs, ) From 211f07aa8f28b80938562501cfb8faf06eae9c46 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 3 Jan 2026 14:39:49 -0300 Subject: [PATCH 19/49] merge --- test/format/format_test.m | 53 -------------------------------- test/format/makeboxes_tests.json | 1 - 2 files changed, 54 deletions(-) delete mode 100755 test/format/format_test.m delete mode 100644 test/format/makeboxes_tests.json diff --git a/test/format/format_test.m b/test/format/format_test.m deleted file mode 100755 index 0d3ad712a..000000000 --- a/test/format/format_test.m +++ /dev/null @@ -1,53 +0,0 @@ -(************************************************************************************** -Run the format tests in WMA. - -Notice that some of the tests that produce meaninful outputs in Mathics, -fails miserably to produce an output in WMA. Also, the results of these tests -in the Notebook interface, the CLI (math) and wolframscript are not fully consistent. - -****************************************************************************************) - - -Print["Read json"]; -data = Import["format_tests.json"]; -Print["Found ", Length[data], " expressions to test."]; - -Print["Run the tests"]; - -Do[key = ToExpression[tests[[1]]]; - fields = tests[[2]]; - mesg = ("msg" /. fields); - text = ("text" /. fields); - latex = ("latex" /. fields); - mathml = ("mathml" /. fields); - Print[Head[key], " msg:", mesg, "<<", key, ">>"]; - Print[" text", "\n -----", "\n"]; - Do[form = ToExpression[subtest[[1]]]; expr = form[key]; - result = ToString[expr]; - expected = subtest[[2]]; - If[result != subtest[[2]], - Print[" * ", form, "(text) [Failed]\n result:", "<<" <> result <> ">>", - "\n expected: ", "<<" <> expected <> ">>\n"], - Print[" * ", form, "(text) [OK]"]];, {subtest, text}]; - (*LaTeX*) - Print[" latex", "\n -----", "\n"]; - Do[form = ToExpression[subtest[[1]]]; expr = form[key]; - result = ToString[expr, TeXForm]; - expected = subtest[[2]]; - If[result != subtest[[2]], - Print[" * ", form, "(latex) [Failed]\n result:", - "<<" <> result <> ">>", "\n expected: ", - "<<" <> expected <> ">>\n"], - Print[" * ", form, "(latex) [OK]"]];, {subtest, latex}]; -(*MathML*) - Print[" mathml", "\n ------", "\n"]; - Do[form = ToExpression[subtest[[1]]]; expr = form[key]; - result = ToString[expr, MathMLForm]; - expected = subtest[[2]]; - If[result != subtest[[2]], - Print[" * ", form, "(mathml) [Failed]\n result:", - "<<" <> result <> ">>", "\n expected: ", - "<<" <> expected <> ">>\n"], - Print[" * ", form, "(mathml) [OK]"]];, {subtest, mathml}]; - - , {tests, data}] diff --git a/test/format/makeboxes_tests.json b/test/format/makeboxes_tests.json deleted file mode 100644 index 8c63b1a7c..000000000 --- a/test/format/makeboxes_tests.json +++ /dev/null @@ -1 +0,0 @@ -{"Basic Forms": {"Arithmetic": {"FullForm": {"expect": "TagBox[StyleBox[RowBox[{\"Plus\", \"[\", RowBox[{\"a\", \",\", RowBox[{\"Times\", \"[\", RowBox[{RowBox[{\"-\", \"1\"}], \",\", \"b\"}], \"]\"}]}], \"]\"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[a-b//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"a - b\", ShowStringCharacters -> True, NumberMarks-> True], InputForm[a - b], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[a-b//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"a - b\\\"\"], OutputForm[a - b], Editable-> False]", "expr": "MakeBoxes[a-b//OutputForm]"}, "StandardForm": {"expect": "RowBox[{\"a\", \"-\", \"b\"}]", "expr": "MakeBoxes[a-b]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"a-b\\\"\", TeXForm[a-b], Editable -> True, AutoDelete-> True]", "expr": "MakeBoxes[a-b//TeXForm]"}}, "Expression": {"Format": {"expect": "TagBox[FormBox[RowBox[List[\"F\", \"[\", \"x\", \"]\"]], StandardForm], (Format[#1, StandardForm])&]", "expr": "MakeBoxes[Format[F[x], StandardForm]]"}, "TraditionalForm": {"expect": "TagBox[FormBox[RowBox[List[\"F\", \"(\", \"x\", \")\"]], TraditionalForm], TraditionalForm, Editable-> True]", "expr": "MakeBoxes[F[x]//TraditionalForm]"}, "FullForm": {"expect": "TagBox[StyleBox[RowBox[{\"F\", \"[\", \"x\", \"]\"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[F[x]//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"F[x]\", ShowStringCharacters -> True, NumberMarks-> True], InputForm[F[x]], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[F[x]//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"F[x]\\\"\"], OutputForm[F[x]], Editable ->False]", "expr": "MakeBoxes[F[x]//OutputForm]"}, "StandardForm": {"expect": "RowBox[{\"F\", \"[\", \"x\", \"]\"}]", "expr": "MakeBoxes[F[x]]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"F(x)\\\"\", TeXForm[F[x]], Editable -> True, AutoDelete-> True]", "expr": "MakeBoxes[F[x]//TeXForm]"}}, "Integer_negative": {"FullForm": {"expect": "TagBox[StyleBox[RowBox[{\"-\", \"14\"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[-14//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"-14\", ShowStringCharacters -> True, NumberMarks -> True], InputForm[-14], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[-14//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"-14\\\"\"], OutputForm[-14], Editable -> False]", "expr": "MakeBoxes[-14//OutputForm]"}, "StandardForm": {"expect": "RowBox[{\"-\", \"14\"}]", "expr": "MakeBoxes[-14]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"-14\\\"\", TeXForm[-14], Editable -> True, AutoDelete-> True]", "expr": "MakeBoxes[-14//TeXForm]"}}, "Integer_positive": {"FullForm": {"expect": "TagBox[StyleBox[\"14\", ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[14//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"14\", ShowStringCharacters -> True, NumberMarks-> True], InputForm[14], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[14//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"14\\\"\"], OutputForm[14], Editable -> False]", "expr": "MakeBoxes[14//OutputForm]"}, "StandardForm": {"expect": "14", "expr": "MakeBoxes[14]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"14\\\"\", TeXForm[14], Editable -> True, AutoDelete-> True]", "expr": "MakeBoxes[14//TeXForm]"}}, "PrecisionReal": {"FullForm": {"expect": "TagBox[StyleBox[RowBox[{\"-\", \"14.`3.\"}], ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[-14.`3//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"-14.`3\", ShowStringCharacters -> True, NumberMarks-> True], InputForm[-14.`3], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[-14.`3//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"-14.0\\\"\"], OutputForm[-14.0], Editable-> False]", "expr": "MakeBoxes[-14.0//OutputForm]"}, "StandardForm": {"expect": "RowBox[{\"-\",\"14.`3\"}]", "expr": "MakeBoxes[-14.`3]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"-14.`3\\\"\", TeXForm[-14.`3], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[14.`3//TeXForm]"}}, "Symbol": {"FullForm": {"expect": "TagBox[StyleBox[\"x\", ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm]", "expr": "MakeBoxes[x//FullForm]"}, "InputForm": {"expect": "InterpretationBox[StyleBox[\"x\", ShowStringCharacters -> True, NumberMarks-> True], InputForm[x], Editable -> True, AutoDelete -> True]", "expr": "MakeBoxes[x//InputForm]"}, "OutputForm": {"expect": "InterpretationBox[PaneBox[\"\\\"x\\\"\"], OutputForm[x], Editable -> False]", "expr": "MakeBoxes[x//OutputForm]"}, "StandardForm": {"expect": "\"x\"", "expr": "MakeBoxes[x]"}, "TeXForm": {"expect": "InterpretationBox[\"\\\"x\\\"\", TeXForm[x], Editable -> True, AutoDelete-> True]", "expr": "MakeBoxes[x//TeXForm]"}}}, "Numbers": {"MachineReal, positive": {"StandardForm": {"expr": "MakeBoxes[1.4`]", "expect": "\"1.4`\"", "msg": "StandardForm always shows a precision mark."}, "OutputForm": {"expr": "MakeBoxes[1.4`//OutputForm]", "expect": "InterpretationBox[PaneBox[\"\\\"1.4\\\"\"], OutputForm[1.4], Editable -> False]", "msg": "MachineReal, OutputForm."}}, "MachineReal, negative": {"StandardForm": {"expr": "MakeBoxes[-1.4`]", "expect": "RowBox[{\"-\", \"1.4`\"}]", "msg": "StandardForm always shows a precision mark."}, "OutputForm": {"expr": "MakeBoxes[-1.4`//OutputForm]", "expect": "InterpretationBox[PaneBox[\"\\\"-1.4\\\"\"], OutputForm[-1.4], Editable -> False]", "msg": "MachineReal, negative, OutputForm."}}, "MachineReal, Large": {"StandardForm": {"expr": "MakeBoxes[34.*^3]", "expect": "\"34000.`\""}}, "MachineReal, Very Large": {"StandardForm": {"expr": "MakeBoxes[34.*^9]", "expect": "\"3.4`*^10\""}}, "PrecisionReal, Few Digits": {"StandardForm": {"expr": "MakeBoxes[3.142`3]", "expect": "\"3.142`3\"", "msg": "StandardForm with PrecisionReal shows all the stored digits, and precision."}, "OutputForm": {"expr": "MakeBoxes[OutputForm[3.142`3]]", "expect": "InterpretationBox[PaneBox[\"\\\"3.14\\\"\"], OutputForm[3.14], Editable -> False]", "msg": "OutputForm trims digits up to precision."}}, "PrecisionReal, Many Digits": {"StandardForm": {"expr": "MakeBoxes[1.4`20]", "expect": "\"1.4`20\""}}, "PrecisionReal, ManyAccuracy": {"StandardForm": {"expr": "MakeBoxes[1.5``20]", "expect": "\"1.5`20\""}}, "PrecisionReal, Zero_LowPrec": {"StandardForm": {"expr": "MakeBoxes[0.`3]", "expect": "\"0.`\""}}, "PrecisionReal, Zero_LargePrec": {"StandardForm": {"expr": "MakeBoxes[0.`30]", "expect": "\"0.`\""}}, "PrecisionReal, Zero_Accuracy": {"StandardForm": {"expr": "MakeBoxes[0.``30]", "expect": "\"0``30\""}}}, "Graphics": {"Disk": {"StandardForm": {"expr": "MakeBoxes[Graphics[{Disk[{0,0}, 1]}]]", "expect": "GraphicsBox[{DiskBox[{0, 0}, 1]}]"}, "OutputForm": {"expr": "MakeBoxes[Graphics[{Disk[{0,0}, 1]}]//OutputForm]", "expect": "InterpretationBox[PaneBox[\"\\\"-Graphics-\\\"\"], OutputForm[Graphics[{Disk[{0, 0}, 1]}]], Editable -> False]"}}, "Sphere": {"StandardForm": {"expr": "MakeBoxes[Graphics3D[{Sphere[{0,0,0}, 1]}]]", "expect": "Graphics3DBox[{SphereBox[{0, 0,0}, 1]}]"}, "OutputForm": {"expr": "MakeBoxes[Graphics3D[{Sphere[{0,0,0}, 1]}]//OutputForm]", "expect": "InterpretationBox[PaneBox[\"\\\"-Graphics3D-\\\"\"], OutputForm[Graphics3D[{Sphere[{0, 0, 0}, 1]}]], Editable -> False]"}}}, "Parsing string form": {"RowBox": {"StandardForm": {"expr": "\\( a, b \\)", "expect": "RowBox[{\"a\", \",\", \"b\"}]", "msg": "TODO: Parsing of special characters (like commas)."}}, "FractionBox": {"StandardForm": {"expr": "\\(x \\/ y + z\\)", "expect": "RowBox[{FractionBox[\"x\", \"y\"], \"+\", \"z\"}]"}}, "FractionBox bracket": {"StandardForm": {"expr": "\\(x \\/ (y + z)\\)", "expect": "FractionBox[\"x\", RowBox[{\"(\", RowBox[{\"y\", \"+\", \"z\"}], \")\"}]]"}}, "SqrtBox": {"StandardForm": {"expr": "\\( \\@ a + b \\)", "expect": "RowBox[{SqrtBox[\"a\"], \"+\", \"b\"}]"}}, "FormBox": {"StandardForm": {"expr": "\\(TraditionalForm \\` a + b\\)", "expect": "FormBox[RowBox[{\"a\", \"+\", \"b\"}], TraditionalForm]"}}}} From 25454a031a142d8cb8efbb3a93646c0c38e364ea Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 4 Jan 2026 16:40:27 -0300 Subject: [PATCH 20/49] remove default options in boxes --- mathics/builtin/box/layout.py | 46 +++++++++++++++--------- mathics/builtin/drawing/plot_plot3d.py | 4 +-- mathics/builtin/options.py | 48 +++++++++++++++++++++---- mathics/eval/drawing/plot.py | 6 ++-- mathics/eval/drawing/plot_vectorized.py | 4 +-- 5 files changed, 78 insertions(+), 30 deletions(-) diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index 4fa9ff4c9..d349b3e64 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -10,7 +10,7 @@ from typing import Tuple from mathics.builtin.box.expression import BoxExpression -from mathics.builtin.options import options_to_rules +from mathics.builtin.options import filter_non_default_values, options_to_rules from mathics.core.atoms import String from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin @@ -35,12 +35,12 @@ def elements_to_expressions( - elements: Tuple[BaseElement], options: dict + self: BoxExpression, elements: Tuple[BaseElement], options: dict ) -> Tuple[BaseElement]: """ Return a tuple of Mathics3 normal atoms or expressions. """ - opts = sorted(options_to_rules(options)) + opts = sorted(options_to_rules(options, filter_non_default_values(self))) expr_elements = [ elem.to_expression() if isinstance(elem, BoxExpression) else elem for elem in elements @@ -106,6 +106,7 @@ def elements(self): if self._elements is None: self._elements = elements_to_expressions( ( + self, self.num, self.den, ), @@ -159,7 +160,7 @@ class GridBox(BoxExpression): @property def elements(self): if self._elements is None: - self._elements = elements_to_expressions(self.items, self.box_options) + self._elements = elements_to_expressions(self, self.items, self.box_options) return self.elements def init(self, *elems, **kwargs): @@ -226,11 +227,12 @@ def init(self, *expr, **options): def elements(self): if self._elements is None: self._elements = elements_to_expressions( + self, ( self.boxed, self.expr, ), - {}, + self.box_options, ) return self._elements @@ -271,7 +273,9 @@ class PaneBox(BoxExpression): @property def elements(self): if self._elements is None: - self._elements = elements_to_expressions((self.boxed,), self.box_options) + self._elements = elements_to_expressions( + self, (self.boxed,), self.box_options + ) return self._elements def init(self, expr, **options): @@ -404,9 +408,13 @@ def elements(self): index = self.index if index is None: # self.box_options - self._elements = elements_to_expressions((self.radicand,), {}) + self._elements = elements_to_expressions( + self, (self.radicand,), self.box_options + ) else: - self._elements = elements_to_expressions((self.radicand, index), {}) + self._elements = elements_to_expressions( + self, (self.radicand, index), self.box_options + ) return self._elements def eval_index(self, radicand, index, evaluation: Evaluation, options: dict): @@ -459,10 +467,12 @@ def elements(self): boxes = self.boxes if style: self._elements = elements_to_expressions( - (boxes, style), self.box_options + self, (boxes, style), self.box_options ) else: - self._elements = elements_to_expressions((boxes,), self.box_options) + self._elements = elements_to_expressions( + self, (boxes,), self.box_options + ) return self._elements def eval_options(self, boxes, evaluation: Evaluation, options: dict): @@ -486,6 +496,7 @@ def init(self, boxes, style=None, **options): boxes = boxes.boxes self.style = style self.box_options = options + assert options is not None self.boxes = boxes assert isinstance(self.boxes, BoxElementMixin), "f{type(self.boxes)}" @@ -513,8 +524,9 @@ class SubscriptBox(BoxExpression): @property def elements(self): if self._elements is None: - # self.box_options - self._elements = elements_to_expressions((self.base, self.subindex), {}) + self._elements = elements_to_expressions( + self, (self.base, self.subindex), self.box_options + ) return self._elements def eval(self, a, b, evaluation: Evaluation, options: dict): @@ -555,11 +567,12 @@ def elements(self): # self.box_options self._elements = elements_to_expressions( ( + self, self.base, self.subindex, self.superindex, ), - {}, + self.box_options, ) return self._elements @@ -600,13 +613,13 @@ class SuperscriptBox(BoxExpression): @property def elements(self): if self._elements is None: - # self.box_options self._elements = elements_to_expressions( + self, ( self.base, self.superindex, ), - {}, + self.box_options, ) return self._elements @@ -652,11 +665,12 @@ def init(self, *elems, **kwargs): def elements(self): if self._elements is None: self._elements = elements_to_expressions( + self, ( self.boxed, self.form, ), - self.box_options, + self.box_option, ) return self._elements diff --git a/mathics/builtin/drawing/plot_plot3d.py b/mathics/builtin/drawing/plot_plot3d.py index ac4f17f65..0ce3b517b 100644 --- a/mathics/builtin/drawing/plot_plot3d.py +++ b/mathics/builtin/drawing/plot_plot3d.py @@ -16,7 +16,7 @@ from mathics.builtin.drawing.graphics3d import Graphics3D from mathics.builtin.graphics import Graphics -from mathics.builtin.options import options_to_rules +from mathics.builtin.options import filter_from_iterable, options_to_rules from mathics.core.attributes import A_HOLD_ALL, A_PROTECTED from mathics.core.builtin import Builtin from mathics.core.convert.expression import to_mathics_list @@ -138,7 +138,7 @@ def eval( # generate the Graphics[3D] result graphics_expr = graphics.generate( - options_to_rules(options, self.graphics_class.options) + options_to_rules(options, filter_from_iterable(self.graphics_class.options)) ) return graphics_expr diff --git a/mathics/builtin/options.py b/mathics/builtin/options.py index 250033a42..4e650bfbd 100644 --- a/mathics/builtin/options.py +++ b/mathics/builtin/options.py @@ -10,6 +10,7 @@ :WMA link: https://reference.wolfram.com/language/guide/OptionsManagement.html """ +from typing import Callable, Optional from mathics.builtin.image.base import Image from mathics.core.atoms import Integer1, String @@ -17,6 +18,7 @@ from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression +from mathics.core.parser import parse_builtin_rule from mathics.core.symbols import Symbol, SymbolList, ensure_context, strip_context from mathics.core.systemsymbols import SymbolDefault, SymbolRule, SymbolRuleDelayed from mathics.eval.patterns import Matcher, get_default_value @@ -570,12 +572,44 @@ def eval(self, symbol: Symbol, options: Expression, evaluation: Evaluation): return ListExpression(*options_list) -def options_to_rules(options, filter=None): +def filter_non_default_values(builtin): + """ + Return a filter function that removes those + options which have associated their default values. + """ + builtin_options = builtin.options + builtin_options = { + strip_context(name): parse_builtin_rule(value) + for name, value in builtin_options.items() + } + + def filter(name, value): + name = strip_context(name) + if name not in builtin_options: + return True + if value.sameQ(builtin_options[name]): + return False + return True + + return filter + + +def filter_from_iterable(elems): + """ + Build a filter function from an iterable. + The filter function returns `True` if + the name after striping its context is in + the interable. + """ + + def filter(name, value): + return strip_context(name) in elems + + return filter + + +def options_to_rules(options, filter: Optional[Callable] = None): items = sorted(options.items()) - if filter: - items = [ - (name, value) - for name, value in items - if strip_context(name) in filter.keys() - ] + if filter is not None: + items = [(name, value) for name, value in items if filter(name, value)] return [Expression(SymbolRule, Symbol(name), value) for name, value in items] diff --git a/mathics/eval/drawing/plot.py b/mathics/eval/drawing/plot.py index 04b08df8f..b74cf59c5 100644 --- a/mathics/eval/drawing/plot.py +++ b/mathics/eval/drawing/plot.py @@ -13,7 +13,7 @@ from mathics.builtin.graphics import Graphics from mathics.builtin.numeric import chop -from mathics.builtin.options import options_to_rules +from mathics.builtin.options import filter_from_iterable, options_to_rules from mathics.builtin.scoping import dynamic_scoping from mathics.core.atoms import Integer, Integer0, Real from mathics.core.builtin import get_option @@ -461,7 +461,7 @@ def eval_ListPlot( return Expression( SymbolGraphics, ListExpression(*graphics), - *options_to_rules(options, Graphics.options), + *options_to_rules(options, filter_from_iterable(Graphics.options)), ) @@ -699,7 +699,7 @@ def find_excl(excl): return Expression( SymbolGraphics, ListExpression(*graphics), - *options_to_rules(options, Graphics.options), + *options_to_rules(options, filter_from_iterable(Graphics.options)), ) diff --git a/mathics/eval/drawing/plot_vectorized.py b/mathics/eval/drawing/plot_vectorized.py index b4a9bbdcb..63182350c 100644 --- a/mathics/eval/drawing/plot_vectorized.py +++ b/mathics/eval/drawing/plot_vectorized.py @@ -6,7 +6,7 @@ import numpy as np from mathics.builtin.graphics import Graphics -from mathics.builtin.options import options_to_rules +from mathics.builtin.options import filter_from_iterable, options_to_rules from mathics.core.convert.lambdify import lambdify_compile from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression @@ -87,6 +87,6 @@ def compile_maybe_list(evaluation, function, names): graphics.add_complex(xys, lines=line, polys=None) # copy options to output and generate the Graphics expr - options = options_to_rules(options, Graphics.options) + options = options_to_rules(options, filter_from_iterable(Graphics.options)) graphics_expr = graphics.generate(options) return graphics_expr From 5627a8168641e32c716362a2215996858fd463d2 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 4 Jan 2026 17:43:21 -0300 Subject: [PATCH 21/49] adjust format renders --- mathics/format/latex.py | 12 +++--------- mathics/format/mathml.py | 12 +++--------- mathics/format/text.py | 4 ++-- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/mathics/format/latex.py b/mathics/format/latex.py index 7adf88656..603405df6 100644 --- a/mathics/format/latex.py +++ b/mathics/format/latex.py @@ -139,18 +139,14 @@ def render(format, string, in_text=False): def interpretation_box(self, **options): - return lookup_conversion_method(self.elements[0], "latex")( - self.elements[0], **options - ) + return lookup_conversion_method(self.boxed, "latex")(self.boxed, **options) add_conversion_fn(InterpretationBox, interpretation_box) def pane_box(self, **options): - content = lookup_conversion_method(self.elements[0], "latex")( - self.elements[0], **options - ) + content = lookup_conversion_method(self.boxed, "latex")(self.boxed, **options) options = self.box_options size = options.get("System`ImageSize", SymbolAutomatic).to_python() if size is SymbolAutomatic: @@ -650,9 +646,7 @@ def graphics3dbox(self, elements=None, **options) -> str: def tag_box(self, **options): - return lookup_conversion_method(self.elements[0], "latex")( - self.elements[0], **options - ) + return lookup_conversion_method(self.boxed, "latex")(self.boxed, **options) add_conversion_fn(TagBox, tag_box) diff --git a/mathics/format/mathml.py b/mathics/format/mathml.py index 149d5fac8..bfae9867a 100644 --- a/mathics/format/mathml.py +++ b/mathics/format/mathml.py @@ -118,18 +118,14 @@ def render(format, string): def interpretation_box(self, **options): - return lookup_conversion_method(self.elements[0], "mathml")( - self.elements[0], **options - ) + return lookup_conversion_method(self.boxed, "mathml")(self.boxed, **options) add_conversion_fn(InterpretationBox, interpretation_box) def pane_box(self, **options): - content = lookup_conversion_method(self.elements[0], "mathml")( - self.elements[0], **options - ) + content = lookup_conversion_method(self.boxed, "mathml")(self.boxed, **options) options = self.box_options size = options.get("System`ImageSize", SymbolAutomatic).to_python() if size is SymbolAutomatic: @@ -371,9 +367,7 @@ def graphics3dbox(self, elements=None, **options) -> str: def tag_box(self, **options): - return lookup_conversion_method(self.elements[0], "mathml")( - self.elements[0], **options - ) + return lookup_conversion_method(self.boxed, "mathml")(self.boxed, **options) add_conversion_fn(TagBox, tag_box) diff --git a/mathics/format/text.py b/mathics/format/text.py index 4253c45d1..f71089b4b 100644 --- a/mathics/format/text.py +++ b/mathics/format/text.py @@ -45,14 +45,14 @@ def string(self, **options) -> str: def interpretation_box(self, **options): - return boxes_to_text(self.elements[0], **options) + return boxes_to_text(self.boxed, **options) add_conversion_fn(InterpretationBox, interpretation_box) def pane_box(self, **options): - return boxes_to_text(self.elements[0], **options) + return boxes_to_text(self.boxed, **options) add_conversion_fn(PaneBox, pane_box) From 555588b30a3cd4784a5a107a599dd4e0c420a026 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 4 Jan 2026 18:39:09 -0300 Subject: [PATCH 22/49] add options to InterpretationBox. Add quotes to PaneBox. --- mathics/builtin/box/layout.py | 2 +- mathics/eval/makeboxes/makeboxes.py | 2 +- mathics/format/text.py | 3 ++- test/format/format_tests.m | 17 +++++++++-------- test/format/makeboxes_tests.m | 4 ++-- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index 4fa9ff4c9..bc8981841 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -230,7 +230,7 @@ def elements(self): self.boxed, self.expr, ), - {}, + self.box_options, ) return self._elements diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 780103c46..d124810db 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -145,7 +145,7 @@ def eval_makeboxes_outputform(expr, evaluation, form): from mathics.format.outputform import expression_to_outputform_text text_outputform = str(expression_to_outputform_text(expr, evaluation, form)) - elem1 = PaneBox(String(text_outputform)) + elem1 = PaneBox(String('"' + text_outputform + '"')) return elem1 diff --git a/mathics/format/text.py b/mathics/format/text.py index f71089b4b..ce6097f6d 100644 --- a/mathics/format/text.py +++ b/mathics/format/text.py @@ -52,7 +52,8 @@ def interpretation_box(self, **options): def pane_box(self, **options): - return boxes_to_text(self.boxed, **options) + result = boxes_to_text(self.boxed, **options) + return result add_conversion_fn(PaneBox, pane_box) diff --git a/test/format/format_tests.m b/test/format/format_tests.m index c73cadf6d..bc43b2734 100755 --- a/test/format/format_tests.m +++ b/test/format/format_tests.m @@ -7,7 +7,8 @@ ****************************************************************************************) -ISMATHICSINTERPRETER=(StringTake[$Version, 8]==="Mathics3") +ISMATHICSINTERPRETER=(StringTake[$Version, 8]==="Mathics3"); +ISMATHICSINTERPRETER=False; Print["Read json"]; @@ -29,10 +30,10 @@ Do[form = ToExpression[subtest[[1]]]; expr = form[key]; result = ToString[expr, CharacterEncoding->"ASCII"]; expected = subtest[[2]]; - If[result != subtest[[2]], - Print[" * ", form, "(text) [Failed]\n result:", "<<" <> result <> ">>(", StringLength[result], + If[result != expected, + Print[" * ", FullForm[expr], " //", form, "(text) [Failed]\n result:", "<<" <> result <> ">>(", StringLength[result], ")\n expected: ", "<<" <> expected <> ">> (", StringLength[expected],")\n"], - Print[" * ", form, "(text) [OK]"]];, + Print[" * ", FullForm[expr], " //", form, "(text) [OK]"]];, {subtest, text}] ]; (*LaTeX*) @@ -42,10 +43,10 @@ result = ToString[expr, TeXForm, CharacterEncoding->"ASCII"]; expected = subtest[[2]]; If[result != subtest[[2]], - Print[" * ", form, "(latex) [Failed]\n result:", + Print[" * ", key, " //", form, "(latex) [Failed]\n result:", "<<" <> result <> ">>", "\n expected: ", "<<" <> expected <> ">>\n"], - Print[" * ", form, "(latex) [OK]"]];, + Print[" * ", key, " //", form, "(latex) [OK]"]];, {subtest, latex}] ]; (*MathML*) @@ -55,9 +56,9 @@ result = ToString[expr, MathMLForm, CharacterEncoding->"ASCII"]; expected = subtest[[2]]; If[result != subtest[[2]], - Print[" * ", form, "(mathml) [Failed]\n result:", + Print[" * ", key, " //", form, "(mathml) [Failed]\n result:", "<<" <> result <> ">>", "\n expected: ", "<<" <> expected <> ">>\n"], - Print[" * ", form, "(mathml) [OK]"]];, {subtest, mathml}]]; + Print[" * ", key, " //", form, "(mathml) [OK]"]];, {subtest, mathml}]]; , {tests, data} ] diff --git a/test/format/makeboxes_tests.m b/test/format/makeboxes_tests.m index 54eecc8c3..49360ed4c 100755 --- a/test/format/makeboxes_tests.m +++ b/test/format/makeboxes_tests.m @@ -28,8 +28,8 @@ result = ToExpression[expr]; expect = ToExpression["expect"/.rul]; If[SameQ[result, expect], - Print[" ", form, " [OK]"], - Print[" ", form, " [Failed]"]; + Print[" ", expr, " //", form, " [OK]"], + Print[" ", expr, " //", form, " [Failed]"]; Print[" expr = ", expr ]; Print[" result = ", result]; Print[" expected= ",expect]; From 6f2ef5e401304d6e0a57c045aabf0b369089fe5a Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 5 Jan 2026 13:33:36 -0300 Subject: [PATCH 23/49] remove unused imports. lexicographic order functions in `mathics.format.output`. Use a register function --- mathics/eval/makeboxes/makeboxes.py | 3 +- mathics/format/outputform.py | 497 +++++++++++++--------------- mathics/format/text.py | 1 - test/format/test_makeboxes.py | 1 - 4 files changed, 225 insertions(+), 277 deletions(-) diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index d124810db..9a4055df1 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -21,7 +21,6 @@ ) from mathics.core.systemsymbols import ( # SymbolRule, SymbolRuleDelayed, SymbolComplex, - SymbolOutputForm, SymbolRational, SymbolStandardForm, ) @@ -141,7 +140,7 @@ def eval_makeboxes_outputform(expr, evaluation, form): """ Build a 2D representation of the expression using only keyboard characters. """ - from mathics.builtin.box.layout import InterpretationBox, PaneBox + from mathics.builtin.box.layout import PaneBox from mathics.format.outputform import expression_to_outputform_text text_outputform = str(expression_to_outputform_text(expr, evaluation, form)) diff --git a/mathics/format/outputform.py b/mathics/format/outputform.py index f57f3a712..d765dc7f9 100644 --- a/mathics/format/outputform.py +++ b/mathics/format/outputform.py @@ -1,5 +1,9 @@ """ -This module builts the 2D string associated to the OutputForm +This module builts the string associated to the OutputForm. + +OutputForm produce a pretty-print-like output, suitable for CLI +and text terminals. + """ from typing import Callable, Dict, List, Union @@ -36,7 +40,7 @@ SymbolPrefix = Symbol("System`Prefix") -expr_to_outputform_text_map: Dict[str, Callable] = {} +EXPR_TO_OUTPUTFORM_TEXT_MAP: Dict[str, Callable] = {} # This Exception if the expression should @@ -53,63 +57,6 @@ class IsNot2DArray(Exception): pass -def parenthesize(expr_str: str) -> str: - """wrap with parenthesis""" - return f"({expr_str})" - - -def bracket(expr_str: str) -> str: - """wrap with square brackets""" - return f"[{expr_str}]" - - -def grid(expr): - # Very basic implementation. - result = "Grid[" - for row in expr: - if result[-1] != "[": - result += "," - - result += "\n{" - for field in row: - if result[-1] != "}": - result += "," - result += field - result += "}" - result += "}]" - return result - - -def expression_to_outputform_text( - expr: BaseElement, evaluation: Evaluation, form=SymbolStandardForm, **kwargs -): - """ - Build a 2d text from an `Expression` - """ - ## TODO: format the expression - format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore - - # Strip HoldForm - while format_expr.has_form("HoldForm", 1): # type: ignore - format_expr = format_expr.elements[0] - - lookup_name = format_expr.get_head().get_lookup_name() - try: - result = expr_to_outputform_text_map[lookup_name]( - format_expr, evaluation, form, **kwargs - ) - return result - except _WrongFormattedExpression: - # If the key is not present, or the execution fails for any reason, use - # the default - pass - except KeyError: - pass - return _default_expression_to_outputform_text( - format_expr, evaluation, form, **kwargs - ) - - def _default_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -146,14 +93,42 @@ def _strip_1_parm_expression_to_outputform_text( return expression_to_outputform_text(expr.elements[0], evaluation, form, **kwargs) -expr_to_outputform_text_map[ - "System`HoldForm" -] = _strip_1_parm_expression_to_outputform_text -expr_to_outputform_text_map[ - "System`InputForm" -] = _strip_1_parm_expression_to_outputform_text +def bracket(expr_str: str) -> str: + """wrap with square brackets""" + return f"[{expr_str}]" + + +def grid(expr): + # Very basic implementation. + result = "Grid[" + for row in expr: + if result[-1] != "[": + result += "," + + result += "\n{" + for field in row: + if result[-1] != "}": + result += "," + result += field + result += "}" + result += "}]" + return result + + +def parenthesize(expr_str: str) -> str: + """wrap with parenthesis""" + return f"({expr_str})" + + +def register_outputform(head_name): + def _register(func): + EXPR_TO_OUTPUTFORM_TEXT_MAP[head_name] = func + return func + + return _register +@register_outputform("System`Derivative") def derivative_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -186,11 +161,7 @@ def derivative_expression_to_outputform_text( return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) -expr_to_outputform_text_map[ - "System`Derivative" -] = derivative_expression_to_outputform_text - - +@register_outputform("System`Divide") def divide_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -200,23 +171,47 @@ def divide_expression_to_outputform_text( return _divide(num, den, evaluation, form, **kwargs) -expr_to_outputform_text_map["System`Divide"] = divide_expression_to_outputform_text +def expression_to_outputform_text( + expr: BaseElement, evaluation: Evaluation, form=SymbolStandardForm, **kwargs +): + """ + Build a pretty-print text from an `Expression` + """ + ## TODO: format the expression + format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore + # Strip HoldForm + while format_expr.has_form("HoldForm", 1): # type: ignore + format_expr = format_expr.elements[0] -def graphics(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: - return "-Graphics-" + lookup_name = format_expr.get_head().get_lookup_name() + try: + result = EXPR_TO_OUTPUTFORM_TEXT_MAP[lookup_name]( + format_expr, evaluation, form, **kwargs + ) + return result + except _WrongFormattedExpression: + # If the key is not present, or the execution fails for any reason, use + # the default + pass + except KeyError: + pass + return _default_expression_to_outputform_text( + format_expr, evaluation, form, **kwargs + ) -expr_to_outputform_text_map["System`Graphics"] = graphics +@register_outputform("System`Graphics") +def graphics(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: + return "-Graphics-" +@register_outputform("System`Graphics3D") def graphics3d(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: return "-Graphics3D-" -expr_to_outputform_text_map["System`Graphics3D"] = graphics3d - - +@register_outputform("System`Grid") def grid_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -245,18 +240,143 @@ def grid_expression_to_outputform_text( return grid(rows) -expr_to_outputform_text_map["System`Grid"] = grid_expression_to_outputform_text +register_outputform("System`HoldForm")(_strip_1_parm_expression_to_outputform_text) +# TODO: Do it better when InputForm be implemented. +register_outputform("System`InputForm")(_strip_1_parm_expression_to_outputform_text) + + +@register_outputform("System`Infix") +def infix_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + elements = expr.elements + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + group = None + precedence = 670 + # Processing the first argument: + head = expr.get_head() + target = expr.elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + operands = list(target.elements) + if len(operands) < 2: + raise _WrongFormattedExpression + # Processing the second argument, if it is there: + if len(elements) > 1: + ops = elements[1] + if head is SymbolInfix: + # This is not the WMA behaviour, but the Mathics current implementation requires it: + num_ops = 1 + if ops.has_form("List", None): + num_ops = len(ops.elements) + ops_lst = [ + expression_to_outputform_text(op, evaluation, form, **kwargs) + for op in ops.elements + ] + else: + ops_lst = [ + expression_to_outputform_text(ops, evaluation, form, **kwargs) + ] + elif head in (SymbolPrefix, SymbolPostfix): + ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolInfix: + num_ops = 1 + default_symb = " ~ " + ops_lst = [ + default_symb + + expression_to_outputform_text(head, evaluation, form, **kwargs) + + default_symb + ] + elif head is SymbolPrefix: + default_symb = " @ " + ops_txt = ( + expression_to_outputform_text(head, evaluation, form, **kwargs) + + default_symb + ) + elif head is SymbolPostfix: + default_symb = " // " + ops_txt = default_symb + expression_to_outputform_text( + head, evaluation, form, **kwargs + ) + + # Processing the third argument, if it is there: + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: + raise _WrongFormattedExpression + + # Processing the forth argument, if it is there: + if len(elements) > 3: + group = elements[3] + if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): + raise _WrongFormattedExpression + if group is SymbolNone: + group = None + + if head is SymbolPrefix: + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + return ops_txt[0] + target_txt + if head is SymbolPostfix: + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + return target_txt + ops_txt[0] + else: # Infix + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + for index, operand in enumerate(operands): + operand_txt = str( + expression_to_outputform_text(operand, evaluation, form, **kwargs) + ) + cmp_precedence = compare_precedence(operand, precedence) + if cmp_precedence is not None and ( + cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) + ): + operand_txt = parenthesize(operand_txt) + + if index == 0: + result = operand_txt + # After the first element, for lateral + # associativity, parenthesized is flipped: + if group in (SymbolLeft, SymbolRight): + parenthesized = not parenthesized + else: + space = " " + result_lst: List[str] + if str(ops_lst[index % num_ops]) != " ": + result_lst = [ + result, + space, + str(ops_lst[index % num_ops]), + space, + operand_txt, + ] + else: + result_lst = [result, space, operand_txt] + + return "".join(result_lst) + + +@register_outputform("System`Integer") def integer_expression_to_outputform_text( n: Integer, evaluation: Evaluation, form: Symbol, **kwargs ): return str(n.value) -expr_to_outputform_text_map["System`Integer"] = integer_expression_to_outputform_text - - +@register_outputform("System`List") def list_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -270,9 +390,7 @@ def list_expression_to_outputform_text( return "{" + result + "}" -expr_to_outputform_text_map["System`List"] = list_expression_to_outputform_text - - +@register_outputform("System`MathMLForm") def mathmlform_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -283,11 +401,7 @@ def mathmlform_expression_to_outputform_text( return boxes.boxes_to_mathml() # type: ignore[union-attr] -expr_to_outputform_text_map[ - "System`MathMLForm" -] = mathmlform_expression_to_outputform_text - - +@register_outputform("System`MatrixForm") def matrixform_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -295,11 +409,7 @@ def matrixform_expression_to_outputform_text( return tableform_expression_to_outputform_text(expr, evaluation, form, **kwargs) -expr_to_outputform_text_map[ - "System`MatrixForm" -] = matrixform_expression_to_outputform_text - - +@register_outputform("System`Plus") def plus_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -367,9 +477,7 @@ def plus_expression_to_outputform_text( return result -expr_to_outputform_text_map["System`Plus"] = plus_expression_to_outputform_text - - +@register_outputform("System`Power") def power_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ): @@ -386,9 +494,19 @@ def power_expression_to_outputform_text( return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) -expr_to_outputform_text_map["System`Power"] = power_expression_to_outputform_text +@register_outputform("System`PrecedenceForm") +def precedenceform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + if len(expr.elements) == 2: + return expression_to_outputform_text( + expr.elements[0], evaluation, form, **kwargs + ) + raise _WrongFormattedExpression +@register_outputform("System`Prefix") +@register_outputform("System`Postfix") def pre_pos_fix_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -449,153 +567,7 @@ def pre_pos_fix_expression_to_outputform_text( return ops_txt[0] + target_txt if head is SymbolPrefix else target_txt + ops_txt[0] -expr_to_outputform_text_map["System`Prefix"] = pre_pos_fix_expression_to_outputform_text -expr_to_outputform_text_map[ - "System`Postfix" -] = pre_pos_fix_expression_to_outputform_text - - -def infix_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - elements = expr.elements - if not (0 <= len(elements) <= 4): - raise _WrongFormattedExpression - - group = None - precedence = 670 - # Processing the first argument: - head = expr.get_head() - target = expr.elements[0] - if isinstance(target, Atom): - raise _WrongFormattedExpression - - operands = list(target.elements) - - if len(operands) < 2: - raise _WrongFormattedExpression - - # Processing the second argument, if it is there: - if len(elements) > 1: - ops = elements[1] - if head is SymbolInfix: - # This is not the WMA behaviour, but the Mathics current implementation requires it: - num_ops = 1 - if ops.has_form("List", None): - num_ops = len(ops.elements) - ops_lst = [ - expression_to_outputform_text(op, evaluation, form, **kwargs) - for op in ops.elements - ] - else: - ops_lst = [ - expression_to_outputform_text(ops, evaluation, form, **kwargs) - ] - elif head in (SymbolPrefix, SymbolPostfix): - ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] - else: - if head is SymbolInfix: - num_ops = 1 - default_symb = " ~ " - ops_lst = [ - default_symb - + expression_to_outputform_text(head, evaluation, form, **kwargs) - + default_symb - ] - elif head is SymbolPrefix: - default_symb = " @ " - ops_txt = ( - expression_to_outputform_text(head, evaluation, form, **kwargs) - + default_symb - ) - elif head is SymbolPostfix: - default_symb = " // " - ops_txt = default_symb + expression_to_outputform_text( - head, evaluation, form, **kwargs - ) - - # Processing the third argument, if it is there: - if len(elements) > 2: - if isinstance(elements[2], Integer): - precedence = elements[2].value - else: - raise _WrongFormattedExpression - - # Processing the forth argument, if it is there: - if len(elements) > 3: - group = elements[3] - if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): - raise _WrongFormattedExpression - if group is SymbolNone: - group = None - - if head is SymbolPrefix: - operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: - target_txt = parenthesize(target_txt) - return ops_txt[0] + target_txt - if head is SymbolPostfix: - operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: - target_txt = parenthesize(target_txt) - return target_txt + ops_txt[0] - else: # Infix - parenthesized = group in (None, SymbolRight, SymbolNonAssociative) - for index, operand in enumerate(operands): - operand_txt = str( - expression_to_outputform_text(operand, evaluation, form, **kwargs) - ) - cmp_precedence = compare_precedence(operand, precedence) - if cmp_precedence is not None and ( - cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) - ): - operand_txt = parenthesize(operand_txt) - - if index == 0: - result = operand_txt - # After the first element, for lateral - # associativity, parenthesized is flipped: - if group in (SymbolLeft, SymbolRight): - parenthesized = not parenthesized - else: - space = " " - result_lst: List[str] - if str(ops_lst[index % num_ops]) != " ": - result_lst = [ - result, - space, - str(ops_lst[index % num_ops]), - space, - operand_txt, - ] - else: - result_lst = [result, space, operand_txt] - - return "".join(result_lst) - - -expr_to_outputform_text_map["System`Infix"] = infix_expression_to_outputform_text - - -def precedenceform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - if len(expr.elements) == 2: - return expression_to_outputform_text( - expr.elements[0], evaluation, form, **kwargs - ) - raise _WrongFormattedExpression - - -expr_to_outputform_text_map[ - "System`PrecedenceForm" -] = precedenceform_expression_to_outputform_text - - +@register_outputform("System`Rational") def rational_expression_to_outputform_text( n: Union[Rational, Expression], evaluation: Evaluation, form: Symbol, **kwargs ): @@ -606,9 +578,7 @@ def rational_expression_to_outputform_text( return _divide(num, den, evaluation, form, **kwargs) -expr_to_outputform_text_map["System`Rational"] = rational_expression_to_outputform_text - - +@register_outputform("System`Real") def real_expression_to_outputform_text( n: Real, evaluation: Evaluation, form: Symbol, **kwargs ): @@ -616,9 +586,7 @@ def real_expression_to_outputform_text( return str(str_n) -expr_to_outputform_text_map["System`Real"] = real_expression_to_outputform_text - - +@register_outputform("System`String") def string_expression_to_outputform_text( expr: String, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -628,9 +596,7 @@ def string_expression_to_outputform_text( return "\n".join(lines) -expr_to_outputform_text_map["System`String"] = string_expression_to_outputform_text - - +@register_outputform("System`StringForm") def stringform_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -682,31 +648,21 @@ def stringform_expression_to_outputform_text( return result -expr_to_outputform_text_map[ - "System`StringForm" -] = stringform_expression_to_outputform_text - - +@register_outputform("System`Symbol") def symbol_expression_to_outputform_text( symb: Symbol, evaluation: Evaluation, form: Symbol, **kwargs ): return evaluation.definitions.shorten_name(symb.name) -expr_to_outputform_text_map["System`Symbol"] = symbol_expression_to_outputform_text - - +@register_outputform("System`TableForm") def tableform_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: return grid_expression_to_outputform_text(expr, evaluation, form) -expr_to_outputform_text_map[ - "System`TableForm" -] = tableform_expression_to_outputform_text - - +@register_outputform("System`TeXForm") def texform_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -717,9 +673,7 @@ def texform_expression_to_outputform_text( return boxes.boxes_to_tex() # type: ignore -expr_to_outputform_text_map["System`TeXForm"] = texform_expression_to_outputform_text - - +@register_outputform("System`Times") def times_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: @@ -787,6 +741,3 @@ def times_expression_to_outputform_text( if prefactor == -1: result = "-" + result return result - - -expr_to_outputform_text_map["System`Times"] = times_expression_to_outputform_text diff --git a/mathics/format/text.py b/mathics/format/text.py index ce6097f6d..b1ba18c5a 100644 --- a/mathics/format/text.py +++ b/mathics/format/text.py @@ -7,7 +7,6 @@ from mathics.builtin.box.graphics import GraphicsBox from mathics.builtin.box.graphics3d import Graphics3DBox from mathics.builtin.box.layout import ( - BoxElementMixin, FractionBox, GridBox, InterpretationBox, diff --git a/test/format/test_makeboxes.py b/test/format/test_makeboxes.py index b7e0bbbce..e4a1386b1 100644 --- a/test/format/test_makeboxes.py +++ b/test/format/test_makeboxes.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import os -import test from test.helper import check_evaluation import pytest From d0857fd311d5fa78f2aea84470786c4e20e90e9c Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 5 Jan 2026 21:09:15 -0300 Subject: [PATCH 24/49] more on mathics.format.outputform --- mathics/builtin/arithfns/basic.py | 4 +- mathics/builtin/makeboxes.py | 1 + mathics/doc/documentation/1-Manual.mdoc | 2 +- mathics/eval/makeboxes/makeboxes.py | 13 +- mathics/format/latex.py | 6 +- mathics/format/outputform.py | 233 ++++++++++++++++++------ 6 files changed, 194 insertions(+), 65 deletions(-) diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index e18605b18..0e3820823 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -686,11 +686,11 @@ def inverse(item): positive = [Expression(SymbolHoldForm, item) for item in positive] negative = [Expression(SymbolHoldForm, item) for item in negative] if positive: - positive = create_infix(positive, op, 400, "None") + positive = create_infix(positive, op, 400, "Left") else: positive = Integer1 if negative: - negative = create_infix(negative, op, 400, "None") + negative = create_infix(negative, op, 400, "Left") result = Expression( SymbolDivide, Expression(SymbolHoldForm, positive), diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index fa401befa..c11ee4946 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -16,6 +16,7 @@ format_element, parenthesize, ) +from mathics.settings import SYSTEM_CHARACTER_ENCODING # TODO: Differently from the current implementation, MakeBoxes should only # accept as its format field the symbols in `$BoxForms`. This is something to diff --git a/mathics/doc/documentation/1-Manual.mdoc b/mathics/doc/documentation/1-Manual.mdoc index 0daad605d..024ebcb2f 100644 --- a/mathics/doc/documentation/1-Manual.mdoc +++ b/mathics/doc/documentation/1-Manual.mdoc @@ -903,7 +903,7 @@ This will even apply to 'TeXForm', because 'TeXForm' implies 'StandardForm': Except some other form is applied first: >> b // OutputForm // TeXForm - = b + = \text{b} 'MakeBoxes' for another form: >> MakeBoxes[b, TeXForm] = "d"; diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 9a4055df1..8cf465cc5 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -21,6 +21,7 @@ ) from mathics.core.systemsymbols import ( # SymbolRule, SymbolRuleDelayed, SymbolComplex, + SymbolOutputForm, SymbolRational, SymbolStandardForm, ) @@ -136,14 +137,16 @@ def int_to_string_shorter_repr(value: int, form: Symbol, max_digits=640): return String(value_str) -def eval_makeboxes_outputform(expr, evaluation, form): +def eval_makeboxes_outputform(expr, evaluation, form, **kwargs): """ Build a 2D representation of the expression using only keyboard characters. """ from mathics.builtin.box.layout import PaneBox from mathics.format.outputform import expression_to_outputform_text - text_outputform = str(expression_to_outputform_text(expr, evaluation, form)) + text_outputform = str( + expression_to_outputform_text(expr, evaluation, form, **kwargs) + ) elem1 = PaneBox(String('"' + text_outputform + '"')) return elem1 @@ -282,6 +285,12 @@ def format_element( while element.get_head() is form: element = element.elements[0] + # By now, eval_makeboxes_outputform is only used when we explicitly + # ask for MakeBoxes[OutputForm[expr], fmt] + # When it get ready, we can uncomment this. + # if form is SymbolOutputForm: + # return eval_makeboxes_outputform(element, evaluation, form, **kwargs) + if element.has_form("FullForm", 1): return eval_makeboxes_fullform(element.elements[0], evaluation) diff --git a/mathics/format/latex.py b/mathics/format/latex.py index 603405df6..26a9460aa 100644 --- a/mathics/format/latex.py +++ b/mathics/format/latex.py @@ -149,9 +149,9 @@ def pane_box(self, **options): content = lookup_conversion_method(self.boxed, "latex")(self.boxed, **options) options = self.box_options size = options.get("System`ImageSize", SymbolAutomatic).to_python() - if size is SymbolAutomatic: - width = "\\textwidth" - height = "" + + if size == "System`Automatic": + return content elif isinstance(size, int): width = f"{size}pt" height = "" diff --git a/mathics/format/outputform.py b/mathics/format/outputform.py index d765dc7f9..a5a45bc15 100644 --- a/mathics/format/outputform.py +++ b/mathics/format/outputform.py @@ -17,12 +17,17 @@ Real, String, ) +from mathics.core.convert.op import operator_to_ascii, operator_to_unicode from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression +from mathics.core.parser.operators import OPERATOR_DATA from mathics.core.symbols import Atom, Symbol, SymbolTimes from mathics.core.systemsymbols import ( + SymbolBlank, + SymbolBlankNullSequence, + SymbolBlankSequence, SymbolDerivative, SymbolInfix, SymbolLeft, @@ -34,12 +39,19 @@ SymbolTraditionalForm, ) from mathics.eval.makeboxes import compare_precedence, do_format # , format_element +from mathics.settings import SYSTEM_CHARACTER_ENCODING SymbolNonAssociative = Symbol("System`NonAssociative") SymbolPostfix = Symbol("System`Postfix") SymbolPrefix = Symbol("System`Prefix") +PRECEDENCES = OPERATOR_DATA.get("operator-precedences") +PRECEDENCE_DEFAULT = PRECEDENCES.get("FunctionApply") +PRECEDENCE_PLUS = PRECEDENCES.get("Plus") +PRECEDENCE_TIMES = PRECEDENCES.get("Times") +PRECEDENCE_POWER = PRECEDENCES.get("Power") + EXPR_TO_OUTPUTFORM_TEXT_MAP: Dict[str, Callable] = {} @@ -57,12 +69,34 @@ class IsNot2DArray(Exception): pass +def get_operator_str(head, evaluation, form, **kwargs) -> str: + encoding = kwargs["encoding"] + if isinstance(head, String): + op_str = head.value + elif isinstance(head, Symbol): + op_str = head.short_name + else: + return expression_to_outputform_text(head, evaluation, form, **kwargs) + + if encoding == "ASCII": + operator = operator_to_ascii.get(op_str, op_str) + else: + operator = operator_to_unicode.get(op_str, op_str) + return operator + + def _default_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: """ Default representation of a function """ + if isinstance(expr, Atom): + result = expr.atom_to_boxes(SymbolOutputForm, evaluation) + if isinstance(result, String): + return result.value + return result.boxes_to_text() + expr_head = expr.head head = expression_to_outputform_text(expr_head, evaluation, form, **kwargs) comma = ", " @@ -80,7 +114,11 @@ def _default_expression_to_outputform_text( def _divide(num, den, evaluation, form, **kwargs): infix_form = Expression( - SymbolInfix, ListExpression(num, den), String("/"), Integer(400), SymbolLeft + SymbolInfix, + ListExpression(num, den), + String("/"), + Integer(PRECEDENCE_TIMES), + SymbolLeft, ) return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) @@ -100,18 +138,15 @@ def bracket(expr_str: str) -> str: def grid(expr): # Very basic implementation. - result = "Grid[" - for row in expr: - if result[-1] != "[": - result += "," - - result += "\n{" - for field in row: - if result[-1] != "}": - result += "," + result = "" + for idx_row, row in enumerate(expr): + if idx_row > 0: + result += "\n\n" + for idx_field, field in enumerate(row): + if idx_field > 0: + result += " " result += field - result += "}" - result += "}]" + return result @@ -128,6 +163,27 @@ def _register(func): return _register +@register_outputform("System`Blank") +@register_outputform("System`BlankSequence") +@register_outputform("System`BlankNullSequence") +def pattern(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs): + elements = expr.elements + if len(elements) > 1: + return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + if elements: + elem = expression_to_outputform_text(elements[0], evaluation, form, **kwargs) + else: + elem = "" + head = expr.head + if head is SymbolBlank: + return "_" + elem + elif head is SymbolBlankSequence: + return "__" + elem + elif head is SymbolBlankNullSequence: + return "___" + elem + return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + + @register_outputform("System`Derivative") def derivative_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs @@ -177,14 +233,12 @@ def expression_to_outputform_text( """ Build a pretty-print text from an `Expression` """ - ## TODO: format the expression - format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore - # Strip HoldForm + format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) while format_expr.has_form("HoldForm", 1): # type: ignore format_expr = format_expr.elements[0] - lookup_name = format_expr.get_head().get_lookup_name() + lookup_name: str = format_expr.get_head().get_lookup_name() try: result = EXPR_TO_OUTPUTFORM_TEXT_MAP[lookup_name]( format_expr, evaluation, form, **kwargs @@ -241,20 +295,40 @@ def grid_expression_to_outputform_text( register_outputform("System`HoldForm")(_strip_1_parm_expression_to_outputform_text) -# TODO: Do it better when InputForm be implemented. -register_outputform("System`InputForm")(_strip_1_parm_expression_to_outputform_text) + + +@register_outputform("System`FullForm") +@register_outputform("System`InputForm") +def other_forms(expr, evaluation, form, **kwargs): + from mathics.eval.makeboxes import format_element + + form = expr.get_head() + expr = expr.elements[0] + result = format_element(expr, evaluation, form, **kwargs) + if isinstance(result, String): + return result.value + return result.boxes_to_text() + + +@register_outputform("System`Image") +def image_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +): + return "-Image-" @register_outputform("System`Infix") def infix_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: + kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) + elements = expr.elements if not (0 <= len(elements) <= 4): raise _WrongFormattedExpression group = None - precedence = 670 + precedence = PRECEDENCE_DEFAULT # Processing the first argument: head = expr.get_head() target = expr.elements[0] @@ -272,16 +346,16 @@ def infix_expression_to_outputform_text( if head is SymbolInfix: # This is not the WMA behaviour, but the Mathics current implementation requires it: num_ops = 1 + # TODO: Handle the case where op is not a String or a Symbol. if ops.has_form("List", None): num_ops = len(ops.elements) + ops_lst = [ - expression_to_outputform_text(op, evaluation, form, **kwargs) + get_operator_str(op, evaluation, form, **kwargs) for op in ops.elements ] else: - ops_lst = [ - expression_to_outputform_text(ops, evaluation, form, **kwargs) - ] + ops_lst = [get_operator_str(ops, evaluation, form, **kwargs)] elif head in (SymbolPrefix, SymbolPostfix): ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] else: @@ -359,14 +433,15 @@ def infix_expression_to_outputform_text( result_lst = [ result, space, - str(ops_lst[index % num_ops]), + str(ops_lst[(index - 1) % num_ops]), space, operand_txt, ] else: result_lst = [result, space, operand_txt] + result = "".join(result_lst) - return "".join(result_lst) + return result @register_outputform("System`Integer") @@ -380,6 +455,10 @@ def integer_expression_to_outputform_text( def list_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: + elements = expr.elements + if not elements: + return "{}" + result, *rest_elems = ( expression_to_outputform_text(elem, evaluation, form, **kwargs) for elem in expr.elements @@ -409,24 +488,36 @@ def matrixform_expression_to_outputform_text( return tableform_expression_to_outputform_text(expr, evaluation, form, **kwargs) +@register_outputform("System`Pattern") +def pattern(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs): + elements = expr.elements + if len(elements) != 2: + return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + name, pat = ( + expression_to_outputform_text(elem, evaluation, form, **kwargs) + for elem in elements + ) + return name + pat + + @register_outputform("System`Plus") def plus_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: elements = expr.elements result = "" - for i, elem in enumerate(elements): - if elem.has_form("Times", None): + for i, term in enumerate(elements): + if term.has_form("Times", None): # If the first element is -1, remove it and use # a minus sign. Otherwise, if negative, do not add a sign. - first = elem.elements[0] + first = term.elements[0] if isinstance(first, Integer): if first.value == -1: result = ( result + " - " + expression_to_outputform_text( - Expression(SymbolTimes, *elem.elements[1:]), + Expression(SymbolTimes, *term.elements[1:]), evaluation, form, **kwargs, @@ -438,7 +529,7 @@ def plus_expression_to_outputform_text( result + " " + expression_to_outputform_text( - elem, evaluation, form, **kwargs + term, evaluation, form, **kwargs ) ) continue @@ -448,31 +539,31 @@ def plus_expression_to_outputform_text( result + " " + expression_to_outputform_text( - elem, evaluation, form, **kwargs + term, evaluation, form, **kwargs ) ) continue result = ( result + " + " - + expression_to_outputform_text(elem, evaluation, form, **kwargs) + + expression_to_outputform_text(term, evaluation, form, **kwargs) ) ## TODO: handle complex numbers? else: - elem_txt = expression_to_outputform_text(elem, evaluation, form, **kwargs) - if (compare_precedence(elem, 310) or -1) < 0: + elem_txt = expression_to_outputform_text(term, evaluation, form, **kwargs) + if (compare_precedence(term, PRECEDENCE_PLUS) or -1) < 0: elem_txt = parenthesize(elem_txt) result = result + " + " + elem_txt elif i == 0 or ( - (isinstance(elem, Integer) and elem.value < 0) - or (isinstance(elem, Real) and elem.value < 0) + (isinstance(term, Integer) and term.value < 0) + or (isinstance(term, Real) and term.value < 0) ): result = result + elem_txt else: result = ( result + " + " - + expression_to_outputform_text(elem, evaluation, form, **kwargs) + + expression_to_outputform_text(term, evaluation, form, **kwargs) ) return result @@ -488,7 +579,7 @@ def power_expression_to_outputform_text( SymbolInfix, ListExpression(*(expr.elements)), String("^"), - Integer(590), + Integer(PRECEDENCE_POWER), SymbolRight, ) return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) @@ -515,7 +606,7 @@ def pre_pos_fix_expression_to_outputform_text( raise _WrongFormattedExpression group = None - precedence = 670 + precedence = PRECEDENCE_DEFAULT # Processing the first argument: head = expr.get_head() target = expr.elements[0] @@ -586,6 +677,34 @@ def real_expression_to_outputform_text( return str(str_n) +@register_outputform("System`Row") +def row_to_outputform_text(expr, evaluation: Evaluation, form: Symbol, **kwargs): + """Row[{...}]""" + elements = expr.elements[0].elements + return "".join( + expression_to_outputform_text(elem, evaluation, form, **kwargs) + for elem in elements + ) + + +@register_outputform("System`Rule") +@register_outputform("System`RuleDelayed") +def rule_to_outputform_text(expr, evaluation: Evaluation, form: Symbol, **kwargs): + """Rule|RuleDelayed[{...}]""" + head = expr.head + elements = expr.elements + kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) + if len(elements) != 2: + return _default_expression_to_outputform_text(expr, evaluation, form, kwargs) + pat, rule = ( + expression_to_outputform_text(elem, evaluation, form, **kwargs) + for elem in elements + ) + + op_str = get_operator_str(head, evaluation, form, **kwargs) + return pat + " " + op_str + " " + rule + + @register_outputform("System`String") def string_expression_to_outputform_text( expr: String, evaluation: Evaluation, form: Symbol, **kwargs @@ -681,9 +800,9 @@ def times_expression_to_outputform_text( num: List[BaseElement] = [] den: List[BaseElement] = [] # First, split factors with integer, negative powers: - for elem in elements: - if elem.has_form("Power", 2): - base, exponent = elem.elements + for factor in elements: + if factor.has_form("Power", 2): + base, exponent = factor.elements if isinstance(exponent, Integer): if exponent.value == -1: den.append(base) @@ -691,17 +810,17 @@ def times_expression_to_outputform_text( elif exponent.value < 0: den.append(Expression(SymbolPower, base, Integer(-exponent.value))) continue - elif isinstance(elem, Rational): - num.append(elem.numerator()) - den.append(elem.denominator()) + elif isinstance(factor, Rational): + num.append(factor.numerator()) + den.append(factor.denominator()) continue - elif elem.has_form("Rational", 2): - elem_elements = elem.elements + elif factor.has_form("Rational", 2): + elem_elements = factor.elements num.append(elem_elements[0]) den.append(elem_elements[1]) continue - num.append(elem) + num.append(factor) # If there are integer, negative powers, process as a fraction: if den: @@ -721,21 +840,21 @@ def times_expression_to_outputform_text( prefactor = 1 result: str = "" - for i, elem in enumerate(num): - if elem is IntegerM1: + for i, factor in enumerate(num): + if factor is IntegerM1: prefactor *= -1 continue - if isinstance(elem, Integer): + if isinstance(factor, Integer): prefactor *= -1 - elem = Integer(-elem.value) + factor = Integer(-factor.value) - elem_txt = expression_to_outputform_text(elem, evaluation, form, **kwargs) - if compare_precedence(elem, 400): - elem_txt = parenthesize(elem_txt) + factor_txt = expression_to_outputform_text(factor, evaluation, form, **kwargs) + if compare_precedence(factor, PRECEDENCE_TIMES): + factor_txt = parenthesize(factor_txt) if i == 0: - result = elem_txt + result = factor_txt else: - result = result + " " + elem_txt + result = result + " " + factor_txt if result == "": result = "1" if prefactor == -1: From ae5533f890e1eed1c90282c4c6e2209c17534097 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Wed, 7 Jan 2026 12:24:27 -0300 Subject: [PATCH 25/49] Fix evaluation of InterpretationBox with options --- mathics/builtin/box/layout.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index 10e5e4059..f813db91e 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -14,7 +14,7 @@ from mathics.core.atoms import String from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin -from mathics.core.element import BaseElement, BoxElementMixin +from mathics.core.element import BaseElement, BoxElementMixin, EvalMixin from mathics.core.evaluation import Evaluation from mathics.core.exceptions import BoxConstructError from mathics.core.expression import Expression @@ -208,6 +208,10 @@ class InterpretationBox(BoxExpression): """ attributes = A_HOLD_ALL_COMPLETE | A_PROTECTED | A_READ_PROTECTED + options = { + "Editable": "Automatic", + "AutoDelete": "Automatic", + } summary_text = "box associated to an input expression" def __repr__(self): @@ -233,9 +237,13 @@ def elements(self): ) return self._elements - def eval_create(self, reprs, expr, evaluation): - """InterpretationBox[reprs_, expr_]""" - return InterpretationBox(reprs, expr) + def eval_create(self, reprs, expr, evaluation, options): + """InterpretationBox[reprs_, expr_, OptionsPattern[]]""" + if isinstance(reprs, EvalMixin): + reprs = reprs.evaluate(evaluation) + if not isinstance(reprs, BoxElementMixin): + return + return InterpretationBox(reprs, expr, **options) def eval_to_expression1(self, boxexpr, evaluation): """ToExpression[boxexpr_InterpretationBox]""" From bf177390ee987ba9a44e1dbbfdeb252c75f2f342 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Wed, 7 Jan 2026 18:07:59 -0500 Subject: [PATCH 26/49] Misc tweaks... (#1599) Start giving meaning to some of the magic precedence values Start a form directory for handing OutputForm, InputForm, etc. (Rendering/Format, and Forms are different) Co-authored-by: Juan Mauricio Matera --- mathics/core/parser/__init__.py | 3 ++- mathics/eval/makeboxes/makeboxes.py | 2 +- mathics/form/__init__.py | 0 mathics/{format => form}/outputform.py | 31 +++++++++++++++----------- 4 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 mathics/form/__init__.py rename mathics/{format => form}/outputform.py (97%) diff --git a/mathics/core/parser/__init__.py b/mathics/core/parser/__init__.py index cdc9ffc6f..cb8178d4e 100644 --- a/mathics/core/parser/__init__.py +++ b/mathics/core/parser/__init__.py @@ -18,7 +18,7 @@ MathicsMultiLineFeeder, MathicsSingleLineFeeder, ) -from mathics.core.parser.operators import all_operator_names +from mathics.core.parser.operators import all_operator_names, operator_precedences from mathics.core.parser.util import parse, parse_builtin_rule __all__ = [ @@ -29,6 +29,7 @@ "MathicsSingleLineFeeder", "all_operator_names", "is_symbol_name", + "operator_precedences", "parse", "parse_builtin_rule", ] diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 8cf465cc5..6c4fb232c 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -142,7 +142,7 @@ def eval_makeboxes_outputform(expr, evaluation, form, **kwargs): Build a 2D representation of the expression using only keyboard characters. """ from mathics.builtin.box.layout import PaneBox - from mathics.format.outputform import expression_to_outputform_text + from mathics.form.outputform import expression_to_outputform_text text_outputform = str( expression_to_outputform_text(expr, evaluation, form, **kwargs) diff --git a/mathics/form/__init__.py b/mathics/form/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mathics/format/outputform.py b/mathics/form/outputform.py similarity index 97% rename from mathics/format/outputform.py rename to mathics/form/outputform.py index a5a45bc15..2fc82ccee 100644 --- a/mathics/format/outputform.py +++ b/mathics/form/outputform.py @@ -1,12 +1,11 @@ """ -This module builts the string associated to the OutputForm. - -OutputForm produce a pretty-print-like output, suitable for CLI -and text terminals. +This module implements the "OutputForm" textual representation of expressions. +OutputForm is two-dimensional keyboard-character-only output, suitable for CLI +and text terminals. """ -from typing import Callable, Dict, List, Union +from typing import Callable, Dict, Final, List, Union from mathics.core.atoms import ( Integer, @@ -22,7 +21,7 @@ from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.parser.operators import OPERATOR_DATA +from mathics.core.parser.operators import OPERATOR_DATA, box_operators from mathics.core.symbols import Atom, Symbol, SymbolTimes from mathics.core.systemsymbols import ( SymbolBlank, @@ -46,11 +45,15 @@ SymbolPrefix = Symbol("System`Prefix") -PRECEDENCES = OPERATOR_DATA.get("operator-precedences") -PRECEDENCE_DEFAULT = PRECEDENCES.get("FunctionApply") -PRECEDENCE_PLUS = PRECEDENCES.get("Plus") -PRECEDENCE_TIMES = PRECEDENCES.get("Times") -PRECEDENCE_POWER = PRECEDENCES.get("Power") +PRECEDENCES: Final = OPERATOR_DATA.get("operator-precedences") +PRECEDENCE_DEFAULT: Final = PRECEDENCES.get("FunctionApply") +PRECEDENCE_PLUS: Final = PRECEDENCES.get("Plus") +PRECEDENCE_TIMES: Final = PRECEDENCES.get("Times") +PRECEDENCE_POWER: Final = PRECEDENCES.get("Power") + +# When new mathics-scanner tables are updagted: +# BOX_GROUP_PRECEDENCE: Final = box_operators["BoxGroup"] +BOX_GROUP_PRECEDENCE: Final = PRECEDENCE_DEFAULT EXPR_TO_OUTPUTFORM_TEXT_MAP: Dict[str, Callable] = {} @@ -328,7 +331,8 @@ def infix_expression_to_outputform_text( raise _WrongFormattedExpression group = None - precedence = PRECEDENCE_DEFAULT + precedence = BOX_GROUP_PRECEDENCE + # Processing the first argument: head = expr.get_head() target = expr.elements[0] @@ -606,7 +610,8 @@ def pre_pos_fix_expression_to_outputform_text( raise _WrongFormattedExpression group = None - precedence = PRECEDENCE_DEFAULT + precedence = BOX_GROUP_PRECEDENCE + # Processing the first argument: head = expr.get_head() target = expr.elements[0] From 80160acc74492e2f349227f3638aa21cf4309f2c Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 9 Jan 2026 15:50:26 -0300 Subject: [PATCH 27/49] restore --- mathics/builtin/forms/output.py | 2 +- mathics/builtin/makeboxes.py | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index afb0d3699..d2e25c882 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -712,7 +712,7 @@ class TeXForm(FormBaseClass): summary_text = "formatted expression as TeX commands" def eval_tex(self, expr, evaluation) -> Expression: - "MakeBoxes[TeXForm[expr_], Alternatives[StandardForm,TraditionalForm]]" + "MakeBoxes[expr_, TeXForm]" return eval_texform(expr, evaluation) diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index aef5b7e48..2d937f41a 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -97,13 +97,8 @@ class MakeBoxes(Builtin): 'MakeBoxes[Infix[head[elements], StringForm["~`1`~", head]], f]' ), "MakeBoxes[expr_]": "MakeBoxes[expr, StandardForm]", - ("MakeBoxes[expr_, (form:TeXForm|MathMLForm)]"): ( - "MakeBoxes[form[expr], StandardForm]" - ), - "MakeBoxes[expr_]": "MakeBoxes[expr, StandardForm]", - ("MakeBoxes[(form:StandardForm|TraditionalForm)[expr_], f_]"): ( - "MakeBoxes[expr, form]" - ), + "MakeBoxes[(form:StandardForm|TraditionalForm|TeXForm|" + "MathMLForm)[expr_], StandardForm|TraditionalForm]": ("MakeBoxes[expr, form]"), "MakeBoxes[(form:StandardForm|OutputForm|MathMLForm|TeXForm)[expr_], OutputForm]": "MakeBoxes[expr, form]", "MakeBoxes[PrecedenceForm[expr_, prec_], f_]": "MakeBoxes[expr, f]", "MakeBoxes[Style[expr_, OptionsPattern[Style]], f_]": ( From 736bbc8a94718218e84d55d383aa8f417819d820 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 9 Jan 2026 16:38:39 -0300 Subject: [PATCH 28/49] comment out a test until we finish fixing TeXForm in another round --- mathics/doc/documentation/1-Manual.mdoc | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mathics/doc/documentation/1-Manual.mdoc b/mathics/doc/documentation/1-Manual.mdoc index 93a58d6e0..88a28ceb5 100644 --- a/mathics/doc/documentation/1-Manual.mdoc +++ b/mathics/doc/documentation/1-Manual.mdoc @@ -901,9 +901,11 @@ This will even apply to 'TeXForm', because 'TeXForm' implies 'StandardForm': >> b // TeXForm = c -Except some other form is applied first: - >> b // OutputForm // TeXForm - = \text{b} + ## This test requires another round of changes in order to + ## pass: + ## Except some other form is applied first: + ## >> b // OutputForm // TeXForm + ## = \text{b} 'MakeBoxes' for another form: >> MakeBoxes[b, TeXForm] = "d"; @@ -934,12 +936,12 @@ For instance, you can override 'MakeBoxes' to format lists in a different way: >> {1, 2, 3} = {1, 2, 3} - #> {1, 2, 3} // TeXForm + >> {1, 2, 3} // TeXForm = \left[1 2 3\right] However, this will not be accepted as input to \Mathics anymore: >> [1 2 3] - : Expression cannot begin with "[1 2 3]" (line 1 of ""). + : Expression cannot begin with "[1 2 3]" (line 1 of ""). >> Clear[MakeBoxes] From e0d8ce30e8eb5bb58b93de75ef4cc8a74895d417 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 9 Jan 2026 19:24:26 -0300 Subject: [PATCH 29/49] comment out --- mathics/builtin/forms/print.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/forms/print.py b/mathics/builtin/forms/print.py index 5834ae399..0450a59e2 100644 --- a/mathics/builtin/forms/print.py +++ b/mathics/builtin/forms/print.py @@ -150,9 +150,9 @@ class MathMLForm(FormBaseClass): >> MathMLForm[\\[Mu]] = ... - # This can causes the TeX to fail - # >> MathMLForm[Graphics[Text["\u03bc"]]] - # = ... + ## This can causes the TeX to fail + ## >> MathMLForm[Graphics[Text["\u03bc"]]] + ## = ... ## The should contain U+2062 INVISIBLE TIMES ## MathMLForm[MatrixForm[{{2*a, 0},{0,0}}]] From c840a6a4b2ac60db68d407ba9d3fa6681a48da72 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 9 Jan 2026 19:34:15 -0300 Subject: [PATCH 30/49] mypy --- mathics/form/outputform.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mathics/form/outputform.py b/mathics/form/outputform.py index 2fc82ccee..f0c245bbe 100644 --- a/mathics/form/outputform.py +++ b/mathics/form/outputform.py @@ -5,7 +5,7 @@ and text terminals. """ -from typing import Callable, Dict, Final, List, Union +from typing import Callable, Dict, Final, List, Optional, Union from mathics.core.atoms import ( Integer, @@ -169,7 +169,7 @@ def _register(func): @register_outputform("System`Blank") @register_outputform("System`BlankSequence") @register_outputform("System`BlankNullSequence") -def pattern(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs): +def blank_pattern(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs): elements = expr.elements if len(elements) > 1: return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) @@ -237,10 +237,14 @@ def expression_to_outputform_text( Build a pretty-print text from an `Expression` """ - format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) + format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore + while format_expr.has_form("HoldForm", 1): # type: ignore format_expr = format_expr.elements[0] + if format_expr is None: + return "" + lookup_name: str = format_expr.get_head().get_lookup_name() try: result = EXPR_TO_OUTPUTFORM_TEXT_MAP[lookup_name]( @@ -700,7 +704,7 @@ def rule_to_outputform_text(expr, evaluation: Evaluation, form: Symbol, **kwargs elements = expr.elements kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) if len(elements) != 2: - return _default_expression_to_outputform_text(expr, evaluation, form, kwargs) + return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) pat, rule = ( expression_to_outputform_text(elem, evaluation, form, **kwargs) for elem in elements From 5fded9f901cb1952b337f2779633ee100fdbf587 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 9 Jan 2026 20:29:36 -0300 Subject: [PATCH 31/49] isort --- mathics/eval/makeboxes/makeboxes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 213757754..963735060 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -21,7 +21,6 @@ ) from mathics.core.systemsymbols import ( # SymbolRule, SymbolRuleDelayed, SymbolComplex, - SymbolOutputForm, SymbolInputForm, SymbolRational, SymbolStandardForm, From 5bcb2bbb7024ee4f116e65a242cd045f0070dfb1 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 10 Jan 2026 14:08:10 -0300 Subject: [PATCH 32/49] fix Image.atom_to_boxes --- mathics/builtin/box/image.py | 14 ++++++++++++-- mathics/builtin/box/uniform_polyhedra.py | 6 +++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/mathics/builtin/box/image.py b/mathics/builtin/box/image.py index beca29b8d..4eabc2726 100644 --- a/mathics/builtin/box/image.py +++ b/mathics/builtin/box/image.py @@ -2,8 +2,6 @@ """ Boxing Symbol for Raster Images """ -# Docs are not yet ready for prime time. Maybe after release 6.0.0. -no_doc = True import base64 import tempfile @@ -18,7 +16,11 @@ from mathics.core.element import BaseElement from mathics.eval.image import pixels_as_ubyte +# Docs are not yet ready for prime time. Maybe after release 6.0.0. +no_doc = True + +# This should be 'RasterBox' class ImageBox(BoxExpression): """
@@ -30,6 +32,14 @@ class ImageBox(BoxExpression): summary_text = "symbol used boxing Image expressions" + def init(self, image, **kwargs): + self.image = image + self.options = kwargs + + @property + def elements(self): + return self.image.elements + def boxes_to_b64text( self, elements: Tuple[BaseElement] = None, **options ) -> Tuple[bytes, Tuple[int, int]]: diff --git a/mathics/builtin/box/uniform_polyhedra.py b/mathics/builtin/box/uniform_polyhedra.py index 31ceb9e39..2c54afc83 100644 --- a/mathics/builtin/box/uniform_polyhedra.py +++ b/mathics/builtin/box/uniform_polyhedra.py @@ -1,6 +1,3 @@ -# Docs are not yet ready for prime time. Maybe after release 6.0.0. -no_doc = True - import numbers from mathics.builtin.box.graphics3d import Coords3D @@ -9,6 +6,9 @@ from mathics.core.exceptions import BoxExpressionError from mathics.core.symbols import Symbol +# Docs are not yet ready for prime time. Maybe after release 6.0.0. +no_doc = True + class UniformPolyhedron3DBox(_GraphicsElementBox): # Let's overwrite the default summary_text here, From 8846c1348783c5879e58034cc2f401766513fcb8 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 10 Jan 2026 14:13:36 -0300 Subject: [PATCH 33/49] fix ImageBox.boxes_to_* --- mathics/builtin/box/image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/box/image.py b/mathics/builtin/box/image.py index 4eabc2726..d3d5f99d9 100644 --- a/mathics/builtin/box/image.py +++ b/mathics/builtin/box/image.py @@ -57,7 +57,7 @@ def boxes_to_png(self, elements=None, **options) -> Tuple[bytes, Tuple[int, int] returns a tuple with the set of bytes with a png representation of the image and the scaled size. """ - image = self.elements[0] if elements is None else elements[0] + image = self.image if elements is None else elements[0] pixels = pixels_as_ubyte(image.color_convert("RGB", True).pixels) shape = pixels.shape @@ -112,6 +112,7 @@ def boxes_to_tex(self, elements=None, **options) -> str: Store the associated image as a png file and return a LaTeX command for including it. """ + image = self.image if elements is None else elements[0] data, size = self.boxes_to_png(elements, **options) res = 100 # pixels/cm From c098a327c11c171295ccf536612fe6a5c7f795cc Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 10 Jan 2026 14:36:18 -0300 Subject: [PATCH 34/49] == bool -> is bool --- mathics/builtin/image/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/image/base.py b/mathics/builtin/image/base.py index 087d39c52..cb2a7cc1e 100644 --- a/mathics/builtin/image/base.py +++ b/mathics/builtin/image/base.py @@ -211,7 +211,7 @@ def storage_type(self): return "Bit16" elif dtype == numpy.uint8: return "Byte" - elif dtype == bool: + elif dtype is bool: return "Bit" else: return str(dtype) From b9fd972efe8ec656087d7aa72b26f4685d70cc31 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 10 Jan 2026 15:12:52 -0300 Subject: [PATCH 35/49] DRY input/outputform --- mathics/core/systemsymbols.py | 3 + mathics/form/inputform.py | 111 ++------------------ mathics/form/outputform.py | 192 +++++++++++++--------------------- mathics/form/util.py | 127 ++++++++++++++++++++++ 4 files changed, 213 insertions(+), 220 deletions(-) create mode 100644 mathics/form/util.py diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index 258ec92ef..5dba18221 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -196,6 +196,7 @@ SymbolNIntegrate = Symbol("System`NIntegrate") SymbolNValues = Symbol("System`NValues") SymbolNeeds = Symbol("System`Needs") +SymbolNonAssociative = Symbol("System`NonAssociative") SymbolNone = Symbol("System`None") SymbolNorm = Symbol("System`Norm") SymbolNormal = Symbol("System`Normal") @@ -235,9 +236,11 @@ SymbolPlus = Symbol("System`Plus") SymbolPoint = Symbol("System`Point") SymbolPolygon = Symbol("System`Polygon") +SymbolPostfix = Symbol("System`Postfix") SymbolPossibleZeroQ = Symbol("System`PossibleZeroQ") SymbolPower = Symbol("System`Power") SymbolPrecision = Symbol("System`Precision") +SymbolPrefix = Symbol("System`Prefix") SymbolPreserveImageOptions = Symbol("System`PreserveImageOptions") SymbolProlog = Symbol("System`Prolog") SymbolQuantity = Symbol("System`Quantity") diff --git a/mathics/form/inputform.py b/mathics/form/inputform.py index e76598780..52d1bba9e 100644 --- a/mathics/form/inputform.py +++ b/mathics/form/inputform.py @@ -25,10 +25,9 @@ """ -from typing import Callable, Dict, Final, FrozenSet, List, Optional, Tuple +from typing import Callable, Dict, Final, FrozenSet from mathics.core.atoms import Integer, String -from mathics.core.convert.op import operator_to_ascii, operator_to_unicode from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.parser.operators import OPERATOR_DATA, operator_to_string @@ -37,16 +36,22 @@ SymbolBlank, SymbolBlankNullSequence, SymbolBlankSequence, - SymbolInfix, SymbolInputForm, SymbolLeft, - SymbolNone, SymbolRight, ) from mathics.eval.makeboxes.formatvalues import do_format # , format_element from mathics.eval.makeboxes.precedence import compare_precedence from mathics.settings import SYSTEM_CHARACTER_ENCODING +from .util import ( + _WrongFormattedExpression, + bracket, + collect_in_pre_post_arguments, + get_operator_str, + parenthesize, +) + SymbolNonAssociative = Symbol("System`NonAssociative") SymbolPostfix = Symbol("System`Postfix") SymbolPrefix = Symbol("System`Prefix") @@ -60,38 +65,6 @@ EXPR_TO_INPUTFORM_TEXT_MAP: Dict[str, Callable] = {} -# This Exception if the expression should -# be processed by the default routine -class _WrongFormattedExpression(Exception): - pass - - -def get_operator_str(head, evaluation, **kwargs) -> str: - encoding = kwargs["encoding"] - if isinstance(head, String): - op_str = head.value - elif isinstance(head, Symbol): - op_str = head.short_name - else: - return render_input_form(head, evaluation, **kwargs) - - if encoding == "ASCII": - operator = operator_to_ascii.get(op_str, op_str) - else: - operator = operator_to_unicode.get(op_str, op_str) - return operator - - -def bracket(expr_str: str) -> str: - """Wrap `expr_str` with square braces""" - return f"[{expr_str}]" - - -def parenthesize(expr_str: str) -> str: - """Wrap `expr_str` with parenthesis""" - return f"({expr_str})" - - def register_inputform(head_name): def _register(func): EXPR_TO_INPUTFORM_TEXT_MAP[head_name] = func @@ -176,70 +149,6 @@ def _list_expression_to_inputform_text( return result + "}" -def collect_in_pre_post_arguments( - expr: Expression, evaluation: Evaluation, **kwargs -) -> Tuple[list, str | List[str], int, Optional[Symbol]]: - """ - Determine operands, operator(s), precedence, and grouping - """ - # Processing the second argument, if it is there: - elements = expr.elements - # expr at least has to have one element - if len(elements) < 1: - raise _WrongFormattedExpression - - target = elements[0] - if isinstance(target, Atom): - raise _WrongFormattedExpression - - if not (0 <= len(elements) <= 4): - raise _WrongFormattedExpression - - head = expr.head - group = None - precedence = PRECEDENCE_BOX_GROUP - operands = list(target.elements) - - # Just one parameter: - if len(elements) == 1: - operator_spec = render_input_form(head, evaluation, **kwargs) - if head is SymbolInfix: - operator_spec = [ - f"{operator_to_string['Infix']}{operator_spec}{operator_to_string['Infix']}" - ] - elif head is SymbolPrefix: - operator_spec = f"{operator_spec}{operator_to_string['Prefix']}" - elif head is SymbolPostfix: - operator_spec = f"{operator_to_string['Postfix']}{operator_spec}" - return operands, operator_spec, precedence, group - - # At least two parameters: get the operator spec. - ops = elements[1] - if head is SymbolInfix: - # This is not the WMA behaviour, but the Mathics3 current implementation requires it: - ops = ops.elements if ops.has_form("List", None) else (ops,) - operator_spec = [get_operator_str(op, evaluation, **kwargs) for op in ops] - else: - operator_spec = get_operator_str(ops, evaluation, **kwargs) - - # At least three arguments: get the precedence - if len(elements) > 2: - if isinstance(elements[2], Integer): - precedence = elements[2].value - else: - raise _WrongFormattedExpression - - # Four arguments: get the grouping: - if len(elements) > 3: - group = elements[3] - if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): - raise _WrongFormattedExpression - if group is SymbolNone: - group = None - - return operands, operator_spec, precedence, group - - ARITHMETIC_OPERATOR_STRINGS: Final[FrozenSet[str]] = frozenset( [ *operator_to_string["Divide"], @@ -389,7 +298,7 @@ def _rule_to_inputform_text(expr, evaluation: Evaluation, **kwargs): if len(elements) != 2: return _generic_to_inputform_text(expr, evaluation, **kwargs) pat, rule = (render_input_form(elem, evaluation, **kwargs) for elem in elements) - + kwargs["_render_function"] = render_input_form op_str = get_operator_str(head, evaluation, **kwargs) # In WMA there are spaces between operators. return pat + f" {op_str} " + rule diff --git a/mathics/form/outputform.py b/mathics/form/outputform.py index f0c245bbe..99fe2bb6f 100644 --- a/mathics/form/outputform.py +++ b/mathics/form/outputform.py @@ -5,7 +5,7 @@ and text terminals. """ -from typing import Callable, Dict, Final, List, Optional, Union +from typing import Callable, Dict, Final, List, Union from mathics.core.atoms import ( Integer, @@ -16,12 +16,11 @@ Real, String, ) -from mathics.core.convert.op import operator_to_ascii, operator_to_unicode from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.parser.operators import OPERATOR_DATA, box_operators +from mathics.core.parser.operators import OPERATOR_DATA from mathics.core.symbols import Atom, Symbol, SymbolTimes from mathics.core.systemsymbols import ( SymbolBlank, @@ -40,10 +39,7 @@ from mathics.eval.makeboxes import compare_precedence, do_format # , format_element from mathics.settings import SYSTEM_CHARACTER_ENCODING -SymbolNonAssociative = Symbol("System`NonAssociative") -SymbolPostfix = Symbol("System`Postfix") -SymbolPrefix = Symbol("System`Prefix") - +from .util import _WrongFormattedExpression, bracket, get_operator_str, parenthesize PRECEDENCES: Final = OPERATOR_DATA.get("operator-precedences") PRECEDENCE_DEFAULT: Final = PRECEDENCES.get("FunctionApply") @@ -57,11 +53,9 @@ EXPR_TO_OUTPUTFORM_TEXT_MAP: Dict[str, Callable] = {} - -# This Exception if the expression should -# be processed by the default routine -class _WrongFormattedExpression(Exception): - pass +SymbolNonAssociative = Symbol("System`NonAssociative") +SymbolPostfix = Symbol("System`Postfix") +SymbolPrefix = Symbol("System`Prefix") class IsNotGrid(Exception): @@ -72,24 +66,8 @@ class IsNot2DArray(Exception): pass -def get_operator_str(head, evaluation, form, **kwargs) -> str: - encoding = kwargs["encoding"] - if isinstance(head, String): - op_str = head.value - elif isinstance(head, Symbol): - op_str = head.short_name - else: - return expression_to_outputform_text(head, evaluation, form, **kwargs) - - if encoding == "ASCII": - operator = operator_to_ascii.get(op_str, op_str) - else: - operator = operator_to_unicode.get(op_str, op_str) - return operator - - def _default_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: """ Default representation of a function @@ -101,7 +79,7 @@ def _default_expression_to_outputform_text( return result.boxes_to_text() expr_head = expr.head - head = expression_to_outputform_text(expr_head, evaluation, form, **kwargs) + head = expression_to_outputform_text(expr_head, evaluation, **kwargs) comma = ", " elements = [ expression_to_outputform_text(elem, evaluation) for elem in expr.elements @@ -110,12 +88,13 @@ def _default_expression_to_outputform_text( while elements: result = result + comma + elements.pop(0) + form = kwargs.get("_Form", SymbolOutputForm) if form is SymbolTraditionalForm: return head + parenthesize(result) return head + bracket(result) -def _divide(num, den, evaluation, form, **kwargs): +def _divide(num, den, evaluation, **kwargs): infix_form = Expression( SymbolInfix, ListExpression(num, den), @@ -123,20 +102,15 @@ def _divide(num, den, evaluation, form, **kwargs): Integer(PRECEDENCE_TIMES), SymbolLeft, ) - return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) + return expression_to_outputform_text(infix_form, evaluation, **kwargs) def _strip_1_parm_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: if len(expr.elements) != 1: raise _WrongFormattedExpression - return expression_to_outputform_text(expr.elements[0], evaluation, form, **kwargs) - - -def bracket(expr_str: str) -> str: - """wrap with square brackets""" - return f"[{expr_str}]" + return expression_to_outputform_text(expr.elements[0], evaluation, **kwargs) def grid(expr): @@ -153,11 +127,6 @@ def grid(expr): return result -def parenthesize(expr_str: str) -> str: - """wrap with parenthesis""" - return f"({expr_str})" - - def register_outputform(head_name): def _register(func): EXPR_TO_OUTPUTFORM_TEXT_MAP[head_name] = func @@ -169,12 +138,12 @@ def _register(func): @register_outputform("System`Blank") @register_outputform("System`BlankSequence") @register_outputform("System`BlankNullSequence") -def blank_pattern(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs): +def blank_pattern(expr: Expression, evaluation: Evaluation, **kwargs): elements = expr.elements if len(elements) > 1: - return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) if elements: - elem = expression_to_outputform_text(elements[0], evaluation, form, **kwargs) + elem = expression_to_outputform_text(elements[0], evaluation, **kwargs) else: elem = "" head = expr.head @@ -184,26 +153,24 @@ def blank_pattern(expr: Expression, evaluation: Evaluation, form: Symbol, **kwar return "__" + elem elif head is SymbolBlankNullSequence: return "___" + elem - return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) @register_outputform("System`Derivative") def derivative_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: """Derivative operator""" head = expr.get_head() if head is SymbolDerivative: - return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) super_head = head.get_head() if super_head is SymbolDerivative: expr_elements = expr.elements if len(expr_elements) != 1: - return _default_expression_to_outputform_text( - expr, evaluation, form, **kwargs - ) + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) function_head = expression_to_outputform_text( - expr_elements[0], evaluation, form, **kwargs + expr_elements[0], evaluation, **kwargs ) derivatives = head.elements if len(derivatives) == 1: @@ -213,26 +180,24 @@ def derivative_expression_to_outputform_text( elif order_iv == Integer2: return function_head + "''" - return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) # Full Function with arguments: delegate to the default conversion. # It will call us again with the head - return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) @register_outputform("System`Divide") def divide_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: if len(expr.elements) != 2: raise _WrongFormattedExpression num, den = expr.elements - return _divide(num, den, evaluation, form, **kwargs) + return _divide(num, den, evaluation, **kwargs) -def expression_to_outputform_text( - expr: BaseElement, evaluation: Evaluation, form=SymbolStandardForm, **kwargs -): +def expression_to_outputform_text(expr: BaseElement, evaluation: Evaluation, **kwargs): """ Build a pretty-print text from an `Expression` """ @@ -248,7 +213,7 @@ def expression_to_outputform_text( lookup_name: str = format_expr.get_head().get_lookup_name() try: result = EXPR_TO_OUTPUTFORM_TEXT_MAP[lookup_name]( - format_expr, evaluation, form, **kwargs + format_expr, evaluation, **kwargs ) return result except _WrongFormattedExpression: @@ -257,24 +222,22 @@ def expression_to_outputform_text( pass except KeyError: pass - return _default_expression_to_outputform_text( - format_expr, evaluation, form, **kwargs - ) + return _default_expression_to_outputform_text(format_expr, evaluation, **kwargs) @register_outputform("System`Graphics") -def graphics(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: +def graphics(expr: Expression, evaluation: Evaluation, **kwargs) -> str: return "-Graphics-" @register_outputform("System`Graphics3D") -def graphics3d(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs) -> str: +def graphics3d(expr: Expression, evaluation: Evaluation, **kwargs) -> str: return "-Graphics3D-" @register_outputform("System`Grid") def grid_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: if len(expr.elements) == 0: raise IsNotGrid @@ -291,12 +254,12 @@ def grid_expression_to_outputform_text( if item.has_form("List", None): rows.append( [ - expression_to_outputform_text(item_elem, evaluation, form, **kwargs) + expression_to_outputform_text(item_elem, evaluation, **kwargs) for item_elem in item.elements ] ) else: - rows.append(expression_to_outputform_text(item, evaluation, form, **kwargs)) + rows.append(expression_to_outputform_text(item, evaluation, **kwargs)) return grid(rows) @@ -306,7 +269,7 @@ def grid_expression_to_outputform_text( @register_outputform("System`FullForm") @register_outputform("System`InputForm") -def other_forms(expr, evaluation, form, **kwargs): +def other_forms(expr, evaluation, **kwargs): from mathics.eval.makeboxes import format_element form = expr.get_head() @@ -318,15 +281,13 @@ def other_forms(expr, evaluation, form, **kwargs): @register_outputform("System`Image") -def image_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -): +def image_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs): return "-Image-" @register_outputform("System`Infix") def infix_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) @@ -355,36 +316,34 @@ def infix_expression_to_outputform_text( # This is not the WMA behaviour, but the Mathics current implementation requires it: num_ops = 1 # TODO: Handle the case where op is not a String or a Symbol. + kwargs["_render_function"] = expression_to_outputform_text if ops.has_form("List", None): num_ops = len(ops.elements) - ops_lst = [ - get_operator_str(op, evaluation, form, **kwargs) - for op in ops.elements + get_operator_str(op, evaluation, **kwargs) for op in ops.elements ] else: - ops_lst = [get_operator_str(ops, evaluation, form, **kwargs)] + ops_lst = [get_operator_str(ops, evaluation, **kwargs)] elif head in (SymbolPrefix, SymbolPostfix): - ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] + ops_txt = [expression_to_outputform_text(ops, evaluation, **kwargs)] else: if head is SymbolInfix: num_ops = 1 default_symb = " ~ " ops_lst = [ default_symb - + expression_to_outputform_text(head, evaluation, form, **kwargs) + + expression_to_outputform_text(head, evaluation, **kwargs) + default_symb ] elif head is SymbolPrefix: default_symb = " @ " ops_txt = ( - expression_to_outputform_text(head, evaluation, form, **kwargs) - + default_symb + expression_to_outputform_text(head, evaluation, **kwargs) + default_symb ) elif head is SymbolPostfix: default_symb = " // " ops_txt = default_symb + expression_to_outputform_text( - head, evaluation, form, **kwargs + head, evaluation, **kwargs ) # Processing the third argument, if it is there: @@ -405,14 +364,14 @@ def infix_expression_to_outputform_text( if head is SymbolPrefix: operand = operands[0] cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) if cmp_precedence is not None and cmp_precedence != -1: target_txt = parenthesize(target_txt) return ops_txt[0] + target_txt if head is SymbolPostfix: operand = operands[0] cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) if cmp_precedence is not None and cmp_precedence != -1: target_txt = parenthesize(target_txt) return target_txt + ops_txt[0] @@ -420,7 +379,7 @@ def infix_expression_to_outputform_text( parenthesized = group in (None, SymbolRight, SymbolNonAssociative) for index, operand in enumerate(operands): operand_txt = str( - expression_to_outputform_text(operand, evaluation, form, **kwargs) + expression_to_outputform_text(operand, evaluation, **kwargs) ) cmp_precedence = compare_precedence(operand, precedence) if cmp_precedence is not None and ( @@ -453,22 +412,20 @@ def infix_expression_to_outputform_text( @register_outputform("System`Integer") -def integer_expression_to_outputform_text( - n: Integer, evaluation: Evaluation, form: Symbol, **kwargs -): +def integer_expression_to_outputform_text(n: Integer, evaluation: Evaluation, **kwargs): return str(n.value) @register_outputform("System`List") def list_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: elements = expr.elements if not elements: return "{}" result, *rest_elems = ( - expression_to_outputform_text(elem, evaluation, form, **kwargs) + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in expr.elements ) comma_tb = ", " @@ -479,9 +436,9 @@ def list_expression_to_outputform_text( @register_outputform("System`MathMLForm") def mathmlform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: - # boxes = format_element(expr.elements[0], evaluation, form) + # boxes = format_element(expr.elements[0], evaluation) boxes = Expression( Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm ).evaluate(evaluation) @@ -490,20 +447,19 @@ def mathmlform_expression_to_outputform_text( @register_outputform("System`MatrixForm") def matrixform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: - # return parenthesize(tableform_expression_to_outputform_text(expr, evaluation, form, **kwargs)) - return tableform_expression_to_outputform_text(expr, evaluation, form, **kwargs) + # return parenthesize(tableform_expression_to_outputform_text(expr, evaluation, **kwargs)) + return tableform_expression_to_outputform_text(expr, evaluation, **kwargs) @register_outputform("System`Pattern") -def pattern(expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs): +def pattern(expr: Expression, evaluation: Evaluation, **kwargs): elements = expr.elements if len(elements) != 2: - return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) name, pat = ( - expression_to_outputform_text(elem, evaluation, form, **kwargs) - for elem in elements + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements ) return name + pat @@ -675,7 +631,7 @@ def rational_expression_to_outputform_text( num, den = n.elements # type: ignore[union-attr] else: num, den = n.numerator(), n.denominator() # type: ignore[union-attr] - return _divide(num, den, evaluation, form, **kwargs) + return _divide(num, den, evaluation, **kwargs) @register_outputform("System`Real") @@ -691,8 +647,7 @@ def row_to_outputform_text(expr, evaluation: Evaluation, form: Symbol, **kwargs) """Row[{...}]""" elements = expr.elements[0].elements return "".join( - expression_to_outputform_text(elem, evaluation, form, **kwargs) - for elem in elements + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements ) @@ -706,17 +661,16 @@ def rule_to_outputform_text(expr, evaluation: Evaluation, form: Symbol, **kwargs if len(elements) != 2: return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) pat, rule = ( - expression_to_outputform_text(elem, evaluation, form, **kwargs) - for elem in elements + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements ) - - op_str = get_operator_str(head, evaluation, form, **kwargs) + kwargs["_render_function"] = expression_to_outputform_text + op_str = get_operator_str(head, evaluation, **kwargs) return pat + " " + op_str + " " + rule @register_outputform("System`String") def string_expression_to_outputform_text( - expr: String, evaluation: Evaluation, form: Symbol, **kwargs + expr: String, evaluation: Evaluation, **kwargs ) -> str: lines = expr.value.split("\n") max_len = max([len(line) for line in lines]) @@ -726,14 +680,14 @@ def string_expression_to_outputform_text( @register_outputform("System`StringForm") def stringform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: strform = expr.elements[0] if not isinstance(strform, String): raise _WrongFormattedExpression items = list( - expression_to_outputform_text(item, evaluation, form, **kwargs) + expression_to_outputform_text(item, evaluation, **kwargs) for item in expr.elements[1:] ) @@ -778,21 +732,21 @@ def stringform_expression_to_outputform_text( @register_outputform("System`Symbol") def symbol_expression_to_outputform_text( - symb: Symbol, evaluation: Evaluation, form: Symbol, **kwargs + symb: Symbol, evaluation: Evaluation, **kwargs ): return evaluation.definitions.shorten_name(symb.name) @register_outputform("System`TableForm") def tableform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: - return grid_expression_to_outputform_text(expr, evaluation, form) + return grid_expression_to_outputform_text(expr, evaluation, **kwargs) @register_outputform("System`TeXForm") def texform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: # boxes = format_element(expr.elements[0], evaluation, form) boxes = Expression( @@ -803,7 +757,7 @@ def texform_expression_to_outputform_text( @register_outputform("System`Times") def times_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: elements = expr.elements num: List[BaseElement] = [] @@ -841,11 +795,11 @@ def times_expression_to_outputform_text( if len(num) == 1 else Integer1 ) - return _divide(num_expr, den_expr, evaluation, form, **kwargs) + return _divide(num_expr, den_expr, evaluation, **kwargs) # there are no integer negative powers: if len(num) == 1: - return expression_to_outputform_text(num[0], evaluation, form, **kwargs) + return expression_to_outputform_text(num[0], evaluation, **kwargs) prefactor = 1 result: str = "" @@ -857,7 +811,7 @@ def times_expression_to_outputform_text( prefactor *= -1 factor = Integer(-factor.value) - factor_txt = expression_to_outputform_text(factor, evaluation, form, **kwargs) + factor_txt = expression_to_outputform_text(factor, evaluation, **kwargs) if compare_precedence(factor, PRECEDENCE_TIMES): factor_txt = parenthesize(factor_txt) if i == 0: diff --git a/mathics/form/util.py b/mathics/form/util.py new file mode 100644 index 000000000..6da0eb756 --- /dev/null +++ b/mathics/form/util.py @@ -0,0 +1,127 @@ +""" + +Common routines and objects used in rendering PrintForms. + +""" +from typing import Final, List, Optional, Tuple + +from mathics.core.atoms import Integer, String +from mathics.core.convert.op import operator_to_ascii, operator_to_unicode +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.parser.operators import OPERATOR_DATA, operator_to_string +from mathics.core.symbols import Atom, Symbol +from mathics.core.systemsymbols import ( + SymbolInfix, + SymbolLeft, + SymbolNonAssociative, + SymbolNone, + SymbolPostfix, + SymbolPrefix, + SymbolRight, +) + + +# This Exception if the expression should +# be processed by the default routine +class _WrongFormattedExpression(Exception): + pass + + +PRECEDENCES: Final = OPERATOR_DATA.get("operator-precedences") +PRECEDENCE_BOX_GROUP: Final[int] = PRECEDENCES.get("BoxGroup", 670) +PRECEDENCE_PLUS: Final[int] = PRECEDENCES.get("Plus", 310) +PRECEDENCE_TIMES: Final[int] = PRECEDENCES.get("Times", 400) +PRECEDENCE_POWER: Final[int] = PRECEDENCES.get("Power", 590) + + +def bracket(expr_str: str) -> str: + """Wrap `expr_str` with square braces""" + return f"[{expr_str}]" + + +def collect_in_pre_post_arguments( + expr: Expression, evaluation: Evaluation, **kwargs +) -> Tuple[list, str | List[str], int, Optional[Symbol]]: + """ + Determine operands, operator(s), precedence, and grouping + """ + # Processing the second argument, if it is there: + elements = expr.elements + # expr at least has to have one element + if len(elements) < 1: + raise _WrongFormattedExpression + + target = elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + head = expr.head + group = None + precedence = PRECEDENCE_BOX_GROUP + operands = list(target.elements) + + # Just one parameter: + if len(elements) == 1: + render_function = kwargs["_render_function"] + operator_spec = render_function(head, evaluation, **kwargs) + if head is SymbolInfix: + operator_spec = [ + f"{operator_to_string['Infix']}{operator_spec}{operator_to_string['Infix']}" + ] + elif head is SymbolPrefix: + operator_spec = f"{operator_spec}{operator_to_string['Prefix']}" + elif head is SymbolPostfix: + operator_spec = f"{operator_to_string['Postfix']}{operator_spec}" + return operands, operator_spec, precedence, group + + # At least two parameters: get the operator spec. + ops = elements[1] + if head is SymbolInfix: + # This is not the WMA behaviour, but the Mathics3 current implementation requires it: + ops = ops.elements if ops.has_form("List", None) else (ops,) + operator_spec = [get_operator_str(op, evaluation, **kwargs) for op in ops] + else: + operator_spec = get_operator_str(ops, evaluation, **kwargs) + + # At least three arguments: get the precedence + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: + raise _WrongFormattedExpression + + # Four arguments: get the grouping: + if len(elements) > 3: + group = elements[3] + if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): + raise _WrongFormattedExpression + if group is SymbolNone: + group = None + + return operands, operator_spec, precedence, group + + +def get_operator_str(head, evaluation, **kwargs) -> str: + encoding = kwargs["encoding"] + if isinstance(head, String): + op_str = head.value + elif isinstance(head, Symbol): + op_str = head.short_name + else: + render_function = kwargs["_render_function"] + return render_function(head, evaluation, **kwargs) + + if encoding == "ASCII": + operator = operator_to_ascii.get(op_str, op_str) + else: + operator = operator_to_unicode.get(op_str, op_str) + return operator + + +def parenthesize(expr_str: str) -> str: + """Wrap `expr_str` with parenthesis""" + return f"({expr_str})" From 1e554ebb075e9d375d986b0a0ede5ea860312456 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 10 Jan 2026 15:21:18 -0300 Subject: [PATCH 36/49] adjust dtype comparison --- mathics/builtin/image/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/image/base.py b/mathics/builtin/image/base.py index cb2a7cc1e..64bf0794d 100644 --- a/mathics/builtin/image/base.py +++ b/mathics/builtin/image/base.py @@ -211,7 +211,7 @@ def storage_type(self): return "Bit16" elif dtype == numpy.uint8: return "Byte" - elif dtype is bool: + elif dtype == numpy.bool: return "Bit" else: return str(dtype) From 83972c6843b51b9be2f2c6d38ea499d4614e3882 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 10 Jan 2026 15:37:15 -0300 Subject: [PATCH 37/49] remove form parameter --- mathics/form/outputform.py | 44 +++++++++++++++----------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/mathics/form/outputform.py b/mathics/form/outputform.py index 99fe2bb6f..6e3fd0cd9 100644 --- a/mathics/form/outputform.py +++ b/mathics/form/outputform.py @@ -483,7 +483,6 @@ def plus_expression_to_outputform_text( + expression_to_outputform_text( Expression(SymbolTimes, *term.elements[1:]), evaluation, - form, **kwargs, ) ) @@ -492,9 +491,7 @@ def plus_expression_to_outputform_text( result = ( result + " " - + expression_to_outputform_text( - term, evaluation, form, **kwargs - ) + + expression_to_outputform_text(term, evaluation, **kwargs) ) continue elif isinstance(first, Real): @@ -502,19 +499,17 @@ def plus_expression_to_outputform_text( result = ( result + " " - + expression_to_outputform_text( - term, evaluation, form, **kwargs - ) + + expression_to_outputform_text(term, evaluation, **kwargs) ) continue result = ( result + " + " - + expression_to_outputform_text(term, evaluation, form, **kwargs) + + expression_to_outputform_text(term, evaluation, **kwargs) ) ## TODO: handle complex numbers? else: - elem_txt = expression_to_outputform_text(term, evaluation, form, **kwargs) + elem_txt = expression_to_outputform_text(term, evaluation, **kwargs) if (compare_precedence(term, PRECEDENCE_PLUS) or -1) < 0: elem_txt = parenthesize(elem_txt) result = result + " + " + elem_txt @@ -527,7 +522,7 @@ def plus_expression_to_outputform_text( result = ( result + " + " - + expression_to_outputform_text(term, evaluation, form, **kwargs) + + expression_to_outputform_text(term, evaluation, **kwargs) ) return result @@ -546,7 +541,7 @@ def power_expression_to_outputform_text( Integer(PRECEDENCE_POWER), SymbolRight, ) - return expression_to_outputform_text(infix_form, evaluation, form, **kwargs) + return expression_to_outputform_text(infix_form, evaluation, **kwargs) @register_outputform("System`PrecedenceForm") @@ -554,9 +549,7 @@ def precedenceform_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> str: if len(expr.elements) == 2: - return expression_to_outputform_text( - expr.elements[0], evaluation, form, **kwargs - ) + return expression_to_outputform_text(expr.elements[0], evaluation, **kwargs) raise _WrongFormattedExpression @@ -585,18 +578,17 @@ def pre_pos_fix_expression_to_outputform_text( # Processing the second argument, if it is there: if len(elements) > 1: ops = elements[1] - ops_txt = [expression_to_outputform_text(ops, evaluation, form, **kwargs)] + ops_txt = [expression_to_outputform_text(ops, evaluation, **kwargs)] else: if head is SymbolPrefix: default_symb = " @ " ops_txt = ( - expression_to_outputform_text(head, evaluation, form, **kwargs) - + default_symb + expression_to_outputform_text(head, evaluation, **kwargs) + default_symb ) elif head is SymbolPostfix: default_symb = " // " ops_txt = default_symb + expression_to_outputform_text( - head, evaluation, form, **kwargs + head, evaluation, **kwargs ) # Processing the third argument, if it is there: @@ -616,7 +608,7 @@ def pre_pos_fix_expression_to_outputform_text( operand = operands[0] cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_outputform_text(operand, evaluation, form, **kwargs) + target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) if cmp_precedence is not None and cmp_precedence != -1: target_txt = parenthesize(target_txt) @@ -625,7 +617,7 @@ def pre_pos_fix_expression_to_outputform_text( @register_outputform("System`Rational") def rational_expression_to_outputform_text( - n: Union[Rational, Expression], evaluation: Evaluation, form: Symbol, **kwargs + n: Union[Rational, Expression], evaluation: Evaluation, **kwargs ): if n.has_form("Rational", 2): num, den = n.elements # type: ignore[union-attr] @@ -635,15 +627,13 @@ def rational_expression_to_outputform_text( @register_outputform("System`Real") -def real_expression_to_outputform_text( - n: Real, evaluation: Evaluation, form: Symbol, **kwargs -): +def real_expression_to_outputform_text(n: Real, evaluation: Evaluation, **kwargs): str_n = n.make_boxes("System`OutputForm").boxes_to_text() # type: ignore[attr-defined] return str(str_n) @register_outputform("System`Row") -def row_to_outputform_text(expr, evaluation: Evaluation, form: Symbol, **kwargs): +def row_to_outputform_text(expr, evaluation: Evaluation, **kwargs): """Row[{...}]""" elements = expr.elements[0].elements return "".join( @@ -653,13 +643,13 @@ def row_to_outputform_text(expr, evaluation: Evaluation, form: Symbol, **kwargs) @register_outputform("System`Rule") @register_outputform("System`RuleDelayed") -def rule_to_outputform_text(expr, evaluation: Evaluation, form: Symbol, **kwargs): +def rule_to_outputform_text(expr, evaluation: Evaluation, **kwargs): """Rule|RuleDelayed[{...}]""" head = expr.head elements = expr.elements kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) if len(elements) != 2: - return _default_expression_to_outputform_text(expr, evaluation, form, **kwargs) + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) pat, rule = ( expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements ) @@ -748,7 +738,7 @@ def tableform_expression_to_outputform_text( def texform_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, **kwargs ) -> str: - # boxes = format_element(expr.elements[0], evaluation, form) + # boxes = format_element(expr.elements[0], evaluation) boxes = Expression( Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm ).evaluate(evaluation) From dbd41735e0625da831cd049708e35f3cfafebf49 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 10 Jan 2026 16:00:27 -0300 Subject: [PATCH 38/49] and more --- mathics/form/inputform.py | 42 ++++++++------------------------------ mathics/form/outputform.py | 38 +++++++++++++++------------------- mathics/form/util.py | 19 +++++++++++++++++ 3 files changed, 45 insertions(+), 54 deletions(-) diff --git a/mathics/form/inputform.py b/mathics/form/inputform.py index 52d1bba9e..95b054e3c 100644 --- a/mathics/form/inputform.py +++ b/mathics/form/inputform.py @@ -30,14 +30,12 @@ from mathics.core.atoms import Integer, String from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.parser.operators import OPERATOR_DATA, operator_to_string -from mathics.core.symbols import Atom, Symbol +from mathics.core.parser.operators import operator_to_string +from mathics.core.symbols import Atom from mathics.core.systemsymbols import ( - SymbolBlank, - SymbolBlankNullSequence, - SymbolBlankSequence, SymbolInputForm, SymbolLeft, + SymbolNonAssociative, SymbolRight, ) from mathics.eval.makeboxes.formatvalues import do_format # , format_element @@ -45,6 +43,8 @@ from mathics.settings import SYSTEM_CHARACTER_ENCODING from .util import ( + ARITHMETIC_OPERATOR_STRINGS, + BLANKS_TO_STRINGS, _WrongFormattedExpression, bracket, collect_in_pre_post_arguments, @@ -52,16 +52,6 @@ parenthesize, ) -SymbolNonAssociative = Symbol("System`NonAssociative") -SymbolPostfix = Symbol("System`Postfix") -SymbolPrefix = Symbol("System`Prefix") - -PRECEDENCES: Final = OPERATOR_DATA.get("operator-precedences") -PRECEDENCE_BOX_GROUP: Final[int] = PRECEDENCES.get("BoxGroup", 670) -PRECEDENCE_PLUS: Final[int] = PRECEDENCES.get("Plus", 310) -PRECEDENCE_TIMES: Final[int] = PRECEDENCES.get("Times", 400) -PRECEDENCE_POWER: Final[int] = PRECEDENCES.get("Power", 590) - EXPR_TO_INPUTFORM_TEXT_MAP: Dict[str, Callable] = {} @@ -149,17 +139,6 @@ def _list_expression_to_inputform_text( return result + "}" -ARITHMETIC_OPERATOR_STRINGS: Final[FrozenSet[str]] = frozenset( - [ - *operator_to_string["Divide"], - *operator_to_string["NonCommutativeMultiply"], - *operator_to_string["Power"], - *operator_to_string["Times"], - " ", - ] -) - - @register_inputform("System`Infix") def _infix_expression_to_inputform_text( expr: Expression, evaluation: Evaluation, **kwargs @@ -270,13 +249,10 @@ def _blanks(expr: Expression, evaluation: Evaluation, **kwargs): else: elem = "" head = expr.head - if head is SymbolBlank: - return "_" + elem - elif head is SymbolBlankSequence: - return "__" + elem - elif head is SymbolBlankNullSequence: - return "___" + elem - return _generic_to_inputform_text(expr, evaluation, **kwargs) + try: + return BLANKS_TO_STRINGS[head] + elem + except KeyError: + return _generic_to_inputform_text(expr, evaluation, **kwargs) @register_inputform("System`Pattern") diff --git a/mathics/form/outputform.py b/mathics/form/outputform.py index 6e3fd0cd9..4e40b1d83 100644 --- a/mathics/form/outputform.py +++ b/mathics/form/outputform.py @@ -5,7 +5,7 @@ and text terminals. """ -from typing import Callable, Dict, Final, List, Union +from typing import Callable, Dict, List, Union from mathics.core.atoms import ( Integer, @@ -20,7 +20,6 @@ from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.parser.operators import OPERATOR_DATA from mathics.core.symbols import Atom, Symbol, SymbolTimes from mathics.core.systemsymbols import ( SymbolBlank, @@ -39,17 +38,17 @@ from mathics.eval.makeboxes import compare_precedence, do_format # , format_element from mathics.settings import SYSTEM_CHARACTER_ENCODING -from .util import _WrongFormattedExpression, bracket, get_operator_str, parenthesize - -PRECEDENCES: Final = OPERATOR_DATA.get("operator-precedences") -PRECEDENCE_DEFAULT: Final = PRECEDENCES.get("FunctionApply") -PRECEDENCE_PLUS: Final = PRECEDENCES.get("Plus") -PRECEDENCE_TIMES: Final = PRECEDENCES.get("Times") -PRECEDENCE_POWER: Final = PRECEDENCES.get("Power") - -# When new mathics-scanner tables are updagted: -# BOX_GROUP_PRECEDENCE: Final = box_operators["BoxGroup"] -BOX_GROUP_PRECEDENCE: Final = PRECEDENCE_DEFAULT +from .util import ( + BLANKS_TO_STRINGS, + BOX_GROUP_PRECEDENCE, + PRECEDENCE_PLUS, + PRECEDENCE_POWER, + PRECEDENCE_TIMES, + _WrongFormattedExpression, + bracket, + get_operator_str, + parenthesize, +) EXPR_TO_OUTPUTFORM_TEXT_MAP: Dict[str, Callable] = {} @@ -147,13 +146,10 @@ def blank_pattern(expr: Expression, evaluation: Evaluation, **kwargs): else: elem = "" head = expr.head - if head is SymbolBlank: - return "_" + elem - elif head is SymbolBlankSequence: - return "__" + elem - elif head is SymbolBlankNullSequence: - return "___" + elem - return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + try: + return BLANKS_TO_STRINGS[head] + elem + except KeyError: + return _generic_to_inputform_text(expr, evaluation, **kwargs) @register_outputform("System`Derivative") @@ -655,7 +651,7 @@ def rule_to_outputform_text(expr, evaluation: Evaluation, **kwargs): ) kwargs["_render_function"] = expression_to_outputform_text op_str = get_operator_str(head, evaluation, **kwargs) - return pat + " " + op_str + " " + rule + return f"{pat} {op_str} {rule}" @register_outputform("System`String") diff --git a/mathics/form/util.py b/mathics/form/util.py index 6da0eb756..ce940c977 100644 --- a/mathics/form/util.py +++ b/mathics/form/util.py @@ -12,6 +12,9 @@ from mathics.core.parser.operators import OPERATOR_DATA, operator_to_string from mathics.core.symbols import Atom, Symbol from mathics.core.systemsymbols import ( + SymbolBlank, + SymbolBlankNullSequence, + SymbolBlankSequence, SymbolInfix, SymbolLeft, SymbolNonAssociative, @@ -28,12 +31,28 @@ class _WrongFormattedExpression(Exception): pass +ARITHMETIC_OPERATOR_STRINGS: Final[FrozenSet[str]] = frozenset( + [ + *operator_to_string["Divide"], + *operator_to_string["NonCommutativeMultiply"], + *operator_to_string["Power"], + *operator_to_string["Times"], + " ", + ] +) + PRECEDENCES: Final = OPERATOR_DATA.get("operator-precedences") PRECEDENCE_BOX_GROUP: Final[int] = PRECEDENCES.get("BoxGroup", 670) PRECEDENCE_PLUS: Final[int] = PRECEDENCES.get("Plus", 310) PRECEDENCE_TIMES: Final[int] = PRECEDENCES.get("Times", 400) PRECEDENCE_POWER: Final[int] = PRECEDENCES.get("Power", 590) +BLANKS_TO_STRINGS = { + SymbolBlank: "_", + SymbolBlankSequence: "__", + SymbolBlankNullSequence: "___", +} + def bracket(expr_str: str) -> str: """Wrap `expr_str` with square braces""" From 98613ab7f7556f29fba1d47204cdc9086472b030 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 11 Jan 2026 01:28:07 -0300 Subject: [PATCH 39/49] more on OutputForm implemetation --- mathics/form/inputform.py | 15 +- mathics/form/outputform.py | 408 +++++++++++++++++++------------------ mathics/form/util.py | 85 +++++++- 3 files changed, 297 insertions(+), 211 deletions(-) diff --git a/mathics/form/inputform.py b/mathics/form/inputform.py index 95b054e3c..879a6cf88 100644 --- a/mathics/form/inputform.py +++ b/mathics/form/inputform.py @@ -25,12 +25,11 @@ """ -from typing import Callable, Dict, Final, FrozenSet +from typing import Callable, Dict from mathics.core.atoms import Integer, String from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.parser.operators import operator_to_string from mathics.core.symbols import Atom from mathics.core.systemsymbols import ( SymbolInputForm, @@ -151,10 +150,12 @@ def _infix_expression_to_inputform_text( # has a head that matches with a symbol associated to an infix # operator, WMA builds its inputform without passing through # its "Infix" form. + print("input infix", expr) kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) operands, ops_lst, precedence, group = collect_in_pre_post_arguments( expr, evaluation, **kwargs ) + print(" ", operands, ops_lst) # Infix needs at least two operands: if len(operands) < 2: raise _WrongFormattedExpression @@ -177,7 +178,7 @@ def _infix_expression_to_inputform_text( parenthesized = not parenthesized else: num_ops = len(ops_lst) - curr_op = ops_lst[index % num_ops] + curr_op = ops_lst[(index - 1) % num_ops] if curr_op not in ARITHMETIC_OPERATOR_STRINGS: # In the tests, we add spaces just for + and -: curr_op = f" {curr_op} " @@ -197,7 +198,7 @@ def _prefix_expression_to_inputform_text( expr: Expression, evaluation: Evaluation, **kwargs ) -> str: """ - Convert Prefix[...] into a InputForm string. + Convert Prefix[...] into a OutputForm string. """ kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) operands, op_head, precedence, group = collect_in_pre_post_arguments( @@ -210,7 +211,7 @@ def _prefix_expression_to_inputform_text( kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) cmp_precedence = compare_precedence(operand, precedence) target_txt = render_input_form(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: + if cmp_precedence is not None and cmp_precedence != 1: target_txt = parenthesize(target_txt) return op_head + target_txt @@ -220,7 +221,7 @@ def _postfix_expression_to_inputform_text( expr: Expression, evaluation: Evaluation, **kwargs ) -> str: """ - Convert Postfix[...] into a InputForm string. + Convert Postfix[...] into a OutputForm string. """ kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) operands, op_head, precedence, group = collect_in_pre_post_arguments( @@ -232,7 +233,7 @@ def _postfix_expression_to_inputform_text( operand = operands[0] cmp_precedence = compare_precedence(operand, precedence) target_txt = render_input_form(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: + if cmp_precedence is not None and cmp_precedence != 1: target_txt = parenthesize(target_txt) return target_txt + op_head diff --git a/mathics/form/outputform.py b/mathics/form/outputform.py index 4e40b1d83..1ec928fdb 100644 --- a/mathics/form/outputform.py +++ b/mathics/form/outputform.py @@ -9,6 +9,7 @@ from mathics.core.atoms import ( Integer, + Integer0, Integer1, Integer2, IntegerM1, @@ -22,13 +23,10 @@ from mathics.core.list import ListExpression from mathics.core.symbols import Atom, Symbol, SymbolTimes from mathics.core.systemsymbols import ( - SymbolBlank, - SymbolBlankNullSequence, - SymbolBlankSequence, SymbolDerivative, SymbolInfix, SymbolLeft, - SymbolNone, + SymbolNonAssociative, SymbolOutputForm, SymbolPower, SymbolRight, @@ -40,22 +38,19 @@ from .util import ( BLANKS_TO_STRINGS, - BOX_GROUP_PRECEDENCE, PRECEDENCE_PLUS, PRECEDENCE_POWER, PRECEDENCE_TIMES, _WrongFormattedExpression, bracket, + collect_in_pre_post_arguments, get_operator_str, parenthesize, + text_cells_to_grid, ) EXPR_TO_OUTPUTFORM_TEXT_MAP: Dict[str, Callable] = {} -SymbolNonAssociative = Symbol("System`NonAssociative") -SymbolPostfix = Symbol("System`Postfix") -SymbolPrefix = Symbol("System`Prefix") - class IsNotGrid(Exception): pass @@ -83,7 +78,7 @@ def _default_expression_to_outputform_text( elements = [ expression_to_outputform_text(elem, evaluation) for elem in expr.elements ] - result = elements.pop(0) if elements else " " + result = elements.pop(0) if elements else "" while elements: result = result + comma + elements.pop(0) @@ -149,7 +144,7 @@ def blank_pattern(expr: Expression, evaluation: Evaluation, **kwargs): try: return BLANKS_TO_STRINGS[head] + elem except KeyError: - return _generic_to_inputform_text(expr, evaluation, **kwargs) + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) @register_outputform("System`Derivative") @@ -197,7 +192,6 @@ def expression_to_outputform_text(expr: BaseElement, evaluation: Evaluation, **k """ Build a pretty-print text from an `Expression` """ - format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore while format_expr.has_form("HoldForm", 1): # type: ignore @@ -257,7 +251,7 @@ def grid_expression_to_outputform_text( else: rows.append(expression_to_outputform_text(item, evaluation, **kwargs)) - return grid(rows) + return text_cells_to_grid(rows) register_outputform("System`HoldForm")(_strip_1_parm_expression_to_outputform_text) @@ -268,9 +262,7 @@ def grid_expression_to_outputform_text( def other_forms(expr, evaluation, **kwargs): from mathics.eval.makeboxes import format_element - form = expr.get_head() - expr = expr.elements[0] - result = format_element(expr, evaluation, form, **kwargs) + result = format_element(expr, evaluation, SymbolStandardForm, **kwargs) if isinstance(result, String): return result.value return result.boxes_to_text() @@ -282,129 +274,62 @@ def image_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs): @register_outputform("System`Infix") -def infix_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, **kwargs -) -> str: +def _infix_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs) -> str: + """ + Convert Infix[...] into a InputForm string. + """ + # In WMA, expressions associated to Infix operators are not + # formatted using this path: in some way, when an expression + # has a head that matches with a symbol associated to an infix + # operator, WMA builds its inputform without passing through + # its "Infix" form. kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) - - elements = expr.elements - if not (0 <= len(elements) <= 4): - raise _WrongFormattedExpression - - group = None - precedence = BOX_GROUP_PRECEDENCE - - # Processing the first argument: - head = expr.get_head() - target = expr.elements[0] - if isinstance(target, Atom): - raise _WrongFormattedExpression - - operands = list(target.elements) - + operands, ops_lst, precedence, group = collect_in_pre_post_arguments( + expr, evaluation, **kwargs + ) + # Infix needs at least two operands: if len(operands) < 2: raise _WrongFormattedExpression - # Processing the second argument, if it is there: - if len(elements) > 1: - ops = elements[1] - if head is SymbolInfix: - # This is not the WMA behaviour, but the Mathics current implementation requires it: - num_ops = 1 - # TODO: Handle the case where op is not a String or a Symbol. - kwargs["_render_function"] = expression_to_outputform_text - if ops.has_form("List", None): - num_ops = len(ops.elements) - ops_lst = [ - get_operator_str(op, evaluation, **kwargs) for op in ops.elements - ] - else: - ops_lst = [get_operator_str(ops, evaluation, **kwargs)] - elif head in (SymbolPrefix, SymbolPostfix): - ops_txt = [expression_to_outputform_text(ops, evaluation, **kwargs)] - else: - if head is SymbolInfix: - num_ops = 1 - default_symb = " ~ " - ops_lst = [ - default_symb - + expression_to_outputform_text(head, evaluation, **kwargs) - + default_symb - ] - elif head is SymbolPrefix: - default_symb = " @ " - ops_txt = ( - expression_to_outputform_text(head, evaluation, **kwargs) + default_symb - ) - elif head is SymbolPostfix: - default_symb = " // " - ops_txt = default_symb + expression_to_outputform_text( - head, evaluation, **kwargs - ) - - # Processing the third argument, if it is there: - if len(elements) > 2: - if isinstance(elements[2], Integer): - precedence = elements[2].value + # Process the operands: + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + for index, operand in enumerate(operands): + operand_txt = str(expression_to_outputform_text(operand, evaluation, **kwargs)) + cmp_precedence = compare_precedence(operand, precedence) + if cmp_precedence is not None and ( + cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) + ): + operand_txt = parenthesize(operand_txt) + + if index == 0: + result = operand_txt + # After the first element, for lateral + # associativity, parenthesized is flipped: + if group in (SymbolLeft, SymbolRight): + parenthesized = not parenthesized else: - raise _WrongFormattedExpression + num_ops = len(ops_lst) + curr_op = ops_lst[(index - 1) % num_ops] + # In OutputForm we always add the spaces, except for + # " " + if curr_op != " ": + curr_op = f" {curr_op} " + + result = "".join( + ( + result, + curr_op, + operand_txt, + ) + ) + return result - # Processing the forth argument, if it is there: - if len(elements) > 3: - group = elements[3] - if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): - raise _WrongFormattedExpression - if group is SymbolNone: - group = None - if head is SymbolPrefix: - operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: - target_txt = parenthesize(target_txt) - return ops_txt[0] + target_txt - if head is SymbolPostfix: - operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: - target_txt = parenthesize(target_txt) - return target_txt + ops_txt[0] - else: # Infix - parenthesized = group in (None, SymbolRight, SymbolNonAssociative) - for index, operand in enumerate(operands): - operand_txt = str( - expression_to_outputform_text(operand, evaluation, **kwargs) - ) - cmp_precedence = compare_precedence(operand, precedence) - if cmp_precedence is not None and ( - cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) - ): - operand_txt = parenthesize(operand_txt) - - if index == 0: - result = operand_txt - # After the first element, for lateral - # associativity, parenthesized is flipped: - if group in (SymbolLeft, SymbolRight): - parenthesized = not parenthesized - else: - space = " " - result_lst: List[str] - if str(ops_lst[index % num_ops]) != " ": - result_lst = [ - result, - space, - str(ops_lst[(index - 1) % num_ops]), - space, - operand_txt, - ] - else: - result_lst = [result, space, operand_txt] - result = "".join(result_lst) +@register_outputform("System`InputForm") +def inputform(expr: Expression, evaluation: Evaluation, **kwargs): + from .inputform import render_input_form - return result + return render_input_form(expr, evaluation, **kwargs) @register_outputform("System`Integer") @@ -436,7 +361,7 @@ def mathmlform_expression_to_outputform_text( ) -> str: # boxes = format_element(expr.elements[0], evaluation) boxes = Expression( - Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + Symbol("System`MakeBoxes"), expr.elements[0], SymbolTraditionalForm ).evaluate(evaluation) return boxes.boxes_to_mathml() # type: ignore[union-attr] @@ -449,6 +374,41 @@ def matrixform_expression_to_outputform_text( return tableform_expression_to_outputform_text(expr, evaluation, **kwargs) +@register_outputform("System`MessageName") +def message_name_outputform(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 2: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + symb, msg = elements + if not (isinstance(symb, Symbol) and isinstance(msg, String)): + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + symbol_name = evaluation.definitions.shorten_name(symb.get_name()) + return f"{symbol_name}::{msg.value}" + + +@register_outputform("System`OutputForm") +def outputform(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 1: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + return expression_to_outputform_text(elements[0], evaluation, **kwargs) + + +@register_outputform("System`Part") +def part(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) == 0: + raise _WrongFormattedExpression + elements_fmt = [ + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements + ] + if len(elements_fmt) == 1: + return elements_fmt[0] + result = elements_fmt[0] + args = ", ".join(elements_fmt[1:]) + return f"{result}[[{args}]]" + + @register_outputform("System`Pattern") def pattern(expr: Expression, evaluation: Evaluation, **kwargs): elements = expr.elements @@ -550,65 +510,44 @@ def precedenceform_expression_to_outputform_text( @register_outputform("System`Prefix") -@register_outputform("System`Postfix") -def pre_pos_fix_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs -) -> str: - elements = expr.elements - if not (0 <= len(elements) <= 4): +def _prefix_output_text(expr: Expression, evaluation: Evaluation, **kwargs) -> str: + """ + Convert Prefix[...] into a InputForm string. + """ + kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) + operands, op_head, precedence, group = collect_in_pre_post_arguments( + expr, evaluation, **kwargs + ) + # Prefix works with just one operand: + if len(operands) != 1: raise _WrongFormattedExpression + operand = operands[0] + kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) + if cmp_precedence is not None and cmp_precedence != 1: + target_txt = parenthesize(target_txt) + return op_head + target_txt - group = None - precedence = BOX_GROUP_PRECEDENCE - - # Processing the first argument: - head = expr.get_head() - target = expr.elements[0] - if isinstance(target, Atom): - raise _WrongFormattedExpression - operands = list(target.elements) +@register_outputform("System`Postfix") +def _postfix_output_text(expr: Expression, evaluation: Evaluation, **kwargs) -> str: + """ + Convert Postfix[...] into a InputForm string. + """ + kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) + operands, op_head, precedence, group = collect_in_pre_post_arguments( + expr, evaluation, **kwargs + ) + # Prefix works with just one operand: if len(operands) != 1: raise _WrongFormattedExpression - - # Processing the second argument, if it is there: - if len(elements) > 1: - ops = elements[1] - ops_txt = [expression_to_outputform_text(ops, evaluation, **kwargs)] - else: - if head is SymbolPrefix: - default_symb = " @ " - ops_txt = ( - expression_to_outputform_text(head, evaluation, **kwargs) + default_symb - ) - elif head is SymbolPostfix: - default_symb = " // " - ops_txt = default_symb + expression_to_outputform_text( - head, evaluation, **kwargs - ) - - # Processing the third argument, if it is there: - if len(elements) > 2: - if isinstance(elements[2], Integer): - precedence = elements[2].value - else: - raise _WrongFormattedExpression - - # Processing the forth argument, if it is there: - if len(elements) > 3: - group = elements[3] - if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): - raise _WrongFormattedExpression - if group is SymbolNone: - group = None - operand = operands[0] cmp_precedence = compare_precedence(operand, precedence) target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: + if cmp_precedence is not None and cmp_precedence != 1: target_txt = parenthesize(target_txt) - - return ops_txt[0] + target_txt if head is SymbolPrefix else target_txt + ops_txt[0] + return target_txt + op_head @register_outputform("System`Rational") @@ -654,6 +593,38 @@ def rule_to_outputform_text(expr, evaluation: Evaluation, **kwargs): return f"{pat} {op_str} {rule}" +@register_outputform("System`Slot") +def _slot_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 1: + raise _WrongFormattedExpression + slot = elements[0] + if isinstance(slot, Integer): + slot_value = slot.value + if slot_value < 0: + raise _WrongFormattedExpression + return f"#{slot_value}" + if isinstance(slot, String): + return f"#{slot.value}" + raise _WrongFormattedExpression + + +@register_outputform("System`SlotSequence") +def _slotsequence_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 1: + raise _WrongFormattedExpression + slot = elements[0] + if isinstance(slot, Integer): + slot_value = slot.value + if slot_value < 0: + raise _WrongFormattedExpression + return f"##{slot_value}" + if isinstance(slot, String): + return f"##{slot.value}" + raise _WrongFormattedExpression + + @register_outputform("System`String") def string_expression_to_outputform_text( expr: String, evaluation: Evaluation, **kwargs @@ -680,38 +651,59 @@ def stringform_expression_to_outputform_text( curr_indx = 0 parts = strform.value.split("`") result = str(parts[0]) - if len(parts) == 1: + if len(parts) <= 1: return result quote_open = True remaining = len(parts) - 1 - + num_items = len(items) for part in parts[1:]: remaining -= 1 + # If quote_open, the part must be a placeholder if quote_open: - if remaining == 0: - result = result + "`" + part - quote_open = False - continue - if len(part) == 0: + # If not remaining, there is a not closed '`' + # character: + if not remaining: + evaluation.message("StringForm", "sfq", strform) + return strform.value + # part must be an index or an empty string. + # If is an empty string, pick the next element: + if part == "": + if curr_indx >= num_items: + evaluation.message( + "StringForm", + "sfr", + Integer(num_items + 1), + Integer(num_items), + strform, + ) + return strform.value result = result + items[curr_indx] + curr_indx += 1 + quote_open = False continue + # Otherwise, must be a positive integer: try: - idx = int(part) + indx = int(part) except ValueError: - idx = None - if idx is not None and str(idx) == part: - curr_indx = idx - 1 - result = result + items[curr_indx] - quote_open = False - continue - else: - result = result + "`" + part + "`" - quote_open = False - continue - else: - result = result + part - quote_open = True + evaluation.message( + "StringForm", "sfr", Integer0, Integer(num_items), strform + ) + return strform.value + # indx must be greater than 0, and not greater than + # the number of items + if indx <= 0 or indx > len(items): + evaluation.message( + "StringForm", "sfr", Integer(indx), Integer(len(items)), strform + ) + return strform.value + result = result + items[indx - 1] + curr_indx = indx + quote_open = False + continue + + result = result + part + quote_open = True return result @@ -741,11 +733,21 @@ def texform_expression_to_outputform_text( return boxes.boxes_to_tex() # type: ignore +@register_outputform("System`TeXForm") +def _texform_outputform(expr, evaluation, **kwargs): + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolTraditionalForm + ).evaluate(evaluation) + return boxes.boxes_to_tex() # type: ignore[union-attr] + + @register_outputform("System`Times") def times_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, **kwargs ) -> str: elements = expr.elements + if len(elements) < 2: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) num: List[BaseElement] = [] den: List[BaseElement] = [] # First, split factors with integer, negative powers: diff --git a/mathics/form/util.py b/mathics/form/util.py index ce940c977..8dc6c5a68 100644 --- a/mathics/form/util.py +++ b/mathics/form/util.py @@ -3,7 +3,7 @@ Common routines and objects used in rendering PrintForms. """ -from typing import Final, List, Optional, Tuple +from typing import Final, FrozenSet, List, Optional, Tuple from mathics.core.atoms import Integer, String from mathics.core.convert.op import operator_to_ascii, operator_to_unicode @@ -47,6 +47,7 @@ class _WrongFormattedExpression(Exception): PRECEDENCE_TIMES: Final[int] = PRECEDENCES.get("Times", 400) PRECEDENCE_POWER: Final[int] = PRECEDENCES.get("Power", 590) + BLANKS_TO_STRINGS = { SymbolBlank: "_", SymbolBlankSequence: "__", @@ -144,3 +145,85 @@ def get_operator_str(head, evaluation, **kwargs) -> str: def parenthesize(expr_str: str) -> str: """Wrap `expr_str` with parenthesis""" return f"({expr_str})" + + +def text_cells_to_grid(cells: List, **kwargs): + """ + Build from a tabulated grid from a list or + a list of lists of elements. + """ + if not cells: + return "" + cells = [[row] if isinstance(row, str) else row for row in cells] + for row in cells: + assert all(isinstance(field, str) for field in row) + + def normalize_rows(rows): + """ + Split each field in each row in a list of lines, and then + make that the number of lines on each field inside the same + row are the same. + """ + rows = [[field.split("\n") for field in row] for row in rows] + max_cols = max(len(row) for row in rows) + new_rows = [] + heights = [] + # TODO: implement vertical aligments + for row in rows: + max_height = max(len(field) for field in row) + new_row = [] + for field in row: + height = len(field) + if height < max_height: + field = field + [""] * (max_height - height) + new_row.append(field) + num_cols = len(new_row) + if num_cols < max_cols: + blank_field = [""] * max_height + new_row.extend([blank_field] * (max_cols - num_cols)) + new_rows.append(new_row) + heights.append(max_height) + return new_rows, heights + + def normalize_cols(rows): + """ + Ensure that all the lines of fields on the same line + have the same width. + """ + # TODO: implement horizontal alignments + col_widths = [0] * len(rows[0]) + for row in rows: + for col, cell in enumerate(row): + for line in cell: + col_widths[col] = max(col_widths[col], len(line)) + rows = [ + [ + [line.ljust(col_widths[col]) for line in cell] + for col, cell in enumerate(row) + ] + for row in rows + ] + return rows, col_widths + + cells, heights = normalize_rows(cells) + cells, col_widths = normalize_cols(cells) + row_sep = "\n\n" + col_sep = " " + result = "" + for row_idx, row in enumerate(cells): + if row_idx != 0: + result += row_sep + new_lines = [""] * heights[row_idx] + for field_no, field in enumerate(row): + for l_no, line in enumerate(field): + if field_no: + new_lines[l_no] += col_sep + line + else: + new_lines[l_no] += line + + # TODO: Remove me at the end... + new_lines = [line.rstrip() for line in new_lines] + new_text_row = "\n".join(new_lines) + result = result + new_text_row + + return result + "\n" From 4c316716bb05ab35e654342d8a73e216d429767d Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 11 Jan 2026 01:36:54 -0300 Subject: [PATCH 40/49] adding messages to StringForm --- mathics/builtin/forms/data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mathics/builtin/forms/data.py b/mathics/builtin/forms/data.py index aff1800c1..ab6cd443b 100644 --- a/mathics/builtin/forms/data.py +++ b/mathics/builtin/forms/data.py @@ -434,6 +434,10 @@ class StringForm(FormBaseClass): in_outputforms = False in_printforms = False + messages = { + "sfr": 'Item `1` requested in "`3`" out of range; `2` items available.', + "sfq": "Unmatched backquote in `1`.", + } summary_text = "format a string from a template and a list of parameters" def eval_makeboxes(self, s, args, form, evaluation): From 47bb5d38a1eb516887f88b8a9c86fb35f8d7550e Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 11 Jan 2026 10:49:03 -0300 Subject: [PATCH 41/49] remove prints --- mathics/form/inputform.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mathics/form/inputform.py b/mathics/form/inputform.py index 879a6cf88..fd4583989 100644 --- a/mathics/form/inputform.py +++ b/mathics/form/inputform.py @@ -150,12 +150,10 @@ def _infix_expression_to_inputform_text( # has a head that matches with a symbol associated to an infix # operator, WMA builds its inputform without passing through # its "Infix" form. - print("input infix", expr) kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) operands, ops_lst, precedence, group = collect_in_pre_post_arguments( expr, evaluation, **kwargs ) - print(" ", operands, ops_lst) # Infix needs at least two operands: if len(operands) < 2: raise _WrongFormattedExpression From 60cc2fa100185c667d6151343491ce242732d4d9 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 11 Jan 2026 10:53:27 -0300 Subject: [PATCH 42/49] range in baseform --- mathics/eval/makeboxes/numberform.py | 38 ++++++++++--------- mathics/form/outputform.py | 56 +++++++++++++--------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/mathics/eval/makeboxes/numberform.py b/mathics/eval/makeboxes/numberform.py index 10b67ac11..ae3b955e0 100644 --- a/mathics/eval/makeboxes/numberform.py +++ b/mathics/eval/makeboxes/numberform.py @@ -17,11 +17,7 @@ convert_base, dps, ) -from mathics.core.systemsymbols import ( - SymbolMakeBoxes, - SymbolOutputForm, - SymbolSubscriptBox, -) +from mathics.core.systemsymbols import SymbolMakeBoxes, SymbolSubscriptBox from mathics.eval.makeboxes import to_boxes @@ -303,11 +299,15 @@ def split_string(s, start, step): return boxed_s -def eval_baseform(self, expr, n, f, evaluation: Evaluation): +def get_baseform_elements(expr, n, evaluation: Evaluation): + if not isinstance(n, Integer): + evaluation.message("BaseForm", "intpm", expr, n) + raise ValueError + base = n.value if base <= 0: evaluation.message("BaseForm", "intpm", expr, n) - return None + raise ValueError if isinstance(expr, PrecisionReal): x = expr.to_sympy() @@ -319,17 +319,21 @@ def eval_baseform(self, expr, n, f, evaluation: Evaluation): x = expr.value p = 0 else: - return to_boxes(Expression(SymbolMakeBoxes, expr, f), evaluation) - + return None, None try: - val = convert_base(x, base, p) + return convert_base(x, base, p), base except ValueError: evaluation.message("BaseForm", "basf", n) - return + raise - if f is SymbolOutputForm: - return to_boxes(String("%s_%d" % (val, base)), evaluation) - else: - return to_boxes( - Expression(SymbolSubscriptBox, String(val), String(base)), evaluation - ) + +def eval_baseform(self, expr, n, f, evaluation: Evaluation): + try: + val, base = get_baseform_elements(expr, n, evaluation) + except ValueError: + return None + if base is None: + return to_boxes(Expression(SymbolMakeBoxes, expr, f), evaluation) + return to_boxes( + Expression(SymbolSubscriptBox, String(val), String(base)), evaluation + ) diff --git a/mathics/form/outputform.py b/mathics/form/outputform.py index 1ec928fdb..86a07e226 100644 --- a/mathics/form/outputform.py +++ b/mathics/form/outputform.py @@ -34,6 +34,7 @@ SymbolTraditionalForm, ) from mathics.eval.makeboxes import compare_precedence, do_format # , format_element +from mathics.eval.makeboxes.numberform import get_baseform_elements from mathics.settings import SYSTEM_CHARACTER_ENCODING from .util import ( @@ -107,20 +108,6 @@ def _strip_1_parm_expression_to_outputform_text( return expression_to_outputform_text(expr.elements[0], evaluation, **kwargs) -def grid(expr): - # Very basic implementation. - result = "" - for idx_row, row in enumerate(expr): - if idx_row > 0: - result += "\n\n" - for idx_field, field in enumerate(row): - if idx_field > 0: - result += " " - result += field - - return result - - def register_outputform(head_name): def _register(func): EXPR_TO_OUTPUTFORM_TEXT_MAP[head_name] = func @@ -129,6 +116,24 @@ def _register(func): return _register +@register_outputform("System`BaseForm") +def _baseform_outputform(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 2: + evaluation.message("BaseForm", "argr", Integer(len(elements)), Integer2) + raise _WrongFormattedExpression + + number, base_expr = elements + try: + val, base = get_baseform_elements(number, base_expr, evaluation) + except ValueError: + raise _WrongFormattedExpression + + if base is None: + return expression_to_outputform_text(number, evaluation, **kwargs) + return val + "_" + str(base) + + @register_outputform("System`Blank") @register_outputform("System`BlankSequence") @register_outputform("System`BlankNullSequence") @@ -629,10 +634,11 @@ def _slotsequence_outputform_text(expr: Expression, evaluation: Evaluation, **kw def string_expression_to_outputform_text( expr: String, evaluation: Evaluation, **kwargs ) -> str: - lines = expr.value.split("\n") - max_len = max([len(line) for line in lines]) - lines = [line + (max_len - len(line)) * " " for line in lines] - return "\n".join(lines) + # lines = expr.value.split("\n") + # max_len = max([len(line) for line in lines]) + # lines = [line + (max_len - len(line)) * " " for line in lines] + # return "\n".join(lines) + return expr.value @register_outputform("System`StringForm") @@ -722,23 +728,13 @@ def tableform_expression_to_outputform_text( return grid_expression_to_outputform_text(expr, evaluation, **kwargs) -@register_outputform("System`TeXForm") -def texform_expression_to_outputform_text( - expr: Expression, evaluation: Evaluation, **kwargs -) -> str: - # boxes = format_element(expr.elements[0], evaluation) - boxes = Expression( - Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm - ).evaluate(evaluation) - return boxes.boxes_to_tex() # type: ignore - - @register_outputform("System`TeXForm") def _texform_outputform(expr, evaluation, **kwargs): boxes = Expression( Symbol("System`MakeBoxes"), expr.elements[0], SymbolTraditionalForm ).evaluate(evaluation) - return boxes.boxes_to_tex() # type: ignore[union-attr] + result = boxes.boxes_to_tex(evaluation=evaluation) # type: ignore[union-attr] + return result @register_outputform("System`Times") From 2816f8ed7471ed0ec8b01b1229ea13b4c9939727 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 11 Jan 2026 12:03:51 -0300 Subject: [PATCH 43/49] bracket->square_bracket --- mathics/form/inputform.py | 4 ++-- mathics/form/outputform.py | 4 ++-- mathics/form/util.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mathics/form/inputform.py b/mathics/form/inputform.py index fd4583989..eb5e10f76 100644 --- a/mathics/form/inputform.py +++ b/mathics/form/inputform.py @@ -45,10 +45,10 @@ ARITHMETIC_OPERATOR_STRINGS, BLANKS_TO_STRINGS, _WrongFormattedExpression, - bracket, collect_in_pre_post_arguments, get_operator_str, parenthesize, + square_bracket, ) EXPR_TO_INPUTFORM_TEXT_MAP: Dict[str, Callable] = {} @@ -119,7 +119,7 @@ def _generic_to_inputform_text( while elements: result = result + comma + elements.pop(0) - return head + bracket(result) + return head + square_bracket(result) @register_inputform("System`List") diff --git a/mathics/form/outputform.py b/mathics/form/outputform.py index 86a07e226..50b4245dd 100644 --- a/mathics/form/outputform.py +++ b/mathics/form/outputform.py @@ -43,10 +43,10 @@ PRECEDENCE_POWER, PRECEDENCE_TIMES, _WrongFormattedExpression, - bracket, collect_in_pre_post_arguments, get_operator_str, parenthesize, + square_bracket, text_cells_to_grid, ) @@ -86,7 +86,7 @@ def _default_expression_to_outputform_text( form = kwargs.get("_Form", SymbolOutputForm) if form is SymbolTraditionalForm: return head + parenthesize(result) - return head + bracket(result) + return head + square_bracket(result) def _divide(num, den, evaluation, **kwargs): diff --git a/mathics/form/util.py b/mathics/form/util.py index 8dc6c5a68..1b752e1fc 100644 --- a/mathics/form/util.py +++ b/mathics/form/util.py @@ -55,8 +55,8 @@ class _WrongFormattedExpression(Exception): } -def bracket(expr_str: str) -> str: - """Wrap `expr_str` with square braces""" +def square_bracket(expr_str: str) -> str: + """Wrap `expr_str` with square parenthesis""" return f"[{expr_str}]" From 3cb0831e40815ab4c25ef87b432f2b0b27b1263e Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 11 Jan 2026 12:06:52 -0300 Subject: [PATCH 44/49] more on tex --- mathics/form/outputform.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/mathics/form/outputform.py b/mathics/form/outputform.py index 50b4245dd..418d171f4 100644 --- a/mathics/form/outputform.py +++ b/mathics/form/outputform.py @@ -5,6 +5,7 @@ and text terminals. """ +import re from typing import Callable, Dict, List, Union from mathics.core.atoms import ( @@ -51,6 +52,7 @@ ) EXPR_TO_OUTPUTFORM_TEXT_MAP: Dict[str, Callable] = {} +MULTI_NEWLINE_RE = re.compile(r"\n{2,}") class IsNotGrid(Exception): @@ -733,8 +735,18 @@ def _texform_outputform(expr, evaluation, **kwargs): boxes = Expression( Symbol("System`MakeBoxes"), expr.elements[0], SymbolTraditionalForm ).evaluate(evaluation) - result = boxes.boxes_to_tex(evaluation=evaluation) # type: ignore[union-attr] - return result + try: + tex = boxes.boxes_to_tex(evaluation=evaluation) # type: ignore[union-attr] + tex = MULTI_NEWLINE_RE.sub("\n", tex) + tex = tex.replace(" \uF74c", " \\, d") # tmp hack for Integrate + return tex + except BoxError: + evaluation.message( + "General", + "notboxes", + Expression(SymbolFullForm, boxes).evaluate(evaluation), + ) + raise _WrongFormattedExpression @register_outputform("System`Times") From 0cb34b36c2f9a66dd99e68dec9e639e4fb72f244 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 11 Jan 2026 14:51:18 -0300 Subject: [PATCH 45/49] adjust docstring in square_bracket. Add process_options for OutputForm. Handle TableForm --- mathics/form/inputform.py | 65 +++++++++---------- mathics/form/outputform.py | 128 ++++++++++++++++++++++++++----------- mathics/form/util.py | 45 ++++++++++--- 3 files changed, 158 insertions(+), 80 deletions(-) diff --git a/mathics/form/inputform.py b/mathics/form/inputform.py index eb5e10f76..c531436d5 100644 --- a/mathics/form/inputform.py +++ b/mathics/form/inputform.py @@ -35,6 +35,7 @@ SymbolInputForm, SymbolLeft, SymbolNonAssociative, + SymbolNone, SymbolRight, ) from mathics.eval.makeboxes.formatvalues import do_format # , format_element @@ -158,36 +159,34 @@ def _infix_expression_to_inputform_text( if len(operands) < 2: raise _WrongFormattedExpression - # Process the operands: - parenthesized = group in (None, SymbolRight, SymbolNonAssociative) - for index, operand in enumerate(operands): + # Process the first operand: + parenthesized = group in (SymbolNone, SymbolRight, SymbolNonAssociative) + operand = operands[0] + result = str(render_input_form(operand, evaluation, **kwargs)) + result = parenthesize(precedence, operand, result, parenthesized) + + if group in (SymbolLeft, SymbolRight): + parenthesized = not parenthesized + + # Process the rest of operands + num_ops = len(ops_lst) + for index, operand in enumerate(operands[1:]): + curr_op = ops_lst[index % num_ops] + # In OutputForm we always add the spaces, except for + # " " + if curr_op not in ARITHMETIC_OPERATOR_STRINGS: + curr_op = f" {curr_op} " + operand_txt = str(render_input_form(operand, evaluation, **kwargs)) - cmp_precedence = compare_precedence(operand, precedence) - if cmp_precedence is not None and ( - cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) - ): - operand_txt = parenthesize(operand_txt) - - if index == 0: - result = operand_txt - # After the first element, for lateral - # associativity, parenthesized is flipped: - if group in (SymbolLeft, SymbolRight): - parenthesized = not parenthesized - else: - num_ops = len(ops_lst) - curr_op = ops_lst[(index - 1) % num_ops] - if curr_op not in ARITHMETIC_OPERATOR_STRINGS: - # In the tests, we add spaces just for + and -: - curr_op = f" {curr_op} " - - result = "".join( - ( - result, - curr_op, - operand_txt, - ) + operand_txt = parenthesize(precedence, operand, operand_txt, parenthesized) + + result = "".join( + ( + result, + curr_op, + operand_txt, ) + ) return result @@ -207,10 +206,9 @@ def _prefix_expression_to_inputform_text( raise _WrongFormattedExpression operand = operands[0] kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) - cmp_precedence = compare_precedence(operand, precedence) target_txt = render_input_form(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != 1: - target_txt = parenthesize(target_txt) + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + target_txt = parenthesize(precedence, operand, target_txt, True) return op_head + target_txt @@ -229,10 +227,9 @@ def _postfix_expression_to_inputform_text( if len(operands) != 1: raise _WrongFormattedExpression operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) target_txt = render_input_form(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != 1: - target_txt = parenthesize(target_txt) + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + target_txt = parenthesize(precedence, operand, target_txt, True) return target_txt + op_head diff --git a/mathics/form/outputform.py b/mathics/form/outputform.py index 418d171f4..0e81f8727 100644 --- a/mathics/form/outputform.py +++ b/mathics/form/outputform.py @@ -22,20 +22,25 @@ from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.symbols import Atom, Symbol, SymbolTimes +from mathics.core.symbols import Atom, Symbol, SymbolList, SymbolTimes from mathics.core.systemsymbols import ( SymbolDerivative, + SymbolGrid, + SymbolInfinity, SymbolInfix, SymbolLeft, SymbolNonAssociative, + SymbolNone, SymbolOutputForm, SymbolPower, SymbolRight, SymbolStandardForm, + SymbolTableForm, SymbolTraditionalForm, ) from mathics.eval.makeboxes import compare_precedence, do_format # , format_element from mathics.eval.makeboxes.numberform import get_baseform_elements +from mathics.eval.testing_expressions import expr_min from mathics.settings import SYSTEM_CHARACTER_ENCODING from .util import ( @@ -47,6 +52,7 @@ collect_in_pre_post_arguments, get_operator_str, parenthesize, + process_options, square_bracket, text_cells_to_grid, ) @@ -87,7 +93,7 @@ def _default_expression_to_outputform_text( form = kwargs.get("_Form", SymbolOutputForm) if form is SymbolTraditionalForm: - return head + parenthesize(result) + return head + f"({result})" return head + square_bracket(result) @@ -298,37 +304,34 @@ def _infix_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs) - if len(operands) < 2: raise _WrongFormattedExpression - # Process the operands: - parenthesized = group in (None, SymbolRight, SymbolNonAssociative) - for index, operand in enumerate(operands): + # Process the first operand: + parenthesized = group in (SymbolNone, SymbolRight, SymbolNonAssociative) + operand = operands[0] + result = str(expression_to_outputform_text(operand, evaluation, **kwargs)) + result = parenthesize(precedence, operand, result, parenthesized) + + if group in (SymbolLeft, SymbolRight): + parenthesized = not parenthesized + + # Process the rest of operands + num_ops = len(ops_lst) + for index, operand in enumerate(operands[1:]): + curr_op = ops_lst[index % num_ops] + # In OutputForm we always add the spaces, except for + # " " + if curr_op != " ": + curr_op = f" {curr_op} " + operand_txt = str(expression_to_outputform_text(operand, evaluation, **kwargs)) - cmp_precedence = compare_precedence(operand, precedence) - if cmp_precedence is not None and ( - cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) - ): - operand_txt = parenthesize(operand_txt) - - if index == 0: - result = operand_txt - # After the first element, for lateral - # associativity, parenthesized is flipped: - if group in (SymbolLeft, SymbolRight): - parenthesized = not parenthesized - else: - num_ops = len(ops_lst) - curr_op = ops_lst[(index - 1) % num_ops] - # In OutputForm we always add the spaces, except for - # " " - if curr_op != " ": - curr_op = f" {curr_op} " - - result = "".join( - ( - result, - curr_op, - operand_txt, - ) + operand_txt = parenthesize(precedence, operand, operand_txt, parenthesized) + + result = "".join( + ( + result, + curr_op, + operand_txt, ) + ) return result @@ -530,10 +533,9 @@ def _prefix_output_text(expr: Expression, evaluation: Evaluation, **kwargs) -> s raise _WrongFormattedExpression operand = operands[0] kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) - cmp_precedence = compare_precedence(operand, precedence) target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != 1: - target_txt = parenthesize(target_txt) + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + target_txt = parenthesize(precedence, operand, target_txt, True) return op_head + target_txt @@ -550,10 +552,9 @@ def _postfix_output_text(expr: Expression, evaluation: Evaluation, **kwargs) -> if len(operands) != 1: raise _WrongFormattedExpression operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != 1: - target_txt = parenthesize(target_txt) + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + target_txt = parenthesize(precedence, operand, target_txt, True) return target_txt + op_head @@ -600,6 +601,15 @@ def rule_to_outputform_text(expr, evaluation: Evaluation, **kwargs): return f"{pat} {op_str} {rule}" +@register_outputform("System`SequenceForm") +def sequenceform_to_outputform_text(expr, evaluation: Evaluation, **kwargs): + """Row[{...}]""" + elements = expr.elements + return "".join( + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements + ) + + @register_outputform("System`Slot") def _slot_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs): elements = expr.elements @@ -716,6 +726,14 @@ def stringform_expression_to_outputform_text( return result +@register_outputform("System`Style") +def style_to_outputform_text(expr: String, evaluation: Evaluation, **kwargs) -> str: + elements = expr.elements + if not elements: + raise _WrongFormattedExpression + return expression_to_outputform_text(elements[0], evaluation, **kwargs) + + @register_outputform("System`Symbol") def symbol_expression_to_outputform_text( symb: Symbol, evaluation: Evaluation, **kwargs @@ -727,7 +745,41 @@ def symbol_expression_to_outputform_text( def tableform_expression_to_outputform_text( expr: Expression, evaluation: Evaluation, **kwargs ) -> str: - return grid_expression_to_outputform_text(expr, evaluation, **kwargs) + from mathics.builtin.tensors import get_dimensions + + elements = expr.elements + + if len(elements) == 0: + raise _WrongFormattedExpression + + table, *opts = elements + dims = len(get_dimensions(table, head=SymbolList)) + process_options(kwargs, opts) + depth = expr_min((Integer(dims), kwargs.pop("TableDepth", SymbolInfinity))).value + if depth is None: + evaluation.message(self.get_name(), "int") + raise _WrongFormattedExpression + if depth <= 0: + return expression_to_outputform_text(table, evaluation, **kwargs) + if depth == 1: + return text_cells_to_grid( + [ + [expression_to_outputform_text(elem, evaluation, **kwargs)] + for elem in table.elements + ] + ) + kwargs["TableDepth"] = Integer(depth - 2) + + def transform_item(item): + if depth > 2: + return tableform_expression_to_outputform_text( + Expression(SymbolTableForm, item), evaluation, **kwargs + ) + else: + return expression_to_outputform_text(item, evaluation, **kwargs) + + grid_array = [[transform_item(elem) for elem in row] for row in table.elements] + return text_cells_to_grid(grid_array) @register_outputform("System`TeXForm") diff --git a/mathics/form/util.py b/mathics/form/util.py index 1b752e1fc..7e7c24bff 100644 --- a/mathics/form/util.py +++ b/mathics/form/util.py @@ -23,6 +23,7 @@ SymbolPrefix, SymbolRight, ) +from mathics.eval.makeboxes import compare_precedence # This Exception if the expression should @@ -55,11 +56,6 @@ class _WrongFormattedExpression(Exception): } -def square_bracket(expr_str: str) -> str: - """Wrap `expr_str` with square parenthesis""" - return f"[{expr_str}]" - - def collect_in_pre_post_arguments( expr: Expression, evaluation: Evaluation, **kwargs ) -> Tuple[list, str | List[str], int, Optional[Symbol]]: @@ -142,9 +138,42 @@ def get_operator_str(head, evaluation, **kwargs) -> str: return operator -def parenthesize(expr_str: str) -> str: - """Wrap `expr_str` with parenthesis""" - return f"({expr_str})" +def parenthesize( + precedence: Optional[int], + element: Expression, + element_str, + when_equal: bool, +) -> Expression: + """ + "Add parenthesis to ``element_str`` according to the precedence of + ``element``. + + If when_equal is True, parentheses will be added if the two + precedence values are equal. + """ + cmp = compare_precedence(element, precedence) + if cmp is not None and (cmp == -1 or cmp == 0 and when_equal): + return f"({element_str})" + return element_str + + +def process_options(opts: dict, rules: Tuple): + for opt in rules: + if not opt.has_form(["Rule", "RuleDelayed"], 2): + raise _WrongFormattedExpression + opt_symb, opt_val = opt.elements + if isinstance(opt_symb, Symbol): + opts[opt_symb.get_name(short=True)] = opt_val + elif isinstance(opt_symb, String): + opts[opt_symb.value] = opt_val + else: + raise _WrongFormattedExpression + return + + +def square_bracket(expr_str: str) -> str: + """Wrap `expr_str` with square brackets""" + return f"[{expr_str}]" def text_cells_to_grid(cells: List, **kwargs): From 36692746d1f4ad22cfc0d0ab507d27ccd9a7a9f9 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 11 Jan 2026 15:55:46 -0300 Subject: [PATCH 46/49] improve StringForm implementation --- mathics/builtin/forms/data.py | 36 +++------------ mathics/builtin/list/predicates.py | 25 +++------- mathics/builtin/numbers/calculus.py | 6 +-- mathics/eval/strings.py | 72 ++++++++++++++++++++++++++++- test/builtin/list/test_list.py | 5 +- 5 files changed, 87 insertions(+), 57 deletions(-) diff --git a/mathics/builtin/forms/data.py b/mathics/builtin/forms/data.py index aff1800c1..efa40e360 100644 --- a/mathics/builtin/forms/data.py +++ b/mathics/builtin/forms/data.py @@ -35,7 +35,7 @@ eval_baseform, eval_tableform, ) -from mathics.eval.strings import eval_ToString +from mathics.eval.strings import eval_StringForm_MakeBoxes, eval_ToString class BaseForm(FormBaseClass): @@ -434,40 +434,16 @@ class StringForm(FormBaseClass): in_outputforms = False in_printforms = False + messages = { + "sfr": 'Item `1` requested in "`3`" out of range; `2` items available.', + "sfq": "Unmatched backquote in `1`.", + } summary_text = "format a string from a template and a list of parameters" def eval_makeboxes(self, s, args, form, evaluation): """MakeBoxes[StringForm[s_String, args___], form:StandardForm|TraditionalForm|OutputForm]""" - - s = s.value - args = args.get_sequence() - result = [] - pos = 0 - last_index = 0 - for match in re.finditer(r"(\`(\d*)\`)", s): - start, end = match.span(1) - if match.group(2): - index = int(match.group(2)) - else: - index = last_index + 1 - last_index = max(index, last_index) - if start > pos: - result.append(to_boxes(String(s[pos:start]), evaluation)) - pos = end - if 1 <= index <= len(args): - arg = args[index - 1] - result.append( - to_boxes(MakeBoxes(arg, form).evaluate(evaluation), evaluation) - ) - if pos < len(s): - result.append(to_boxes(String(s[pos:]), evaluation)) - return RowBox( - *tuple( - r.evaluate(evaluation) if isinstance(r, EvalMixin) else r - for r in result - ) - ) + return eval_StringForm_MakeBoxes(s, args.get_sequence(), form, evaluation) class TableForm(FormBaseClass): diff --git a/mathics/builtin/list/predicates.py b/mathics/builtin/list/predicates.py index f7b3a6b84..f80cf84c0 100644 --- a/mathics/builtin/list/predicates.py +++ b/mathics/builtin/list/predicates.py @@ -51,18 +51,8 @@ class ContainsOnly(Builtin): summary_text = "test if all the elements of a list appears into another list" - def check_options(self, expr, evaluation, options): - for key in options: - if key != "System`SameTest": - if expr is None: - evaluation.message("ContainsOnly", "optx", Symbol(key)) - else: - evaluation.message("ContainsOnly", "optx", Symbol(key), expr) - - return None - def eval(self, list1, list2, evaluation, options={}): - "ContainsOnly[list1_List, list2_List, OptionsPattern[ContainsOnly]]" + "ContainsOnly[list1_List, list2_List, OptionsPattern[]]" same_test = self.get_option(options, "SameTest", evaluation) @@ -71,31 +61,28 @@ def sameQ(a, b) -> bool: result = Expression(same_test, a, b).evaluate(evaluation) return result is SymbolTrue - self.check_options(None, evaluation, options) for a in list1.elements: if not any(sameQ(a, b) for b in list2.elements): return SymbolFalse return SymbolTrue - def eval_msg(self, e1, e2, evaluation, options={}): - "ContainsOnly[e1_, e2_, OptionsPattern[ContainsOnly]]" - + def eval_msg(self, e1, e2, evaluation, expression, options={}): + "expression:(ContainsOnly[e1_, e2_, OptionsPattern[]])" opts = ( options_to_rules(options) if len(options) <= 1 else [ListExpression(*options_to_rules(options))] ) - expr = Expression(SymbolContainsOnly, e1, e2, *opts) if not isinstance(e1, Symbol) and not e1.has_form("List", None): evaluation.message("ContainsOnly", "lsa", e1) - return self.check_options(expr, evaluation, options) + return expression if not isinstance(e2, Symbol) and not e2.has_form("List", None): evaluation.message("ContainsOnly", "lsa", e2) - return self.check_options(expr, evaluation, options) + return expression - return self.check_options(expr, evaluation, options) + return expression # TODO: ContainsAll, ContainsNone ContainsAny ContainsExactly diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index 1f9c3bcc0..3bed82a6d 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -1410,9 +1410,9 @@ class NIntegrate(Builtin): messages.update( { "bdmtd": "The Method option should be a " - + "built-in method name in {`" - + "`, `".join(list(methods)) - + "`}. Using `Automatic`" + + "built-in method name in {\\[RawBackquote]" + + "\\[RawBackquote], \\[RawBackquote]".join(list(methods)) + + "\\[RawBackquote]}. Using \\[RawBackquote]Automatic\\[RawBackquote]" } ) diff --git a/mathics/eval/strings.py b/mathics/eval/strings.py index 96a73a2c4..f63f118cf 100644 --- a/mathics/eval/strings.py +++ b/mathics/eval/strings.py @@ -3,7 +3,8 @@ """ import re -from mathics.core.atoms import Integer1, Integer3, String +from mathics.builtin.box.layout import RowBox +from mathics.core.atoms import Integer, Integer0, Integer1, Integer3, String from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_bool from mathics.core.convert.regex import to_regex @@ -139,3 +140,72 @@ def convert_rule(r): ) else: return self._find(py_strings, py_rules, py_n, flags, evaluation) + + +def eval_StringForm_MakeBoxes(strform, items, form, evaluation): + """MakeBoxes[StringForm[s_String, items___], form_]""" + + if not isinstance(strform, String): + raise _WrongFormattedExpression + + items = [format_element(item, evaluation, form) for item in items] + + curr_indx = 0 + parts = strform.value.split("`") + parts = [part.replace("\\[RawBackquote]", "`") for part in parts] + result = [String(parts[0])] + if len(parts) <= 1: + return result[0] + + quote_open = True + remaining = len(parts) - 1 + num_items = len(items) + for part in parts[1:]: + remaining -= 1 + # If quote_open, the part must be a placeholder + if quote_open: + # If not remaining, there is a not closed '`' + # character: + if not remaining: + evaluation.message("StringForm", "sfq", strform) + return strform.value + # part must be an index or an empty string. + # If is an empty string, pick the next element: + if part == "": + if curr_indx >= num_items: + evaluation.message( + "StringForm", + "sfr", + Integer(num_items + 1), + Integer(num_items), + strform, + ) + return strform.value + result.append(items[curr_indx]) + curr_indx += 1 + quote_open = False + continue + # Otherwise, must be a positive integer: + try: + indx = int(part) + except ValueError: + evaluation.message( + "StringForm", "sfr", Integer0, Integer(num_items), strform + ) + return strform.value + # indx must be greater than 0, and not greater than + # the number of items + if indx <= 0 or indx > len(items): + evaluation.message( + "StringForm", "sfr", Integer(indx), Integer(len(items)), strform + ) + return strform.value + result.append(items[indx - 1]) + curr_indx = indx + quote_open = False + continue + + result.append(String(part)) + quote_open = True + + return RowBox(ListExpression(*result)) diff --git a/test/builtin/list/test_list.py b/test/builtin/list/test_list.py index ddbcff8ff..dc6c07ac5 100644 --- a/test/builtin/list/test_list.py +++ b/test/builtin/list/test_list.py @@ -172,10 +172,7 @@ def test_rearrange_private_doctests( ), ( "ContainsOnly[{c, a}, {a, b, c}, IgnoreCase -> True]", - ( - "Unknown option IgnoreCase -> True in ContainsOnly.", - "Unknown option IgnoreCase in .", - ), + ("Unknown option IgnoreCase -> True in ContainsOnly.",), "True", None, ), From 1f0c8339ff5af4886eca410cb6196a9e76f67df8 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 12 Jan 2026 09:35:57 -0300 Subject: [PATCH 47/49] adding tests. Handlind escaped backquotes. --- mathics/builtin/numbers/calculus.py | 6 +-- mathics/eval/strings.py | 17 +++++- test/builtin/test_forms.py | 84 +++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index 3bed82a6d..4ea600ca8 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -1410,9 +1410,9 @@ class NIntegrate(Builtin): messages.update( { "bdmtd": "The Method option should be a " - + "built-in method name in {\\[RawBackquote]" - + "\\[RawBackquote], \\[RawBackquote]".join(list(methods)) - + "\\[RawBackquote]}. Using \\[RawBackquote]Automatic\\[RawBackquote]" + + r"built-in method name in {\`" + + r"\`, \`".join(list(methods)) + + r"\`}. Using \`Automatic\`." } ) diff --git a/mathics/eval/strings.py b/mathics/eval/strings.py index f63f118cf..ae57acebe 100644 --- a/mathics/eval/strings.py +++ b/mathics/eval/strings.py @@ -142,6 +142,19 @@ def convert_rule(r): return self._find(py_strings, py_rules, py_n, flags, evaluation) +def safe_backquotes(string: str): + """Handle escaped backquotes.""" + # TODO: Fix in the scanner how escaped backslashes + # are parsed. + # "\\`" must be parsed as "\\`" in order this + # works properly, but the parser converts `\\` + # into `\`. + string = string.replace(r"\\", r"\[RawBackslash]") + string = string.replace(r"\`", r"\[RawBackquote]") + string = string.replace(r"\[RawBackslash]", r"\\") + return string + + def eval_StringForm_MakeBoxes(strform, items, form, evaluation): """MakeBoxes[StringForm[s_String, items___], form_]""" @@ -151,7 +164,9 @@ def eval_StringForm_MakeBoxes(strform, items, form, evaluation): items = [format_element(item, evaluation, form) for item in items] curr_indx = 0 - parts = strform.value.split("`") + strform_str = safe_backquotes(strform.value) + + parts = strform_str.split("`") parts = [part.replace("\\[RawBackquote]", "`") for part in parts] result = [String(parts[0])] if len(parts) <= 1: diff --git a/test/builtin/test_forms.py b/test/builtin/test_forms.py index 25a1d6d88..b5c9d6577 100644 --- a/test/builtin/test_forms.py +++ b/test/builtin/test_forms.py @@ -407,3 +407,87 @@ def test_private_doctests_output(str_expr, msgs, str_expected, fail_msg): failure_message=fail_msg, expected_messages=msgs, ) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "fail_msg", "msgs"), + [ + ( + 'StringForm["This is symbol ``.", A]', + '"This is symbol A."', + "empty placeholder", + None, + ), + ( + 'StringForm["This is symbol `1`.", A]', + '"This is symbol A."', + "numerated placeholder", + None, + ), + ( + 'StringForm["This is symbol `0`.", A]', + 'StringForm["This is symbol `0`.", A]', + "placeholder index must be positive", + ( + 'Item 0 requested in "This is symbol `0`." out of range; 1 items available.', + ), + ), + ( + 'StringForm["This is symbol `symbol`.", A]', + 'StringForm["This is symbol `symbol`.", A]', + "placeholder must be an integer", + ( + 'Item 0 requested in "This is symbol `symbol`." out of range; 1 items available.', + ), + ), + ( + 'StringForm["This is symbol `5`.", A]', + 'StringForm["This is symbol `5`.", A]', + "placeholder index too large", + ( + 'Item 5 requested in "This is symbol `5`." out of range; 1 items available.', + ), + ), + ( + 'StringForm["This is symbol `2`, then `1`.", A, B]', + '"This is symbol B, then A."', + "numerated placeholder", + None, + ), + ( + 'StringForm["This is symbol `1`, then ``.", A, B]', + '"This is symbol A, then B."', + "empty placeholder use the next avaliable entry.", + None, + ), + ( + 'StringForm["This is symbol `2`, then ``.", A, B]', + 'StringForm["This is symbol `2`, then ``.", A, B]', + "no more available entry.", + ( + 'Item 3 requested in "This is symbol `2`, then ``." out of range; 2 items available.', + ), + ), + ( + 'StringForm["This is symbol `.", A]', + 'StringForm["This is symbol `.", A]', + "Unbalanced", + ("Unmatched backquote in This is symbol `..",), + ), + ( + r'StringForm["This is symbol \`.", A]', + '"This is symbol `."', + "literal backquote", + None, + ), + ], +) +def test_stringform(str_expr, str_expected, fail_msg, msgs): + """ """ + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + failure_message=fail_msg, + expected_messages=msgs, + ) From 7d635f98f153b98446b7e84d3643707a72d3dc85 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 12 Jan 2026 09:41:54 -0300 Subject: [PATCH 48/49] another trailing dot --- test/builtin/numbers/test_nintegrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/builtin/numbers/test_nintegrate.py b/test/builtin/numbers/test_nintegrate.py index 8e9dee43d..369a5700d 100644 --- a/test/builtin/numbers/test_nintegrate.py +++ b/test/builtin/numbers/test_nintegrate.py @@ -46,7 +46,7 @@ "1.", None, [ - r"The Method option should be a built-in method name in {`Automatic`, `Internal`, `Simpson`, `NQuadrature`, `Quadrature`}. Using `Automatic`" + r"The Method option should be a built-in method name in {`Automatic`, `Internal`, `Simpson`, `NQuadrature`, `Quadrature`}. Using `Automatic`." ], ), ], @@ -62,7 +62,7 @@ "1.", None, [ - r"The Method option should be a built-in method name in {`Automatic`, `Internal`, `Simpson`}. Using `Automatic`" + r"The Method option should be a built-in method name in {`Automatic`, `Internal`, `Simpson`}. Using `Automatic`." ], ), ] From 379fd012e3fe6db6ce128d718b49b64838984a89 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Tue, 13 Jan 2026 16:15:59 -0300 Subject: [PATCH 49/49] NumberForm refactor --- mathics/builtin/forms/data.py | 484 +++++++++++++++------ mathics/core/atoms/numerics.py | 8 +- mathics/eval/makeboxes/__init__.py | 9 +- mathics/eval/makeboxes/numberform.py | 622 ++++++++++++++++++--------- test/builtin/test_forms.py | 51 ++- test/builtin/test_number_form.py | 40 +- 6 files changed, 842 insertions(+), 372 deletions(-) diff --git a/mathics/builtin/forms/data.py b/mathics/builtin/forms/data.py index 374209721..6273843de 100644 --- a/mathics/builtin/forms/data.py +++ b/mathics/builtin/forms/data.py @@ -10,33 +10,35 @@ which are intended to work over all kinds of data. """ import re +from typing import Any, Callable, Dict, List, Optional from mathics.builtin.box.layout import RowBox, to_boxes from mathics.builtin.forms.base import FormBaseClass from mathics.builtin.makeboxes import MakeBoxes from mathics.core.atoms import Integer, Real, String from mathics.core.builtin import Builtin -from mathics.core.element import EvalMixin +from mathics.core.element import BaseElement, EvalMixin from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.number import dps -from mathics.core.symbols import Symbol, SymbolFalse, SymbolNull, SymbolTrue +from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolNull, SymbolTrue from mathics.core.systemsymbols import ( SymbolAutomatic, SymbolInfinity, SymbolMakeBoxes, - SymbolNumberForm, SymbolRowBox, - SymbolRuleDelayed, SymbolSuperscriptBox, ) from mathics.eval.makeboxes import ( - NumberForm_to_String, StringLParen, StringRParen, eval_baseform, + eval_generic_makeboxes, eval_tableform, + format_element, + get_numberform_parameters, + numberform_to_boxes, ) from mathics.eval.strings import eval_ToString @@ -96,7 +98,7 @@ class BaseForm(FormBaseClass): def eval_makeboxes(self, expr, n, f, evaluation: Evaluation): """MakeBoxes[BaseForm[expr_, n_], f:StandardForm|TraditionalForm|OutputForm]""" - return eval_baseform(self, expr, n, f, evaluation) + return eval_baseform(expr, n, f, evaluation) class _NumberForm(Builtin): @@ -108,32 +110,78 @@ class _NumberForm(Builtin): default_NumberFormat = None in_outputforms = True messages = { - "npad": "Value for option NumberPadding -> `1` should be a string or a pair of strings.", - "dblk": "Value for option DigitBlock should be a positive integer, Infinity, or a pair of positive integers.", + "argm": ("`` called with `` arguments; 1 or more " "arguments are expected."), + "argct": "`` called with `` arguments.", + "npad": ( + "Value for option NumberPadding -> `1` should be a string or " + "a pair of strings." + ), + "dblk": ( + "Value for option DigitBlock should be a positive integer, " + "Infinity, or a pair of positive integers." + ), + "estep": "Value of option `1` -> `2` is not a positive integer.", + "iprf": ( + "Formatting specification `1` should be a positive integer " + "or a pair of positive integers." + ), # NumberFormat only "npt": "Value for option `1` -> `2` is expected to be a string.", - "nsgn": "Value for option NumberSigns -> `1` should be a pair of strings or two pairs of strings.", - "nspr": "Value for option NumberSeparator -> `1` should be a string or a pair of strings.", + "nsgn": ( + "Value for option NumberSigns -> `1` should be a pair of " + "strings or two pairs of strings." + ), + "nspr": ( + "Value for option NumberSeparator -> `1` should be a string " + "or a pair of strings." + ), "opttf": "Value of option `1` -> `2` should be True or False.", - "estep": "Value of option `1` -> `2` is not a positive integer.", - "iprf": "Formatting specification `1` should be a positive integer or a pair of positive integers.", # NumberFormat only - "sigz": "In addition to the number of digits requested, one or more zeros will appear as placeholders.", + "sigz": ( + "In addition to the number of digits requested, one or more " + "zeros will appear as placeholders." + ), } - def check_options(self, options: dict, evaluation: Evaluation): + def check_and_convert_options(self, options: dict, evaluation: Evaluation): """ Checks options are valid and converts them to python. """ result = {} + default_options = evaluation.definitions.get_options(self.get_name()) for option_name in self.options: + context_option_name = "System`" + option_name method = getattr(self, "check_" + option_name) - arg = options["System`" + option_name] + arg = options[context_option_name] value = method(arg, evaluation) - if value is None: - return None + if value is not None: + result[option_name] = value + continue + # If the value is None, try with the default value + arg = default_options[context_option_name] + value = method(arg, evaluation) + # If fails, handle None in situ. result[option_name] = value + return result - def check_DigitBlock(self, value, evaluation: Evaluation): + def check_DigitBlock(self, value, evaluation: Evaluation) -> Optional[List[int]]: + """ + Check and convert to Python the DigitBlock option value. + + Parameters + ---------- + value : BaseElement + The value of the option. + evaluation : Evaluation + used for messages. + + Returns + ------- + Optional[List[int]] + If the specification is valid, a list with + two elements specifying the size of the blocks + at the left and right of the decimal separator. `None` otherwise. + + """ py_value = value.get_int_value() if value.sameQ(SymbolInfinity): return [0, 0] @@ -158,8 +206,29 @@ def check_DigitBlock(self, value, evaluation: Evaluation): if None not in result: return result evaluation.message(self.get_name(), "dblk", value) + return None + + def check_ExponentFunction( + self, value: BaseElement, evaluation: Evaluation + ) -> Callable[BaseElement, BaseElement]: + """ + Check and convert the ExponentFunction option value + + Parameters + ---------- + value : BaseElement + Automatic, or a Function to be applyied to the expression to + format the exponent. + evaluation : Evaluation + evaluation object to send messages. + + Returns + ------- + Callable[BaseElement, BaseElement] + A Python function that implements the format. + + """ - def check_ExponentFunction(self, value, evaluation: Evaluation): if value.sameQ(SymbolAutomatic): return self.default_ExponentFunction @@ -168,60 +237,251 @@ def exp_function(x): return exp_function - def check_NumberFormat(self, value, evaluation: Evaluation): + def check_NumberFormat( + self, value: BaseElement, evaluation: Evaluation + ) -> Callable[BaseElement, BaseElement]: + """ + Function that implement custumozed number formatting. + + Parameters + ---------- + value : BaseElement + Automatic, or a function to be applied to the expression to get + it formatted. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Callable[BaseElement, BaseElement] + A function that implements the formatting. + + """ if value.sameQ(SymbolAutomatic): return self.default_NumberFormat - def num_function(man, base, exp, options): + def num_function(man, base, exp, _): return Expression(value, man, base, exp).evaluate(evaluation) return num_function - def check_NumberMultiplier(self, value, evaluation: Evaluation): + def check_NumberMultiplier( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[str]: + """ + Character used when two numbers are multiplied. Used in Scientific + notation. + + Parameters + ---------- + value : BaseElement + Value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[str] + If the value is valid, the value of the option. `None` otherwise. + + """ result = value.get_string_value() if result is None: evaluation.message(self.get_name(), "npt", "NumberMultiplier", value) return result - def check_NumberPoint(self, value, evaluation: Evaluation): + def check_NumberPoint( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[str]: + """ + The decimal separator + + Parameters + ---------- + value : BaseElement + Option value. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[str] + If the value is valid, the value of the option. `None` otherwise. + + """ result = value.get_string_value() if result is None: evaluation.message(self.get_name(), "npt", "NumberPoint", value) return result - def check_ExponentStep(self, value, evaluation: Evaluation): + def check_ExponentStep( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[int]: + """ + The round step for exponents in Scientific notation. This number + decides for example if format 10000 as "10x10^3" or "1x10^4" + + Parameters + ---------- + value : BaseElemenet + The value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[int] + If the value is valid, the value of the option. `None` otherwise. + + """ result = value.get_int_value() if result is None or result <= 0: evaluation.message(self.get_name(), "estep", "ExponentStep", value) - return + return None return result - def check_SignPadding(self, value, evaluation: Evaluation): + def check_SignPadding( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[bool]: + """ + True if the left padding is used between the sign of the number of + its magnitude. False otherwise. + + Parameters + ---------- + value : BaseElement + The value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[bool] + If the value is valid, the value of the option. `None` otherwise. + + """ if value.sameQ(SymbolTrue): return True - elif value.sameQ(SymbolFalse): + if value.sameQ(SymbolFalse): return False evaluation.message(self.get_name(), "opttf", value) + return None - def _check_List2str(self, value, msg, evaluation: Evaluation): + def _check_List2str( + self, value, msg, evaluation: Evaluation + ) -> Optional[List[str]]: if value.has_form("List", 2): result = [element.get_string_value() for element in value.elements] if None not in result: return result evaluation.message(self.get_name(), msg, value) + return None + + def check_NumberSigns( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[List[str]]: + """ + + Parameters + ---------- + value : BaseElement + The value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[bool] + If the value is valid, the value of the option. `None` otherwise. + + """ - def check_NumberSigns(self, value, evaluation: Evaluation): return self._check_List2str(value, "nsgn", evaluation) - def check_NumberPadding(self, value, evaluation: Evaluation): + def check_NumberPadding( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[List[str]]: + """ + + Parameters + ---------- + value : BaseElement + The value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[bool] + If the value is valid, the value of the option. `None` otherwise. + + """ + return self._check_List2str(value, "npad", evaluation) - def check_NumberSeparator(self, value, evaluation: Evaluation): + def check_NumberSeparator( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[List[str]]: + """ + + Parameters + ---------- + value : BaseElement + The value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[bool] + If the value is valid, the value of the option. `None` otherwise. + + """ + py_str = value.get_string_value() if py_str is not None: return [py_str, py_str] return self._check_List2str(value, "nspr", evaluation) + def eval_number_makeboxes_nonatomic(self, expr, form, evaluation): + """MakeBoxes[expr_%(name)s, form_]""" + # Generic form. If parameters are OK, + # distribute the form on the head and elements. + # If the expression is an Atom, leave it alone. + # First, collect the parts of the expression + num_form = expr.head + try: + target, prec_parms, _ = get_numberform_parameters(expr, evaluation) + except ValueError: + return eval_generic_makeboxes(expr, form, evaluation) + + # Atoms are not processed here + if isinstance(target, Atom): + return None + + # This part broadcast the format to the head and elements + # of the expression. In the future, this is going to happend + # by passing parameters to a makeboxes evaluation function. + + if prec_parms is None: + option_rules = expr.elements[1:] + + def wrapper(elem): + return Expression(num_form, elem, *option_rules) + + else: + option_rules = expr.elements[2:] + + def wrapper(elem): + return Expression(num_form, elem, prec_parms, *option_rules) + + head = target.head + if not isinstance(head, Symbol): + head = wrapper(target.head) + elements = (wrapper(elem) for elem in target.elements) + expr = Expression(head, *elements) + return format_element(expr, evaluation, form) + class NumberForm(_NumberForm): """ @@ -260,18 +520,46 @@ class NumberForm(_NumberForm): "NumberSigns": '{"-", ""}', "SignPadding": "False", } - summary_text = "format expression to at most a number of digits of all approximate real numbers " + summary_text = ( + "format expression to at most a number of digits of all " + "approximate real numbers " + ) @staticmethod - def default_ExponentFunction(value): + def default_ExponentFunction(value: Integer): + """The default function used to format exponent.""" + n = value.get_int_value() if -5 <= n <= 5: return SymbolNull - else: - return value + + return value @staticmethod - def default_NumberFormat(man, base, exp, options): + def default_NumberFormat( + man: BaseElement, base: BaseElement, exp: BaseElement, options: Dict[str, Any] + ) -> BaseElement: + """ + The default function used to format numbers from its mantisa, and + an exponential factor base^exp. + + Parameters + ---------- + man : BaseElement + mantisa. + base : BaseElement + base used for scientific notation. + exp : BaseElement + exponent. + options : Dict[str, Any] + more format options. + + Returns + ------- + Expression + An valid box expression representing the number. + """ + py_exp = exp.get_string_value() if py_exp: mul = String(options["NumberMultiplier"]) @@ -279,101 +567,44 @@ def default_NumberFormat(man, base, exp, options): SymbolRowBox, ListExpression(man, mul, Expression(SymbolSuperscriptBox, base, exp)), ) - else: - return man - - def eval_list_n(self, expr, n, evaluation, options) -> Expression: - "NumberForm[expr_List, n_, OptionsPattern[NumberForm]]" - options = [ - Expression(SymbolRuleDelayed, Symbol(key), value) - for key, value in options.items() - ] - return ListExpression( - *[ - Expression(SymbolNumberForm, element, n, *options) - for element in expr.elements - ] - ) - def eval_list_nf(self, expr, n, f, evaluation, options) -> Expression: - "NumberForm[expr_List, {n_, f_}, OptionsPattern[NumberForm]]" - options = [ - Expression(SymbolRuleDelayed, Symbol(key), value) - for key, value in options.items() - ] - return ListExpression( - *[ - Expression(SymbolNumberForm, element, ListExpression(n, f), *options) - for element in expr.elements - ], - ) + return man - def eval_makeboxes(self, expr, form, evaluation, options={}): - """MakeBoxes[NumberForm[expr_, OptionsPattern[NumberForm]], + def eval_makeboxes(self, fexpr, form, evaluation): + """MakeBoxes[fexpr:NumberForm[_?AtomQ, ___], form:StandardForm|TraditionalForm|OutputForm]""" - - fallback = Expression(SymbolMakeBoxes, expr, form) - - py_options = self.check_options(options, evaluation) - if py_options is None: - return fallback - - if isinstance(expr, Integer): - py_n = len(str(abs(expr.get_int_value()))) - elif isinstance(expr, Real): - if expr.is_machine_precision(): - py_n = 6 - else: - py_n = dps(expr.get_precision()) - else: - py_n = None + try: + target, prec_parms, py_options = get_numberform_parameters( + fexpr, evaluation + ) + except ValueError: + return eval_generic_makeboxes(fexpr, form, evaluation) + + assert all(isinstance(key, str) for key in py_options) + + py_f = py_n = None + if prec_parms is None: + if isinstance(target, Integer): + py_n = len(str(abs(target.get_int_value()))) + elif isinstance(target, Real): + if target.is_machine_precision(): + py_n = 6 + else: + py_n = dps(target.get_precision()) + elif isinstance(prec_parms, Integer): + if isinstance(target, (Integer, Real)): + py_n = prec_parms.value + elif prec_parms.has_form("List", 2): + if isinstance(target, (Integer, Real)): + n, f = prec_parms.elements + py_n = n.value + py_f = f.value if py_n is not None: py_options["_Form"] = form.get_name() - return NumberForm_to_String(expr, py_n, None, evaluation, py_options) - return Expression(SymbolMakeBoxes, expr, form) - - def eval_makeboxes_n(self, expr, n, form, evaluation, options={}): - """MakeBoxes[NumberForm[expr_, n_?NotOptionQ, OptionsPattern[NumberForm]], - form:StandardForm|TraditionalForm|OutputForm]""" - fallback = Expression(SymbolMakeBoxes, expr, form) - - py_n = n.get_int_value() - if py_n is None or py_n <= 0: - evaluation.message("NumberForm", "iprf", n) - return fallback - - py_options = self.check_options(options, evaluation) - if py_options is None: - return fallback - - if isinstance(expr, (Integer, Real)): - py_options["_Form"] = form.get_name() - return NumberForm_to_String(expr, py_n, None, evaluation, py_options) - return Expression(SymbolMakeBoxes, expr, form) - - def eval_makeboxes_nf(self, expr, n, f, form, evaluation, options={}): - """MakeBoxes[NumberForm[expr_, {n_, f_}, OptionsPattern[NumberForm]], - form:StandardForm|TraditionalForm|OutputForm]""" - - fallback = Expression(SymbolMakeBoxes, expr, form) - - nf = ListExpression(n, f) - py_n = n.get_int_value() - py_f = f.get_int_value() - if py_n is None or py_n <= 0 or py_f is None or py_f < 0: - evaluation.message("NumberForm", "iprf", nf) - return fallback - - py_options = self.check_options(options, evaluation) - if py_options is None: - return fallback - - if isinstance(expr, (Integer, Real)): - py_options["_Form"] = form.get_name() - return NumberForm_to_String(expr, py_n, py_f, evaluation, py_options) - return Expression(SymbolMakeBoxes, expr, form) + return numberform_to_boxes(target, py_n, py_f, evaluation, py_options) + return Expression(SymbolMakeBoxes, target, form) class SequenceForm(FormBaseClass): @@ -553,8 +784,7 @@ def eval_makeboxes_matrix(self, table, form, evaluation, options): """MakeBoxes[%(name)s[table_, OptionsPattern[%(name)s]], form:StandardForm|TraditionalForm]""" - result = super(MatrixForm, self).eval_makeboxes( - table, form, evaluation, options - ) + result = super().eval_makeboxes(table, form, evaluation, options) if result.get_head_name() == "System`GridBox": return RowBox(StringLParen, result, StringRParen) + return None diff --git a/mathics/core/atoms/numerics.py b/mathics/core/atoms/numerics.py index 3e4f78429..362831b54 100644 --- a/mathics/core/atoms/numerics.py +++ b/mathics/core/atoms/numerics.py @@ -522,11 +522,11 @@ def is_machine_precision(self) -> bool: return True def make_boxes(self, form): - from mathics.eval.makeboxes import NumberForm_to_String + from mathics.eval.makeboxes import numberform_to_boxes _number_form_options["_Form"] = form # passed to _NumberFormat n = 6 if form == "System`OutputForm" else None - num_str = NumberForm_to_String(self, n, None, None, _number_form_options) + num_str = numberform_to_boxes(self, n, None, None, _number_form_options) return num_str @property @@ -641,11 +641,11 @@ def is_zero(self) -> bool: return self.value.is_zero or False def make_boxes(self, form): - from mathics.eval.makeboxes import NumberForm_to_String + from mathics.eval.makeboxes import numberform_to_boxes _number_form_options["_Form"] = form # passed to _NumberFormat digits = dps(self.get_precision()) if form == "System`OutputForm" else None - return NumberForm_to_String(self, digits, None, None, _number_form_options) + return numberform_to_boxes(self, digits, None, None, _number_form_options) def round(self, d: Optional[int] = None) -> Union[MachineReal, "PrecisionReal"]: if d is None: diff --git a/mathics/eval/makeboxes/__init__.py b/mathics/eval/makeboxes/__init__.py index 876ccccfd..86b856746 100644 --- a/mathics/eval/makeboxes/__init__.py +++ b/mathics/eval/makeboxes/__init__.py @@ -12,7 +12,11 @@ int_to_string_shorter_repr, to_boxes, ) -from mathics.eval.makeboxes.numberform import NumberForm_to_String, eval_baseform +from mathics.eval.makeboxes.numberform import ( + eval_baseform, + get_numberform_parameters, + numberform_to_boxes, +) from mathics.eval.makeboxes.operators import eval_infix, eval_postprefix from mathics.eval.makeboxes.outputforms import ( eval_mathmlform, @@ -26,7 +30,7 @@ ) __all__ = [ - "NumberForm_to_String", + "numberform_to_boxes", "StringLParen", "StringRParen", "_boxed_string", @@ -43,6 +47,7 @@ "eval_tableform", "eval_texform", "format_element", + "get_numberform_parameters", "int_to_string_shorter_repr", "parenthesize", "render_input_form", diff --git a/mathics/eval/makeboxes/numberform.py b/mathics/eval/makeboxes/numberform.py index 935a2c15a..7f9d55f81 100644 --- a/mathics/eval/makeboxes/numberform.py +++ b/mathics/eval/makeboxes/numberform.py @@ -3,12 +3,19 @@ """ from math import ceil -from typing import Optional, Tuple, Union +from typing import Any, Dict, Optional, Tuple, Union import mpmath -from mathics.core.atoms import Integer, MachineReal, PrecisionReal, Real, String -from mathics.core.element import BoxElementMixin +from mathics.core.atoms import ( + Integer, + Integer0, + MachineReal, + PrecisionReal, + Real, + String, +) +from mathics.core.element import BaseElement, BoxElementMixin from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.number import ( @@ -17,13 +24,29 @@ convert_base, dps, ) +from mathics.core.symbols import Symbol, SymbolNull from mathics.core.systemsymbols import ( + SymbolFullForm, SymbolMakeBoxes, SymbolOutputForm, SymbolSubscriptBox, ) from mathics.eval.makeboxes import to_boxes +DEFAULT_NUMBERFORM_OPTIONS = { + "DigitBlock": [0, 0], + "ExponentFunction": lambda x: (SymbolNull if abs(x.value) <= 5 else x), + "ExponentStep": 1, + "NumberFormat": lambda x: x, + "NumberMultiplier": "×", + "NumberPadding": ["", "0"], + "NumberPoint": ".", + "NumberSeparator": [",", ""], + "NumberSigns": ["-", ""], + "SignPadding": False, + "_Form": "System`FullForm", +} + def int_to_tuple_info(integer: Integer) -> Tuple[str, int, bool]: """ @@ -43,74 +66,255 @@ def int_to_tuple_info(integer: Integer) -> Tuple[str, int, bool]: return s, exponent, is_nonnegative -def real_to_tuple_info(real: Real, digits: Optional[int]) -> Tuple[str, int, bool]: +def real_to_tuple_info( + real: Real, digits: Optional[int] +) -> Tuple[str, int, bool, int, int]: """ Convert ``real`` to a tuple representing that value. The tuple consists of: * the string absolute value of ``integer`` with decimal point removed from the string; the position of the decimal point is determined by the exponent below, * the exponent, base 10, to be used, and * True if the value is nonnegative or False otherwise. + * Updated value of digits, according to the number precision. + * the decimal precision. If ``digits`` is None, we use the default precision. """ + binary_precision = real.get_precision() + precision = dps(binary_precision) + if digits is None: + digits = precision + 1 + else: + digits = min(digits, precision + 1) + if real.is_zero: s = "0" if real.is_machine_precision(): exponent = 0 else: - p = real.get_precision() - exponent = -dps(p) + exponent = -precision is_nonnegative = True - else: - if digits is None: - if real.is_machine_precision(): - value = real.value - s = repr(value) - else: - with mpmath.workprec(real.get_precision()): - value = real.to_mpmath() - s = mpmath.nstr(value, dps(real.get_precision()) + 1) + return s, exponent, is_nonnegative, digits, precision + + if digits is None: + if real.is_machine_precision(): + value = real.value + s = repr(value) else: - with mpmath.workprec(real.get_precision()): + with mpmath.workprec(binary_precision): value = real.to_mpmath() - s = mpmath.nstr(value, digits) + s = mpmath.nstr(value, precision + 1) + else: + with mpmath.workprec(binary_precision): + value = real.to_mpmath() + s = mpmath.nstr(value, digits) - # Set sign prefix. - if s[0] == "-": - assert value < 0 - is_nonnegative = False - s = s[1:] - else: - assert value >= 0 - is_nonnegative = True - # Set exponent. ``exponent`` is actual, ``pexp`` of ``NumberForm_to_string()`` is printed. - if "e" in s: - s, exponent = s.split("e") - exponent = int(exponent) - if len(s) > 1 and s[1] == ".": - # str(float) doesn't always include '.' if 'e' is present. - s = s[0] + s[2:].rstrip("0") + # Set sign prefix. + if s[0] == "-": + assert value < 0 + is_nonnegative = False + s = s[1:] + else: + assert value >= 0 + is_nonnegative = True + # Set exponent. ``exponent`` is actual, ``pexp`` of ``NumberForm_to_string()`` is printed. + if "e" in s: + s, exponent = s.split("e") + exponent = int(exponent) + if len(s) > 1 and s[1] == ".": + # str(float) doesn't always include '.' if 'e' is present. + s = s[0] + s[2:].rstrip("0") + else: + exponent = s.index(".") - 1 + s = s[: exponent + 1] + s[exponent + 2 :].rstrip("0") + + # Normalize exponent: remove leading '0's after the decimal point + # and adjust the exponent accordingly. + i = 0 + while i < len(s) and s[i] == "0": + i += 1 + exponent -= 1 + s = s[i:] + + # Add trailing zeros for precision reals. + if digits is not None and not real.is_machine_precision() and len(s) < digits: + s = s + "0" * (digits - len(s)) + return s, exponent, is_nonnegative, digits, precision + + +def eval_baseform( + expr: BaseElement, n: BaseElement, f: Symbol, evaluation: Evaluation +) -> BoxElementMixin: + """ + Evaluate MakeBoxes[BaseForm[expr_, n_], f_] + + Parameters + ---------- + expr : BaseElement + the expression. + n : BaseElement + the base. + f : Symbol + Form (StandardForm/TraditionalForm). + evaluation : Evaluation + Evaluation object used for messages. + + Returns + ------- + BoxElementMixin + A String or a box expression representing `expr` in base `n`. + + """ + try: + val, base = get_baseform_elements(expr, n, evaluation) + except ValueError: + return None + if base is None: + return to_boxes(Expression(SymbolMakeBoxes, expr, f), evaluation) + if f is SymbolOutputForm: + return to_boxes(String(f"{val}_{base}"), evaluation) + + return to_boxes( + Expression(SymbolSubscriptBox, String(val), String(base)), evaluation + ) + + +def get_baseform_elements( + expr: BaseElement, n: BaseElement, evaluation: Evaluation +) -> Dict[str, Any]: + """ + Collect the options for BaseForm expressions. + + Parameters + ---------- + expr : BaseElement + Expression to be formatted. + n : BaseElement + The base of the numeration. + evaluation : Evaluation + Evaluation object used for show messages. + + Raises + ------ + ValueError + If some of the parameters is not valid. + + Returns + ------- + Dict[str, Any] + A dictionary with the option values. + + """ + + if not isinstance(n, Integer): + evaluation.message("BaseForm", "intpm", expr, n) + raise ValueError + + base = n.value + if base <= 0: + evaluation.message("BaseForm", "intpm", expr, n) + raise ValueError + + if isinstance(expr, PrecisionReal): + x = expr.to_sympy() + p = int(ceil(expr.get_precision() / LOG2_10) + 1) + elif isinstance(expr, MachineReal): + x = expr.value + p = RECONSTRUCT_MACHINE_PRECISION_DIGITS + elif isinstance(expr, Integer): + x = expr.value + p = 0 + else: + return None, None + try: + return convert_base(x, base, p), base + except ValueError: + evaluation.message("BaseForm", "basf", n) + raise + + +def get_numberform_parameters( + full_expr, evaluation +) -> Tuple[BaseElement, BaseElement, Dict[str, Any]]: + """Collect the parameters of a NumberForm[...] expression. + Return a tuple with the expression, to be formatted, + the precision especification and a dictionary of options + with Python values + """ + # Pick the + num_form = full_expr.head + elements = full_expr.elements + form_name = num_form.get_name() + full_expr = Expression(SymbolFullForm, full_expr) + # This picks the builtin object used to do the option + # checks... + self = evaluation.definitions.builtin[form_name].builtin + default_options: [str, BaseElement] = evaluation.definitions.get_options(form_name) + options: Dict[str, BaseElement] = {} + py_options: Dict = {} + + if len(elements) == 0: + evaluation.message(form_name, "argm", num_form, Integer0) + raise ValueError + # Just one parameter. Silently return: + if len(elements) == 1: + py_options = self.check_and_convert_options(default_options, evaluation) + if py_options is None: + raise ValueError + + return elements[0], None, py_options + # expr is now the target expression: + expr, *elements = elements + # Collect options + pos = len(elements) + options.update(default_options) + for elem in elements[::-1]: + pos = pos - 1 + if elem.has_form(("Rule", "RuleDelayed"), 2): + key, val = elem.elements + if isinstance(key, Symbol): + key_name = key.get_name() + if key_name not in options: + evaluation.message(form_name, "optx", key, num_form, *elements) + raise ValueError + options[key_name] = val + else: + evaluation.message(form_name, "optx", key, num_form, *elements) + raise ValueError else: - exponent = s.index(".") - 1 - s = s[: exponent + 1] + s[exponent + 2 :].rstrip("0") - - # Normalize exponent: remove leading '0's after the decimal point - # and adjust the exponent accordingly. - i = 0 - while i < len(s) and s[i] == "0": - i += 1 - exponent -= 1 - s = s[i:] - - # Add trailing zeros for precision reals. - if digits is not None and not real.is_machine_precision() and len(s) < digits: - s = s + "0" * (digits - len(s)) - return s, exponent, is_nonnegative + break + + # To many non-option arguments + if pos > 0: + evaluation.message(form_name, "argct", num_form, Integer(len(elements) + 1)) + raise ValueError + # Check for validity of the values: + if pos == 0: + precision_parms = elements[0] + else: + precision_parms = None + + py_options = self.check_and_convert_options(options, evaluation) + + if isinstance(precision_parms, Integer): + val = precision_parms.value + if val <= 0: + evaluation.message(form_name, "iprf", precision_parms) + precision_parms = None + elif precision_parms.has_form("List", 2): + if any( + not isinstance(x, Integer) or x.value <= 0 for x in precision_parms.elements + ): + evaluation.message(form_name, "iprf", precision_parms) + precision_parms = None + else: + evaluation.message(form_name, "iprf", precision_parms) + precision_parms = None + + return expr, precision_parms, py_options -# FIXME: the return type should be a NumberForm, not a String. -# when this is fixed, rename the function. -def NumberForm_to_String( +def numberform_to_boxes( value: Union[Real, Integer], digits: Optional[int], digits_after_decimal_point: Optional[int], @@ -118,7 +322,7 @@ def NumberForm_to_String( options: dict, ) -> BoxElementMixin: """ - Converts a Real or Integer value to a String. + Converts a Real or Integer value to a String or a BoxExpression. ``digits`` is the number of digits of precision and ``digits_after_decimal_point`` is the number of digits after the @@ -133,121 +337,126 @@ def NumberForm_to_String( from the converted number, that is, otherwise the number may be padded on the right-hand side with zeros. """ - form = options["_Form"] - assert isinstance(digits, int) and digits > 0 or digits is None - assert digits_after_decimal_point is None or ( - isinstance(digits_after_decimal_point, int) and digits_after_decimal_point >= 0 - ) + # Ensure that all the options are valid options + for key, val in DEFAULT_NUMBERFORM_OPTIONS.items(): + if options.get(key, None) is None: + options[key] = val + + form = options["_Form"] + # Get information about `value` is_int = False if isinstance(value, Integer): assert digits is not None + precision = None s, exp, is_nonnegative = int_to_tuple_info(value) if digits_after_decimal_point is None: is_int = True elif isinstance(value, Real): - precision = dps(value.get_precision()) - if digits is None: - digits = precision + 1 - else: - digits = min(digits, precision + 1) - s, exp, is_nonnegative = real_to_tuple_info(value, digits) - if digits is None: - digits = len(s) + s, exp, is_nonnegative, digits, precision = real_to_tuple_info(value, digits) else: raise ValueError("Expected Real or Integer.") - assert isinstance(digits, int) and digits > 0 - - sign_prefix = options["NumberSigns"][1 if is_nonnegative else 0] - - # round exponent to ExponentStep - rexp = (exp // options["ExponentStep"]) * options["ExponentStep"] + options["_digits_after_decimal_point"] = digits_after_decimal_point - if is_int: - # integer never uses scientific notation - pexp = "" - else: - method = options["ExponentFunction"] - pexp = method(Integer(rexp)).get_int_value() - if pexp is not None: - exp -= pexp - pexp = str(pexp) - else: - pexp = "" + ( + left, + right, + exp, + pexp, + ) = _format_exponent(s, exp, is_int, evaluation, options) + left, right = _do_pre_paddings(left, right, form, exp, options) - # pad right with '0'. - if len(s) < exp + 1: - if evaluation is not None: - evaluation.message("NumberForm", "sigz") - # TODO NumberPadding? + digit_block = options["DigitBlock"] + number_sep = options["NumberSeparator"] + if digit_block[0]: + left = _add_digit_block_separators( + left, len(left) % digit_block[0], digit_block[0], number_sep[0] + ) + if digit_block[1]: + right = _add_digit_block_separators(right, 0, digit_block[1], number_sep[1]) + prefix, s = _do_padding( + ( + left, + right, + ), + digits, + is_nonnegative, + is_int, + options, + ) + s = _attach_precision(s, value, form, precision) - s = s + "0" * (1 + exp - len(s)) - # pad left with '0'. - if exp < 0: - s = "0" * (-exp) + s - exp = 0 + # PrintForms attach the prefix to the number. FullForm and $BoxForms + # put the prefix and the number in a RowBox: + if form not in ("System`StandardForm", "System`TraditionalForm", "System`FullForm"): + s = prefix + s + prefix = "" - # left and right of NumberPoint - left, right = s[: exp + 1], s[exp + 1 :] - - def _round(number, ndigits): - """ - python round() for integers but with correct rounding. - e.g. `_round(14225, -1)` is `14230` not `14220`. - """ - assert isinstance(ndigits, int) - assert ndigits < 0 - assert isinstance(number, int) - assert number >= 0 - number += 5 * int(10 ** -(1 + ndigits)) - number //= int(10**-ndigits) - return number + # build number + boxed_s = String(s) + if pexp: + # base + boxed_s = options["NumberFormat"]( + boxed_s, + String("10"), + String(pexp), + options, + ) + if prefix: + from mathics.builtin.box.layout import RowBox - # pad with NumberPadding - if digits_after_decimal_point is None: - if form != "System`OutputForm": - # Other forms strip trailing zeros: + boxed_s = RowBox(String(prefix), boxed_s) + return boxed_s - while len(right) > 0: - if right[-1] == "0": - right = right[:-1] - else: - break - else: - if len(right) < digits_after_decimal_point: - # pad right - right = ( - right - + (digits_after_decimal_point - len(right)) - * options["NumberPadding"][1] - ) - elif len(right) > digits_after_decimal_point: - # round right - tmp = int(left + right) - tmp = _round(tmp, digits_after_decimal_point - len(right)) - tmp = str(tmp) - left, right = tmp[: exp + 1], tmp[exp + 1 :] +def _add_digit_block_separators( + part: str, start: int, block_size: int, num_sep: str +) -> str: + """Add the digit block separator""" - def split_string(s, start, step): + def _split_string(s, start, step): if start > 0: yield s[:start] for i in range(start, len(s), step): yield s[i : i + step] - # insert NumberSeparator - digit_block = options["DigitBlock"] - if digit_block[0] != 0: - left = split_string(left, len(left) % digit_block[0], digit_block[0]) - left = options["NumberSeparator"][0].join(left) - if digit_block[1] != 0: - right = split_string(right, 0, digit_block[1]) - right = options["NumberSeparator"][1].join(right) + part = _split_string(part, start, block_size) + part = num_sep.join(part) + return part + + +def _attach_precision(s: str, value, form: str, precision: float) -> str: + """Add the precision mark if needed.""" + + if isinstance(value, MachineReal): + if form not in ("System`InputForm", "System`OutputForm"): + s = s + "`" + elif isinstance(value, PrecisionReal): + if form not in ("System`OutputForm"): + str_precision = str(precision) + if "." not in str_precision: + str_precision += "." + s = s + "`" + str_precision + return s + +def _do_padding( + parts: Tuple[str, str], + digits: int, + is_nonnegative: bool, + is_int: bool, + options: Dict[str, Any], +) -> Tuple[str, str]: + """ + Rebuild the prefix and magnitud + """ + left, right = parts left_padding = 0 - max_sign_len = max(len(options["NumberSigns"][0]), len(options["NumberSigns"][1])) + sign_prefix = options["NumberSigns"][1 if is_nonnegative else 0] + max_sign_len = max(len(ns) for ns in options["NumberSigns"]) + i = len(sign_prefix) + len(left) + len(right) - max_sign_len if i < digits: left_padding = digits - i @@ -265,82 +474,81 @@ def split_string(s, start, step): s = left else: s = left + options["NumberPoint"] + right + return prefix, s - # PrintForms attach the prefix to the number. FullForm and $BoxForms - # put the prefix and the number in a RowBox: - if form not in ("System`StandardForm", "System`TraditionalForm", "System`FullForm"): - s = prefix + s - prefix = "" - if isinstance(value, MachineReal): - if form not in ("System`InputForm", "System`OutputForm"): - s = s + "`" - elif isinstance(value, PrecisionReal): - if form not in ("System`OutputForm"): - str_precision = str(precision) - if "." not in str_precision: - str_precision += "." - s = s + "`" + str_precision +def _do_pre_paddings( + left: str, right: str, form: str, exp: int, options +) -> Tuple[str, str]: + # pad with NumberPadding + daadp = options["_digits_after_decimal_point"] + if daadp is None: + if form != "System`OutputForm": + # Other forms strip trailing zeros: - # build number - boxed_s = String(s) - if pexp: - # base - base = "10" - method = options["NumberFormat"] - boxed_base = String(base) - boxed_pexp = String(pexp) - boxed_s = method( - boxed_s, - boxed_base, - boxed_pexp, - options, - ) - if prefix: - from mathics.builtin.box.layout import RowBox + while len(right) > 0: + if right[-1] == "0": + right = right[:-1] + else: + break + else: + if len(right) < daadp: + # pad right + right = right + (daadp - len(right)) * options["NumberPadding"][1] + elif len(right) > daadp: + # round right + tmp = int(left + right) + tmp = _round(tmp, daadp - len(right)) + tmp = str(tmp) + left, right = tmp[: exp + 1], tmp[exp + 1 :] + return left, right - boxed_s = RowBox(String(prefix), boxed_s) - return boxed_s +def _format_exponent( + s: str, exp: int, is_int: bool, evaluation: Evaluation, options: Dict[str, Any] +) -> Tuple[str, str, int, str]: + # round exponent to ExponentStep + exponent_step = options["ExponentStep"] + rexp = (exp // exponent_step) * exponent_step -def get_baseform_elements(expr, n, evaluation: Evaluation): - if not isinstance(n, Integer): - evaluation.message("BaseForm", "intpm", expr, n) - raise ValueError + if is_int: + # integer never uses scientific notation + pexp = "" + else: + method = options["ExponentFunction"] + pexp = method(Integer(rexp)).get_int_value() + if pexp is not None: + exp -= pexp + pexp = str(pexp) + else: + pexp = "" - base = n.value - if base <= 0: - evaluation.message("BaseForm", "intpm", expr, n) - raise ValueError + # pad right with '0'. + if len(s) < exp + 1: + if evaluation is not None: + evaluation.message("NumberForm", "sigz") - if isinstance(expr, PrecisionReal): - x = expr.to_sympy() - p = int(ceil(expr.get_precision() / LOG2_10) + 1) - elif isinstance(expr, MachineReal): - x = expr.value - p = RECONSTRUCT_MACHINE_PRECISION_DIGITS - elif isinstance(expr, Integer): - x = expr.value - p = 0 - else: - return None, None - try: - return convert_base(x, base, p), base - except ValueError: - evaluation.message("BaseForm", "basf", n) - raise + # TODO NumberPadding instead 0? + s = s + "0" * (1 + exp - len(s)) + # pad left with '0'. + if exp < 0: + s = "0" * (-exp) + s + exp = 0 + # left and right of NumberPoint + sleft, sright = s[: exp + 1], s[exp + 1 :] + return sleft, sright, exp, pexp -def eval_baseform(self, expr, n, f, evaluation: Evaluation): - try: - val, base = get_baseform_elements(expr, n, evaluation) - except ValueError: - return None - if base is None: - return to_boxes(Expression(SymbolMakeBoxes, expr, f), evaluation) - if f is SymbolOutputForm: - return to_boxes(String("%s_%d" % (val, base)), evaluation) - return to_boxes( - Expression(SymbolSubscriptBox, String(val), String(base)), evaluation - ) +def _round(number, ndigits): + """ + python round() for integers but with correct rounding. + e.g. `_round(14225, -1)` is `14230` not `14220`. + """ + assert isinstance(ndigits, int) + assert ndigits < 0 + assert isinstance(number, int) + assert number >= 0 + number += 5 * int(10 ** -(1 + ndigits)) + number //= int(10**-ndigits) + return number diff --git a/test/builtin/test_forms.py b/test/builtin/test_forms.py index 25a1d6d88..b8bd66c9a 100644 --- a/test/builtin/test_forms.py +++ b/test/builtin/test_forms.py @@ -147,32 +147,32 @@ def test_makeboxes_form(expr, form, head, subhead): ( "Value for option DigitBlock should be a positive integer, Infinity, or a pair of positive integers.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( "NumberForm[1.2345, 3, DigitBlock -> x]", ( "Value for option DigitBlock should be a positive integer, Infinity, or a pair of positive integers.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( "NumberForm[1.2345, 3, DigitBlock -> {x, 3}]", ( "Value for option DigitBlock should be a positive integer, Infinity, or a pair of positive integers.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( "NumberForm[1.2345, 3, DigitBlock -> {5, -3}]", ( "Value for option DigitBlock should be a positive integer, Infinity, or a pair of positive integers.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ## ExponentFunction ( @@ -213,14 +213,14 @@ def test_makeboxes_form(expr, form, head, subhead): ( "NumberForm[1.2345, 3, ExponentStep -> x]", ("Value of option ExponentStep -> x is not a positive integer.",), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( "NumberForm[1.2345, 3, ExponentStep -> 0]", ("Value of option ExponentStep -> 0 is not a positive integer.",), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( "NumberForm[y, 10, ExponentStep -> 6]", @@ -239,8 +239,8 @@ def test_makeboxes_form(expr, form, head, subhead): ( "NumberForm[1.2345, 3, NumberMultiplier -> 0]", ("Value for option NumberMultiplier -> 0 is expected to be a string.",), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( 'NumberForm[N[10^ 7 Pi], 15, NumberMultiplier -> "*"]', @@ -253,8 +253,8 @@ def test_makeboxes_form(expr, form, head, subhead): ( "NumberForm[1.2345, 3, NumberPoint -> 0]", ("Value for option NumberPoint -> 0 is expected to be a string.",), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ## NumberPadding ("NumberForm[1.41, {10, 5}]", None, "1.41000", None), @@ -281,8 +281,8 @@ def test_makeboxes_form(expr, form, head, subhead): ( "Value for option NumberPadding -> 0 should be a string or a pair of strings.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( 'NumberForm[1.41, 10, NumberPadding -> {"X", "Y"}, NumberSigns -> {"-------------", ""}]', @@ -326,8 +326,8 @@ def test_makeboxes_form(expr, form, head, subhead): ( "Value for option NumberSeparator -> 0 should be a string or a pair of strings.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ## NumberSigns ('NumberForm[1.2345, 5, NumberSigns -> {"-", "+"}]', None, "+1.2345", None), @@ -337,8 +337,8 @@ def test_makeboxes_form(expr, form, head, subhead): ( "Value for option NumberSigns -> 0 should be a pair of strings or two pairs of strings.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ## SignPadding ( @@ -394,6 +394,13 @@ def test_makeboxes_form(expr, form, head, subhead): "2 \u2062 a 0\n\n0 0\n", "Issue #182", ), + ## + ( + "NumberForm[N[10^ 5 Pi], 15, DigitBlock -> {4, 2}, ExponentStep->x]", + ("Value of option ExponentStep -> x is not a positive integer.",), + "31,4159.26 53 58 97 9", + "Options with wrong values are discarded, but other properties are kept.", + ), ], ) def test_private_doctests_output(str_expr, msgs, str_expected, fail_msg): diff --git a/test/builtin/test_number_form.py b/test/builtin/test_number_form.py index 9da4aa423..e49b3219b 100644 --- a/test/builtin/test_number_form.py +++ b/test/builtin/test_number_form.py @@ -26,7 +26,15 @@ def test_int_to_tuple_info( @pytest.mark.parametrize( - ("real", "digits", "expected", "exponent", "is_nonnegative"), + ( + "real", + "digits", + "expected", + "exponent", + "is_nonnegative", + "red_digits", + "precision", + ), [ # Using older uncorrected version of Real() # ( @@ -34,16 +42,28 @@ def test_int_to_tuple_info( # if Version(sympy.__version__) < Version("1.13.0") # else (Real(sympy.Float(0.0, 10)), 10, "0000000000", -1, True) # ), - (Real(sympy.Float(0.0, 10)), 10, "0", -10, True), - (Real(0), 1, "0", 0, True), - (Real(0), 2, "0", 0, True), - (Real(0.1), 2, "1", -1, True), - (Real(0.12), 2, "12", -1, True), - (Real(-0.12), 2, "12", -1, False), - (Real(3.141593), 10, "3141593", 0, True), + (Real(sympy.Float(0.0, 10)), 10, "0", -10, True, 10, 10), + (Real(0), 1, "0", 0, True, 1, 15), + (Real(0), 2, "0", 0, True, 2, 15), + (Real(0.1), 2, "1", -1, True, 2, 15), + (Real(0.12), 2, "12", -1, True, 2, 15), + (Real(-0.12), 2, "12", -1, False, 2, 15), + (Real(3.141593), 10, "3141593", 0, True, 10, 15), ], ) def test_real_to_tuple_info( - real: Real, digits: int, expected: str, exponent: int, is_nonnegative: bool + real: Real, + digits: int, + expected: str, + exponent: int, + is_nonnegative: bool, + red_digits: int, + precision: int, ): - assert real_to_tuple_info(real, digits) == (expected, exponent, is_nonnegative) + assert real_to_tuple_info(real, digits) == ( + expected, + exponent, + is_nonnegative, + red_digits, + precision, + )