Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions mathics/builtin/forms/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from typing import Callable

import mathics.core.definitions as definitions
from mathics.builtin.base import Builtin
from mathics.core.element import BaseElement
from mathics.core.symbols import Symbol

form_symbol_to_class = {}
Expand Down Expand Up @@ -36,6 +39,57 @@ def __new__(cls, *args, **kwargs):
form_symbol_to_class[Symbol(name)] = cls
return instance

@classmethod
def box(cls, element: BaseElement, evaluation):
"""
This method is is called for each element that can be boxed.
("box" here is a an active verb, not a noun).

This is a generic routine which calls the specific boxing routine
that has been regstered in class variable ``form_box_methods`` previously.

If nothing has been registered, we just return ``element`` back unmodified
as we do in evaluation.

Specific and custom method need to be implemented for each Form
and element_type that perform some kind of boxing.
"""
method = cls.form_box_methods.get((cls, element.head), None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is something that I am not seeing in this mechanism: Suppose that we set inside a session a makeboxes rule. Let's say

MakeBoxes[F[x_],f_]:=SubscriptBox[RowBox[{"<<",ToString[x],">>"}], "F"]
  • Where is this new rule stored?
  • How MakeBoxes find this rule?

Copy link
Member Author

@rocky rocky Oct 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is something that I am not seeing in this mechanism: Suppose that we set inside a session a makeboxes rule. Let's say

MakeBoxes[F[x_],f_]:=SubscriptBox[RowBox[{"<<",ToString[x],">>"}], "F"]
  • Where is this new rule stored?
  • How MakeBoxes find this rule?

I am glad you asked this question. Let me direct or attention to this comment in the demo program.

This code is a sketch. It does not change MakeBoxes. Instead the goal was to give an organization where boxing, and later, formatting decisions have no notion of the form that they are embedded in.

It is possible and possibly likely that instead of doing this in methods it needs to be done as rules. In that case, instead of the box_divide(), box_power() methods inside mathics.builtin.forms.default_boxing, there would be rules instead. Or maybe this could be totally written as default_boxing_rules.m and autoloaded.

Here is a challenge for you if you are up for it...

Using MakeExpression, MakeBoxes and Format, write a new form like MMateraForm in WL code. It would simulate one of the non-trivial forms like StandardForm.

You are free to "cheat" (actually, you are encouraged to) and use rules from FormatValues or any information that a WL implementation will tell you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am glad you asked this question. Let me direct or attention to this comment in the demo program.

OK, I saw that. What I was trying to ask about is how you are envisioning that this could work.

This code is a sketch. It does not change MakeBoxes. Instead the goal was to give an organization where boxing, and later, formatting decisions have no notion of the form that they are embedded in.

It is possible and possily likely that instead of doing this in methods it needs to be done as rules. In that case, instead of the box_divide(), box_power() methods inside mathics.builtin.forms.default_boxing, there would be rules instead. Or maybe this could be totally written as default_boxing_rules.m and autoloaded.

Here is a challenge for you if you are up for it...

Using MakeExpression, MakeBoxes and Format, write a new form like MMateraForm in WL code. It would simulate one of the non-trivial forms like StandardForm.

You are free to "cheat" and use rules from FormatValues or any information that an WL implementation will tell you.

OK, I could provide you with some examples of that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other I hope small request here. Please start out with simple rules and Forms that lead to simplicity. There is this tendency to lead off with complicated and convoluted edge-case examples involving features we don't currently support in Mathics. And then we go down a rabbit hole focusing on this which doesn't change the base situation.

Instead, it would be awesome if we could start out with simple rules or Forms mimics that cover the majority of the common cases.

This is not to say that we won't get to the complicated and convoluted cases, just that we might do that later after we have some simple mastery of doing things more closely as it is done in Mathematica. (In general, I'd say this is a good thing to practice doing anyway.)

if method is None:
# The default class should have been registered under FormBaseClass
method = cls.form_box_methods.get((FormBaseClass, element.head), None)
if method is None:
# Just return the element unmodified.
# Note: this is what we do in evaluation when we don't have a match
return element

return method(element, evaluation)

@classmethod
def register_box_method(cls, symbol: Symbol, method: Callable):
"""
Register ``method`` so method(element, ...) is called when
``form.box(element, ...)`` is called.

"form" is something like ``StandardForm``, ``TraditionalForm``, etc.

To register the default boxing routine, register under the class
``FormBaseClass``
"""

