From 8e42c967eb416ca1e9e5d05905f5f2b29e4729ac Mon Sep 17 00:00:00 2001 From: mmatera Date: Thu, 5 Jan 2023 21:11:23 -0300 Subject: [PATCH 01/15] not ready --- mathics/builtin/arithfns/basic.py | 4 +- mathics/builtin/base.py | 2 +- mathics/builtin/box/layout.py | 27 ++ mathics/builtin/forms/output.py | 17 +- mathics/builtin/makeboxes.py | 32 +- mathics/core/convert/prettyprint.py | 618 ++++++++++++++++++++++++++++ mathics/core/prettyprint.py | 232 +++++++++++ mathics/eval/makeboxes.py | 55 ++- mathics/format/latex.py | 12 + mathics/format/mathml.py | 12 + mathics/format/text.py | 11 + 11 files changed, 991 insertions(+), 31 deletions(-) create mode 100644 mathics/core/convert/prettyprint.py create mode 100644 mathics/core/prettyprint.py diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index b4875bae9..394e165a4 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -191,7 +191,7 @@ class Divide(BinaryOperator): } formats = { - (("InputForm", "OutputForm"), "Divide[x_, y_]"): ( + (("InputForm",), "Divide[x_, y_]"): ( 'Infix[{HoldForm[x], HoldForm[y]}, "/", 400, Left]' ), } @@ -573,7 +573,7 @@ class Power(BinaryOperator, _MPMathFunction): Expression(SymbolPattern, Symbol("x"), Expression(SymbolBlank)), RationalOneHalf, ): "HoldForm[Sqrt[x]]", - (("InputForm", "OutputForm"), "x_ ^ y_"): ( + (("InputForm",), "x_ ^ y_"): ( 'Infix[{HoldForm[x], HoldForm[y]}, "^", 590, Right]' ), ("", "x_ ^ y_"): ( diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index 7475c8e1e..8822211cb 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -9,7 +9,7 @@ import sympy -from mathics.core.atoms import Integer, MachineReal, PrecisionReal, String +from mathics.core.atoms import Integer, MachineReal, PrecisionReal, Real, String from mathics.core.attributes import A_NO_ATTRIBUTES, A_PROTECTED from mathics.core.convert.expression import to_expression, to_numeric_sympy_args from mathics.core.convert.op import ascii_operator_to_symbol diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index 74b83a2e6..6adfc5792 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -192,6 +192,33 @@ def apply_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(boxexpr, 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 06061e9a4..3c016472c 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -14,7 +14,13 @@ from typing import Optional from mathics.builtin.base import Builtin -from mathics.builtin.box.layout import GridBox, RowBox, to_boxes +from mathics.builtin.box.layout import ( + GridBox, + InterpretationBox, + PaneBox, + RowBox, + to_boxes, +) from mathics.builtin.comparison import expr_min from mathics.builtin.forms.base import FormBaseClass from mathics.builtin.makeboxes import MakeBoxes, number_form @@ -27,6 +33,7 @@ String, StringFromPython, ) +from mathics.core.convert.prettyprint import expression_to_2d_text from mathics.core.expression import BoxError, Expression from mathics.core.list import ListExpression from mathics.core.number import convert_base, dps, machine_precision, reconstruct_digits @@ -751,6 +758,14 @@ class OutputForm(FormBaseClass): summary_text = "plain-text output format" + def apply_makeboxes(self, expr, form, evaluation): + """MakeBoxes[OutputForm[expr_], form_]""" + text2d = expression_to_2d_text(expr, evaluation, form).text + elem1 = PaneBox(String(text2d)) + elem2 = Expression(SymbolOutputForm, expr) + result = InterpretationBox(elem1, elem2) + return result + class PythonForm(FormBaseClass): """ diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index ed906e3ef..8c6d79037 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -20,7 +20,7 @@ from mathics.core.number import dps from mathics.core.symbols import Atom, Symbol from mathics.core.systemsymbols import SymbolInputForm, SymbolOutputForm, SymbolRowBox -from mathics.eval.makeboxes import _boxed_string, format_element +from mathics.eval.makeboxes import _boxed_string, compare_precedence, format_element def int_to_s_exp(expr, n): @@ -36,26 +36,12 @@ def int_to_s_exp(expr, n): def parenthesize(precedence, element, element_boxes, when_equal): - from mathics.builtin import builtins_precedence - - while element.has_form("HoldForm", 1): - element = element.elements[0] - - if element.has_form(("Infix", "Prefix", "Postfix"), 3, None): - element_prec = element.elements[2].value - elif element.has_form("PrecedenceForm", 2): - element_prec = element.elements[1].value - # For negative values, ensure that the element_precedence is at least the precedence. (Fixes #332) - elif isinstance(element, (Integer, Real)) and element.value < 0: - element_prec = precedence - else: - element_prec = builtins_precedence.get(element.get_head_name()) - if precedence is not None and element_prec is not None: - if precedence > element_prec or (precedence == element_prec and when_equal): - return Expression( - SymbolRowBox, - ListExpression(String("("), element_boxes, String(")")), - ) + cmp = compare_precedence(element, precedence) + if cmp is not None and (cmp == -1 or cmp == 0 and when_equal): + return Expression( + SymbolRowBox, + ListExpression(String("("), element_boxes, String(")")), + ) return element_boxes @@ -359,9 +345,9 @@ 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:StandardForm|MathMLForm|TeXForm)[expr_], OutputForm]": "MakeBoxes[expr, form]", "MakeBoxes[(form:FullForm|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_]": ( diff --git a/mathics/core/convert/prettyprint.py b/mathics/core/convert/prettyprint.py new file mode 100644 index 000000000..8764b4624 --- /dev/null +++ b/mathics/core/convert/prettyprint.py @@ -0,0 +1,618 @@ +""" +This module builts the 2D string associated to the OutputForm +""" + +from mathics.core.atoms import Integer, IntegerM1, Rational, Real, String +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.prettyprint import ( + TextBlock, + bracket, + fraction, + integral_definite, + integral_indefinite, + parenthesize, + sqrt_block, + subscript, + subsuperscript, + superscript, +) +from mathics.core.symbols import Atom, Symbol, SymbolTimes +from mathics.core.systemsymbols import ( + SymbolInfix, + SymbolNone, + SymbolOutputForm, + SymbolPower, + SymbolStandardForm, + SymbolTraditionalForm, +) +from mathics.eval.makeboxes import compare_precedence, do_format + +SymbolNonAssociative = Symbol("System`NonAssociative") +SymbolPostfix = Symbol("System`Postfix") +SymbolPrefix = Symbol("System`Prefix") +SymbolRight = Symbol("System`Right") +SymbolLeft = Symbol("System`Left") + +#### Functions that convert Expressions in TextBlock + + +expr_to_2d_text_map = {} + + +# This Exception if the expression should +# be processed by the default routine +class _WrongFormattedExpression(Exception): + pass + + +def default_expression_to_2d_text(expr, evaluation, form, **kwargs): + head = expression_to_2d_text(expr.head, evaluation) + comma = TextBlock(", ") + elements = [expression_to_2d_text(elem, evaluation) for elem in expr.elements] + result = elements.pop(0) if elements else TextBlock(" ") + 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): + if kwargs.get("2d", False): + return fraction( + *( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in expr.elements + ) + ) + infix_form = Expression( + SymbolInfix, ListExpression(num, den), String("/"), Integer(400), SymbolLeft + ) + return expression_to_2d_text(infix_form, evaluation, form, **kwargs) + + +def divide_expression_to_2d_text(expr, evaluation, form, **kwargs): + if len(expr.elements) != 2: + raise _WrongFormattedExpression + num, den = expr.elements + return _divide(num, den, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Divide"] = divide_expression_to_2d_text + + +def graphics(expr, evaluation, form, **kwargs): + return TextBlock("-Graphics-") + + +expr_to_2d_text_map["System`Graphics"] = graphics + + +def graphics3d(expr, evaluation, form, **kwargs): + return TextBlock("-Graphics3D-") + + +expr_to_2d_text_map["System`Graphics3D"] = graphics3d + + +def holdform_expression_to_2d_text(expr, evaluation, form, **kwargs): + if len(expr.elements) != 1: + raise _WrongFormattedExpression + return expression_to_2d_text(expr.elements[0], evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`HoldForm"] = holdform_expression_to_2d_text + + +def pre_pos_infix_expression_to_2d_text(expr, evaluation, form, **kwargs): + 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 head in (SymbolPrefix, SymbolPostfix): + if len(operands) != 1: + raise _WrongFormattedExpression + elif head is SymbolInfix: + if len(operands) < 2: + raise _WrongFormattedExpression + else: + 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_2d_text(op, evaluation, form, **kwargs) + for op in ops.elements + ] + else: + ops_lst = [expression_to_2d_text(ops, evaluation, form, **kwargs)] + elif head in (SymbolPrefix, SymbolPostfix): + ops_txt = [expression_to_2d_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolInfix: + num_ops = 1 + default_symb = TextBlock(" ~ ") + ops_lst = [ + default_symb + + expression_to_2d_text(head, evaluation, form, **kwargs) + + default_symb + ] + elif head is SymbolPrefix: + default_symb = TextBlock(" @ ") + ops_txt = ( + expression_to_2d_text(head, evaluation, form, **kwargs) + default_symb + ) + elif head is SymbolPostfix: + default_symb = TextBlock(" // ") + ops_txt = default_symb + expression_to_2d_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_2d_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_2d_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, SymbolLeft, SymbolNonAssociative) + for index, operand in enumerate(operands): + operand_txt = expression_to_2d_text(operand, evaluation, form, **kwargs) + cmp_precedence = compare_precedence(operand, precedence) + print(" ", [operand, precedence, cmp_precedence, parenthesized, group]) + if 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: + if ops_lst[index % num_ops].text != " ": + result = result + " " + ops_lst[index % num_ops] + " " + operand_txt + else: + result = result + " " + operand_txt + + return result + + +expr_to_2d_text_map["System`Prefix"] = pre_pos_infix_expression_to_2d_text +expr_to_2d_text_map["System`Postfix"] = pre_pos_infix_expression_to_2d_text +expr_to_2d_text_map["System`Infix"] = pre_pos_infix_expression_to_2d_text + + +def integer_expression_to_2d_text(n, evaluation, form, **kwargs): + return TextBlock(str(n.value)) + + +expr_to_2d_text_map["System`Integer"] = integer_expression_to_2d_text + + +def integrate_expression_to_2d_text(expr, evaluation, form, **kwargs): + elems = list(expr.elements) + if len(elems) > 2 or not kwargs.get("2d", False): + return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + + result = expression_to_2d_text(elems.pop(0), evaluation, form, **kwargs) + while elems: + var = elems.pop(0) + if var.has_form("List", 3): + var_txt, a, b = ( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in var.elements + ) + result = integral_definite(result, var_txt, a, b) + elif isinstance(var, Symbol): + var_txt = expression_to_2d_text(var, evaluation, form, **kwargs) + result = integral_indefinite(result, var_txt) + else: + break + return result + + +expr_to_2d_text_map["System`Integrate"] = integrate_expression_to_2d_text + + +def list_expression_to_2d_text(expr, evaluation, form, **kwargs): + return ( + TextBlock("{") + + TextBlock(", ").join( + [ + expression_to_2d_text(elem, evaluation, form, **kwargs) + for elem in expr.elements + ] + ) + + TextBlock("}") + ) + + +expr_to_2d_text_map["System`List"] = list_expression_to_2d_text + + +def plus_expression_to_2d_text(expr, evaluation, form, **kwargs): + elements = expr.elements + result = TextBlock("") + 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_2d_text( + Expression(SymbolTimes, *elem.elements[1:]), + evaluation, + form, + **kwargs + ) + ) + continue + elif first.value < 0: + result = ( + result + + " " + + expression_to_2d_text(elem, evaluation, form, **kwargs) + ) + continue + elif isinstance(first, Real): + if first.value < 0: + result = ( + result + + " " + + expression_to_2d_text(elem, evaluation, form, **kwargs) + ) + continue + result = ( + result + " + " + expression_to_2d_text(elem, evaluation, form, **kwargs) + ) + ## TODO: handle complex numbers? + else: + elem_txt = expression_to_2d_text(elem, evaluation, form, **kwargs) + if compare_precedence(elem, 310) < 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_2d_text(elem, evaluation, form, **kwargs) + ) + return result + + +expr_to_2d_text_map["System`Plus"] = plus_expression_to_2d_text + + +def power_expression_to_2d_text(expr, evaluation, form, **kwargs): + if len(expr.elements) != 2: + return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + if kwargs.get("2d", False): + base, exponent = ( + expression_to_2d_text(elem, evaluation, form, **kwargs) + for elem in expr.elements + ) + base_precedence = builtins_precedence.get( + expr.elements[0].get_head_name(), None + ) + if compare_precedence(expr.elements[0], 590) == -1: + base = parenthesize(base) + return superscript(base, exponent) + + infix_form = Expression( + SymbolInfix, + ListExpression(*(expr.elements)), + String("^"), + Integer(590), + SymbolRight, + ) + return expression_to_2d_text(infix_form, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Power"] = power_expression_to_2d_text + + +def precedenceform_expression_to_2d_text(expr, evaluation, form, **kwargs): + if len(expr.elements) == 2: + return expression_to_2d_text(expr.elements[0], evaluation, form, **kwargs) + return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`PrecedenceForm"] = precedenceform_expression_to_2d_text + + +def rational_expression_to_2d_text(n, evaluation, form, **kwargs): + if n.has_form("Rational", 2): + num, den = n.elements + else: + num, den = n.numerator(), n.denominator() + return _divide(num, den, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Rational"] = rational_expression_to_2d_text + + +def real_expression_to_2d_text(n, evaluation, form, **kwargs): + str_n = n.make_boxes("System`OutputForm").value + return TextBlock(str(str_n)) + + +expr_to_2d_text_map["System`Real"] = real_expression_to_2d_text + + +def sqrt_expression_to_2d_text(expr, evaluation, form, **kwargs): + if not 1 <= len(expr.elements) <= 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + return sqrt_block( + *( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in expr.elements + ) + ) + return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Sqrt"] = sqrt_expression_to_2d_text + + +def subscript_expression_to_2d_text(expr, evaluation, form, **kwargs): + if len(expr.elements) != 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + return subscript( + *( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in expr.elements + ) + ) + return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Subscript"] = subscript_expression_to_2d_text + + +def subsuperscript_expression_to_2d_text(expr, evaluation, form, **kwargs): + if len(expr.elements) != 3: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + return subsuperscript( + *( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in expr.elements + ) + ) + return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Subsuperscript"] = subsuperscript_expression_to_2d_text + + +def string_expression_to_2d_text(expr, evaluation, form, **kwargs): + return TextBlock(expr.value) + + +expr_to_2d_text_map["System`String"] = string_expression_to_2d_text + + +def stringform_expression_to_2d_text(expr, evaluation, form, **kwargs): + strform = expr.elements[0] + if not isinstance(strform, String): + raise _WrongFormattedExpression + + items = list( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in expr.elements[1:] + ) + + curr_indx = 0 + parts = strform.value.split("`") + result = TextBlock(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 len(part) == 0: + result = result + items[curr_indx] + continue + if remaining == 0: + result = result + "`" + part + quote_open = False + 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 + + return result + + +expr_to_2d_text_map["System`StringForm"] = stringform_expression_to_2d_text + + +def superscript_expression_to_2d_text(expr, evaluation, form, **kwargs): + if len(expr.elements) != 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + return superscript( + *( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in expr.elements + ) + ) + infix_form = Expression( + SymbolInfix, + ListExpression(*(expr.elements)), + String("^"), + Integer(590), + SymbolRight, + ) + return expression_to_2d_text(infix_form, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Superscript"] = superscript_expression_to_2d_text + + +def symbol_expression_to_2d_text(symb, evaluation, form, **kwargs): + return TextBlock(evaluation.definitions.shorten_name(symb.name)) + + +expr_to_2d_text_map["System`Symbol"] = symbol_expression_to_2d_text + + +def times_expression_to_2d_text(expr, evaluation, form, **kwargs): + elements = expr.elements + num = [] + den = [] + # 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): + 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_2d_text(num[0], evaluation, form, **kwargs) + + prefactor = 1 + result = TextBlock("") + 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_2d_text(elem, evaluation, form, **kwargs) + if compare_precedence(elem, 400): + elem = parenthesize(elem_txt) + if i == 0: + result = elem_txt + else: + result = result + " " + elem_txt + if result.text == "": + result = TextBlock("1") + if prefactor == -1: + result = TextBlock("-") + result + return result + + +expr_to_2d_text_map["System`Times"] = times_expression_to_2d_text + + +def expression_to_2d_text( + expr, evaluation: Evaluation, form=SymbolStandardForm, **kwargs +): + ## TODO: format the expression + format_expr = do_format(expr, evaluation, form) + # Strip HoldForm + while format_expr.has_form("HoldForm", 1): + format_expr = format_expr.elements[0] + + lookup_name = format_expr.get_head().get_lookup_name() + print([lookup_name, format_expr]) + try: + return expr_to_2d_text_map[lookup_name](format_expr, evaluation, form, **kwargs) + 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_2d_text(format_expr, evaluation, form, **kwargs) diff --git a/mathics/core/prettyprint.py b/mathics/core/prettyprint.py new file mode 100644 index 000000000..ff1096ca5 --- /dev/null +++ b/mathics/core/prettyprint.py @@ -0,0 +1,232 @@ +""" +This module produces a "pretty-print" inspired 2d text representation. +""" + + +class TextBlock: + def __init__(self, text, padding=0, base=0, height=1, width=0): + self.height = height + self.padding = padding + text = text.replace("\t", " ") + lines = text.split("\n") + for line in lines: + width = max(len(line), width) + + lines = [ + line if len(line) == width else (line + (width - len(line)) * " ") + for line in lines + ] + self.width = width + padding + lines = [padding * " " + line for line in lines] + if base < 0: + height = height - base + lines = (-base) * [width * " "] + lines + base = -base + if height > len(lines): + lines = lines + (height - len(lines)) * [width * " "] + else: + height = len(lines) + self.height = height + self.text = "\n".join(lines) + self.base = base + + def box(self): + top = "+" + self.width * "-" + "+" + out = "\n".join("|" + line + "|" for line in self.text.split("\n")) + out = top + "\n" + out + "\n" + top + return TextBlock(out, self.base + 1) + + def __repr__(self): + return self.text + + def __add__(self, tb): + if isinstance(tb, str): + tb = TextBlock(tb) + base = self.base + other_base = tb.base + left_lines = self.text.split("\n") + right_lines = tb.text.split("\n") + offset = other_base - base + if offset > 0: + left_lines = left_lines + offset * [self.width * " "] + base = other_base + elif offset < 0: + offset = -offset + right_lines = right_lines + offset * [tb.width * " "] + + offset = len(right_lines) - len(left_lines) + if offset > 0: + left_lines = offset * [self.width * " "] + left_lines + elif offset < 0: + right_lines = (-offset) * [tb.width * " "] + right_lines + + new_str = "\n".join(l + r for l, r in zip(left_lines, right_lines)) + return TextBlock(new_str, base=base) + + def join(self, iter): + result = TextBlock("") + for i, item in enumerate(iter): + if i == 0: + result = item + else: + result = result + self + item + return result + + def stack(self, other, align="c"): + if isinstance(other, str): + other = TextBlock(other) + + self_lines = self.text.split("\n") + other_lines = other.text.split("\n") + self_width, other_width = self.width, other.width + diff_width = self_width - other_width + + def padding(lines, diff): + if diff > 0: + if align == "c": + left_pad = int(diff / 2) + right_pad = diff - left_pad + lines = [ + (left_pad * " " + line + right_pad * " ") for line in lines + ] + elif align == "r": + lines = [(diff * " " + line) for line in lines] + else: + lines = [(line + diff * " ") for line in lines] + return lines + + if diff_width > 0: + other_lines = padding(other_lines, diff_width) + elif diff_width < 0: + self_lines = padding(self_lines, -diff_width) + + lines = other_lines + self_lines + return TextBlock("\n".join(lines), base=self.base) + + +def subsuperscript(base, a, b): + if isinstance(base, str): + base = TextBlock(base) + if isinstance(b, str): + b = TextBlock(b) + + text2 = b.stack((base.height - 1) * "\n", align="l").stack(a, align="l") + text2.base = base.base + b.height + return base + text2 + + +def superscript(base, a): + if isinstance(base, str): + base = TextBlock(base) + text2 = TextBlock((base.height - 1) * "\n", base=base.base).stack(a, align="l") + return base + text2 + + +def subscript(base, a): + if isinstance(a, str): + a = TextBlock(a) + text2 = a.stack(TextBlock((base.height - 1) * "\n", base=base.base), align="l") + text2.base = base.base + a.height + return base + text2 + + +def sqrt_block(a, index=None): + if isinstance(a, str): + a = TextBlock(a) + if isinstance(index, str): + index = TextBlock(index) + + a_height = a.height + result_2 = TextBlock( + "\n".join("|" + line for line in a.text.split("\n")), base=a.base + ) + result_2 = result_2.stack((a.width + 1) * "_", align="l") + half_height = int(a_height / 2) + + result_1 = TextBlock( + "\n".join( + [ + (int(i) * " " + "\\" + int((half_height - i - 1)) * " ") + for i in range(half_height) + ] + ), + base=a.base, + ) + if index is not None: + result_1 = result_1.stack(index, align="c") + return result_1 + result_2 + + +def fraction(a, b): + if isinstance(a, str): + a = TextBlock(a) + if isinstance(b, str): + b = TextBlock(b) + width = max(b.width, a.width) + 2 + frac_bar = TextBlock(width * "-") + result = frac_bar.stack(a) + result = b.stack(result) + result.base = b.height + return result + + +def integral_indefinite(integrand, var): + if isinstance(var, str): + var = TextBlock(var) + if isinstance(integrand, str): + integrand = TextBlock(integrand) + + height = integrand.height + int_symb = TextBlock( + " /+ \n" + "\n".join(height * [" | "]) + "\n +/ ", base=int((height + 1) / 2) + ) + return int_symb + integrand + " d" + var + + +def integral_definite(integrand, var, a, b): + if isinstance(var, str): + var = TextBlock(var) + if isinstance(integrand, str): + integrand = TextBlock(integrand) + if isinstance(a, str): + a = TextBlock(a) + if isinstance(b, str): + b = TextBlock(b) + + height = integrand.height + int_symb = TextBlock( + " /+ \n" + "\n".join(height * [" | "]) + "\n +/ ", base=int((height + 1) / 2) + ) + return subsuperscript(int_symb, a, b) + " " + integrand + " d" + var + + +def bracket(inner): + if isinstance(inner, str): + inner = TextBlock(inner) + height = inner.height + if height == 1: + left_br, right_br = TextBlock("["), TextBlock("]") + else: + left_br = TextBlock( + "+-\n" + "\n".join((height) * ["| "]) + "\n+-", base=inner.base + 1 + ) + right_br = TextBlock( + "-+ \n" + "\n".join((height) * [" |"]) + "\n-+", base=inner.base + 1 + ) + return left_br + inner + right_br + + +def parenthesize(inner): + if isinstance(inner, str): + inner = TextBlock(inner) + height = inner.height + if height == 1: + left_br, right_br = TextBlock("("), TextBlock(")") + else: + left_br = TextBlock( + "/ \n" + "\n".join((height) * ["| "]) + "\n\\ ", base=inner.base + 1 + ) + right_br = TextBlock( + " \\ \n" + "\n".join((height) * [" |"]) + "\n /", base=inner.base + 1 + ) + return left_br + inner + right_br diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 7c51be55f..fd4e22c48 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -7,9 +7,9 @@ import typing -from typing import Any +from typing import Any, Optional -from mathics.core.atoms import Complex, Integer, Rational, String, SymbolI +from mathics.core.atoms import Complex, Integer, Rational, Real, String, SymbolI from mathics.core.convert.expression import to_expression_with_specialization from mathics.core.definitions import OutputForms from mathics.core.element import BaseElement, BoxElementMixin, EvalMixin @@ -39,6 +39,7 @@ SymbolOutputForm, SymbolRational, SymbolStandardForm, + SymbolTraditionalForm, ) element_formatters = {} @@ -51,7 +52,42 @@ def _boxed_string(string: str, **options): return StyleBox(String(string), **options) -def eval_makeboxes(self, expr, evaluation, f=SymbolStandardForm): +def compare_precedence( + element: BaseElement, precedence: Optional[int] = None +) -> Optional[int]: + """ + compare the precedence of the element regarding a precedence value. + If both precedences are equivalent, return 0. If precedence of element + is higher, return 1, otherwise -1. + If precedences cannot be compared, return None. + """ + + from mathics.builtin import builtins_precedence + + while element.has_form("HoldForm", 1): + element = element.elements[0] + + if precedence is None: + return None + if element.has_form(("Infix", "Prefix", "Postfix"), 3, None): + element_prec = element.elements[2].value + elif element.has_form("PrecedenceForm", 2): + element_prec = element.elements[1].value + # For negative values, ensure that the element_precedence is at least the precedence. (Fixes #332) + elif isinstance(element, (Integer, Real)) and element.value < 0: + element_prec = precedence + else: + element_prec = builtins_precedence.get(element.get_head_name()) + + if element_prec is None: + return None + + if element_prec == precedence: + return 0 + return 1 if element_prec > precedence else -1 + + +def eval_makeboxes(expr, evaluation, f=SymbolStandardForm): """ This function takes the definitions prodived by the evaluation object, and produces a boxed form for expr. @@ -66,8 +102,19 @@ def format_element( """ Applies formats associated to the expression, and then calls Makeboxes """ + + from mathics.core.convert.prettyprint import expression_to_2d_text + + if False and form is SymbolOutputForm: + txt2d_form = expression_to_2d_text(element, evaluation, form, **kwargs) + return String(txt2d_form.text) + expr = do_format(element, evaluation, form) - result = Expression(SymbolMakeBoxes, expr, form) + if form in (SymbolStandardForm, SymbolTraditionalForm): + result = Expression(SymbolMakeBoxes, expr, form) + else: + expr = Expression(form, expr) + result = Expression(SymbolMakeBoxes, expr, SymbolStandardForm) result_box = result.evaluate(evaluation) if isinstance(result_box, String): return result_box diff --git a/mathics/format/latex.py b/mathics/format/latex.py index 46b3f9ea6..6a51f0fde 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, @@ -142,6 +144,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 8e432fa27..422a3f7b5 100644 --- a/mathics/format/mathml.py +++ b/mathics/format/mathml.py @@ -13,6 +13,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/text.py b/mathics/format/text.py index 7a60c9202..12a6899de 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,15 @@ def string(self, **options) -> str: add_conversion_fn(String, string) +def interpretation_panebox(self, **options): + print("interpretation_panebox to text", self.elements[0]) + 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) From d990b15c67d56dd507e8fa876e9399a32fe4ea0f Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 6 Jan 2023 15:52:42 -0300 Subject: [PATCH 02/15] second round --- mathics/core/convert/prettyprint.py | 4 +--- mathics/format/text.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/mathics/core/convert/prettyprint.py b/mathics/core/convert/prettyprint.py index 8764b4624..83b21cf5a 100644 --- a/mathics/core/convert/prettyprint.py +++ b/mathics/core/convert/prettyprint.py @@ -197,11 +197,10 @@ def pre_pos_infix_expression_to_2d_text(expr, evaluation, form, **kwargs): target_txt = parenthesize(target_txt) return target_txt + ops_txt[0] else: # Infix - parenthesized = group in (None, SymbolLeft, SymbolNonAssociative) + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) for index, operand in enumerate(operands): operand_txt = expression_to_2d_text(operand, evaluation, form, **kwargs) cmp_precedence = compare_precedence(operand, precedence) - print(" ", [operand, precedence, cmp_precedence, parenthesized, group]) if cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized): operand_txt = parenthesize(operand_txt) @@ -606,7 +605,6 @@ def expression_to_2d_text( format_expr = format_expr.elements[0] lookup_name = format_expr.get_head().get_lookup_name() - print([lookup_name, format_expr]) try: return expr_to_2d_text_map[lookup_name](format_expr, evaluation, form, **kwargs) except _WrongFormattedExpression: diff --git a/mathics/format/text.py b/mathics/format/text.py index 12a6899de..8bed6811a 100644 --- a/mathics/format/text.py +++ b/mathics/format/text.py @@ -43,7 +43,6 @@ def string(self, **options) -> str: def interpretation_panebox(self, **options): - print("interpretation_panebox to text", self.elements[0]) return boxes_to_text(self.elements[0], **options) From 2b51c377cca0ad8db8d89ce2aaa2e0659538e011 Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 6 Jan 2023 16:27:28 -0300 Subject: [PATCH 03/15] tmp --- mathics/core/convert/prettyprint.py | 9 ++++++++- mathics/eval/makeboxes.py | 6 +----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mathics/core/convert/prettyprint.py b/mathics/core/convert/prettyprint.py index 83b21cf5a..e62ed604a 100644 --- a/mathics/core/convert/prettyprint.py +++ b/mathics/core/convert/prettyprint.py @@ -108,6 +108,7 @@ def holdform_expression_to_2d_text(expr, evaluation, form, **kwargs): def pre_pos_infix_expression_to_2d_text(expr, evaluation, form, **kwargs): + print("processing ", expr) elements = expr.elements if not (0 <= len(elements) <= 4): raise _WrongFormattedExpression @@ -201,7 +202,11 @@ def pre_pos_infix_expression_to_2d_text(expr, evaluation, form, **kwargs): for index, operand in enumerate(operands): operand_txt = expression_to_2d_text(operand, evaluation, form, **kwargs) cmp_precedence = compare_precedence(operand, precedence) - if cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized): + print("calling compare_precedence", [operand, precedence, cmp_precedence]) + if cmp_precedence is not None and ( + cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) + ): + print("->parenthesize") operand_txt = parenthesize(operand_txt) if index == 0: @@ -335,6 +340,7 @@ def plus_expression_to_2d_text(expr, evaluation, form, **kwargs): def power_expression_to_2d_text(expr, evaluation, form, **kwargs): + print("@@@@@@@@@@@power_expr") if len(expr.elements) != 2: return default_expression_to_2d_text(expr, evaluation, form, **kwargs) if kwargs.get("2d", False): @@ -497,6 +503,7 @@ def stringform_expression_to_2d_text(expr, evaluation, form, **kwargs): def superscript_expression_to_2d_text(expr, evaluation, form, **kwargs): + print("Superscript", [expr]) if len(expr.elements) != 2: raise _WrongFormattedExpression if kwargs.get("2d", False): diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index fd4e22c48..817f863b1 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -61,7 +61,6 @@ def compare_precedence( is higher, return 1, otherwise -1. If precedences cannot be compared, return None. """ - from mathics.builtin import builtins_precedence while element.has_form("HoldForm", 1): @@ -81,10 +80,7 @@ def compare_precedence( if element_prec is None: return None - - if element_prec == precedence: - return 0 - return 1 if element_prec > precedence else -1 + return 0 if element_prec == precedence else (1 if element_prec > precedence else -1) def eval_makeboxes(expr, evaluation, f=SymbolStandardForm): From 9595b079685f2687eb697efed5e18ff5aca476f0 Mon Sep 17 00:00:00 2001 From: mmatera Date: Thu, 12 Jan 2023 11:00:33 -0300 Subject: [PATCH 04/15] another little step --- mathics/builtin/arithfns/basic.py | 4 +- mathics/builtin/atomic/symbols.py | 2 +- mathics/core/convert/prettyprint.py | 400 ++++++++++++++++------------ mathics/core/prettyprint.py | 317 ++++++++++++++++++---- mathics/eval/makeboxes.py | 2 +- 5 files changed, 500 insertions(+), 225 deletions(-) diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index 394e165a4..69d4037dc 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -577,7 +577,9 @@ class Power(BinaryOperator, _MPMathFunction): 'Infix[{HoldForm[x], HoldForm[y]}, "^", 590, Right]' ), ("", "x_ ^ y_"): ( - "PrecedenceForm[Superscript[PrecedenceForm[HoldForm[x], 590]," + # "PrecedenceForm[Superscript[PrecedenceForm[HoldForm[x], 590]," + # " HoldForm[y]], 590]" + "PrecedenceForm[Superscript[HoldForm[x]," " HoldForm[y]], 590]" ), ("", "x_ ^ y_?Negative"): ( diff --git a/mathics/builtin/atomic/symbols.py b/mathics/builtin/atomic/symbols.py index 01f1ac0ce..c277828fe 100644 --- a/mathics/builtin/atomic/symbols.py +++ b/mathics/builtin/atomic/symbols.py @@ -556,7 +556,7 @@ def lhs(expr): return Expression(SymbolFormat, expr, Symbol(format)) def rhs(expr): - if expr.has_formf(SymbolInfix, None): + if expr.has_form(SymbolInfix, None): expr = Expression( Expression(SymbolHoldForm, expr.head), *expr.elements ) diff --git a/mathics/core/convert/prettyprint.py b/mathics/core/convert/prettyprint.py index e62ed604a..fff3b2a18 100644 --- a/mathics/core/convert/prettyprint.py +++ b/mathics/core/convert/prettyprint.py @@ -2,7 +2,7 @@ This module builts the 2D string associated to the OutputForm """ -from mathics.core.atoms import Integer, IntegerM1, Rational, Real, String +from mathics.core.atoms import Integer, Integer1, IntegerM1, Rational, Real, String from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression @@ -10,6 +10,7 @@ TextBlock, bracket, fraction, + grid, integral_definite, integral_indefinite, parenthesize, @@ -27,7 +28,7 @@ SymbolStandardForm, SymbolTraditionalForm, ) -from mathics.eval.makeboxes import compare_precedence, do_format +from mathics.eval.makeboxes import compare_precedence, do_format # , format_element SymbolNonAssociative = Symbol("System`NonAssociative") SymbolPostfix = Symbol("System`Postfix") @@ -47,7 +48,40 @@ class _WrongFormattedExpression(Exception): pass -def default_expression_to_2d_text(expr, evaluation, form, **kwargs): +class IsNotGrid(Exception): + pass + + +class IsNot2DArray(Exception): + pass + + +def expression_to_2d_text( + expr, evaluation: Evaluation, form=SymbolStandardForm, **kwargs +): + """ + Build a 2d text from an `Expression` + """ + ## TODO: format the expression + format_expr = do_format(expr, evaluation, SymbolOutputForm) + + # Strip HoldForm + while format_expr.has_form("HoldForm", 1): + format_expr = format_expr.elements[0] + + lookup_name = format_expr.get_head().get_lookup_name() + try: + return expr_to_2d_text_map[lookup_name](format_expr, evaluation, form, **kwargs) + 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_2d_text(format_expr, evaluation, form, **kwargs) + + +def _default_expression_to_2d_text(expr, evaluation, form, **kwargs): head = expression_to_2d_text(expr.head, evaluation) comma = TextBlock(", ") elements = [expression_to_2d_text(elem, evaluation) for elem in expr.elements] @@ -63,10 +97,8 @@ def default_expression_to_2d_text(expr, evaluation, form, **kwargs): def _divide(num, den, evaluation, form, **kwargs): if kwargs.get("2d", False): return fraction( - *( - expression_to_2d_text(item, evaluation, form, **kwargs) - for item in expr.elements - ) + expression_to_2d_text(num, evaluation, form, **kwargs), + expression_to_2d_text(den, evaluation, form, **kwargs), ) infix_form = Expression( SymbolInfix, ListExpression(num, den), String("/"), Integer(400), SymbolLeft @@ -74,6 +106,16 @@ def _divide(num, den, evaluation, form, **kwargs): return expression_to_2d_text(infix_form, evaluation, form, **kwargs) +def _strip_1_parm_expression_to_2d_text(expr, evaluation, form, **kwargs): + if len(expr.elements) != 1: + raise _WrongFormattedExpression + return expression_to_2d_text(expr.elements[0], evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`HoldForm"] = _strip_1_parm_expression_to_2d_text +expr_to_2d_text_map["System`InputForm"] = _strip_1_parm_expression_to_2d_text + + def divide_expression_to_2d_text(expr, evaluation, form, **kwargs): if len(expr.elements) != 2: raise _WrongFormattedExpression @@ -98,135 +140,33 @@ def graphics3d(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Graphics3D"] = graphics3d -def holdform_expression_to_2d_text(expr, evaluation, form, **kwargs): - if len(expr.elements) != 1: - raise _WrongFormattedExpression - return expression_to_2d_text(expr.elements[0], evaluation, form, **kwargs) - - -expr_to_2d_text_map["System`HoldForm"] = holdform_expression_to_2d_text - - -def pre_pos_infix_expression_to_2d_text(expr, evaluation, form, **kwargs): - print("processing ", expr) - 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 head in (SymbolPrefix, SymbolPostfix): - if len(operands) != 1: - raise _WrongFormattedExpression - elif head is SymbolInfix: - if len(operands) < 2: - raise _WrongFormattedExpression - else: - 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_2d_text(op, evaluation, form, **kwargs) - for op in ops.elements +def grid_expression_to_2d_text(expr, evaluation, form, **kwargs): + 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_2d_text(item_elem, evaluation, form, **kwargs) + for item_elem in item.elements ] - else: - ops_lst = [expression_to_2d_text(ops, evaluation, form, **kwargs)] - elif head in (SymbolPrefix, SymbolPostfix): - ops_txt = [expression_to_2d_text(ops, evaluation, form, **kwargs)] - else: - if head is SymbolInfix: - num_ops = 1 - default_symb = TextBlock(" ~ ") - ops_lst = [ - default_symb - + expression_to_2d_text(head, evaluation, form, **kwargs) - + default_symb - ] - elif head is SymbolPrefix: - default_symb = TextBlock(" @ ") - ops_txt = ( - expression_to_2d_text(head, evaluation, form, **kwargs) + default_symb - ) - elif head is SymbolPostfix: - default_symb = TextBlock(" // ") - ops_txt = default_symb + expression_to_2d_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_2d_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_2d_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 = expression_to_2d_text(operand, evaluation, form, **kwargs) - cmp_precedence = compare_precedence(operand, precedence) - print("calling compare_precedence", [operand, precedence, cmp_precedence]) - if cmp_precedence is not None and ( - cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) - ): - print("->parenthesize") - 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: - if ops_lst[index % num_ops].text != " ": - result = result + " " + ops_lst[index % num_ops] + " " + operand_txt - else: - result = result + " " + operand_txt + rows.append(expression_to_2d_text(item, evaluation, form, **kwargs)) - return result + return grid(rows) -expr_to_2d_text_map["System`Prefix"] = pre_pos_infix_expression_to_2d_text -expr_to_2d_text_map["System`Postfix"] = pre_pos_infix_expression_to_2d_text -expr_to_2d_text_map["System`Infix"] = pre_pos_infix_expression_to_2d_text +expr_to_2d_text_map["System`Grid"] = grid_expression_to_2d_text def integer_expression_to_2d_text(n, evaluation, form, **kwargs): @@ -239,7 +179,7 @@ def integer_expression_to_2d_text(n, evaluation, form, **kwargs): def integrate_expression_to_2d_text(expr, evaluation, form, **kwargs): elems = list(expr.elements) if len(elems) > 2 or not kwargs.get("2d", False): - return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + raise _WrongFormattedExpression result = expression_to_2d_text(elems.pop(0), evaluation, form, **kwargs) while elems: @@ -277,6 +217,25 @@ def list_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`List"] = list_expression_to_2d_text +def mathmlform_expression_to_2d_text(expr, evaluation, form, **kwargs): + # boxes = format_element(expr.elements[0], evaluation, form) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + ).evaluate(evaluation) + return TextBlock(boxes.boxes_to_mathml()) + + +expr_to_2d_text_map["System`MathMLForm"] = mathmlform_expression_to_2d_text + + +def matrixform_expression_to_2d_text(expr, evaluation, form, **kwargs): + # return parenthesize(tableform_expression_to_2d_text(expr, evaluation, form, **kwargs)) + return tableform_expression_to_2d_text(expr, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`MatrixForm"] = matrixform_expression_to_2d_text + + def plus_expression_to_2d_text(expr, evaluation, form, **kwargs): elements = expr.elements result = TextBlock("") @@ -340,9 +299,8 @@ def plus_expression_to_2d_text(expr, evaluation, form, **kwargs): def power_expression_to_2d_text(expr, evaluation, form, **kwargs): - print("@@@@@@@@@@@power_expr") if len(expr.elements) != 2: - return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + raise _WrongFormattedExpression if kwargs.get("2d", False): base, exponent = ( expression_to_2d_text(elem, evaluation, form, **kwargs) @@ -368,10 +326,129 @@ def power_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Power"] = power_expression_to_2d_text +def pre_pos_infix_expression_to_2d_text(expr, evaluation, form, **kwargs): + 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 head in (SymbolPrefix, SymbolPostfix): + if len(operands) != 1: + raise _WrongFormattedExpression + elif head is SymbolInfix: + if len(operands) < 2: + raise _WrongFormattedExpression + else: + 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_2d_text(op, evaluation, form, **kwargs) + for op in ops.elements + ] + else: + ops_lst = [expression_to_2d_text(ops, evaluation, form, **kwargs)] + elif head in (SymbolPrefix, SymbolPostfix): + ops_txt = [expression_to_2d_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolInfix: + num_ops = 1 + default_symb = TextBlock(" ~ ") + ops_lst = [ + default_symb + + expression_to_2d_text(head, evaluation, form, **kwargs) + + default_symb + ] + elif head is SymbolPrefix: + default_symb = TextBlock(" @ ") + ops_txt = ( + expression_to_2d_text(head, evaluation, form, **kwargs) + default_symb + ) + elif head is SymbolPostfix: + default_symb = TextBlock(" // ") + ops_txt = default_symb + expression_to_2d_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_2d_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_2d_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 = expression_to_2d_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: + if ops_lst[index % num_ops].text != " ": + result = result + " " + ops_lst[index % num_ops] + " " + operand_txt + else: + result = result + " " + operand_txt + + return result + + +expr_to_2d_text_map["System`Prefix"] = pre_pos_infix_expression_to_2d_text +expr_to_2d_text_map["System`Postfix"] = pre_pos_infix_expression_to_2d_text +expr_to_2d_text_map["System`Infix"] = pre_pos_infix_expression_to_2d_text + + def precedenceform_expression_to_2d_text(expr, evaluation, form, **kwargs): if len(expr.elements) == 2: return expression_to_2d_text(expr.elements[0], evaluation, form, **kwargs) - return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + raise _WrongFormattedExpression expr_to_2d_text_map["System`PrecedenceForm"] = precedenceform_expression_to_2d_text @@ -406,7 +483,7 @@ def sqrt_expression_to_2d_text(expr, evaluation, form, **kwargs): for item in expr.elements ) ) - return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + raise _WrongFormattedExpression expr_to_2d_text_map["System`Sqrt"] = sqrt_expression_to_2d_text @@ -422,7 +499,7 @@ def subscript_expression_to_2d_text(expr, evaluation, form, **kwargs): for item in expr.elements ) ) - return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + raise _WrongFormattedExpression expr_to_2d_text_map["System`Subscript"] = subscript_expression_to_2d_text @@ -438,7 +515,7 @@ def subsuperscript_expression_to_2d_text(expr, evaluation, form, **kwargs): for item in expr.elements ) ) - return default_expression_to_2d_text(expr, evaluation, form, **kwargs) + raise _WrongFormattedExpression expr_to_2d_text_map["System`Subsuperscript"] = subsuperscript_expression_to_2d_text @@ -469,17 +546,17 @@ def stringform_expression_to_2d_text(expr, evaluation, form, **kwargs): quote_open = True remaining = len(parts) - 1 + for part in parts[1:]: remaining -= 1 if quote_open: - if len(part) == 0: - result = result + items[curr_indx] - continue 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: @@ -495,6 +572,7 @@ def stringform_expression_to_2d_text(expr, evaluation, form, **kwargs): continue else: result = result + part + quote_open = True return result @@ -503,7 +581,6 @@ def stringform_expression_to_2d_text(expr, evaluation, form, **kwargs): def superscript_expression_to_2d_text(expr, evaluation, form, **kwargs): - print("Superscript", [expr]) if len(expr.elements) != 2: raise _WrongFormattedExpression if kwargs.get("2d", False): @@ -533,6 +610,24 @@ def symbol_expression_to_2d_text(symb, evaluation, form, **kwargs): expr_to_2d_text_map["System`Symbol"] = symbol_expression_to_2d_text +def tableform_expression_to_2d_text(expr, evaluation, form, **kwargs): + return grid_expression_to_2d_text(expr, evaluation, form) + + +expr_to_2d_text_map["System`TableForm"] = tableform_expression_to_2d_text + + +def texform_expression_to_2d_text(expr, evaluation, form, **kwargs): + # boxes = format_element(expr.elements[0], evaluation, form) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + ).evaluate(evaluation) + return TextBlock(boxes.boxes_to_tex()) + + +expr_to_2d_text_map["System`TeXForm"] = texform_expression_to_2d_text + + def times_expression_to_2d_text(expr, evaluation, form, **kwargs): elements = expr.elements num = [] @@ -600,24 +695,3 @@ def times_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Times"] = times_expression_to_2d_text - - -def expression_to_2d_text( - expr, evaluation: Evaluation, form=SymbolStandardForm, **kwargs -): - ## TODO: format the expression - format_expr = do_format(expr, evaluation, form) - # Strip HoldForm - while format_expr.has_form("HoldForm", 1): - format_expr = format_expr.elements[0] - - lookup_name = format_expr.get_head().get_lookup_name() - try: - return expr_to_2d_text_map[lookup_name](format_expr, evaluation, form, **kwargs) - 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_2d_text(format_expr, evaluation, form, **kwargs) diff --git a/mathics/core/prettyprint.py b/mathics/core/prettyprint.py index ff1096ca5..e3e8f8315 100644 --- a/mathics/core/prettyprint.py +++ b/mathics/core/prettyprint.py @@ -1,51 +1,74 @@ """ -This module produces a "pretty-print" inspired 2d text representation. +This module produces a "pretty-print" inspired 2d text representation. """ class TextBlock: - def __init__(self, text, padding=0, base=0, height=1, width=0): - self.height = height - self.padding = padding - text = text.replace("\t", " ") - lines = text.split("\n") - for line in lines: - width = max(len(line), width) + @staticmethod + def _build_attributes(lines, width=0, height=0, base=0): + width = max(width, max(len(line) for line in lines)) if lines else 0 + # complete lines: lines = [ line if len(line) == width else (line + (width - len(line)) * " ") for line in lines ] - self.width = width + padding - lines = [padding * " " + line for line in lines] + if base < 0: height = height - base - lines = (-base) * [width * " "] + lines + empty_line = width * " " + lines = (-base) * [empty_line] + lines base = -base if height > len(lines): - lines = lines + (height - len(lines)) * [width * " "] + empty_line = width * " " + lines = lines + (height - len(lines)) * [empty_line] else: height = len(lines) - self.height = height - self.text = "\n".join(lines) - self.base = base - def box(self): - top = "+" + self.width * "-" + "+" - out = "\n".join("|" + line + "|" for line in self.text.split("\n")) - out = top + "\n" + out + "\n" + top - return TextBlock(out, self.base + 1) + return (lines, width, height, base) + + def __init__(self, text, padding=0, base=0, height=1, width=0): + if isinstance(text, str): + if text == "": + lines = [] + else: + lines = text.split("\n") + else: + lines = sum((line.split("\n") for line in text), []) + if padding: + padding_spaces = padding * " " + lines = [padding_spaces + line.replace("\t", " ") for line in lines] + else: + lines = [line.replace("\t", " ") for line in lines] + + self.lines, self.width, self.height, self.base = self._build_attributes( + lines, width, height, base + ) + + @property + def text(self): + return "\n".join(self.lines) + + @text.setter + def text(self, value): + raise TypeError("TextBlock is inmutable") def __repr__(self): return self.text def __add__(self, tb): + result = TextBlock("") + result += self + result += tb + return result + + def __iadd__(self, tb): if isinstance(tb, str): tb = TextBlock(tb) base = self.base other_base = tb.base - left_lines = self.text.split("\n") - right_lines = tb.text.split("\n") + left_lines = self.lines + right_lines = tb.lines offset = other_base - base if offset > 0: left_lines = left_lines + offset * [self.width * " "] @@ -60,27 +83,26 @@ def __add__(self, tb): elif offset < 0: right_lines = (-offset) * [tb.width * " "] + right_lines - new_str = "\n".join(l + r for l, r in zip(left_lines, right_lines)) - return TextBlock(new_str, base=base) - - def join(self, iter): - result = TextBlock("") - for i, item in enumerate(iter): - if i == 0: - result = item - else: - result = result + self + item - return result + return TextBlock( + list(left + right for left, right in zip(left_lines, right_lines)), + base=base, + ) - def stack(self, other, align="c"): - if isinstance(other, str): - other = TextBlock(other) + def ajust_base(self, base): + """ + if base is larger than self.base, + adds lines at the bottom of the text + and update self.base + """ + if base > self.base: + diff = base - self.base + result = TextBlock( + self.lines + diff * [" "], self.width, self.height, self.base + ) - self_lines = self.text.split("\n") - other_lines = other.text.split("\n") - self_width, other_width = self.width, other.width - diff_width = self_width - other_width + return result + def ajust_width(self, width, align="c"): def padding(lines, diff): if diff > 0: if align == "c": @@ -95,23 +117,74 @@ def padding(lines, diff): lines = [(line + diff * " ") for line in lines] return lines - if diff_width > 0: - other_lines = padding(other_lines, diff_width) - elif diff_width < 0: - self_lines = padding(self_lines, -diff_width) + diff_width = width - self.width + if diff_width <= 0: + return self - lines = other_lines + self_lines - return TextBlock("\n".join(lines), base=self.base) + new_lines = padding(self.lines, diff_width) + return TextBlock(new_lines, base=self.base) + + def box(self): + top = "+" + self.width * "-" + "+" + out = "\n".join("|" + line + "|" for line in self.lines) + out = top + "\n" + out + "\n" + top + return TextBlock(out, self.base + 1) + + def join(self, iter): + result = TextBlock("") + for i, item in enumerate(iter): + if i == 0: + result = item + else: + result = result + self + item + return result + + def stack(self, top, align="c"): + if isinstance(top, str): + top = TextBlock(top) + + bottom = self + bottom_width, top_width = bottom.width, top.width + + if bottom_width > top_width: + top = top.ajust_width(bottom_width, align=align) + elif bottom_width < top_width: + bottom = bottom.ajust_width(top_width, align=align) + + return TextBlock(top.lines + bottom.lines, base=self.base) + + +def _draw_integral_symbol(height: int) -> TextBlock: + return TextBlock( + (" /+ \n" + "\n".join(height * [" | "]) + "\n+/ "), base=int((height + 1) / 2) + ) + + +def draw_vertical(pen: str, height, base=0, left_padding=0, right_padding=0): + """ + build a TextBlock with a vertical line of height `height` + using the string `pen`. If paddings are given, + spaces are added to the sides. + For example, `draw_vertical("=", 3)` produces + TextBlock(("=\n" + "=\n" + "=", base=base + ) + """ + pen = (left_padding * " ") + str(pen) + (right_padding * " ") + return TextBlock("\n".join(height * [pen]), base=base) def subsuperscript(base, a, b): if isinstance(base, str): base = TextBlock(base) + if isinstance(a, str): + a = TextBlock(a) if isinstance(b, str): b = TextBlock(b) - text2 = b.stack((base.height - 1) * "\n", align="l").stack(a, align="l") - text2.base = base.base + b.height + text2 = a.stack((base.height - 1) * "\n", align="l").stack(b, align="l") + text2.base = base.base + a.height return base + text2 @@ -125,7 +198,10 @@ def superscript(base, a): def subscript(base, a): if isinstance(a, str): a = TextBlock(a) - text2 = a.stack(TextBlock((base.height - 1) * "\n", base=base.base), align="l") + if isinstance(base, str): + base = TextBlock(base) + + text2 = a.stack(TextBlock(base.height * [""], base=base.base), align="l") text2.base = base.base + a.height return base + text2 @@ -170,20 +246,126 @@ def fraction(a, b): return result +def grid(items: list, **options) -> str: + result: TextBlock = TextBlock("") + + if not items: + return result + + # Ensure that items is a list + items = list(items) + # Ensure that all are TextBlock or list + items = [TextBlock(item) if isinstance(item, str) else item for item in items] + + # options + col_border = options.get("col_border", False) + row_border = options.get("row_border", False) + + # normalize widths: + widths: list = [1] + try: + widths = [1] * max( + len(item) for item in items if isinstance(item, (tuple, list)) + ) + except ValueError: + pass + + full_width: int = 0 + for row in items: + if isinstance(row, TextBlock): + full_width = max(full_width, row.width) + else: + for index, item in enumerate(row): + widths[index] = max(widths[index], item.width) + + total_width: int = sum(widths) + max(0, len(widths) - 1) * 3 + + if full_width > total_width: + widths[-1] = widths[-1] + full_width - total_width + total_width = full_width + + # Set the borders + + if row_border: + if col_border: + interline = TextBlock("+" + "+".join((w + 2) * "-" for w in widths) + "+") + else: + interline = TextBlock((sum(w + 3 for w in widths) - 2) * "-") + full_width = interline.width - 4 + else: + if col_border: + interline = ( + TextBlock("|") + + TextBlock("|".join((w + 2) * " " for w in widths)) + + TextBlock("|") + ) + full_width = max(0, interline.width - 4) + else: + interline = TextBlock((sum(w + 3 for w in widths) - 3) * " ") + full_width = max(0, interline.width - 4) + + def normalize_widths(row): + if isinstance(row, TextBlock): + return [row.ajust_width(max(0, full_width), align="l")] + return [item.ajust_width(widths[i]) for i, item in enumerate(row)] + + items = [normalize_widths(row) for row in items] + + if col_border: + for i, row in enumerate(items): + row_height: int = max(item.height for item in row) + row_base: int = max(item.base for item in row) + col_sep = draw_vertical( + "|", height=row_height, base=row_base, left_padding=1, right_padding=1 + ) + + new_row_txt = col_sep.join(row) + new_row_txt = ( + draw_vertical("|", row_height, base=row_base, right_padding=1) + + new_row_txt + + draw_vertical("|", row_height, base=row_base, left_padding=1) + ) + if i == 0: + if row_border: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt + else: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt.stack(result, align="l") + else: + for i, row in enumerate(items): + new_row_txt = TextBlock(" ").join(row) + if i == 0: + if row_border: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt + else: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt.stack(result, align="l") + + if row_border: + result = interline.stack(result, align="l") + + result.base = int(result.height / 2) + return result + + def integral_indefinite(integrand, var): + # TODO: handle list of vars + # TODO: use utf as an option if isinstance(var, str): var = TextBlock(var) + if isinstance(integrand, str): integrand = TextBlock(integrand) - height = integrand.height - int_symb = TextBlock( - " /+ \n" + "\n".join(height * [" | "]) + "\n +/ ", base=int((height + 1) / 2) - ) + int_symb: TextBlock = _draw_integral_symbol(integrand.height) return int_symb + integrand + " d" + var def integral_definite(integrand, var, a, b): + # TODO: handle list of vars + # TODO: use utf as an option if isinstance(var, str): var = TextBlock(var) if isinstance(integrand, str): @@ -193,10 +375,7 @@ def integral_definite(integrand, var, a, b): if isinstance(b, str): b = TextBlock(b) - height = integrand.height - int_symb = TextBlock( - " /+ \n" + "\n".join(height * [" | "]) + "\n +/ ", base=int((height + 1) / 2) - ) + int_symb = _draw_integral_symbol(integrand.height) return subsuperscript(int_symb, a, b) + " " + integrand + " d" + var @@ -224,9 +403,29 @@ def parenthesize(inner): left_br, right_br = TextBlock("("), TextBlock(")") else: left_br = TextBlock( - "/ \n" + "\n".join((height) * ["| "]) + "\n\\ ", base=inner.base + 1 + "/ \n" + "\n".join((height - 2) * ["| "]) + "\n\\ ", base=inner.base + ) + right_br = TextBlock( + " \\ \n" + "\n".join((height - 2) * [" |"]) + "\n /", base=inner.base + ) + return left_br + inner + right_br + + +def curly_braces(inner): + if isinstance(inner, str): + inner = TextBlock(inner) + height = inner.height + if height == 1: + left_br, right_br = TextBlock("{"), TextBlock("}") + else: + half_height = max(1, int((height - 3) / 2)) + half_line = "\n".join(half_height * [" |"]) + left_br = TextBlock( + "\n".join([" /", half_line, "< ", half_line, " \\"]), base=half_height + 1 ) + half_line = "\n".join(half_height * ["| "]) right_br = TextBlock( - " \\ \n" + "\n".join((height) * [" |"]) + "\n /", base=inner.base + 1 + "\n".join(["\\ ", half_line, " >", half_line, "/ "]), base=half_height + 1 ) + return left_br + inner + right_br diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 817f863b1..5b7fed10b 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -101,7 +101,7 @@ def format_element( from mathics.core.convert.prettyprint import expression_to_2d_text - if False and form is SymbolOutputForm: + if form is SymbolOutputForm: txt2d_form = expression_to_2d_text(element, evaluation, form, **kwargs) return String(txt2d_form.text) From 14889d7d73d518ecbb258e24634f296b48aba296 Mon Sep 17 00:00:00 2001 From: mmatera Date: Mon, 4 Nov 2024 15:59:03 -0300 Subject: [PATCH 05/15] partial --- mathics/builtin/arithfns/basic.py | 7 +- mathics/builtin/forms/output.py | 15 +-- mathics/builtin/layout.py | 12 ++- mathics/builtin/list/associations.py | 146 ++++++++++++++++++++------ mathics/builtin/makeboxes.py | 4 +- mathics/builtin/numbers/diffeqns.py | 4 +- mathics/builtin/recurrence.py | 8 +- mathics/core/builtin.py | 6 ++ mathics/eval/makeboxes.py | 62 +++++++++-- test/builtin/numbers/test_diffeqns.py | 10 +- test/format/test_format.py | 7 ++ 11 files changed, 214 insertions(+), 67 deletions(-) diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index b9a5e1566..08da3d38a 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -159,6 +159,7 @@ class Divide(BinaryOperator): grouping = "Left" operator = "/" + rules = { "Divide[x_, y_]": "Times[x, Power[y, -1]]", "MakeBoxes[Divide[x_, y_], f:StandardForm|TraditionalForm]": ( @@ -405,13 +406,11 @@ class Power(BinaryOperator, MPMathFunction): Expression(SymbolPattern, Symbol("x"), Expression(SymbolBlank)), RationalOneHalf, ): "HoldForm[Sqrt[x]]", - (("InputForm",), "x_ ^ y_"): ( + (("InputForm", "OutputForm"), "x_ ^ y_"): ( 'Infix[{HoldForm[x], HoldForm[y]}, "^", 590, Right]' ), ("", "x_ ^ y_"): ( - # "PrecedenceForm[Superscript[PrecedenceForm[HoldForm[x], 590]," - # " HoldForm[y]], 590]" - "PrecedenceForm[Superscript[HoldForm[x]," + "PrecedenceForm[Superscript[PrecedenceForm[HoldForm[x], 590]," " HoldForm[y]], 590]" ), ("", "x_ ^ y_?Negative"): ( diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index 056b40dd1..fe21fdb81 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -568,15 +568,16 @@ class OutputForm(FormBaseClass): = -Graphics- """ + formats = {"OutputForm[s_String]": "s"} summary_text = "plain-text output format" - def apply_makeboxes(self, expr, form, evaluation): - """MakeBoxes[OutputForm[expr_], form_]""" - text2d = expression_to_2d_text(expr, evaluation, form).text - elem1 = PaneBox(String(text2d)) - elem2 = Expression(SymbolOutputForm, expr) - result = InterpretationBox(elem1, elem2) - return result + # def apply_makeboxes(self, expr, form, evaluation): + # """MakeBoxes[OutputForm[expr_], form_]""" + # text2d = expression_to_2d_text(expr, evaluation, form).text + # elem1 = PaneBox(String(text2d)) + # elem2 = Expression(SymbolOutputForm, expr) + # result = InterpretationBox(elem1, elem2) + # return result class PythonForm(FormBaseClass): diff --git a/mathics/builtin/layout.py b/mathics/builtin/layout.py index 31ec0d399..0fef52f5a 100644 --- a/mathics/builtin/layout.py +++ b/mathics/builtin/layout.py @@ -16,7 +16,13 @@ from mathics.builtin.makeboxes import MakeBoxes from mathics.builtin.options import options_to_rules from mathics.core.atoms import Real, String -from mathics.core.builtin import BinaryOperator, Builtin, Operator +from mathics.core.builtin import ( + BinaryOperator, + Builtin, + Operator, + PostfixOperator, + PrefixOperator, +) from mathics.core.expression import Evaluation, Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol @@ -214,7 +220,7 @@ class NonAssociative(Builtin): summary_text = "non-associative operator" -class Postfix(BinaryOperator): +class Postfix(PostfixOperator): """ :WMA link:https://reference.wolfram.com/language/ref/Postfix.html @@ -294,7 +300,7 @@ class PrecedenceForm(Builtin): summary_text = "parenthesize with a precedence" -class Prefix(BinaryOperator): +class Prefix(PrefixOperator): """ :WMA link:https://reference.wolfram.com/language/ref/Prefix.html diff --git a/mathics/builtin/list/associations.py b/mathics/builtin/list/associations.py index f28930bc2..511c1a955 100644 --- a/mathics/builtin/list/associations.py +++ b/mathics/builtin/list/associations.py @@ -10,15 +10,83 @@ from mathics.builtin.box.layout import RowBox -from mathics.core.atoms import Integer +from mathics.builtin.layout import Row +from mathics.core.atoms import Integer, String from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED from mathics.core.builtin import Builtin, Test from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import Symbol, SymbolTrue -from mathics.core.systemsymbols import SymbolAssociation, SymbolMakeBoxes, SymbolMissing -from mathics.eval.lists import list_boxes +from mathics.core.systemsymbols import ( + SymbolAssociation, + SymbolHoldForm, + SymbolInputForm, + SymbolMakeBoxes, + SymbolMathMLForm, + SymbolMissing, + SymbolOutputForm, + SymbolStandardForm, + SymbolTeXForm, + SymbolTraditionalForm, +) +from mathics.eval.lists import list_boxes, riffle +from mathics.eval.makeboxes import do_format +from mathics.eval.strings import eval_ToString + + +class NotAnAssociationItem(Exception): + pass + + +SymbolInterpretation = Symbol("System`Interpretation") + +ASSOCIATION_DELIMITER_FORMATS = { + SymbolInputForm: {"start": String("<|"), "sep": String(", "), "end": String("|>")}, + SymbolOutputForm: {"start": String("<|"), "sep": String(","), "end": String("|>")}, + SymbolStandardForm: { + "start": String("<|"), + "sep": String(","), + "end": String("|>"), + }, + SymbolTraditionalForm: { + "start": String("<|"), + "sep": String(","), + "end": String("|>"), + }, + SymbolTeXForm: {"start": String("<|"), "sep": String(", "), "end": String("|>")}, + SymbolMathMLForm: {"start": String("<|"), "sep": String(","), "end": String("|>")}, +} + + +def format_association(rules: tuple, evaluation: Evaluation, form: Symbol): + """Association[rules___]""" + delimiters = ASSOCIATION_DELIMITER_FORMATS[form] + + def yield_rules(rule_tuple): + for rule in rule_tuple: + if rule.has_form(("Rule", "RuleDelayed"), 2): + yield rule + elif rule.has_form( + ( + "List", + "Association", + ), + None, + ): + for subrule in yield_rules(rule.elements): + yield subrule + else: + raise NotAnAssociationItem + + try: + items = riffle( + [do_format(rule, evaluation, form) for rule in yield_rules(rules)], + delimiters["sep"], + ) + return Row(to_mathics_list(delimiters["start"], *items, delimiters["end"])) + except NotAnAssociationItem: + return None class Association(Builtin): @@ -54,34 +122,52 @@ class Association(Builtin): summary_text = "an association between keys and values" - def eval_makeboxes(self, rules, f, evaluation: Evaluation): - """MakeBoxes[<|rules___|>, - f:StandardForm|TraditionalForm|OutputForm|InputForm]""" - - def validate(exprs): - for expr in exprs: - if expr.has_form(("Rule", "RuleDelayed"), 2): - pass - elif expr.has_form(("List", "Association"), None): - if not validate(expr.elements): - return False - else: - return False - return True - - rules = rules.get_sequence() - if self.error_idx == 0 and validate(rules) is True: - expr = RowBox(*list_boxes(rules, f, evaluation, "<|", "|>")) - else: - self.error_idx += 1 - symbol = Expression(SymbolMakeBoxes, SymbolAssociation, f) - expr = RowBox( - symbol.evaluate(evaluation), *list_boxes(rules, f, evaluation, "[", "]") + def format_association_input(self, rules, evaluation: Evaluation, expression): + """InputForm: Association[rules___]""" + print("format association input", rules) + formatted = format_association( + rules.get_sequence(), evaluation, SymbolInputForm + ) + if formatted is None: + return None + print(" formatted elements:") + elements = formatted.elements[0].elements + for elem in elements: + print(" ", elem) + elems = tuple( + ( + eval_ToString(elem, SymbolOutputForm, "unicode", evaluation).value + for elem in elements ) - - if self.error_idx > 0: - self.error_idx -= 1 - return expr + ) + elems = tuple((elem[1:-1] if elem[0] == '"' else elem for elem in elems)) + print(" elems", elems) + result_str = "".join(elems) + result = Expression(SymbolOutputForm, String(result_str)) + print(" result->", result) + return result + + def format_association_output(self, rules, evaluation: Evaluation): + """OutputForm: Association[rules___]""" + return format_association(rules.get_sequence(), evaluation, SymbolOutputForm) + + def format_association_standard(self, rules, evaluation: Evaluation): + """StandardForm: Association[rules___]""" + return format_association(rules.get_sequence(), evaluation, SymbolStandardForm) + + def format_association_traditional(self, rules, evaluation: Evaluation): + """TraditionalForm: Association[rules___]""" + return format_association( + rules.get_sequence(), evaluation, SymbolTraditionalForm + ) + + def format_association_tex(self, rules, evaluation: Evaluation): + """TeXForm: Association[rules___]""" + return format_association(rules.get_sequence(), evaluation, SymbolTeXForm) + + def format_association_mathml(self, rules, evaluation: Evaluation): + """MathMLForm: Association[rules___]""" + return format_association(rules.get_sequence(), evaluation, SymbolMathMLForm) def eval(self, rules, evaluation: Evaluation): "Association[rules__]" diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index e5afd9954..afdd32408 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -374,9 +374,9 @@ class MakeBoxes(Builtin): 'MakeBoxes[Infix[head[elements], StringForm["~`1`~", head]], f]' ), "MakeBoxes[expr_]": "MakeBoxes[expr, StandardForm]", - "MakeBoxes[(form:StandardForm|TraditionalForm|TeXForm|" + "MakeBoxes[(form:StandardForm|TraditionalForm|OutputForm|TeXForm|" "MathMLForm)[expr_], StandardForm|TraditionalForm]": ("MakeBoxes[expr, form]"), - "MakeBoxes[(form:StandardForm|MathMLForm|TeXForm)[expr_], OutputForm]": "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[PrecedenceForm[expr_, prec_], f_]": "MakeBoxes[expr, f]", "MakeBoxes[Style[expr_, OptionsPattern[Style]], f_]": ( diff --git a/mathics/builtin/numbers/diffeqns.py b/mathics/builtin/numbers/diffeqns.py index 4c8083e24..7c685f769 100644 --- a/mathics/builtin/numbers/diffeqns.py +++ b/mathics/builtin/numbers/diffeqns.py @@ -31,11 +31,11 @@ class DSolve(Builtin): = {{y[x] -> C[1] E ^ (-x) + C[2] E ^ x}} >> DSolve[y''[x] == y[x], y, x] - = {{y -> (Function[{x}, C[1] E ^ (-x) + C[2] E ^ x])}} + = {{y -> Function[{x}, C[1] E ^ (-x) + C[2] E ^ x]}} DSolve can also solve basic PDE >> DSolve[D[f[x, y], x] / f[x, y] + 3 D[f[x, y], y] / f[x, y] == 2, f, {x, y}] - = {{f -> (Function[{x, y}, E ^ (x / 5 + 3 y / 5) C[1][3 x - y]])}} + = {{f -> Function[{x, y}, E ^ (x / 5 + 3 y / 5) C[1][3 x - y]]}} >> DSolve[D[f[x, y], x] x + D[f[x, y], y] y == 2, f[x, y], {x, y}] = {{f[x, y] -> 2 Log[x] + C[1][y / x]}} diff --git a/mathics/builtin/recurrence.py b/mathics/builtin/recurrence.py index e4a166036..d48c49131 100644 --- a/mathics/builtin/recurrence.py +++ b/mathics/builtin/recurrence.py @@ -40,18 +40,18 @@ class RSolve(Builtin): No boundary conditions gives two general parameters: >> RSolve[{a[n + 2] == a[n]}, a, n] - = {{a -> (Function[{n}, C[0] + C[1] (-1) ^ n])}} + = {{a -> Function[{n}, C[0] + C[1] (-1) ^ n]}} Include one boundary condition: >> RSolve[{a[n + 2] == a[n], a[0] == 1}, a, n] = ... ## Order of terms depends on interpreter: - ## PyPy: {{a -> (Function[{n}, 1 - C[1] + C[1] -1 ^ n])}} - ## CPython: {{a -> (Function[{n}, 1 + C[1] -1 ^ n - C[1]])} + ## PyPy: {{a -> Function[{n}, 1 - C[1] + C[1] -1 ^ n]}} + ## CPython: {{a -> Function[{n}, 1 + C[1] -1 ^ n - C[1]]} Geta "pure function" solution for a with two boundary conditions: >> RSolve[{a[n + 2] == a[n], a[0] == 1, a[1] == 4}, a, n] - = {{a -> (Function[{n}, 5 / 2 - 3 (-1) ^ n / 2])}} + = {{a -> Function[{n}, 5 / 2 - 3 (-1) ^ n / 2]}} """ messages = { diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index d17e4b984..2d88ddea0 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -1167,6 +1167,12 @@ def __init__(self, *args, **kwargs): operator = ascii_operator_to_symbol.get(self.operator, self.__class__.__name__) if self.default_formats: + formats = { + op_pattern: "HoldForm[Infix[{%s}, %s, %d, %s]]" + % (replace_items, operator, self.precedence, self.grouping) + } + formats.update(self.formats) + self.formats = formats formatted = "MakeBoxes[Infix[{%s}, %s, %d,%s], form]" % ( replace_items, operator, diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 76c68ec09..4c09b8e94 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -33,6 +33,7 @@ ) from mathics.core.systemsymbols import ( SymbolComplex, + SymbolGrid, SymbolMinus, SymbolOutputForm, SymbolRational, @@ -211,6 +212,25 @@ def format_element( """ Applies formats associated to the expression, and then calls Makeboxes """ + 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): + return result_box + if isinstance(result_box, BoxElementMixin): + return result_box + else: + return format_element(element, evaluation, SymbolFullForm, **kwargs) + + +def new_format_element( + element: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> Optional[BaseElement]: + """ + Applies formats associated to the expression, and then calls Makeboxes + """ from mathics.core.convert.prettyprint import expression_to_2d_text @@ -252,7 +272,6 @@ def do_format_element( Applies formats associated to the expression and removes superfluous enclosing formats. """ - from mathics.core.definitions import OutputForms evaluation.inc_recursion_depth() @@ -275,6 +294,7 @@ def do_format_element( if include_form: expr = Expression(form, expr) return expr + # Repeated and RepeatedNull confuse the formatter, # so we need to hardlink their format rules: if head is SymbolRepeated: @@ -320,8 +340,8 @@ def format_expr(expr): formatted = format_expr(expr) if isinstance(expr, EvalMixin) else None if formatted is not None: - do_format = element_formatters.get(type(formatted), do_format_element) - result = do_format(formatted, evaluation, form) + do_format_fn = element_formatters.get(type(formatted), do_format_element) + result = do_format_fn(formatted, evaluation, form) if include_form and result is not None: result = Expression(form, result) return result @@ -338,8 +358,8 @@ def format_expr(expr): # just return it as it is. if len(expr.get_elements()) != 1: return expr - do_format = element_formatters.get(type(element), do_format_element) - result = do_format(expr, evaluation, form) + do_format_fn = element_formatters.get(type(element), do_format_element) + result = do_format_fn(expr, evaluation, form) if isinstance(result, Expression): expr = result @@ -349,12 +369,34 @@ def format_expr(expr): and head not in (SymbolGraphics, SymbolGraphics3D) ): # print("Not inside graphics or numberform, and not is atom") - new_elements = [ - element_formatters.get(type(element), do_format_element)( - element, evaluation, form + options = [] + new_elements = expr.get_elements() + option_values = head.get_option_values(evaluation, allow_symbols=True) + if option_values and elements: + num_elements = len(new_elements) + last_option_position = 1 + num_elems = len(expr.elements) + for last_option_position in range(num_elements): + next_position = last_option_position + 1 + if not elements[-next_position].has_form( + ["Rule", "RuleDelayed"], 2 + ): + break + last_option_position = next_position + if last_option_position: + options = elements[-last_option_position:] + new_elements = new_elements[:-last_option_position] + + new_elements = tuple( + ( + element_formatters.get(type(element), do_format_element)( + element, evaluation, form + ) + for element in new_elements ) - for element in expr.elements - ] + ) + if options: + new_elements = new_elements + options expr_head = expr.head do_format = element_formatters.get(type(expr_head), do_format_element) head = do_format(expr_head, evaluation, form) diff --git a/test/builtin/numbers/test_diffeqns.py b/test/builtin/numbers/test_diffeqns.py index 46a8579d7..2e0a7d75f 100644 --- a/test/builtin/numbers/test_diffeqns.py +++ b/test/builtin/numbers/test_diffeqns.py @@ -45,19 +45,19 @@ ( "DSolve[f'[x] == f[x], f, x] /. {C[1] -> 1}", None, - "{{f -> (Function[{x}, 1 E ^ x])}}", + "{{f -> Function[{x}, 1 E ^ x]}}", None, ), ( "DSolve[f'[x] == f[x], f, x] /. {C -> D}", None, - "{{f -> (Function[{x}, D[1] E ^ x])}}", + "{{f -> Function[{x}, D[1] E ^ x]}}", None, ), ( "DSolve[f'[x] == f[x], f, x] /. {C[1] -> C[0]}", None, - "{{f -> (Function[{x}, C[0] E ^ x])}}", + "{{f -> Function[{x}, C[0] E ^ x]}}", None, ), ( @@ -70,7 +70,7 @@ ( "DSolve[D[f[x, y], x] == D[f[x, y], y], f, {x, y}]", None, - "{{f -> (Function[{x, y}, C[1][-x - y]])}}", + "{{f -> Function[{x, y}, C[1][-x - y]]}}", None, ), ( @@ -88,7 +88,7 @@ ( "DSolve[\\[Gamma]'[x] == 0, \\[Gamma], x]", None, - "{{γ -> (Function[{x}, C[1]])}}", + "{{γ -> Function[{x}, C[1]]}}", "sympy #11669 test", ), ], diff --git a/test/format/test_format.py b/test/format/test_format.py index fe33f8be7..942a62594 100644 --- a/test/format/test_format.py +++ b/test/format/test_format.py @@ -774,6 +774,13 @@ }, } +# TODO: REMOVE ME ONCE THE ISSUE WITH FORMATTING ASSOCIATIONS GET SOLVED. +omit = tuple( + (key for key, val in all_test.items() if "Association" in val.get("msg", "")) +) +for key in omit: + del all_test[key] + def load_tests(key): """ From 331c9a72426a13828af879547e725bf89a5a3d81 Mon Sep 17 00:00:00 2001 From: mmatera Date: Mon, 4 Nov 2024 18:09:35 -0300 Subject: [PATCH 06/15] * Improving precedence comparison. * Improving handling for Infix, Prefix and Postfix format --- mathics/builtin/atomic/symbols.py | 2 +- mathics/builtin/layout.py | 12 +++- mathics/builtin/numbers/diffeqns.py | 4 +- mathics/builtin/patterns/rules.py | 4 +- mathics/builtin/recurrence.py | 8 +-- mathics/core/builtin.py | 13 ++++- mathics/eval/makeboxes.py | 84 ++++++++++++++++----------- test/builtin/numbers/test_diffeqns.py | 10 ++-- 8 files changed, 82 insertions(+), 55 deletions(-) diff --git a/mathics/builtin/atomic/symbols.py b/mathics/builtin/atomic/symbols.py index 401ae4fc0..7f5e4b32e 100644 --- a/mathics/builtin/atomic/symbols.py +++ b/mathics/builtin/atomic/symbols.py @@ -527,7 +527,7 @@ def lhs(expr): return Expression(SymbolFormat, expr, Symbol(format)) def rhs(expr): - if expr.has_formf(SymbolInfix, None): + if expr.has_form(SymbolInfix, None): expr = Expression( Expression(SymbolHoldForm, expr.head), *expr.elements ) diff --git a/mathics/builtin/layout.py b/mathics/builtin/layout.py index 31ec0d399..0fef52f5a 100644 --- a/mathics/builtin/layout.py +++ b/mathics/builtin/layout.py @@ -16,7 +16,13 @@ from mathics.builtin.makeboxes import MakeBoxes from mathics.builtin.options import options_to_rules from mathics.core.atoms import Real, String -from mathics.core.builtin import BinaryOperator, Builtin, Operator +from mathics.core.builtin import ( + BinaryOperator, + Builtin, + Operator, + PostfixOperator, + PrefixOperator, +) from mathics.core.expression import Evaluation, Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol @@ -214,7 +220,7 @@ class NonAssociative(Builtin): summary_text = "non-associative operator" -class Postfix(BinaryOperator): +class Postfix(PostfixOperator): """ :WMA link:https://reference.wolfram.com/language/ref/Postfix.html @@ -294,7 +300,7 @@ class PrecedenceForm(Builtin): summary_text = "parenthesize with a precedence" -class Prefix(BinaryOperator): +class Prefix(PrefixOperator): """ :WMA link:https://reference.wolfram.com/language/ref/Prefix.html diff --git a/mathics/builtin/numbers/diffeqns.py b/mathics/builtin/numbers/diffeqns.py index 4c8083e24..7c685f769 100644 --- a/mathics/builtin/numbers/diffeqns.py +++ b/mathics/builtin/numbers/diffeqns.py @@ -31,11 +31,11 @@ class DSolve(Builtin): = {{y[x] -> C[1] E ^ (-x) + C[2] E ^ x}} >> DSolve[y''[x] == y[x], y, x] - = {{y -> (Function[{x}, C[1] E ^ (-x) + C[2] E ^ x])}} + = {{y -> Function[{x}, C[1] E ^ (-x) + C[2] E ^ x]}} DSolve can also solve basic PDE >> DSolve[D[f[x, y], x] / f[x, y] + 3 D[f[x, y], y] / f[x, y] == 2, f, {x, y}] - = {{f -> (Function[{x, y}, E ^ (x / 5 + 3 y / 5) C[1][3 x - y]])}} + = {{f -> Function[{x, y}, E ^ (x / 5 + 3 y / 5) C[1][3 x - y]]}} >> DSolve[D[f[x, y], x] x + D[f[x, y], y] y == 2, f[x, y], {x, y}] = {{f[x, y] -> 2 Log[x] + C[1][y / x]}} diff --git a/mathics/builtin/patterns/rules.py b/mathics/builtin/patterns/rules.py index 57d65ed35..4571b51bc 100644 --- a/mathics/builtin/patterns/rules.py +++ b/mathics/builtin/patterns/rules.py @@ -503,11 +503,11 @@ class Rule_(BinaryOperator): = a """ - name = "Rule" - operator = "->" attributes = A_SEQUENCE_HOLD | A_PROTECTED grouping = "Right" + name = "Rule" needs_verbatim = True + operator = "->" summary_text = "a replacement rule" def eval_rule(self, elems, evaluation): diff --git a/mathics/builtin/recurrence.py b/mathics/builtin/recurrence.py index e4a166036..d48c49131 100644 --- a/mathics/builtin/recurrence.py +++ b/mathics/builtin/recurrence.py @@ -40,18 +40,18 @@ class RSolve(Builtin): No boundary conditions gives two general parameters: >> RSolve[{a[n + 2] == a[n]}, a, n] - = {{a -> (Function[{n}, C[0] + C[1] (-1) ^ n])}} + = {{a -> Function[{n}, C[0] + C[1] (-1) ^ n]}} Include one boundary condition: >> RSolve[{a[n + 2] == a[n], a[0] == 1}, a, n] = ... ## Order of terms depends on interpreter: - ## PyPy: {{a -> (Function[{n}, 1 - C[1] + C[1] -1 ^ n])}} - ## CPython: {{a -> (Function[{n}, 1 + C[1] -1 ^ n - C[1]])} + ## PyPy: {{a -> Function[{n}, 1 - C[1] + C[1] -1 ^ n]}} + ## CPython: {{a -> Function[{n}, 1 + C[1] -1 ^ n - C[1]]} Geta "pure function" solution for a with two boundary conditions: >> RSolve[{a[n + 2] == a[n], a[0] == 1, a[1] == 4}, a, n] - = {{a -> (Function[{n}, 5 / 2 - 3 (-1) ^ n / 2])}} + = {{a -> Function[{n}, 5 / 2 - 3 (-1) ^ n / 2]}} """ messages = { diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index d17e4b984..37d2e752a 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -1151,22 +1151,29 @@ def __init__(self, *args, **kwargs): # Prevent pattern matching symbols from gaining meaning here using # Verbatim - name = f"Verbatim[{name}]" + verbatim_name = f"Verbatim[{name}]" # For compatibility, allow grouping symbols in builtins to be # specified without System`. self.grouping = ensure_context(self.grouping) if self.grouping in ("System`None", "System`NonAssociative"): - op_pattern = f"{name}[items__]" + op_pattern = f"{verbatim_name}[items__]" replace_items = "items" else: - op_pattern = f"{name}[x_, y_]" + op_pattern = f"{verbatim_name}[x_, y_]" replace_items = "x, y" operator = ascii_operator_to_symbol.get(self.operator, self.__class__.__name__) if self.default_formats: + if name not in ("Rule", "RuleDelayed"): + formats = { + op_pattern: "HoldForm[Infix[{%s}, %s, %d, %s]]" + % (replace_items, operator, self.precedence, self.grouping) + } + formats.update(self.formats) + self.formats = formats formatted = "MakeBoxes[Infix[{%s}, %s, %d,%s], form]" % ( replace_items, operator, diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 8cb11043c..396992845 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -6,8 +6,7 @@ """ -import typing -from typing import Any, Callable, Dict, Optional, Type +from typing import Any, Callable, Dict, List, Optional, Type from mathics.core.atoms import Complex, Integer, Rational, Real, String, SymbolI from mathics.core.convert.expression import to_expression_with_specialization @@ -70,6 +69,35 @@ def _boxed_string(string: str, **options): return StyleBox(String(string), **options) +def compare_precedence( + element: BaseElement, precedence: Optional[int] = None +) -> Optional[int]: + """ + compare the precedence of the element regarding a precedence value. + If both precedences are equivalent, return 0. If precedence of element + is higher, return 1, otherwise -1. + If precedences cannot be compared, return None. + """ + while element.has_form("HoldForm", 1): + element = element.elements[0] + + if precedence is None: + return None + if element.has_form(("Infix", "Prefix", "Postfix"), 3, None): + element_prec = element.elements[2].value + elif element.has_form("PrecedenceForm", 2): + element_prec = element.elements[1].value + # For negative values, ensure that the element_precedence is at least the precedence. (Fixes #332) + elif isinstance(element, (Integer, Real)) and element.value < 0: + element_prec = precedence + else: + element_prec = builtins_precedence.get(element.get_head_name()) + + if element_prec is None: + return None + return 0 if element_prec == precedence else (1 if element_prec > precedence else -1) + + # 640 = sys.int_info.str_digits_check_threshold. # Someday when 3.11 is the minimum version of Python supported, # we can replace the magic value 640 below with sys.int.str_digits_check_threshold. @@ -211,7 +239,6 @@ def do_format_element( Applies formats associated to the expression and removes superfluous enclosing formats. """ - from mathics.core.definitions import OutputForms evaluation.inc_recursion_depth() @@ -234,6 +261,7 @@ def do_format_element( if include_form: expr = Expression(form, expr) return expr + # Repeated and RepeatedNull confuse the formatter, # so we need to hardlink their format rules: if head is SymbolRepeated: @@ -279,8 +307,8 @@ def format_expr(expr): formatted = format_expr(expr) if isinstance(expr, EvalMixin) else None if formatted is not None: - do_format = element_formatters.get(type(formatted), do_format_element) - result = do_format(formatted, evaluation, form) + do_format_fn = element_formatters.get(type(formatted), do_format_element) + result = do_format_fn(formatted, evaluation, form) if include_form and result is not None: result = Expression(form, result) return result @@ -297,8 +325,8 @@ def format_expr(expr): # just return it as it is. if len(expr.get_elements()) != 1: return expr - do_format = element_formatters.get(type(element), do_format_element) - result = do_format(expr, evaluation, form) + do_format_fn = element_formatters.get(type(element), do_format_element) + result = do_format_fn(expr, evaluation, form) if isinstance(result, Expression): expr = result @@ -307,13 +335,14 @@ def format_expr(expr): and not isinstance(expr, (Atom, BoxElementMixin)) and head not in (SymbolGraphics, SymbolGraphics3D) ): - # print("Not inside graphics or numberform, and not is atom") - new_elements = [ - element_formatters.get(type(element), do_format_element)( - element, evaluation, form + new_elements = tuple( + ( + element_formatters.get(type(element), do_format_element)( + element, evaluation, form + ) + for element in expr.elements ) - for element in expr.elements - ] + ) expr_head = expr.head do_format = element_formatters.get(type(expr_head), do_format_element) head = do_format(expr_head, evaluation, form) @@ -367,7 +396,7 @@ def do_format_complex( form, ) - parts: typing.List[Any] = [] + parts: List[Any] = [] if element.is_machine_precision() or not element.real.is_zero: parts.append(element.real) if element.imag.sameQ(Integer(1)): @@ -418,27 +447,12 @@ def parenthesize( If when_equal is True, parentheses will be added if the two precedence values are equal. """ - while element.has_form("HoldForm", 1): - element = element.elements[0] - - if element.has_form(("Infix", "Prefix", "Postfix"), 3, None): - element_prec = element.elements[2].value - elif element.has_form("PrecedenceForm", 2): - element_prec = element.elements[1].value - # If "element" is a negative number, we need to parenthesize the number. (Fixes #332) - elif isinstance(element, (Integer, Real)) and element.value < 0: - # Force parenthesis by adjusting the surrounding context's precedence value, - # We can't change the precedence for the number since it, doesn't - # have a precedence value. - element_prec = precedence - else: - element_prec = builtins_precedence.get(element.get_head()) - if precedence is not None and element_prec is not None: - if precedence > element_prec or (precedence == element_prec and when_equal): - return Expression( - SymbolRowBox, - ListExpression(StringLParen, element_boxes, StringRParen), - ) + cmp = compare_precedence(element, precedence) + if cmp is not None and (cmp == -1 or cmp == 0 and when_equal): + return Expression( + SymbolRowBox, + ListExpression(String("("), element_boxes, String(")")), + ) return element_boxes diff --git a/test/builtin/numbers/test_diffeqns.py b/test/builtin/numbers/test_diffeqns.py index 46a8579d7..2e0a7d75f 100644 --- a/test/builtin/numbers/test_diffeqns.py +++ b/test/builtin/numbers/test_diffeqns.py @@ -45,19 +45,19 @@ ( "DSolve[f'[x] == f[x], f, x] /. {C[1] -> 1}", None, - "{{f -> (Function[{x}, 1 E ^ x])}}", + "{{f -> Function[{x}, 1 E ^ x]}}", None, ), ( "DSolve[f'[x] == f[x], f, x] /. {C -> D}", None, - "{{f -> (Function[{x}, D[1] E ^ x])}}", + "{{f -> Function[{x}, D[1] E ^ x]}}", None, ), ( "DSolve[f'[x] == f[x], f, x] /. {C[1] -> C[0]}", None, - "{{f -> (Function[{x}, C[0] E ^ x])}}", + "{{f -> Function[{x}, C[0] E ^ x]}}", None, ), ( @@ -70,7 +70,7 @@ ( "DSolve[D[f[x, y], x] == D[f[x, y], y], f, {x, y}]", None, - "{{f -> (Function[{x, y}, C[1][-x - y]])}}", + "{{f -> Function[{x, y}, C[1][-x - y]]}}", None, ), ( @@ -88,7 +88,7 @@ ( "DSolve[\\[Gamma]'[x] == 0, \\[Gamma], x]", None, - "{{γ -> (Function[{x}, C[1]])}}", + "{{γ -> Function[{x}, C[1]]}}", "sympy #11669 test", ), ], From 03dc038aa32deca4d541142f12d4b02f165bc6f7 Mon Sep 17 00:00:00 2001 From: mmatera Date: Tue, 5 Nov 2024 13:43:08 -0300 Subject: [PATCH 07/15] partial --- mathics/builtin/forms/output.py | 15 +-- mathics/builtin/list/associations.py | 146 ++++++--------------------- mathics/builtin/patterns/rules.py | 4 +- mathics/core/builtin.py | 19 ++-- mathics/core/convert/prettyprint.py | 3 +- mathics/eval/makeboxes.py | 14 +++ test/format/test_format.py | 7 -- 7 files changed, 66 insertions(+), 142 deletions(-) diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index fe21fdb81..1b462041f 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -571,13 +571,14 @@ class OutputForm(FormBaseClass): formats = {"OutputForm[s_String]": "s"} summary_text = "plain-text output format" - # def apply_makeboxes(self, expr, form, evaluation): - # """MakeBoxes[OutputForm[expr_], form_]""" - # text2d = expression_to_2d_text(expr, evaluation, form).text - # elem1 = PaneBox(String(text2d)) - # elem2 = Expression(SymbolOutputForm, expr) - # result = InterpretationBox(elem1, elem2) - # return result + def eval_makeboxes(self, expr, form, evaluation): + """MakeBoxes[OutputForm[expr_], form_]""" + print(" eval Makeboxes outputform") + text2d = expression_to_2d_text(expr, evaluation, form).text + elem1 = PaneBox(String(text2d)) + elem2 = Expression(SymbolOutputForm, expr) + result = InterpretationBox(elem1, elem2) + return result class PythonForm(FormBaseClass): diff --git a/mathics/builtin/list/associations.py b/mathics/builtin/list/associations.py index 511c1a955..f28930bc2 100644 --- a/mathics/builtin/list/associations.py +++ b/mathics/builtin/list/associations.py @@ -10,83 +10,15 @@ from mathics.builtin.box.layout import RowBox -from mathics.builtin.layout import Row -from mathics.core.atoms import Integer, String +from mathics.core.atoms import Integer from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED from mathics.core.builtin import Builtin, Test from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import Symbol, SymbolTrue -from mathics.core.systemsymbols import ( - SymbolAssociation, - SymbolHoldForm, - SymbolInputForm, - SymbolMakeBoxes, - SymbolMathMLForm, - SymbolMissing, - SymbolOutputForm, - SymbolStandardForm, - SymbolTeXForm, - SymbolTraditionalForm, -) -from mathics.eval.lists import list_boxes, riffle -from mathics.eval.makeboxes import do_format -from mathics.eval.strings import eval_ToString - - -class NotAnAssociationItem(Exception): - pass - - -SymbolInterpretation = Symbol("System`Interpretation") - -ASSOCIATION_DELIMITER_FORMATS = { - SymbolInputForm: {"start": String("<|"), "sep": String(", "), "end": String("|>")}, - SymbolOutputForm: {"start": String("<|"), "sep": String(","), "end": String("|>")}, - SymbolStandardForm: { - "start": String("<|"), - "sep": String(","), - "end": String("|>"), - }, - SymbolTraditionalForm: { - "start": String("<|"), - "sep": String(","), - "end": String("|>"), - }, - SymbolTeXForm: {"start": String("<|"), "sep": String(", "), "end": String("|>")}, - SymbolMathMLForm: {"start": String("<|"), "sep": String(","), "end": String("|>")}, -} - - -def format_association(rules: tuple, evaluation: Evaluation, form: Symbol): - """Association[rules___]""" - delimiters = ASSOCIATION_DELIMITER_FORMATS[form] - - def yield_rules(rule_tuple): - for rule in rule_tuple: - if rule.has_form(("Rule", "RuleDelayed"), 2): - yield rule - elif rule.has_form( - ( - "List", - "Association", - ), - None, - ): - for subrule in yield_rules(rule.elements): - yield subrule - else: - raise NotAnAssociationItem - - try: - items = riffle( - [do_format(rule, evaluation, form) for rule in yield_rules(rules)], - delimiters["sep"], - ) - return Row(to_mathics_list(delimiters["start"], *items, delimiters["end"])) - except NotAnAssociationItem: - return None +from mathics.core.systemsymbols import SymbolAssociation, SymbolMakeBoxes, SymbolMissing +from mathics.eval.lists import list_boxes class Association(Builtin): @@ -122,52 +54,34 @@ class Association(Builtin): summary_text = "an association between keys and values" - def format_association_input(self, rules, evaluation: Evaluation, expression): - """InputForm: Association[rules___]""" - print("format association input", rules) - formatted = format_association( - rules.get_sequence(), evaluation, SymbolInputForm - ) - if formatted is None: - return None - print(" formatted elements:") - elements = formatted.elements[0].elements - for elem in elements: - print(" ", elem) - elems = tuple( - ( - eval_ToString(elem, SymbolOutputForm, "unicode", evaluation).value - for elem in elements + def eval_makeboxes(self, rules, f, evaluation: Evaluation): + """MakeBoxes[<|rules___|>, + f:StandardForm|TraditionalForm|OutputForm|InputForm]""" + + def validate(exprs): + for expr in exprs: + if expr.has_form(("Rule", "RuleDelayed"), 2): + pass + elif expr.has_form(("List", "Association"), None): + if not validate(expr.elements): + return False + else: + return False + return True + + rules = rules.get_sequence() + if self.error_idx == 0 and validate(rules) is True: + expr = RowBox(*list_boxes(rules, f, evaluation, "<|", "|>")) + else: + self.error_idx += 1 + symbol = Expression(SymbolMakeBoxes, SymbolAssociation, f) + expr = RowBox( + symbol.evaluate(evaluation), *list_boxes(rules, f, evaluation, "[", "]") ) - ) - elems = tuple((elem[1:-1] if elem[0] == '"' else elem for elem in elems)) - print(" elems", elems) - result_str = "".join(elems) - result = Expression(SymbolOutputForm, String(result_str)) - print(" result->", result) - return result - - def format_association_output(self, rules, evaluation: Evaluation): - """OutputForm: Association[rules___]""" - return format_association(rules.get_sequence(), evaluation, SymbolOutputForm) - - def format_association_standard(self, rules, evaluation: Evaluation): - """StandardForm: Association[rules___]""" - return format_association(rules.get_sequence(), evaluation, SymbolStandardForm) - - def format_association_traditional(self, rules, evaluation: Evaluation): - """TraditionalForm: Association[rules___]""" - return format_association( - rules.get_sequence(), evaluation, SymbolTraditionalForm - ) - - def format_association_tex(self, rules, evaluation: Evaluation): - """TeXForm: Association[rules___]""" - return format_association(rules.get_sequence(), evaluation, SymbolTeXForm) - - def format_association_mathml(self, rules, evaluation: Evaluation): - """MathMLForm: Association[rules___]""" - return format_association(rules.get_sequence(), evaluation, SymbolMathMLForm) + + if self.error_idx > 0: + self.error_idx -= 1 + return expr def eval(self, rules, evaluation: Evaluation): "Association[rules__]" diff --git a/mathics/builtin/patterns/rules.py b/mathics/builtin/patterns/rules.py index 57d65ed35..4571b51bc 100644 --- a/mathics/builtin/patterns/rules.py +++ b/mathics/builtin/patterns/rules.py @@ -503,11 +503,11 @@ class Rule_(BinaryOperator): = a """ - name = "Rule" - operator = "->" attributes = A_SEQUENCE_HOLD | A_PROTECTED grouping = "Right" + name = "Rule" needs_verbatim = True + operator = "->" summary_text = "a replacement rule" def eval_rule(self, elems, evaluation): diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index 2d88ddea0..37d2e752a 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -1151,28 +1151,29 @@ def __init__(self, *args, **kwargs): # Prevent pattern matching symbols from gaining meaning here using # Verbatim - name = f"Verbatim[{name}]" + verbatim_name = f"Verbatim[{name}]" # For compatibility, allow grouping symbols in builtins to be # specified without System`. self.grouping = ensure_context(self.grouping) if self.grouping in ("System`None", "System`NonAssociative"): - op_pattern = f"{name}[items__]" + op_pattern = f"{verbatim_name}[items__]" replace_items = "items" else: - op_pattern = f"{name}[x_, y_]" + op_pattern = f"{verbatim_name}[x_, y_]" replace_items = "x, y" operator = ascii_operator_to_symbol.get(self.operator, self.__class__.__name__) if self.default_formats: - formats = { - op_pattern: "HoldForm[Infix[{%s}, %s, %d, %s]]" - % (replace_items, operator, self.precedence, self.grouping) - } - formats.update(self.formats) - self.formats = formats + if name not in ("Rule", "RuleDelayed"): + formats = { + op_pattern: "HoldForm[Infix[{%s}, %s, %d, %s]]" + % (replace_items, operator, self.precedence, self.grouping) + } + formats.update(self.formats) + self.formats = formats formatted = "MakeBoxes[Infix[{%s}, %s, %d,%s], form]" % ( replace_items, operator, diff --git a/mathics/core/convert/prettyprint.py b/mathics/core/convert/prettyprint.py index e7bba39b9..42005b44d 100644 --- a/mathics/core/convert/prettyprint.py +++ b/mathics/core/convert/prettyprint.py @@ -181,7 +181,8 @@ def integrate_expression_to_2d_text(expr, evaluation, form, **kwargs): if len(elems) > 2 or not kwargs.get("2d", False): raise _WrongFormattedExpression - result = expression_to_2d_text(elems.pop(0), evaluation, form, **kwargs) + integrand = elems.pop(0) + result = expression_to_2d_text(integrand, evaluation, form, **kwargs) while elems: var = elems.pop(0) if var.has_form("List", 3): diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 4c09b8e94..45b27d0a0 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -206,12 +206,26 @@ def eval_makeboxes( return Expression(SymbolMakeBoxes, expr, form).evaluate(evaluation) +def make_output_form(expr, evaluation, form): + """ """ + from mathics.builtin.box.layout import InterpretationBox, PaneBox + from mathics.core.convert.prettyprint import expression_to_2d_text + + text2d = expression_to_2d_text(expr, evaluation, form, **{"2d": True}).text + elem1 = PaneBox(String(8 * " " + text2d)) + elem2 = Expression(SymbolOutputForm, expr) + return InterpretationBox(elem1, elem2) + + def format_element( element: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs ) -> Optional[BaseElement]: """ Applies formats associated to the expression, and then calls Makeboxes """ + if element.has_form("OutputForm", 1): + return make_output_form(element.elements[0], evaluation, form) + expr = do_format(element, evaluation, form) if expr is None: return None diff --git a/test/format/test_format.py b/test/format/test_format.py index 942a62594..fe33f8be7 100644 --- a/test/format/test_format.py +++ b/test/format/test_format.py @@ -774,13 +774,6 @@ }, } -# TODO: REMOVE ME ONCE THE ISSUE WITH FORMATTING ASSOCIATIONS GET SOLVED. -omit = tuple( - (key for key, val in all_test.items() if "Association" in val.get("msg", "")) -) -for key in omit: - del all_test[key] - def load_tests(key): """ From c37afe75ca88ff300f6a653eaa8e35b2770cc2dc Mon Sep 17 00:00:00 2001 From: mmatera Date: Tue, 5 Nov 2024 13:52:04 -0300 Subject: [PATCH 08/15] rocky's observations --- mathics/builtin/layout.py | 8 +------- mathics/builtin/recurrence.py | 2 +- mathics/eval/makeboxes.py | 4 ++-- test/builtin/numbers/test_diffeqns.py | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/mathics/builtin/layout.py b/mathics/builtin/layout.py index 0fef52f5a..bfa4bcaa8 100644 --- a/mathics/builtin/layout.py +++ b/mathics/builtin/layout.py @@ -16,13 +16,7 @@ from mathics.builtin.makeboxes import MakeBoxes from mathics.builtin.options import options_to_rules from mathics.core.atoms import Real, String -from mathics.core.builtin import ( - BinaryOperator, - Builtin, - Operator, - PostfixOperator, - PrefixOperator, -) +from mathics.core.builtin import Builtin, Operator, PostfixOperator, PrefixOperator from mathics.core.expression import Evaluation, Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol diff --git a/mathics/builtin/recurrence.py b/mathics/builtin/recurrence.py index d48c49131..019029aab 100644 --- a/mathics/builtin/recurrence.py +++ b/mathics/builtin/recurrence.py @@ -49,7 +49,7 @@ class RSolve(Builtin): ## PyPy: {{a -> Function[{n}, 1 - C[1] + C[1] -1 ^ n]}} ## CPython: {{a -> Function[{n}, 1 + C[1] -1 ^ n - C[1]]} - Geta "pure function" solution for a with two boundary conditions: + Get a "pure function" solution for a with two boundary conditions: >> RSolve[{a[n + 2] == a[n], a[0] == 1, a[1] == 4}, a, n] = {{a -> Function[{n}, 5 / 2 - 3 (-1) ^ n / 2]}} """ diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 396992845..5437d039d 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -74,8 +74,8 @@ def compare_precedence( ) -> Optional[int]: """ compare the precedence of the element regarding a precedence value. - If both precedences are equivalent, return 0. If precedence of element - is higher, return 1, otherwise -1. + If both precedences are equal, return 0. If precedence of the + first element is higher, return 1, otherwise -1. If precedences cannot be compared, return None. """ while element.has_form("HoldForm", 1): diff --git a/test/builtin/numbers/test_diffeqns.py b/test/builtin/numbers/test_diffeqns.py index 2e0a7d75f..7f445957a 100644 --- a/test/builtin/numbers/test_diffeqns.py +++ b/test/builtin/numbers/test_diffeqns.py @@ -66,7 +66,7 @@ "DSolve[f[x] == 0, f, {}]", None, ), - ## Order of arguments shoudn't matter + # # Order of arguments shoudn't matter ( "DSolve[D[f[x, y], x] == D[f[x, y], y], f, {x, y}]", None, From 845fb9993356cd96bd1f5f2e5516f4ba16547e3d Mon Sep 17 00:00:00 2001 From: mmatera Date: Tue, 5 Nov 2024 19:34:15 -0300 Subject: [PATCH 09/15] working --- mathics/builtin/arithfns/basic.py | 38 ++++- mathics/builtin/forms/output.py | 3 +- mathics/builtin/forms/variables.py | 38 ++++- mathics/core/atoms.py | 1 + mathics/core/convert/prettyprint.py | 168 ++++++++++++++++----- mathics/core/prettyprint.py | 220 ++++++++++++++++------------ mathics/eval/makeboxes.py | 16 +- test/format/test_2d.py | 44 ++++++ 8 files changed, 385 insertions(+), 143 deletions(-) create mode 100644 test/format/test_2d.py diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index 08da3d38a..c3b8559da 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -14,6 +14,7 @@ Integer1, Integer3, Integer310, + Integer400, IntegerM1, Number, Rational, @@ -48,6 +49,7 @@ SymbolNull, SymbolPower, SymbolTimes, + SymbolTrue, ) from mathics.core.systemsymbols import ( SymbolBlank, @@ -152,7 +154,7 @@ class Divide(BinaryOperator): default_formats = False formats = { - (("InputForm", "OutputForm"), "Divide[x_, y_]"): ( + ("InputForm", "Divide[x_, y_]"): ( 'Infix[{HoldForm[x], HoldForm[y]}, "/", 400, Left]' ), } @@ -166,9 +168,26 @@ class Divide(BinaryOperator): "FractionBox[MakeBoxes[x, f], MakeBoxes[y, f]]" ), } - summary_text = "divide" + def format_outputform(self, x, y, evaluation): + "OutputForm: Divide[x_, y_]" + use_2d = ( + evaluation.definitions.get_ownvalues("System`$Use2DOutputForm")[0].replace + is SymbolTrue + ) + if not use_2d: + return Expression( + SymbolInfix, + ListExpression( + Expression(SymbolHoldForm, x), Expression(SymbolHoldForm, y) + ), + String("/"), + Integer400, + SymbolLeft, + ) + return None + class Minus(PrefixOperator): """ @@ -406,10 +425,21 @@ class Power(BinaryOperator, MPMathFunction): Expression(SymbolPattern, Symbol("x"), Expression(SymbolBlank)), RationalOneHalf, ): "HoldForm[Sqrt[x]]", - (("InputForm", "OutputForm"), "x_ ^ y_"): ( + (("InputForm",), "x_ ^ y_"): ( 'Infix[{HoldForm[x], HoldForm[y]}, "^", 590, Right]' ), - ("", "x_ ^ y_"): ( + (("OutputForm",), "x_ ^ y_"): ( + "If[$Use2DOutputForm, " + "Superscript[HoldForm[x], HoldForm[y]], " + 'Infix[{HoldForm[x], HoldForm[y]}, "^", 590, Right]]' + ), + ( + ( + "StandardForm", + "TraditionalForm", + ), + "x_ ^ y_", + ): ( "PrecedenceForm[Superscript[PrecedenceForm[HoldForm[x], 590]," " HoldForm[y]], 590]" ), diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index 1b462041f..f7695ea59 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -61,6 +61,7 @@ SymbolOutputForm, SymbolRowBox, SymbolRuleDelayed, + SymbolStandardForm, SymbolSubscriptBox, SymbolSuperscriptBox, ) @@ -724,7 +725,7 @@ class TeXForm(FormBaseClass): def eval_tex(self, expr, evaluation) -> Expression: "MakeBoxes[expr_, TeXForm]" - boxes = MakeBoxes(expr).evaluate(evaluation) + boxes = format_element(expr, evaluation, SymbolStandardForm) try: # Here we set ``show_string_characters`` to False, to reproduce # the standard behaviour in WMA. Remove this parameter to recover the diff --git a/mathics/builtin/forms/variables.py b/mathics/builtin/forms/variables.py index b02500f0d..8b7e8e81e 100644 --- a/mathics/builtin/forms/variables.py +++ b/mathics/builtin/forms/variables.py @@ -3,11 +3,47 @@ """ -from mathics.core.attributes import A_LOCKED, A_PROTECTED +from mathics.core.attributes import A_LOCKED, A_NO_ATTRIBUTES, A_PROTECTED from mathics.core.builtin import Predefined from mathics.core.list import ListExpression +class Use2DOutputForm_(Predefined): + r""" +
+
'$Use2DOutputForm' +
internal variable that controls if 'OutputForm[expr]' is shown \ + in one line (standard Mathics behavior) or \ + or in a prettyform-like multiline output (the standard way in WMA). + The default value is 'False', keeping the standard Mathics behavior. +
+ + >> $Use2DOutputForm + = False + >> OutputForm[a^b] + = a ^ b + >> $Use2DOutputForm = True; OutputForm[a ^ b] + = + . b + . a + + Notice that without the 'OutputForm' wrapper, we fall back to the normal + behavior: + >> a ^ b + = Superscript[a, b] + Setting the variable back to False go back to the normal behavior: + >> $Use2DOutputForm = False; OutputForm[a ^ b] + = a ^ b + """ + + attributes = A_NO_ATTRIBUTES + name = "$Use2DOutputForm" + rules = { + "$Use2DOutputForm": "False", + } + summary_text = "use the 2D OutputForm" + + class PrintForms_(Predefined): r"""
diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py index ad98756bc..c1aa0a4df 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -330,6 +330,7 @@ def is_zero(self) -> bool: Integer2 = Integer(2) Integer3 = Integer(3) Integer310 = Integer(310) +Integer400 = Integer(400) Integer10 = Integer(10) IntegerM1 = Integer(-1) diff --git a/mathics/core/convert/prettyprint.py b/mathics/core/convert/prettyprint.py index 42005b44d..59d2be18b 100644 --- a/mathics/core/convert/prettyprint.py +++ b/mathics/core/convert/prettyprint.py @@ -2,7 +2,16 @@ This module builts the 2D string associated to the OutputForm """ -from mathics.core.atoms import Integer, Integer1, IntegerM1, Rational, Real, String +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 @@ -21,6 +30,7 @@ ) from mathics.core.symbols import Atom, Symbol, SymbolTimes from mathics.core.systemsymbols import ( + SymbolDerivative, SymbolInfix, SymbolNone, SymbolOutputForm, @@ -81,8 +91,13 @@ def expression_to_2d_text( return _default_expression_to_2d_text(format_expr, evaluation, form, **kwargs) -def _default_expression_to_2d_text(expr, evaluation, form, **kwargs): - head = expression_to_2d_text(expr.head, evaluation) +def _default_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + """ + Default representation of a function + """ + head = expression_to_2d_text(expr.head, evaluation, form, **kwargs) comma = TextBlock(", ") elements = [expression_to_2d_text(elem, evaluation) for elem in expr.elements] result = elements.pop(0) if elements else TextBlock(" ") @@ -106,7 +121,9 @@ def _divide(num, den, evaluation, form, **kwargs): return expression_to_2d_text(infix_form, evaluation, form, **kwargs) -def _strip_1_parm_expression_to_2d_text(expr, evaluation, form, **kwargs): +def _strip_1_parm_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: if len(expr.elements) != 1: raise _WrongFormattedExpression return expression_to_2d_text(expr.elements[0], evaluation, form, **kwargs) @@ -116,7 +133,50 @@ def _strip_1_parm_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`InputForm"] = _strip_1_parm_expression_to_2d_text -def divide_expression_to_2d_text(expr, evaluation, form, **kwargs): +def derivative_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + """Derivative operator""" + head = expr.get_head() + if head is SymbolDerivative: + return _default_expression_to_2d_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_2d_text(expr, evaluation, form, **kwargs) + function_head = expression_to_2d_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 + "''" + + if not kwargs["2d"]: + return _default_expression_to_2d_text(expr, evaluation, form, **kwargs) + + superscript_tb = TextBlock(",").join( + expression_to_2d_text(order, evaluation, form, **kwargs) + for order in derivatives + ) + superscript_tb = parenthesize(superscript_tb) + return superscript(function_head, superscript_tb) + + # Full Function with arguments: delegate to the default conversion. + # It will call us again with the head + return _default_expression_to_2d_text(expr, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Derivative"] = derivative_expression_to_2d_text + + +def divide_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: if len(expr.elements) != 2: raise _WrongFormattedExpression num, den = expr.elements @@ -126,21 +186,27 @@ def divide_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Divide"] = divide_expression_to_2d_text -def graphics(expr, evaluation, form, **kwargs): +def graphics( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: return TextBlock("-Graphics-") expr_to_2d_text_map["System`Graphics"] = graphics -def graphics3d(expr, evaluation, form, **kwargs): +def graphics3d( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: return TextBlock("-Graphics3D-") expr_to_2d_text_map["System`Graphics3D"] = graphics3d -def grid_expression_to_2d_text(expr, evaluation, form, **kwargs): +def grid_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: if len(expr.elements) == 0: raise IsNotGrid if len(expr.elements) > 1 and not expr.elements[1].has_form( @@ -176,7 +242,9 @@ def integer_expression_to_2d_text(n, evaluation, form, **kwargs): expr_to_2d_text_map["System`Integer"] = integer_expression_to_2d_text -def integrate_expression_to_2d_text(expr, evaluation, form, **kwargs): +def integrate_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: elems = list(expr.elements) if len(elems) > 2 or not kwargs.get("2d", False): raise _WrongFormattedExpression @@ -202,7 +270,9 @@ def integrate_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Integrate"] = integrate_expression_to_2d_text -def list_expression_to_2d_text(expr, evaluation, form, **kwargs): +def list_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: return ( TextBlock("{") + TextBlock(", ").join( @@ -218,7 +288,7 @@ def list_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`List"] = list_expression_to_2d_text -def mathmlform_expression_to_2d_text(expr, evaluation, form, **kwargs): +def mathmlform_expression_to_2d_text(expr, evaluation, form, **kwargs) -> TextBlock: # boxes = format_element(expr.elements[0], evaluation, form) boxes = Expression( Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm @@ -229,7 +299,7 @@ def mathmlform_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`MathMLForm"] = mathmlform_expression_to_2d_text -def matrixform_expression_to_2d_text(expr, evaluation, form, **kwargs): +def matrixform_expression_to_2d_text(expr, evaluation, form, **kwargs) -> TextBlock: # return parenthesize(tableform_expression_to_2d_text(expr, evaluation, form, **kwargs)) return tableform_expression_to_2d_text(expr, evaluation, form, **kwargs) @@ -237,7 +307,9 @@ def matrixform_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`MatrixForm"] = matrixform_expression_to_2d_text -def plus_expression_to_2d_text(expr, evaluation, form, **kwargs): +def plus_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: elements = expr.elements result = TextBlock("") for i, elem in enumerate(elements): @@ -299,7 +371,9 @@ def plus_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Plus"] = plus_expression_to_2d_text -def power_expression_to_2d_text(expr, evaluation, form, **kwargs): +def power_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +): if len(expr.elements) != 2: raise _WrongFormattedExpression if kwargs.get("2d", False): @@ -307,10 +381,7 @@ def power_expression_to_2d_text(expr, evaluation, form, **kwargs): expression_to_2d_text(elem, evaluation, form, **kwargs) for elem in expr.elements ) - base_precedence = builtins_precedence.get( - expr.elements[0].get_head_name(), None - ) - if compare_precedence(expr.elements[0], 590) == -1: + if (compare_precedence(expr.elements[0], 590) or 1) == -1: base = parenthesize(base) return superscript(base, exponent) @@ -327,7 +398,9 @@ def power_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Power"] = power_expression_to_2d_text -def pre_pos_infix_expression_to_2d_text(expr, evaluation, form, **kwargs): +def pre_pos_infix_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: elements = expr.elements if not (0 <= len(elements) <= 4): raise _WrongFormattedExpression @@ -446,7 +519,9 @@ def pre_pos_infix_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Infix"] = pre_pos_infix_expression_to_2d_text -def precedenceform_expression_to_2d_text(expr, evaluation, form, **kwargs): +def precedenceform_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: if len(expr.elements) == 2: return expression_to_2d_text(expr.elements[0], evaluation, form, **kwargs) raise _WrongFormattedExpression @@ -467,14 +542,16 @@ def rational_expression_to_2d_text(n, evaluation, form, **kwargs): def real_expression_to_2d_text(n, evaluation, form, **kwargs): - str_n = n.make_boxes("System`OutputForm").value + str_n = n.make_boxes("System`OutputForm").boxes_to_text() return TextBlock(str(str_n)) expr_to_2d_text_map["System`Real"] = real_expression_to_2d_text -def sqrt_expression_to_2d_text(expr, evaluation, form, **kwargs): +def sqrt_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: if not 1 <= len(expr.elements) <= 2: raise _WrongFormattedExpression if kwargs.get("2d", False): @@ -490,7 +567,9 @@ def sqrt_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Sqrt"] = sqrt_expression_to_2d_text -def subscript_expression_to_2d_text(expr, evaluation, form, **kwargs): +def subscript_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: if len(expr.elements) != 2: raise _WrongFormattedExpression if kwargs.get("2d", False): @@ -506,7 +585,9 @@ def subscript_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Subscript"] = subscript_expression_to_2d_text -def subsuperscript_expression_to_2d_text(expr, evaluation, form, **kwargs): +def subsuperscript_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: if len(expr.elements) != 3: raise _WrongFormattedExpression if kwargs.get("2d", False): @@ -522,14 +603,18 @@ def subsuperscript_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`Subsuperscript"] = subsuperscript_expression_to_2d_text -def string_expression_to_2d_text(expr, evaluation, form, **kwargs): +def string_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: return TextBlock(expr.value) expr_to_2d_text_map["System`String"] = string_expression_to_2d_text -def stringform_expression_to_2d_text(expr, evaluation, form, **kwargs): +def stringform_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: strform = expr.elements[0] if not isinstance(strform, String): raise _WrongFormattedExpression @@ -581,16 +666,21 @@ def stringform_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`StringForm"] = stringform_expression_to_2d_text -def superscript_expression_to_2d_text(expr, evaluation, form, **kwargs): - if len(expr.elements) != 2: +def superscript_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + elements = expr.elements + if len(elements) != 2: raise _WrongFormattedExpression if kwargs.get("2d", False): - return superscript( - *( - expression_to_2d_text(item, evaluation, form, **kwargs) - for item in expr.elements - ) + base, exponent = elements + base_tb, exponent_tb = ( + expression_to_2d_text(item, evaluation, form, **kwargs) for item in elements ) + precedence = compare_precedence(base, 590) or 1 + if precedence < 0: + base_tb = parenthesize(base_tb) + return superscript(base_tb, exponent_tb) infix_form = Expression( SymbolInfix, ListExpression(*(expr.elements)), @@ -611,14 +701,18 @@ def symbol_expression_to_2d_text(symb, evaluation, form, **kwargs): expr_to_2d_text_map["System`Symbol"] = symbol_expression_to_2d_text -def tableform_expression_to_2d_text(expr, evaluation, form, **kwargs): +def tableform_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: return grid_expression_to_2d_text(expr, evaluation, form) expr_to_2d_text_map["System`TableForm"] = tableform_expression_to_2d_text -def texform_expression_to_2d_text(expr, evaluation, form, **kwargs): +def texform_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: # boxes = format_element(expr.elements[0], evaluation, form) boxes = Expression( Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm @@ -629,7 +723,9 @@ def texform_expression_to_2d_text(expr, evaluation, form, **kwargs): expr_to_2d_text_map["System`TeXForm"] = texform_expression_to_2d_text -def times_expression_to_2d_text(expr, evaluation, form, **kwargs): +def times_expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: elements = expr.elements num = [] den = [] diff --git a/mathics/core/prettyprint.py b/mathics/core/prettyprint.py index e3e8f8315..1eb86ce0d 100644 --- a/mathics/core/prettyprint.py +++ b/mathics/core/prettyprint.py @@ -2,6 +2,8 @@ This module produces a "pretty-print" inspired 2d text representation. """ +from typing import List, Optional, Union + class TextBlock: @staticmethod @@ -63,6 +65,7 @@ def __add__(self, tb): return result def __iadd__(self, tb): + """In-place addition""" if isinstance(tb, str): tb = TextBlock(tb) base = self.base @@ -160,7 +163,45 @@ def _draw_integral_symbol(height: int) -> TextBlock: ) -def draw_vertical(pen: str, height, base=0, left_padding=0, right_padding=0): +def bracket(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + height = inner.height + if height == 1: + left_br, right_br = TextBlock("["), TextBlock("]") + else: + left_br = TextBlock( + "+-\n" + "\n".join((height) * ["| "]) + "\n+-", base=inner.base + 1 + ) + right_br = TextBlock( + "-+ \n" + "\n".join((height) * [" |"]) + "\n-+", base=inner.base + 1 + ) + return left_br + inner + right_br + + +def curly_braces(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + height = inner.height + if height == 1: + left_br, right_br = TextBlock("{"), TextBlock("}") + else: + half_height = max(1, int((height - 3) / 2)) + half_line = "\n".join(half_height * [" |"]) + left_br = TextBlock( + "\n".join([" /", half_line, "< ", half_line, " \\"]), base=half_height + 1 + ) + half_line = "\n".join(half_height * ["| "]) + right_br = TextBlock( + "\n".join(["\\ ", half_line, " >", half_line, "/ "]), base=half_height + 1 + ) + + return left_br + inner + right_br + + +def draw_vertical( + pen: str, height, base=0, left_padding=0, right_padding=0 +) -> TextBlock: """ build a TextBlock with a vertical line of height `height` using the string `pen`. If paddings are given, @@ -175,65 +216,11 @@ def draw_vertical(pen: str, height, base=0, left_padding=0, right_padding=0): return TextBlock("\n".join(height * [pen]), base=base) -def subsuperscript(base, a, b): - if isinstance(base, str): - base = TextBlock(base) - if isinstance(a, str): - a = TextBlock(a) - if isinstance(b, str): - b = TextBlock(b) - - text2 = a.stack((base.height - 1) * "\n", align="l").stack(b, align="l") - text2.base = base.base + a.height - return base + text2 - - -def superscript(base, a): - if isinstance(base, str): - base = TextBlock(base) - text2 = TextBlock((base.height - 1) * "\n", base=base.base).stack(a, align="l") - return base + text2 - - -def subscript(base, a): - if isinstance(a, str): - a = TextBlock(a) - if isinstance(base, str): - base = TextBlock(base) - - text2 = a.stack(TextBlock(base.height * [""], base=base.base), align="l") - text2.base = base.base + a.height - return base + text2 - - -def sqrt_block(a, index=None): - if isinstance(a, str): - a = TextBlock(a) - if isinstance(index, str): - index = TextBlock(index) - - a_height = a.height - result_2 = TextBlock( - "\n".join("|" + line for line in a.text.split("\n")), base=a.base - ) - result_2 = result_2.stack((a.width + 1) * "_", align="l") - half_height = int(a_height / 2) - - result_1 = TextBlock( - "\n".join( - [ - (int(i) * " " + "\\" + int((half_height - i - 1)) * " ") - for i in range(half_height) - ] - ), - base=a.base, - ) - if index is not None: - result_1 = result_1.stack(index, align="c") - return result_1 + result_2 - - -def fraction(a, b): +def fraction(a: Union[TextBlock, str], b: Union[TextBlock, str]) -> TextBlock: + """ + A TextBlock representation of + a Fraction + """ if isinstance(a, str): a = TextBlock(a) if isinstance(b, str): @@ -246,7 +233,10 @@ def fraction(a, b): return result -def grid(items: list, **options) -> str: +def grid(items: List[Union[TextBlock, str]], **options) -> TextBlock: + """ + Process items and build a TextBlock + """ result: TextBlock = TextBlock("") if not items: @@ -350,7 +340,9 @@ def normalize_widths(row): return result -def integral_indefinite(integrand, var): +def integral_indefinite( + integrand: Union[TextBlock, str], var: Union[TextBlock, str] +) -> TextBlock: # TODO: handle list of vars # TODO: use utf as an option if isinstance(var, str): @@ -363,7 +355,12 @@ def integral_indefinite(integrand, var): return int_symb + integrand + " d" + var -def integral_definite(integrand, var, a, b): +def integral_definite( + integrand: Union[TextBlock, str], + var: Union[TextBlock, str], + a: Union[TextBlock, str], + b: Union[TextBlock, str], +) -> TextBlock: # TODO: handle list of vars # TODO: use utf as an option if isinstance(var, str): @@ -379,23 +376,7 @@ def integral_definite(integrand, var, a, b): return subsuperscript(int_symb, a, b) + " " + integrand + " d" + var -def bracket(inner): - if isinstance(inner, str): - inner = TextBlock(inner) - height = inner.height - if height == 1: - left_br, right_br = TextBlock("["), TextBlock("]") - else: - left_br = TextBlock( - "+-\n" + "\n".join((height) * ["| "]) + "\n+-", base=inner.base + 1 - ) - right_br = TextBlock( - "-+ \n" + "\n".join((height) * [" |"]) + "\n-+", base=inner.base + 1 - ) - return left_br + inner + right_br - - -def parenthesize(inner): +def parenthesize(inner: Union[str, TextBlock]) -> TextBlock: if isinstance(inner, str): inner = TextBlock(inner) height = inner.height @@ -411,21 +392,66 @@ def parenthesize(inner): return left_br + inner + right_br -def curly_braces(inner): - if isinstance(inner, str): - inner = TextBlock(inner) - height = inner.height - if height == 1: - left_br, right_br = TextBlock("{"), TextBlock("}") - else: - half_height = max(1, int((height - 3) / 2)) - half_line = "\n".join(half_height * [" |"]) - left_br = TextBlock( - "\n".join([" /", half_line, "< ", half_line, " \\"]), base=half_height + 1 - ) - half_line = "\n".join(half_height * ["| "]) - right_br = TextBlock( - "\n".join(["\\ ", half_line, " >", half_line, "/ "]), base=half_height + 1 - ) +def sqrt_block( + a: Union[TextBlock, str], index: Optional[Union[TextBlock, str]] = None +) -> TextBlock: + """ + Sqrt Text Block + """ + if isinstance(a, str): + a = TextBlock(a) + if isinstance(index, str): + index = TextBlock(index) - return left_br + inner + right_br + a_height = a.height + result_2 = TextBlock( + "\n".join("|" + line for line in a.text.split("\n")), base=a.base + ) + result_2 = result_2.stack((a.width + 1) * "_", align="l") + half_height = int(a_height / 2) + + result_1 = TextBlock( + "\n".join( + [ + (int(i) * " " + "\\" + int((half_height - i - 1)) * " ") + for i in range(half_height) + ] + ), + base=a.base, + ) + if index is not None: + result_1 = result_1.stack(index, align="c") + return result_1 + result_2 + + +def subscript(base: Union[TextBlock, str], a: Union[TextBlock, str]) -> TextBlock: + if isinstance(a, str): + a = TextBlock(a) + if isinstance(base, str): + base = TextBlock(base) + + text2 = a.stack(TextBlock(base.height * [""], base=base.base), align="l") + text2.base = base.base + a.height + return base + text2 + + +def subsuperscript( + base: Union[TextBlock, str], a: Union[TextBlock, str], b: Union[TextBlock, str] +) -> TextBlock: + if isinstance(base, str): + base = TextBlock(base) + if isinstance(a, str): + a = TextBlock(a) + if isinstance(b, str): + b = TextBlock(b) + + text2 = a.stack((base.height - 1) * "\n", align="l").stack(b, align="l") + text2.base = base.base + a.height + return base + text2 + + +def superscript(base: Union[TextBlock, str], a: Union[TextBlock, str]) -> TextBlock: + if isinstance(base, str): + base = TextBlock(base) + text2 = TextBlock((base.height - 1) * "\n", base=base.base).stack(a, align="l") + return base + text2 diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 7fd6cbff4..0e6b87e97 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -30,10 +30,10 @@ SymbolRepeated, SymbolRepeatedNull, SymbolTimes, + SymbolTrue, ) from mathics.core.systemsymbols import ( SymbolComplex, - SymbolGrid, SymbolMinus, SymbolOutputForm, SymbolRational, @@ -207,12 +207,20 @@ def eval_makeboxes( def make_output_form(expr, evaluation, form): - """ """ + """ + Build a 2D text representation of the expression. + """ from mathics.builtin.box.layout import InterpretationBox, PaneBox from mathics.core.convert.prettyprint import expression_to_2d_text - text2d = expression_to_2d_text(expr, evaluation, form, **{"2d": True}).text - elem1 = PaneBox(String(8 * " " + text2d)) + use_2d = ( + evaluation.definitions.get_ownvalues("System`$Use2DOutputForm")[0].replace + is SymbolTrue + ) + text2d = expression_to_2d_text(expr, evaluation, form, **{"2d": use_2d}).text + if "\n" in text2d: + text2d = "\n" + text2d + elem1 = PaneBox(String(text2d)) elem2 = Expression(SymbolOutputForm, expr) return InterpretationBox(elem1, elem2) diff --git a/test/format/test_2d.py b/test/format/test_2d.py new file mode 100644 index 000000000..14a774a39 --- /dev/null +++ b/test/format/test_2d.py @@ -0,0 +1,44 @@ +""" +Test 2d Output form +""" + +from test.helper import session + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("$Use2DOutputForm=True;", "Null", "Set the 2D form"), + ( + '"Hola\nCómo estás?"', + ("\n" "Hola \n" "Cómo estás?"), + "String", + ), + ("a^b", ("\n" " b\n" "a "), "power"), + ("(-a)^b", ("\n" " b\n" "(-a) "), "power of negative"), + ("(a+b)^c", ("\n" " c\n" "(a + b) "), "power with composite basis"), + ("Derivative[1][f][x]", "f'[x]", "first derivative"), + ("Derivative[2][f][x]", "f''[x]", "second derivative"), + ("Derivative[3][f][x]", ("\n" " (3) \n" "f [x]"), "Third derivative"), + ( + "Derivative[0,2][f][x]", + ("\n" " (0,2) \n" "f [x]"), + "partial derivative", + ), + ( + "Integrate[f[x]^2,x]", + ("\n" " /+ \n" " | 2 \n" " | f[x] dx\n" "+/ "), + "Indefinite integral", + ), + ("$Use2DOutputForm=False;", "Null", "Go back to the standard behavior."), + ], +) +def test_Output2D(str_expr: str, str_expected: str, msg: str): + test_expr = f"OutputForm[{str_expr}]" + result = session.evaluate_as_in_cli(test_expr).result + if msg: + assert result == str_expected, msg + else: + assert result == str_expected From 760e04e05101520e5cca7e68eb6904b1bc330db0 Mon Sep 17 00:00:00 2001 From: mmatera Date: Tue, 5 Nov 2024 22:51:51 -0300 Subject: [PATCH 10/15] mypy --- mathics/core/convert/prettyprint.py | 104 ++++++++++++++++------------ mathics/core/prettyprint.py | 19 +++-- 2 files changed, 71 insertions(+), 52 deletions(-) diff --git a/mathics/core/convert/prettyprint.py b/mathics/core/convert/prettyprint.py index 59d2be18b..fb017cd5f 100644 --- a/mathics/core/convert/prettyprint.py +++ b/mathics/core/convert/prettyprint.py @@ -2,6 +2,8 @@ This module builts the 2D string associated to the OutputForm """ +from typing import Any, Callable, Dict, List, Optional, Union + from mathics.core.atoms import ( Integer, Integer1, @@ -49,7 +51,7 @@ #### Functions that convert Expressions in TextBlock -expr_to_2d_text_map = {} +expr_to_2d_text_map: Dict[str, Callable] = {} # This Exception if the expression should @@ -67,16 +69,16 @@ class IsNot2DArray(Exception): def expression_to_2d_text( - expr, evaluation: Evaluation, form=SymbolStandardForm, **kwargs + expr: BaseElement, evaluation: Evaluation, form=SymbolStandardForm, **kwargs ): """ Build a 2d text from an `Expression` """ ## TODO: format the expression - format_expr = do_format(expr, evaluation, SymbolOutputForm) + format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore # Strip HoldForm - while format_expr.has_form("HoldForm", 1): + while format_expr.has_form("HoldForm", 1): # type: ignore format_expr = format_expr.elements[0] lookup_name = format_expr.get_head().get_lookup_name() @@ -92,12 +94,13 @@ def expression_to_2d_text( def _default_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: """ Default representation of a function """ - head = expression_to_2d_text(expr.head, evaluation, form, **kwargs) + expr_head = expr.head + head = expression_to_2d_text(expr_head, evaluation, form, **kwargs) comma = TextBlock(", ") elements = [expression_to_2d_text(elem, evaluation) for elem in expr.elements] result = elements.pop(0) if elements else TextBlock(" ") @@ -122,7 +125,7 @@ def _divide(num, den, evaluation, form, **kwargs): def _strip_1_parm_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: if len(expr.elements) != 1: raise _WrongFormattedExpression @@ -134,7 +137,7 @@ def _strip_1_parm_expression_to_2d_text( def derivative_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: """Derivative operator""" head = expr.get_head() @@ -175,7 +178,7 @@ def derivative_expression_to_2d_text( def divide_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: if len(expr.elements) != 2: raise _WrongFormattedExpression @@ -187,7 +190,7 @@ def divide_expression_to_2d_text( def graphics( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: return TextBlock("-Graphics-") @@ -196,7 +199,7 @@ def graphics( def graphics3d( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: return TextBlock("-Graphics3D-") @@ -205,7 +208,7 @@ def graphics3d( def grid_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: if len(expr.elements) == 0: raise IsNotGrid @@ -235,7 +238,9 @@ def grid_expression_to_2d_text( expr_to_2d_text_map["System`Grid"] = grid_expression_to_2d_text -def integer_expression_to_2d_text(n, evaluation, form, **kwargs): +def integer_expression_to_2d_text( + n: Integer, evaluation: Evaluation, form: Symbol, **kwargs +): return TextBlock(str(n.value)) @@ -243,7 +248,7 @@ def integer_expression_to_2d_text(n, evaluation, form, **kwargs): def integrate_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: elems = list(expr.elements) if len(elems) > 2 or not kwargs.get("2d", False): @@ -271,7 +276,7 @@ def integrate_expression_to_2d_text( def list_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: return ( TextBlock("{") @@ -288,18 +293,22 @@ def list_expression_to_2d_text( expr_to_2d_text_map["System`List"] = list_expression_to_2d_text -def mathmlform_expression_to_2d_text(expr, evaluation, form, **kwargs) -> TextBlock: +def mathmlform_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: # boxes = format_element(expr.elements[0], evaluation, form) boxes = Expression( Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm ).evaluate(evaluation) - return TextBlock(boxes.boxes_to_mathml()) + return TextBlock(boxes.boxes_to_mathml()) # type: ignore[union-attr] expr_to_2d_text_map["System`MathMLForm"] = mathmlform_expression_to_2d_text -def matrixform_expression_to_2d_text(expr, evaluation, form, **kwargs) -> TextBlock: +def matrixform_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: # return parenthesize(tableform_expression_to_2d_text(expr, evaluation, form, **kwargs)) return tableform_expression_to_2d_text(expr, evaluation, form, **kwargs) @@ -308,7 +317,7 @@ def matrixform_expression_to_2d_text(expr, evaluation, form, **kwargs) -> TextBl def plus_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: elements = expr.elements result = TextBlock("") @@ -351,7 +360,7 @@ def plus_expression_to_2d_text( ## TODO: handle complex numbers? else: elem_txt = expression_to_2d_text(elem, evaluation, form, **kwargs) - if compare_precedence(elem, 310) < 0: + if (compare_precedence(elem, 310) or -1) < 0: elem_txt = parenthesize(elem_txt) result = result + " + " + elem_txt elif i == 0 or ( @@ -372,7 +381,7 @@ def plus_expression_to_2d_text( def power_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ): if len(expr.elements) != 2: raise _WrongFormattedExpression @@ -399,7 +408,7 @@ def power_expression_to_2d_text( def pre_pos_infix_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: elements = expr.elements if not (0 <= len(elements) <= 4): @@ -520,7 +529,7 @@ def pre_pos_infix_expression_to_2d_text( def precedenceform_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: if len(expr.elements) == 2: return expression_to_2d_text(expr.elements[0], evaluation, form, **kwargs) @@ -530,19 +539,21 @@ def precedenceform_expression_to_2d_text( expr_to_2d_text_map["System`PrecedenceForm"] = precedenceform_expression_to_2d_text -def rational_expression_to_2d_text(n, evaluation, form, **kwargs): +def rational_expression_to_2d_text( + n: Union[Rational, Expression], evaluation: Evaluation, form: Symbol, **kwargs +): if n.has_form("Rational", 2): - num, den = n.elements + num, den = n.elements # type: ignore[union-attr] else: - num, den = n.numerator(), n.denominator() + num, den = n.numerator(), n.denominator() # type: ignore[union-attr] return _divide(num, den, evaluation, form, **kwargs) expr_to_2d_text_map["System`Rational"] = rational_expression_to_2d_text -def real_expression_to_2d_text(n, evaluation, form, **kwargs): - str_n = n.make_boxes("System`OutputForm").boxes_to_text() +def real_expression_to_2d_text(n: Real, evaluation: Evaluation, form: Symbol, **kwargs): + str_n = n.make_boxes("System`OutputForm").boxes_to_text() # type: ignore[attr-defined] return TextBlock(str(str_n)) @@ -550,7 +561,7 @@ def real_expression_to_2d_text(n, evaluation, form, **kwargs): def sqrt_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: if not 1 <= len(expr.elements) <= 2: raise _WrongFormattedExpression @@ -568,7 +579,7 @@ def sqrt_expression_to_2d_text( def subscript_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: if len(expr.elements) != 2: raise _WrongFormattedExpression @@ -586,7 +597,7 @@ def subscript_expression_to_2d_text( def subsuperscript_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: if len(expr.elements) != 3: raise _WrongFormattedExpression @@ -604,7 +615,7 @@ def subsuperscript_expression_to_2d_text( def string_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: String, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: return TextBlock(expr.value) @@ -613,7 +624,7 @@ def string_expression_to_2d_text( def stringform_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: strform = expr.elements[0] if not isinstance(strform, String): @@ -667,7 +678,7 @@ def stringform_expression_to_2d_text( def superscript_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: elements = expr.elements if len(elements) != 2: @@ -694,7 +705,9 @@ def superscript_expression_to_2d_text( expr_to_2d_text_map["System`Superscript"] = superscript_expression_to_2d_text -def symbol_expression_to_2d_text(symb, evaluation, form, **kwargs): +def symbol_expression_to_2d_text( + symb: Symbol, evaluation: Evaluation, form: Symbol, **kwargs +): return TextBlock(evaluation.definitions.shorten_name(symb.name)) @@ -702,7 +715,7 @@ def symbol_expression_to_2d_text(symb, evaluation, form, **kwargs): def tableform_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: return grid_expression_to_2d_text(expr, evaluation, form) @@ -711,24 +724,24 @@ def tableform_expression_to_2d_text( def texform_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: # boxes = format_element(expr.elements[0], evaluation, form) boxes = Expression( Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm ).evaluate(evaluation) - return TextBlock(boxes.boxes_to_tex()) + return TextBlock(boxes.boxes_to_tex()) # type: ignore expr_to_2d_text_map["System`TeXForm"] = texform_expression_to_2d_text def times_expression_to_2d_text( - expr: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: elements = expr.elements - num = [] - den = [] + num: List[BaseElement] = [] + den: List[BaseElement] = [] # First, split factors with integer, negative powers: for elem in elements: if elem.has_form("Power", 2): @@ -745,8 +758,9 @@ def times_expression_to_2d_text( den.append(elem.denominator()) continue elif elem.has_form("Rational", 2): - num.append(elem.elements[0]) - den.append(elem.elements[1]) + elem_elements = elem.elements + num.append(elem_elements[0]) + den.append(elem_elements[1]) continue num.append(elem) @@ -768,7 +782,7 @@ def times_expression_to_2d_text( return expression_to_2d_text(num[0], evaluation, form, **kwargs) prefactor = 1 - result = TextBlock("") + result: TextBlock = TextBlock("") for i, elem in enumerate(num): if elem is IntegerM1: prefactor *= -1 @@ -779,7 +793,7 @@ def times_expression_to_2d_text( elem_txt = expression_to_2d_text(elem, evaluation, form, **kwargs) if compare_precedence(elem, 400): - elem = parenthesize(elem_txt) + elem_txt = parenthesize(elem_txt) if i == 0: result = elem_txt else: diff --git a/mathics/core/prettyprint.py b/mathics/core/prettyprint.py index 1eb86ce0d..05f3d08e3 100644 --- a/mathics/core/prettyprint.py +++ b/mathics/core/prettyprint.py @@ -6,6 +6,11 @@ class TextBlock: + lines: List[str] + width: int + height: int + base: int + @staticmethod def _build_attributes(lines, width=0, height=0, base=0): width = max(width, max(len(line) for line in lines)) if lines else 0 @@ -91,7 +96,7 @@ def __iadd__(self, tb): base=base, ) - def ajust_base(self, base): + def ajust_base(self, base: int): """ if base is larger than self.base, adds lines at the bottom of the text @@ -105,7 +110,7 @@ def ajust_base(self, base): return result - def ajust_width(self, width, align="c"): + def ajust_width(self, width: int, align: str = "c"): def padding(lines, diff): if diff > 0: if align == "c": @@ -133,16 +138,16 @@ def box(self): out = top + "\n" + out + "\n" + top return TextBlock(out, self.base + 1) - def join(self, iter): + def join(self, iterable): result = TextBlock("") - for i, item in enumerate(iter): + for i, item in enumerate(iterable): if i == 0: result = item else: result = result + self + item return result - def stack(self, top, align="c"): + def stack(self, top, align: str = "c"): if isinstance(top, str): top = TextBlock(top) @@ -154,7 +159,7 @@ def stack(self, top, align="c"): elif bottom_width < top_width: bottom = bottom.ajust_width(top_width, align=align) - return TextBlock(top.lines + bottom.lines, base=self.base) + return TextBlock(top.lines + bottom.lines, base=self.base) # type: ignore[union-attr] def _draw_integral_symbol(height: int) -> TextBlock: @@ -233,7 +238,7 @@ def fraction(a: Union[TextBlock, str], b: Union[TextBlock, str]) -> TextBlock: return result -def grid(items: List[Union[TextBlock, str]], **options) -> TextBlock: +def grid(items: list, **options) -> TextBlock: """ Process items and build a TextBlock """ From a59174251c78be2f7a242b7b9cc67e8ee15ca5b3 Mon Sep 17 00:00:00 2001 From: mmatera Date: Wed, 6 Nov 2024 06:28:49 -0300 Subject: [PATCH 11/15] removing testing function --- mathics/eval/makeboxes.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 0e6b87e97..3041b7d40 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -247,36 +247,6 @@ def format_element( return format_element(element, evaluation, SymbolFullForm, **kwargs) -def new_format_element( - element: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs -) -> Optional[BaseElement]: - """ - Applies formats associated to the expression, and then calls Makeboxes - """ - - from mathics.core.convert.prettyprint import expression_to_2d_text - - if form is SymbolOutputForm: - txt2d_form = expression_to_2d_text(element, evaluation, form, **kwargs) - return String(txt2d_form.text) - - expr = do_format(element, evaluation, form) - if expr is None: - return None - if form in (SymbolStandardForm, SymbolTraditionalForm): - result = Expression(SymbolMakeBoxes, expr, form) - else: - expr = Expression(form, expr) - result = Expression(SymbolMakeBoxes, expr, SymbolStandardForm) - result_box = result.evaluate(evaluation) - if isinstance(result_box, String): - return result_box - if isinstance(result_box, BoxElementMixin): - return result_box - else: - return format_element(element, evaluation, SymbolFullForm, **kwargs) - - # do_format_* From 1cd0bc8170e3b1102c3601e96c6121f28409c6e3 Mon Sep 17 00:00:00 2001 From: mmatera Date: Sat, 9 Nov 2024 10:40:54 -0300 Subject: [PATCH 12/15] moving and renaming modules --- mathics/builtin/forms/output.py | 2 +- mathics/eval/makeboxes.py | 3 ++- .../prettyprint.py => format/pane_text.py} | 3 +++ .../{core/convert => format}/prettyprint.py | 24 +++++++++---------- 4 files changed, 18 insertions(+), 14 deletions(-) rename mathics/{core/prettyprint.py => format/pane_text.py} (99%) rename mathics/{core/convert => format}/prettyprint.py (99%) diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index f7695ea59..cb7698b0c 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -35,7 +35,6 @@ StringFromPython, ) from mathics.core.builtin import Builtin -from mathics.core.convert.prettyprint import expression_to_2d_text from mathics.core.evaluation import Evaluation from mathics.core.expression import BoxError, Expression from mathics.core.list import ListExpression @@ -67,6 +66,7 @@ ) from mathics.eval.makeboxes import StringLParen, StringRParen, format_element from mathics.eval.testing_expressions import expr_min +from mathics.format.prettyprint import expression_to_2d_text MULTI_NEWLINE_RE = re.compile(r"\n{2,}") diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 3041b7d40..bd6b46ed8 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -211,13 +211,14 @@ def make_output_form(expr, evaluation, form): Build a 2D text representation of the expression. """ from mathics.builtin.box.layout import InterpretationBox, PaneBox - from mathics.core.convert.prettyprint import expression_to_2d_text + from mathics.format.prettyprint import expression_to_2d_text use_2d = ( evaluation.definitions.get_ownvalues("System`$Use2DOutputForm")[0].replace is SymbolTrue ) text2d = expression_to_2d_text(expr, evaluation, form, **{"2d": use_2d}).text + if "\n" in text2d: text2d = "\n" + text2d elem1 = PaneBox(String(text2d)) diff --git a/mathics/core/prettyprint.py b/mathics/format/pane_text.py similarity index 99% rename from mathics/core/prettyprint.py rename to mathics/format/pane_text.py index 05f3d08e3..687f56fe6 100644 --- a/mathics/core/prettyprint.py +++ b/mathics/format/pane_text.py @@ -1,5 +1,8 @@ """ This module produces a "pretty-print" inspired 2d text representation. + +This code is completely independent from Mathics objects, so it could live +alone in a different package. """ from typing import List, Optional, Union diff --git a/mathics/core/convert/prettyprint.py b/mathics/format/prettyprint.py similarity index 99% rename from mathics/core/convert/prettyprint.py rename to mathics/format/prettyprint.py index fb017cd5f..4ee8cb47f 100644 --- a/mathics/core/convert/prettyprint.py +++ b/mathics/format/prettyprint.py @@ -17,7 +17,18 @@ from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.prettyprint import ( +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 +from mathics.format.pane_text import ( TextBlock, bracket, fraction, @@ -30,17 +41,6 @@ subsuperscript, superscript, ) -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") From baa276e6522086142741bac98bcbc8eda17c88b2 Mon Sep 17 00:00:00 2001 From: mmatera Date: Sat, 9 Nov 2024 17:03:53 -0300 Subject: [PATCH 13/15] fixing formatting in fractions and square roots --- mathics/format/pane_text.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mathics/format/pane_text.py b/mathics/format/pane_text.py index 687f56fe6..d8b693530 100644 --- a/mathics/format/pane_text.py +++ b/mathics/format/pane_text.py @@ -233,7 +233,7 @@ def fraction(a: Union[TextBlock, str], b: Union[TextBlock, str]) -> TextBlock: a = TextBlock(a) if isinstance(b, str): b = TextBlock(b) - width = max(b.width, a.width) + 2 + width = max(b.width, a.width) frac_bar = TextBlock(width * "-") result = frac_bar.stack(a) result = b.stack(result) @@ -416,7 +416,7 @@ def sqrt_block( "\n".join("|" + line for line in a.text.split("\n")), base=a.base ) result_2 = result_2.stack((a.width + 1) * "_", align="l") - half_height = int(a_height / 2) + half_height = int(a_height / 2 + 1) result_1 = TextBlock( "\n".join( From d92fffba902aabb370d1ecb03198197d505e7097 Mon Sep 17 00:00:00 2001 From: mmatera Date: Sun, 10 Nov 2024 17:48:36 -0300 Subject: [PATCH 14/15] build expressions using sympy.printing.pretty.stringpict --- mathics/eval/makeboxes.py | 2 +- mathics/format/pane_text.py | 162 +++++++++++++++++++++------------- mathics/format/prettyprint.py | 130 +++++++++++++++++++++------ test/format/test_2d.py | 2 +- 4 files changed, 207 insertions(+), 89 deletions(-) diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index bd6b46ed8..aa79443f6 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -217,7 +217,7 @@ def make_output_form(expr, evaluation, form): evaluation.definitions.get_ownvalues("System`$Use2DOutputForm")[0].replace is SymbolTrue ) - text2d = expression_to_2d_text(expr, evaluation, form, **{"2d": use_2d}).text + text2d = str(expression_to_2d_text(expr, evaluation, form, **{"2d": use_2d})) if "\n" in text2d: text2d = "\n" + text2d diff --git a/mathics/format/pane_text.py b/mathics/format/pane_text.py index d8b693530..6a643937b 100644 --- a/mathics/format/pane_text.py +++ b/mathics/format/pane_text.py @@ -7,8 +7,50 @@ from typing import List, Optional, Union +from sympy.printing.pretty.pretty_symbology import line_width, vobj +from sympy.printing.pretty.stringpict import prettyForm, stringPict -class TextBlock: + +class TextBlock(prettyForm): + def __init__(self, text, base=0, padding=0, height=1, width=0): + super().__init__(text, base) + assert padding == 0 + assert height == 1 + assert width == 0 + + def root(self, n=None): + """Produce a nice root symbol. + Produces ugly results for big n inserts. + """ + # XXX not used anywhere + # XXX duplicate of root drawing in pretty.py + # put line over expression + result = TextBlock(*self.above("_" * self.width())) + # construct right half of root symbol + height = self.height() + slash = "\n".join(" " * (height - i - 1) + "/" + " " * i for i in range(height)) + slash = stringPict(slash, height - 1) + # left half of root symbol + if height > 2: + downline = stringPict("\\ \n \\", 1) + else: + downline = stringPict("\\") + # put n on top, as low as possible + if n is not None and n.width() > downline.width(): + downline = downline.left(" " * (n.width() - downline.width())) + downline = downline.above(n) + # build root symbol + root = TextBlock(*downline.right(slash)) + # glue it on at the proper height + # normally, the root symbel is as high as self + # which is one less than result + # this moves the root symbol one down + # if the root became higher, the baseline has to grow too + root.baseline = result.baseline - result.height() + root.height() + return result.left(root) + + +class OldTextBlock: lines: List[str] width: int height: int @@ -37,7 +79,7 @@ def _build_attributes(lines, width=0, height=0, base=0): return (lines, width, height, base) - def __init__(self, text, padding=0, base=0, height=1, width=0): + def __init__(self, text, base=0, padding=0, height=1, width=0): if isinstance(text, str): if text == "": lines = [] @@ -63,6 +105,9 @@ def text(self): def text(self, value): raise TypeError("TextBlock is inmutable") + def __str__(self): + return self.text + def __repr__(self): return self.text @@ -166,45 +211,23 @@ def stack(self, top, align: str = "c"): def _draw_integral_symbol(height: int) -> TextBlock: - return TextBlock( - (" /+ \n" + "\n".join(height * [" | "]) + "\n+/ "), base=int((height + 1) / 2) - ) + if height % 2 == 0: + height = height + 1 + result = TextBlock(vobj("int", height), (height - 1) // 2) + return result def bracket(inner: Union[str, TextBlock]) -> TextBlock: if isinstance(inner, str): inner = TextBlock(inner) - height = inner.height - if height == 1: - left_br, right_br = TextBlock("["), TextBlock("]") - else: - left_br = TextBlock( - "+-\n" + "\n".join((height) * ["| "]) + "\n+-", base=inner.base + 1 - ) - right_br = TextBlock( - "-+ \n" + "\n".join((height) * [" |"]) + "\n-+", base=inner.base + 1 - ) - return left_br + inner + right_br + + return TextBlock(*inner.parens("[", "]")) def curly_braces(inner: Union[str, TextBlock]) -> TextBlock: if isinstance(inner, str): inner = TextBlock(inner) - height = inner.height - if height == 1: - left_br, right_br = TextBlock("{"), TextBlock("}") - else: - half_height = max(1, int((height - 3) / 2)) - half_line = "\n".join(half_height * [" |"]) - left_br = TextBlock( - "\n".join([" /", half_line, "< ", half_line, " \\"]), base=half_height + 1 - ) - half_line = "\n".join(half_height * ["| "]) - right_br = TextBlock( - "\n".join(["\\ ", half_line, " >", half_line, "/ "]), base=half_height + 1 - ) - - return left_br + inner + right_br + return TextBlock(*inner.parens("{", "}")) def draw_vertical( @@ -233,11 +256,7 @@ def fraction(a: Union[TextBlock, str], b: Union[TextBlock, str]) -> TextBlock: a = TextBlock(a) if isinstance(b, str): b = TextBlock(b) - width = max(b.width, a.width) - frac_bar = TextBlock(width * "-") - result = frac_bar.stack(a) - result = b.stack(result) - result.base = b.height + return a / b return result @@ -359,8 +378,8 @@ def integral_indefinite( if isinstance(integrand, str): integrand = TextBlock(integrand) - int_symb: TextBlock = _draw_integral_symbol(integrand.height) - return int_symb + integrand + " d" + var + int_symb: TextBlock = _draw_integral_symbol(integrand.height()) + return TextBlock(*TextBlock.next(int_symb, integrand, TextBlock(" d"), var)) def integral_definite( @@ -380,24 +399,20 @@ def integral_definite( if isinstance(b, str): b = TextBlock(b) - int_symb = _draw_integral_symbol(integrand.height) - return subsuperscript(int_symb, a, b) + " " + integrand + " d" + var + h_int = integrand.height() + symbol_height = h_int + # for ascii, symbol_height +=2 + int_symb = _draw_integral_symbol(symbol_height) + orig_baseline = int_symb.baseline + int_symb = subsuperscript(int_symb, a, b) + return TextBlock(*TextBlock.next(int_symb, integrand, TextBlock(" d"), var)) def parenthesize(inner: Union[str, TextBlock]) -> TextBlock: if isinstance(inner, str): inner = TextBlock(inner) - height = inner.height - if height == 1: - left_br, right_br = TextBlock("("), TextBlock(")") - else: - left_br = TextBlock( - "/ \n" + "\n".join((height - 2) * ["| "]) + "\n\\ ", base=inner.base - ) - right_br = TextBlock( - " \\ \n" + "\n".join((height - 2) * [" |"]) + "\n /", base=inner.base - ) - return left_br + inner + right_br + + return TextBlock(*inner.parens()) def sqrt_block( @@ -408,9 +423,13 @@ def sqrt_block( """ if isinstance(a, str): a = TextBlock(a) + if index is None: + index = "" if isinstance(index, str): index = TextBlock(index) + return TextBlock(*a.root(index)) + a_height = a.height result_2 = TextBlock( "\n".join("|" + line for line in a.text.split("\n")), base=a.base @@ -433,19 +452,26 @@ def sqrt_block( def subscript(base: Union[TextBlock, str], a: Union[TextBlock, str]) -> TextBlock: + """ + Join b with a as a subscript. + """ if isinstance(a, str): a = TextBlock(a) if isinstance(base, str): base = TextBlock(base) - text2 = a.stack(TextBlock(base.height * [""], base=base.base), align="l") - text2.base = base.base + a.height - return base + text2 + a = TextBlock(*TextBlock.next(TextBlock(base.width() * " "), a)) + base = TextBlock(*TextBlock.next(base, TextBlock(a.width() * " "))) + result = TextBlock(*TextBlock.below(base, a)) + return result def subsuperscript( base: Union[TextBlock, str], a: Union[TextBlock, str], b: Union[TextBlock, str] ) -> TextBlock: + """ + Join base with a as a superscript and b as a subscript + """ if isinstance(base, str): base = TextBlock(base) if isinstance(a, str): @@ -453,13 +479,31 @@ def subsuperscript( if isinstance(b, str): b = TextBlock(b) - text2 = a.stack((base.height - 1) * "\n", align="l").stack(b, align="l") - text2.base = base.base + a.height - return base + text2 + # Ensure that a and b have the same width + width_diff = a.width() - b.width() + if width_diff < 0: + a = TextBlock(*TextBlock.next(a, TextBlock((-width_diff) * " "))) + elif width_diff > 0: + b = TextBlock(*TextBlock.next(b, TextBlock((width_diff) * " "))) + + indx_spaces = b.width() * " " + base_spaces = base.width() * " " + a = TextBlock(*TextBlock.next(TextBlock(base_spaces), a)) + b = TextBlock(*TextBlock.next(TextBlock(base_spaces), b)) + base = TextBlock(*TextBlock.next(base, TextBlock(base_spaces))) + result = TextBlock(*TextBlock.below(base, a)) + result = TextBlock(*TextBlock.above(result, b)) + return result def superscript(base: Union[TextBlock, str], a: Union[TextBlock, str]) -> TextBlock: + if isinstance(a, str): + a = TextBlock(a) if isinstance(base, str): base = TextBlock(base) - text2 = TextBlock((base.height - 1) * "\n", base=base.base).stack(a, align="l") - return base + text2 + + base_width, a_width = base.width(), a.width() + a = TextBlock(*TextBlock.next(TextBlock(base_width * " "), a)) + base = TextBlock(*TextBlock.next(base, TextBlock(a_width * " "))) + result = TextBlock(*TextBlock.above(base, a)) + return result diff --git a/mathics/format/prettyprint.py b/mathics/format/prettyprint.py index 4ee8cb47f..21d0dda63 100644 --- a/mathics/format/prettyprint.py +++ b/mathics/format/prettyprint.py @@ -31,6 +31,7 @@ from mathics.format.pane_text import ( TextBlock, bracket, + curly_braces, fraction, grid, integral_definite, @@ -83,7 +84,10 @@ def expression_to_2d_text( lookup_name = format_expr.get_head().get_lookup_name() try: - return expr_to_2d_text_map[lookup_name](format_expr, evaluation, form, **kwargs) + result = expr_to_2d_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 @@ -109,7 +113,7 @@ def _default_expression_to_2d_text( if form is SymbolTraditionalForm: return head + parenthesize(result) - return head + bracket(result) + return TextBlock(*TextBlock.next(head, bracket(result))) def _divide(num, den, evaluation, form, **kwargs): @@ -155,17 +159,21 @@ def derivative_expression_to_2d_text( if len(derivatives) == 1: order_iv = derivatives[0] if order_iv == Integer1: - return function_head + "'" + return TextBlock(*TextBlock.next(function_head, TextBlock("'"))) elif order_iv == Integer2: - return function_head + "''" + return TextBlock(*TextBlock.next(function_head, TextBlock("''"))) if not kwargs["2d"]: return _default_expression_to_2d_text(expr, evaluation, form, **kwargs) - superscript_tb = TextBlock(",").join( + comma = TextBlock(",") + superscript_tb, *rest_derivatives = ( expression_to_2d_text(order, evaluation, form, **kwargs) for order in derivatives ) + for order in rest_derivatives: + superscript_tb = TextBlock(*TextBlock.next(superscript_tb, comma, order)) + superscript_tb = parenthesize(superscript_tb) return superscript(function_head, superscript_tb) @@ -278,16 +286,14 @@ def integrate_expression_to_2d_text( def list_expression_to_2d_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: - return ( - TextBlock("{") - + TextBlock(", ").join( - [ - expression_to_2d_text(elem, evaluation, form, **kwargs) - for elem in expr.elements - ] - ) - + TextBlock("}") + result, *rest_elems = ( + expression_to_2d_text(elem, evaluation, form, **kwargs) + for elem in expr.elements ) + comma_tb = TextBlock(", ") + for next_elem in rest_elems: + result = TextBlock(*TextBlock.next(result, comma_tb, next_elem)) + return curly_braces(result) expr_to_2d_text_map["System`List"] = list_expression_to_2d_text @@ -407,7 +413,7 @@ def power_expression_to_2d_text( expr_to_2d_text_map["System`Power"] = power_expression_to_2d_text -def pre_pos_infix_expression_to_2d_text( +def pre_pos_fix_expression_to_2d_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: elements = expr.elements @@ -423,14 +429,74 @@ def pre_pos_infix_expression_to_2d_text( 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_2d_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolPrefix: + default_symb = TextBlock(" @ ") + ops_txt = ( + expression_to_2d_text(head, evaluation, form, **kwargs) + default_symb + ) + elif head is SymbolPostfix: + default_symb = TextBlock(" // ") + ops_txt = default_symb + expression_to_2d_text( + head, evaluation, form, **kwargs + ) - if head in (SymbolPrefix, SymbolPostfix): - if len(operands) != 1: + # Processing the third argument, if it is there: + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: raise _WrongFormattedExpression - elif head is SymbolInfix: - if len(operands) < 2: + + # 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 - else: + if group is SymbolNone: + group = None + + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_2d_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + + if head is SymbolPrefix: + return TextBlock(*TextBlock.next(ops_txt[0], target_txt)) + if head is SymbolPostfix: + return TextBlock(*TextBlock.next(target_txt, ops_txt[0])) + + +expr_to_2d_text_map["System`Prefix"] = pre_pos_fix_expression_to_2d_text +expr_to_2d_text_map["System`Postfix"] = pre_pos_fix_expression_to_2d_text + + +def infix_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + 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: @@ -515,17 +581,22 @@ def pre_pos_infix_expression_to_2d_text( if group in (SymbolLeft, SymbolRight): parenthesized = not parenthesized else: - if ops_lst[index % num_ops].text != " ": - result = result + " " + ops_lst[index % num_ops] + " " + operand_txt + space = TextBlock(" ") + if str(ops_lst[index % num_ops]) != " ": + result_lst = ( + result, + space, + ops_lst[index % num_ops], + space, + operand_txt, + ) else: - result = result + " " + operand_txt + result_lst = (result, space, operand_txt) - return result + return TextBlock(*TextBlock.next(*result_lst)) -expr_to_2d_text_map["System`Prefix"] = pre_pos_infix_expression_to_2d_text -expr_to_2d_text_map["System`Postfix"] = pre_pos_infix_expression_to_2d_text -expr_to_2d_text_map["System`Infix"] = pre_pos_infix_expression_to_2d_text +expr_to_2d_text_map["System`Infix"] = infix_expression_to_2d_text def precedenceform_expression_to_2d_text( @@ -617,7 +688,10 @@ def subsuperscript_expression_to_2d_text( def string_expression_to_2d_text( expr: String, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: - return TextBlock(expr.value) + 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 TextBlock("\n".join(lines)) expr_to_2d_text_map["System`String"] = string_expression_to_2d_text diff --git a/test/format/test_2d.py b/test/format/test_2d.py index 14a774a39..1165415a2 100644 --- a/test/format/test_2d.py +++ b/test/format/test_2d.py @@ -29,7 +29,7 @@ ), ( "Integrate[f[x]^2,x]", - ("\n" " /+ \n" " | 2 \n" " | f[x] dx\n" "+/ "), + ("\n" "⌠ 2 \n" "⎮f[x] dx\n" "⌡ "), "Indefinite integral", ), ("$Use2DOutputForm=False;", "Null", "Go back to the standard behavior."), From 002f18327f3ef9a60f39f86c1d0d1c5e16be4833 Mon Sep 17 00:00:00 2001 From: mmatera Date: Mon, 11 Nov 2024 15:41:58 -0300 Subject: [PATCH 15/15] partial --- mathics/builtin/forms/output.py | 2 +- mathics/format/pane_text.py | 95 +++++++++++--- mathics/format/prettyprint.py | 217 ++++++++++++++++---------------- 3 files changed, 189 insertions(+), 125 deletions(-) diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index cb7698b0c..b1e58ecc5 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -575,7 +575,7 @@ class OutputForm(FormBaseClass): def eval_makeboxes(self, expr, form, evaluation): """MakeBoxes[OutputForm[expr_], form_]""" print(" eval Makeboxes outputform") - text2d = expression_to_2d_text(expr, evaluation, form).text + text2d = str(expression_to_2d_text(expr, evaluation, form)) elem1 = PaneBox(String(text2d)) elem2 = Expression(SymbolOutputForm, expr) result = InterpretationBox(elem1, elem2) diff --git a/mathics/format/pane_text.py b/mathics/format/pane_text.py index 6a643937b..7081955f7 100644 --- a/mathics/format/pane_text.py +++ b/mathics/format/pane_text.py @@ -18,6 +18,39 @@ def __init__(self, text, base=0, padding=0, height=1, width=0): assert height == 1 assert width == 0 + @staticmethod + def stack(self, *args, align="c"): + if align == "c": + return super.stack(*args) + max_width = max((block.width() for block in args)) + if align == "l": + new_args = [] + for block in args: + block_width = block.width() + if block_width == max_width: + new_args.append(block) + else: + fill_block = TextBlock((max_width - block_width) * " ") + new_block = TextBlock(*TextBlock.next(block, fill_block)) + new_args.append(new_block) + return super.stack(*args) + else: # align=="r" + new_args = [] + for block in args: + block_width = block.width() + if block_width == max_width: + new_args.append(block) + else: + fill_block = TextBlock((max_width - block_width) * " ") + new_block = TextBlock( + *TextBlock.next( + fill_block, + block, + ) + ) + new_args.append(new_block) + return super.stack(*args) + def root(self, n=None): """Produce a nice root symbol. Produces ugly results for big n inserts. @@ -93,7 +126,7 @@ def __init__(self, text, base=0, padding=0, height=1, width=0): else: lines = [line.replace("\t", " ") for line in lines] - self.lines, self.width, self.height, self.base = self._build_attributes( + self.lines, self.width, self.height, self.baseline = self._build_attributes( lines, width, height, base ) @@ -257,7 +290,6 @@ def fraction(a: Union[TextBlock, str], b: Union[TextBlock, str]) -> TextBlock: if isinstance(b, str): b = TextBlock(b) return a / b - return result def grid(items: list, **options) -> TextBlock: @@ -290,10 +322,10 @@ def grid(items: list, **options) -> TextBlock: full_width: int = 0 for row in items: if isinstance(row, TextBlock): - full_width = max(full_width, row.width) + full_width = max(full_width, row.width()) else: for index, item in enumerate(row): - widths[index] = max(widths[index], item.width) + widths[index] = max(widths[index], item.width()) total_width: int = sum(widths) + max(0, len(widths) - 1) * 3 @@ -308,7 +340,7 @@ def grid(items: list, **options) -> TextBlock: interline = TextBlock("+" + "+".join((w + 2) * "-" for w in widths) + "+") else: interline = TextBlock((sum(w + 3 for w in widths) - 2) * "-") - full_width = interline.width - 4 + full_width = interline.width() - 4 else: if col_border: interline = ( @@ -316,10 +348,10 @@ def grid(items: list, **options) -> TextBlock: + TextBlock("|".join((w + 2) * " " for w in widths)) + TextBlock("|") ) - full_width = max(0, interline.width - 4) + full_width = max(0, interline.width() - 4) else: interline = TextBlock((sum(w + 3 for w in widths) - 3) * " ") - full_width = max(0, interline.width - 4) + full_width = max(0, interline.width() - 4) def normalize_widths(row): if isinstance(row, TextBlock): @@ -336,11 +368,20 @@ def normalize_widths(row): "|", height=row_height, base=row_base, left_padding=1, right_padding=1 ) - new_row_txt = col_sep.join(row) - new_row_txt = ( - draw_vertical("|", row_height, base=row_base, right_padding=1) - + new_row_txt - + draw_vertical("|", row_height, base=row_base, left_padding=1) + if row: + field, *rest_row_txt = row + new_row_txt = field + for field in rest_row_txt: + new_row_txt = TextBlock( + *TextBlock.next(new_row_txt, col_sep, field) + ) + else: + new_row_txt = TextBlock("") + vertical_line = draw_vertical( + "|", row_height, base=row_base, left_padding=1 + ) + new_row_txt = TextBlock( + *TextBlock.next(vertical_line, new_row_txt, vertical_line) ) if i == 0: if row_border: @@ -351,7 +392,16 @@ def normalize_widths(row): result = new_row_txt.stack(result, align="l") else: for i, row in enumerate(items): - new_row_txt = TextBlock(" ").join(row) + separator = TextBlock(" ") + if row: + field, *rest = row + new_row_txt = field + for field in rest: + new_row_txt = TextBlock( + *TextBlock.next(new_row_txt, separator, field) + ) + else: + new_row_txt = TextBlock("") if i == 0: if row_border: new_row_txt = new_row_txt.stack(interline, align="l") @@ -363,7 +413,7 @@ def normalize_widths(row): if row_border: result = interline.stack(result, align="l") - result.base = int(result.height / 2) + result.baseline = int(result.height() / 2) return result @@ -507,3 +557,20 @@ def superscript(base: Union[TextBlock, str], a: Union[TextBlock, str]) -> TextBl base = TextBlock(*TextBlock.next(base, TextBlock(a_width * " "))) result = TextBlock(*TextBlock.above(base, a)) return result + + +def join_blocks(*blocks) -> TextBlock: + """ + Concatenate blocks. + The same that the idiom + TextBlock(*TextBlock.next(*blocks)) + """ + return TextBlock(*TextBlock.next(*blocks)) + + +TEXTBLOCK_COMMA = TextBlock(",") +TEXTBLOCK_MINUS = TextBlock("-") +TEXTBLOCK_NULL = TextBlock("") +TEXTBLOCK_PLUS = TextBlock("+") +TEXTBLOCK_QUOTE = TextBlock("'") +TEXTBLOCK_SPACE = TextBlock(" ") diff --git a/mathics/format/prettyprint.py b/mathics/format/prettyprint.py index 21d0dda63..068f1bbca 100644 --- a/mathics/format/prettyprint.py +++ b/mathics/format/prettyprint.py @@ -29,6 +29,12 @@ ) from mathics.eval.makeboxes import compare_precedence, do_format # , format_element from mathics.format.pane_text import ( + TEXTBLOCK_COMMA, + TEXTBLOCK_MINUS, + TEXTBLOCK_NULL, + TEXTBLOCK_PLUS, + TEXTBLOCK_QUOTE, + TEXTBLOCK_SPACE, TextBlock, bracket, curly_braces, @@ -36,6 +42,7 @@ grid, integral_definite, integral_indefinite, + join_blocks, parenthesize, sqrt_block, subscript, @@ -49,6 +56,15 @@ SymbolRight = Symbol("System`Right") SymbolLeft = Symbol("System`Left") + +TEXTBLOCK_ARROBA = TextBlock("@") +TEXTBLOCK_BACKQUOTE = TextBlock("`") +TEXTBLOCK_DOUBLESLASH = TextBlock("//") +TEXTBLOCK_GRAPHICS = TextBlock("-Graphics-") +TEXTBLOCK_GRAPHICS3D = TextBlock("-Graphics3D-") +TEXTBLOCK_ONE = TextBlock("1") +TEXTBLOCK_TILDE = TextBlock("~") + #### Functions that convert Expressions in TextBlock @@ -105,15 +121,15 @@ def _default_expression_to_2d_text( """ expr_head = expr.head head = expression_to_2d_text(expr_head, evaluation, form, **kwargs) - comma = TextBlock(", ") + comma = join_blocks(TEXTBLOCK_COMMA, TEXTBLOCK_SPACE) elements = [expression_to_2d_text(elem, evaluation) for elem in expr.elements] - result = elements.pop(0) if elements else TextBlock(" ") + result = elements.pop(0) if elements else TEXTBLOCK_SPACE while elements: - result = result + comma + elements.pop(0) + result = join_blocks(result, comma, elements.pop(0)) if form is SymbolTraditionalForm: - return head + parenthesize(result) - return TextBlock(*TextBlock.next(head, bracket(result))) + return join_blocks(head, parenthesize(result)) + return join_blocks(head, bracket(result)) def _divide(num, den, evaluation, form, **kwargs): @@ -159,20 +175,20 @@ def derivative_expression_to_2d_text( if len(derivatives) == 1: order_iv = derivatives[0] if order_iv == Integer1: - return TextBlock(*TextBlock.next(function_head, TextBlock("'"))) + return join_blocks(function_head, TEXTBLOCK_QUOTE) elif order_iv == Integer2: - return TextBlock(*TextBlock.next(function_head, TextBlock("''"))) + return join_blocks(function_head, TEXTBLOCK_QUOTE, TEXTBLOCK_QUOTE) if not kwargs["2d"]: return _default_expression_to_2d_text(expr, evaluation, form, **kwargs) - comma = TextBlock(",") + comma = TEXTBLOCK_COMMA superscript_tb, *rest_derivatives = ( expression_to_2d_text(order, evaluation, form, **kwargs) for order in derivatives ) for order in rest_derivatives: - superscript_tb = TextBlock(*TextBlock.next(superscript_tb, comma, order)) + superscript_tb = join_blocks(superscript_tb, comma, order) superscript_tb = parenthesize(superscript_tb) return superscript(function_head, superscript_tb) @@ -200,7 +216,7 @@ def divide_expression_to_2d_text( def graphics( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: - return TextBlock("-Graphics-") + return TEXTBLOCK_GRAPHICS expr_to_2d_text_map["System`Graphics"] = graphics @@ -209,7 +225,7 @@ def graphics( def graphics3d( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: - return TextBlock("-Graphics3D-") + return TEXTBLOCK_GRAPHICS3D expr_to_2d_text_map["System`Graphics3D"] = graphics3d @@ -290,7 +306,7 @@ def list_expression_to_2d_text( expression_to_2d_text(elem, evaluation, form, **kwargs) for elem in expr.elements ) - comma_tb = TextBlock(", ") + comma_tb = join_blocks(TEXTBLOCK_COMMA, TEXTBLOCK_SPACE) for next_elem in rest_elems: result = TextBlock(*TextBlock.next(result, comma_tb, next_elem)) return curly_braces(result) @@ -326,7 +342,9 @@ def plus_expression_to_2d_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: elements = expr.elements - result = TextBlock("") + result = TEXTBLOCK_NULL + tb_minus = join_blocks(TEXTBLOCK_SPACE, TEXTBLOCK_MINUS, TEXTBLOCK_SPACE) + tb_plus = join_blocks(TEXTBLOCK_SPACE, TEXTBLOCK_PLUS, TEXTBLOCK_SPACE) for i, elem in enumerate(elements): if elem.has_form("Times", None): # If the first element is -1, remove it and use @@ -334,51 +352,51 @@ def plus_expression_to_2d_text( first = elem.elements[0] if isinstance(first, Integer): if first.value == -1: - result = ( - result - + " - " - + expression_to_2d_text( + result = join_blocks( + result, + tb_minus, + expression_to_2d_text( Expression(SymbolTimes, *elem.elements[1:]), evaluation, form, **kwargs, - ) + ), ) continue elif first.value < 0: - result = ( - result - + " " - + expression_to_2d_text(elem, evaluation, form, **kwargs) + result = join_blocks( + result, + TEXTBLOCK_SPACE, + expression_to_2d_text(elem, evaluation, form, **kwargs), ) continue elif isinstance(first, Real): if first.value < 0: - result = ( - result - + " " - + expression_to_2d_text(elem, evaluation, form, **kwargs) + result = join_blocks( + result, + TEXTBLOCK_SPACE, + expression_to_2d_text(elem, evaluation, form, **kwargs), ) continue - result = ( - result + " + " + expression_to_2d_text(elem, evaluation, form, **kwargs) + result = join_blocks( + result, tb_plus, expression_to_2d_text(elem, evaluation, form, **kwargs) ) ## TODO: handle complex numbers? else: elem_txt = expression_to_2d_text(elem, evaluation, form, **kwargs) if (compare_precedence(elem, 310) or -1) < 0: elem_txt = parenthesize(elem_txt) - result = result + " + " + elem_txt + result = join_blocks(result, tb_plus, 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 + result = join_blocks(result, elem_txt) else: - result = ( - result - + " + " - + expression_to_2d_text(elem, evaluation, form, **kwargs) + result = join_blocks( + result, + tb_plus, + expression_to_2d_text(elem, evaluation, form, **kwargs), ) return result @@ -438,14 +456,14 @@ def pre_pos_fix_expression_to_2d_text( ops_txt = [expression_to_2d_text(ops, evaluation, form, **kwargs)] else: if head is SymbolPrefix: - default_symb = TextBlock(" @ ") - ops_txt = ( - expression_to_2d_text(head, evaluation, form, **kwargs) + default_symb + default_symb = TEXTBLOCK_ARROBA + ops_txt = join_blocks( + expression_to_2d_text(head, evaluation, form, **kwargs), default_symb ) - elif head is SymbolPostfix: - default_symb = TextBlock(" // ") - ops_txt = default_symb + expression_to_2d_text( - head, evaluation, form, **kwargs + else: # head is SymbolPostfix: + default_symb = TEXTBLOCK_DOUBLESLASH + ops_txt = join_blocks( + default_symb, expression_to_2d_text(head, evaluation, form, **kwargs) ) # Processing the third argument, if it is there: @@ -469,10 +487,11 @@ def pre_pos_fix_expression_to_2d_text( if cmp_precedence is not None and cmp_precedence != -1: target_txt = parenthesize(target_txt) - if head is SymbolPrefix: - return TextBlock(*TextBlock.next(ops_txt[0], target_txt)) - if head is SymbolPostfix: - return TextBlock(*TextBlock.next(target_txt, ops_txt[0])) + return ( + join_blocks(ops_txt[0], target_txt) + if head is SymbolPrefix + else join_blocks(target_txt, ops_txt[0]) + ) expr_to_2d_text_map["System`Prefix"] = pre_pos_fix_expression_to_2d_text @@ -516,24 +535,15 @@ def infix_expression_to_2d_text( elif head in (SymbolPrefix, SymbolPostfix): ops_txt = [expression_to_2d_text(ops, evaluation, form, **kwargs)] else: - if head is SymbolInfix: - num_ops = 1 - default_symb = TextBlock(" ~ ") - ops_lst = [ - default_symb - + expression_to_2d_text(head, evaluation, form, **kwargs) - + default_symb - ] - elif head is SymbolPrefix: - default_symb = TextBlock(" @ ") - ops_txt = ( - expression_to_2d_text(head, evaluation, form, **kwargs) + default_symb - ) - elif head is SymbolPostfix: - default_symb = TextBlock(" // ") - ops_txt = default_symb + expression_to_2d_text( - head, evaluation, form, **kwargs + num_ops = 1 + default_symb = join_blocks(TEXTBLOCK_SPACE, TEXTBLOCK_TILDE, TEXTBLOCK_SPACE) + ops_lst = [ + join_blocks( + default_symb, + expression_to_2d_text(head, evaluation, form, **kwargs), + default_symb, ) + ] # Processing the third argument, if it is there: if len(elements) > 2: @@ -550,50 +560,35 @@ def infix_expression_to_2d_text( if group is SymbolNone: group = None - if head is SymbolPrefix: - operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_2d_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] + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + for index, operand in enumerate(operands): + operand_txt = expression_to_2d_text(operand, evaluation, form, **kwargs) cmp_precedence = compare_precedence(operand, precedence) - target_txt = expression_to_2d_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 = expression_to_2d_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 + 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 = TEXTBLOCK_SPACE + if str(ops_lst[index % num_ops]) != " ": + result_lst = [ + result, + space, + ops_lst[index % num_ops], + space, + operand_txt, + ] else: - space = TextBlock(" ") - if str(ops_lst[index % num_ops]) != " ": - result_lst = ( - result, - space, - ops_lst[index % num_ops], - space, - operand_txt, - ) - else: - result_lst = (result, space, operand_txt) + result_lst = [result, space, operand_txt] - return TextBlock(*TextBlock.next(*result_lst)) + return join_blocks(*result_lst) expr_to_2d_text_map["System`Infix"] = infix_expression_to_2d_text @@ -738,11 +733,13 @@ def stringform_expression_to_2d_text( quote_open = False continue else: - result = result + "`" + part + "`" + result = join_blocks( + result, TEXTBLOCK_BACKQUOTE, part, TEXTBLOCK_BACKQUOTE + ) quote_open = False continue else: - result = result + part + result = join_blocks(result, part) quote_open = True return result @@ -856,7 +853,7 @@ def times_expression_to_2d_text( return expression_to_2d_text(num[0], evaluation, form, **kwargs) prefactor = 1 - result: TextBlock = TextBlock("") + result: TextBlock = TEXTBLOCK_NULL for i, elem in enumerate(num): if elem is IntegerM1: prefactor *= -1 @@ -871,11 +868,11 @@ def times_expression_to_2d_text( if i == 0: result = elem_txt else: - result = result + " " + elem_txt - if result.text == "": - result = TextBlock("1") + result = join_blocks(result, TEXTBLOCK_SPACE, elem_txt) + if str(result) == "": + result = TEXTBLOCK_ONE if prefactor == -1: - result = TextBlock("-") + result + result = join_blocks(TEXTBLOCK_MINUS, result) return result