diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py index 0f88784ef..1d9704c46 100755 --- a/mathics/builtin/__init__.py +++ b/mathics/builtin/__init__.py @@ -36,6 +36,8 @@ mathics_to_python, ) from mathics.core.pattern import pattern_objects +from mathics.core.symbols import Symbol +from mathics.eval.makeboxes import builtins_precedence from mathics.settings import ENABLE_FILES_MODULE from mathics.version import __version__ # noqa used in loading to check consistency. @@ -60,7 +62,7 @@ def add_builtins(new_builtins): # print("XXX1", sympy_name) sympy_to_mathics[sympy_name] = builtin if isinstance(builtin, Operator): - builtins_precedence[name] = builtin.precedence + builtins_precedence[Symbol(name)] = builtin.precedence if isinstance(builtin, PatternObject): pattern_objects[name] = builtin.__class__ _builtins.update(dict(new_builtins)) @@ -237,8 +239,6 @@ def name_is_builtin_symbol(module, name: str) -> Optional[type]: mathics_to_sympy = {} # here we have: name -> sympy object sympy_to_mathics = {} -builtins_precedence = {} - new_builtins = _builtins_list # FIXME: some magic is going on here.. diff --git a/mathics/builtin/layout.py b/mathics/builtin/layout.py index 26419763f..8812a3026 100644 --- a/mathics/builtin/layout.py +++ b/mathics/builtin/layout.py @@ -1,33 +1,164 @@ # -*- coding: utf-8 -*- """ -This module contains symbols used to define the high level layout for +This module contains symbols used to define the high level layout for \ expression formatting. -For instance, to represent a set of consecutive expressions in a row, -we can use ``Row`` +For instance, to represent a set of consecutive expressions in a row, \ +we can use ``Row``. """ - +from typing import Optional, Union from mathics.builtin.base import BinaryOperator, Builtin, Operator from mathics.builtin.box.layout import GridBox, RowBox, to_boxes from mathics.builtin.lists import list_boxes from mathics.builtin.makeboxes import MakeBoxes from mathics.builtin.options import options_to_rules -from mathics.core.atoms import Real, String +from mathics.core.atoms import Integer, Integer1, Real, String +from mathics.core.convert.op import operator_to_ascii, operator_to_unicode +from mathics.core.element import BaseElement from mathics.core.expression import Evaluation, Expression from mathics.core.list import ListExpression -from mathics.core.symbols import Symbol -from mathics.core.systemsymbols import SymbolMakeBoxes -from mathics.eval.makeboxes import format_element - +from mathics.core.symbols import Atom, Symbol +from mathics.core.systemsymbols import ( + SymbolFullForm, + SymbolInputForm, + SymbolLeft, + SymbolMakeBoxes, + SymbolNone, + SymbolOutputForm, + SymbolRight, + SymbolRowBox, +) +from mathics.eval.makeboxes import eval_fullform_makeboxes, format_element, parenthesize + +SymbolNonAssociative = Symbol("System`NonAssociative") SymbolSubscriptBox = Symbol("System`SubscriptBox") +#################################################################### +# This section might get moved to mathics.box +# +# Some of the code below get replace Mathics code or may get put in +# collection of boxing modules, +# e.g. ``mathics.box.boxes_operators.box_infix()``. +# +#################################################################### + + +def box_infix( + expr: Expression, + operator: BaseElement, + form: Symbol, + evaluation: Evaluation, + precedence_value: Optional[int] = 0, + grouping: Optional[str] = None, +) -> Optional[Expression]: + """Implements MakeBoxes[Infix[...]]. + This function kicks off boxing for Infix operators. + + Operators are processed to add spaces and to use the right encoding. + """ + + # FIXME: this should go into a some formatter. + def format_operator(operator) -> Union[String, BaseElement]: + """ + Format infix operator `operator`. To do this outside parameter form is used. + Sometimes no changes are made and operator is returned unchanged. + + This function probably should be rewritten be more scalable across other forms + and moved to a module that contiaing similar formatting routines. + """ + if not isinstance(operator, String): + return MakeBoxes(operator, form) + + op_str = operator.value + + # FIXME: performing a check using the operator symbol representation feels a bit + # fragile. The operator name seems more straightforward and more robust. + if form == SymbolInputForm and op_str in ["*", "^", " "]: + return operator + elif ( + form in (SymbolInputForm, SymbolOutputForm) + and not op_str.startswith(" ") + and not op_str.endswith(" ") + ): + # FIXME: Again, testing on specific forms is fragile and not scalable. + op = String(" " + op_str + " ") + return op + return operator + + if isinstance(expr, Atom): + evaluation.message("Infix", "normal", Integer1) + return None + + elements = expr.elements + if len(elements) > 1: + if operator.has_form("List", len(elements) - 1): + operator = [format_operator(op) for op in operator.elements] + return box_infix_elements( + elements, operator, precedence_value, grouping, form + ) + else: + encoding_rule = evaluation.definitions.get_ownvalue("$CharacterEncoding") + encoding = "UTF8" if encoding_rule is None else encoding_rule.replace.value + op_str = ( + operator.value if isinstance(operator, String) else operator.short_name + ) + if encoding == "ASCII": + operator = format_operator( + String(operator_to_ascii.get(op_str, op_str)) + ) + else: + operator = format_operator( + String(operator_to_unicode.get(op_str, op_str)) + ) + + return box_infix_elements(elements, operator, precedence_value, grouping, form) + + elif len(elements) == 1: + return MakeBoxes(elements[0], form) + else: + return MakeBoxes(expr, form) + + +# FIXME: op should be a string, so remove the Union. +def box_infix_elements( + elements, op: Union[String, list], precedence: int, grouping, form: Symbol +) -> Expression: + result = [] + for index, element in enumerate(elements): + if index > 0: + if isinstance(op, list): + result.append(op[index - 1]) + else: + result.append(op) + parenthesized = False + if grouping == "System`NonAssociative": + parenthesized = True + elif grouping == "System`Left" and index > 0: + parenthesized = True + elif grouping == "System`Right" and index == 0: + parenthesized = True + + element_boxes = Expression(SymbolMakeBoxes, element, form) + element = parenthesize(precedence, element, element_boxes, parenthesized) + + result.append(element) + return Expression(SymbolRowBox, ListExpression(*result)) + + +#################################################################### +# End of section of code that might be in mathics.box. +#################################################################### + + class Center(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Center.html + + :WMA link: + https://reference.wolfram.com/language/ref/Center.html
'Center' @@ -93,7 +224,7 @@ class Grid(Builtin): options = GridBox.options summary_text = " 2D layout containing arbitrary objects" - def eval_makeboxes(self, array, f, evaluation, options) -> Expression: + def eval_makeboxes(self, array, f, evaluation: Evaluation, options) -> Expression: """MakeBoxes[Grid[array_?MatrixQ, OptionsPattern[Grid]], f:StandardForm|TraditionalForm|OutputForm]""" return GridBox( @@ -145,10 +276,53 @@ class Infix(Builtin): """ messages = { + "argb": "Infix called with `1` arguments; between 1 and 4 arguments are expected.", + "group": "Infix::group: Grouping specification `1` is not NonAssociative, None, Left, or Right.", + "intm": "Machine-sized integer expected at position 3 in `1`", "normal": "Nonatomic expression expected at position `1`", } summary_text = "infix form" + # the right rule should be + # mbexpression:MakeBoxes[Infix[___], form] + def eval_makeboxes(self, expression, form, evaluation: Evaluation): + """MakeBoxes[Infix[___], + form:StandardForm|TraditionalForm|OutputForm|InputForm]""" + infix_expr = expression.elements[0] + elements = list(infix_expr.elements) + num_parms = len(elements) + + if num_parms == 0 or num_parms > 4: + evaluation.message("Infix", "argb", Integer(num_parms)) + return eval_fullform_makeboxes(infix_expr, evaluation, form) + + if num_parms == 1: + expr = elements[0] + return box_infix(expr, String("~"), form, evaluation) + if num_parms == 2: + expr, operator = elements + return box_infix(expr, operator, form, evaluation) + + expr, operator, precedence = elements[:3] + if not isinstance(precedence, Integer): + evaluation.message( + "Infix", + "intm", + Expression(SymbolFullForm, infix_expr), + ) + return eval_fullform_makeboxes(infix_expr, evaluation, form) + + grouping = SymbolNone if num_parms < 4 else elements[3] + if grouping is SymbolNone: + return box_infix(expr, operator, form, evaluation, precedence.value) + if grouping in (SymbolNonAssociative, SymbolLeft, SymbolRight): + return box_infix( + expr, operator, form, evaluation, precedence.value, grouping.get_name() + ) + + evaluation.message("Infix", "argb", grouping) + return eval_fullform_makeboxes(infix_expr, evaluation, form) + class Left(Builtin): """ @@ -156,7 +330,8 @@ class Left(Builtin):
'Left' -
is used with operator formatting constructs to specify a left-associative operator. +
is used with operator formatting constructs to specify a \ + left-associative operator.
""" @@ -171,7 +346,8 @@ class NonAssociative(Builtin):
'NonAssociative' -
is used with operator formatting constructs to specify a non-associative operator. +
is used with operator formatting constructs to specify a \ + non-associative operator.
""" @@ -288,7 +464,8 @@ class Right(Builtin):
'Right' -
is used with operator formatting constructs to specify a right-associative operator. +
is used with operator formatting constructs to specify a \ + right-associative operator.
""" @@ -307,21 +484,21 @@ class Row(Builtin): summary_text = "1D layouts containing arbitrary objects in a row" - def eval_makeboxes(self, items, sep, f, evaluation: Evaluation): + def eval_makeboxes(self, items, sep, form, evaluation: Evaluation): """MakeBoxes[Row[{items___}, sep_:""], - f:StandardForm|TraditionalForm|OutputForm]""" + form:StandardForm|TraditionalForm|OutputForm]""" items = items.get_sequence() if not isinstance(sep, String): - sep = MakeBoxes(sep, f) + sep = MakeBoxes(sep, form) if len(items) == 1: - return MakeBoxes(items[0], f) + return MakeBoxes(items[0], form) else: result = [] for index, item in enumerate(items): if index > 0 and not sep.sameQ(String("")): result.append(to_boxes(sep, evaluation)) - item = MakeBoxes(item, f).evaluate(evaluation) + item = MakeBoxes(item, form).evaluate(evaluation) item = to_boxes(item, evaluation) result.append(item) return RowBox(*result) @@ -334,22 +511,31 @@ class Style(Builtin):
'Style[$expr$, options]'
displays $expr$ formatted using the specified option settings. +
'Style[$expr$, "style"]'
uses the option settings for the specified style in the current notebook. +
'Style[$expr$, $color$]'
displays using the specified color. +
'Style[$expr$, $Bold$]'
displays with fonts made bold. +
'Style[$expr$, $Italic$]'
displays with fonts made italic. +
'Style[$expr$, $Underlined$]'
displays with fonts underlined. +
'Style[$expr$, $Larger$]
displays with fonts made larger. +
'Style[$expr$, $Smaller$]'
displays with fonts made smaller. +
'Style[$expr$, $n$]'
displays with font size n. +
'Style[$expr$, $Tiny$]'
'Style[$expr$, $Small$]', etc.
display with fonts that are tiny, small, etc. @@ -397,7 +583,9 @@ def eval_makeboxes(self, x, y, f, evaluation) -> Expression: class Subsuperscript(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Subsuperscript.html + + :WMA link: + https://reference.wolfram.com/language/ref/Subsuperscript.html
'Subsuperscript[$a$, $b$, $c$]' @@ -419,7 +607,9 @@ class Subsuperscript(Builtin): class Superscript(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Superscript.html + + :WMA link: + https://reference.wolfram.com/language/ref/Superscript.html
'Superscript[$x$, $y$]' diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index 8dbac5fb7..e58005889 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -19,8 +19,13 @@ from mathics.core.list import ListExpression 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.core.systemsymbols import ( + SymbolInputForm, + SymbolNone, + SymbolOutputForm, + SymbolRowBox, +) +from mathics.eval.makeboxes import _boxed_string, format_element, parenthesize def int_to_s_exp(expr, n): @@ -35,30 +40,6 @@ def int_to_s_exp(expr, n): return s, exp, nonnegative -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(")")), - ) - return element_boxes - - # FIXME: op should be a string, so remove the Union. def make_boxes_infix( elements, op: Union[String, list], precedence: int, grouping, form: Symbol @@ -451,12 +432,38 @@ def eval_postprefix(self, p, expr, h, prec, f, evaluation): else: return MakeBoxes(expr, f).evaluate(evaluation) + # FIXME: prec is sometimes not an Integer for a ListExpession. + # And this is recent with respect to PredefinedSymbol revision. + # How does this get set and why? def eval_infix( - self, expr, operator, prec: Integer, grouping, form: Symbol, evaluation + self, expr, operator, precedence: Integer, grouping, form: Symbol, evaluation ): - """MakeBoxes[Infix[expr_, operator_, prec_:None, grouping_:None], + """MakeBoxes[Infix[expr_, operator_, precedence_:None, grouping_:None], form:StandardForm|TraditionalForm|OutputForm|InputForm]""" + # In WMA, this is covered with two different rules: + # * ```MakeBoxes[Infix[expr_, operator_]``` + # * ```MakeBoxes[Infix[expr_, operator_, precedence_, grouping_:None]``` + # In the second form, precedence must be an Integer. Otherwise, a message is + # shown and fall back to the standard MakeBoxes form. + # Here we allow Precedence to be ```System`None```, to have just one rule. + + if precedence is SymbolNone: + precedence_value = None + elif isinstance(precedence, Integer): + precedence_value = precedence.value + else: + if grouping is not SymbolNone: + # Here I use a String as a head to avoid a circular evaluation. + # TODO: Use SymbolInfix when the MakeBoxes refactor be done. + expr = Expression(String("Infix"), expr, operator, precedence, grouping) + else: + expr = Expression(String("Infix"), expr, operator, precedence) + evaluation.message("Infix", "intm", expr) + return self.eval_general(expr, form, evaluation) + + grouping = grouping + ## FIXME: this should go into a some formatter. def format_operator(operator) -> Union[String, BaseElement]: """ @@ -485,7 +492,7 @@ def format_operator(operator) -> Union[String, BaseElement]: return op return operator - precedence = prec.value if hasattr(prec, "value") else 0 + precedence = precedence.value if hasattr(precedence, "value") else 0 grouping = grouping.get_name() if isinstance(expr, Atom): @@ -496,7 +503,9 @@ def format_operator(operator) -> Union[String, BaseElement]: if len(elements) > 1: if operator.has_form("List", len(elements) - 1): operator = [format_operator(op) for op in operator.elements] - return make_boxes_infix(elements, operator, precedence, grouping, form) + return make_boxes_infix( + elements, operator, precedence_value, grouping, form + ) else: encoding_rule = evaluation.definitions.get_ownvalue( "$CharacterEncoding" @@ -518,7 +527,9 @@ def format_operator(operator) -> Union[String, BaseElement]: String(operator_to_unicode.get(op_str, op_str)) ) - return make_boxes_infix(elements, operator, precedence, grouping, form) + return make_boxes_infix( + elements, operator, precedence_value, grouping, form + ) elif len(elements) == 1: return MakeBoxes(elements[0], form) diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py index db8b9f9a4..225601a1e 100644 --- a/mathics/core/symbols.py +++ b/mathics/core/symbols.py @@ -341,7 +341,11 @@ class Symbol(Atom, NumericOperators, EvalMixin): also associated with a ``Definition``. But it has a ``DownValue`` that is used in its evaluation. - We also have Symbols which in contrast to Variables Symbols have + A Function Symbol, like a Variable Symbol, is a ``Symbol`` that is + also associated with a ``Definition``. But it has a ``DownValue`` + that is used in its evaluation. + + We also have Symbols which, in contrast to Variables Symbols, have a constant value that cannot change. System`True and System`False are like this. diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 700a6c9d4..d248b48a6 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -9,7 +9,7 @@ import typing from typing import Any, Dict, Type -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 @@ -38,6 +38,7 @@ SymbolMinus, SymbolOutputForm, SymbolRational, + SymbolRowBox, SymbolStandardForm, ) @@ -299,6 +300,38 @@ def do_format_expression( return expr +def parenthesize( + precedence: int, element: Type[BaseElement], element_boxes, when_equal: bool +) -> Type[Expression]: + """ + "Determines if ``element_boxes`` needs to be surrounded with parenthesis. + This is done based on ``precedence`` and the computed preceence of + ``element``. The adjusted ListExpression is returned. + + 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 + # 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()) + 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(")")), + ) + return element_boxes + + element_formatters[Rational] = do_format_rational element_formatters[Complex] = do_format_complex element_formatters[Expression] = do_format_expression