cls.form_box_methods[cls, symbol] = method


def box(element: BaseElement, evaluation, form: Symbol):
"""
Basically redirects the "box" call from a form symbol name to the "box" method off of
the Form class named by the symbol.
"""
form_class = form_symbol_to_class.get(form, None)
if form_class is None:
return element
return form_class.box(element, evaluation)


# FormBaseClass is a public Builtin class that
# should not get added as a definition (and therefore not added to
Expand Down
112 changes: 112 additions & 0 deletions test/formtest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""
Example to show how new Format-directed Boxing might work.

This is a minimal example so we can discuss feasiblity.
"""
from mathics.builtin.forms.base import box
from mathics.core.atoms import IntegerM1, Integer1, Integer2, Rational
from mathics.core.expression import Expression
from mathics.core.parser import parse, MathicsSingleLineFeeder
from mathics.core.symbols import SymbolDivide
from mathics.core.systemsymbols import (
SymbolFractionBox,
SymbolMakeBoxes,
SymbolPower,
SymbolSqrt,
SymbolStandardForm,
)
from mathics.session import MathicsSession

session = MathicsSession(character_encoding="ASCII")


# Hacky pseudo boxing rules.
# Currently in MakeBox rule rewriting occurs via MakeBox rewrite rules
# which are attached to various Builtin classes.

# The rewrite is performed as a part of rewriting portion of Expression evaluation.
# We probably want to segregate these rules from other kinds of rules.
#
# The below hacky code is to simulate the rule behavior for
# just a few kinds of things so we don't have to hook into the
# complex rewrite mechanism in use in general evaluation. The implementation we use
# is just good enough for the new kinds of things we need.
# It is not intended to be used in a final implementation.


def fractionbox_fn(expr):
# To be continued...
return Expression(SymbolFractionBox, *expr.elements)


def sqrtbox_fn(expr):
return Expression(SymbolSqrt, expr.elements[0])


def powerbox_fn(expr):
new_expr = expr.elements[0]
if new_expr.elements[-1] == IntegerM1:
return Expression(SymbolDivide, Integer1, new_expr.elements[0])
elif new_expr.elements[-1] in (Rational(1, 2), Integer1 * (Integer2**IntegerM1)):
return Expression(SymbolSqrt, new_expr.elements[0])
return new_expr


boxform_rules = {
# SymbolTimes: fractionbox_fn,
SymbolPower: powerbox_fn,
SymbolSqrt: sqrtbox_fn,
}


def apply_formatvalues_rules(expr, evaluation):
"""
Hacky replacement for rules() found in Expression rewrite_apply_eval().
Note, we need to add builtin FormatValues() and the internals that go with that.
"""
if expr.elements[-1] not in (SymbolStandardForm,): # Or more generally $BoxForms
# For other forms, there might be other transformations too, and this should
# be discussed.
# For simplicity, we will just handle a small number of $BoxForms rules
return expr

# Remove the Form from expression "expr"
unboxed_expr = expr.elements[0]

if unboxed_expr.head in boxform_rules:
new_expr = boxform_rules[unboxed_expr.head](expr)
return new_expr
return unboxed_expr


# Begin demo code.

for expr_str in (
# FIXME;
# "1 / x", # Show off "Division" boxing
"a ^ b", # Show off "Power" boxing
"Sqrt[a]", # "Square-root boxing"
"a ^ (1/2)", # "Square-root boxing"
):
print("expression: ", expr_str)

# Parse, but don't evaluate expression.
expr = parse(session.definitions, MathicsSingleLineFeeder(expr_str))
print("Parsed expression: ", expr)

# Here is how Mathics currently evaluates MakeBoxes
boxed_expr = Expression(SymbolMakeBoxes, expr, SymbolStandardForm)
print("Mathics MakeBoxes: ", boxed_expr)

# Evaluate to get final printed/rendered form
print("Eval'd Makeboxes: ", boxed_expr.evaluate(session.evaluation))

# Here is how Mathics might better box an expression.
# First we apply MakeBox boxing transformation rules.
# This handles expression rewriting.
transformed_boxed_expr = apply_formatvalues_rules(boxed_expr, session.evaluation)

boxed_expr2 = box(transformed_boxed_expr, session.evaluation, SymbolStandardForm)
print("New MakeBoxes: ", boxed_expr2)
print("-" * 30)
print("")