diff --git a/mathics/builtin/compilation.py b/mathics/builtin/compilation.py
index b0468d58a5..3893a3e1c1 100644
--- a/mathics/builtin/compilation.py
+++ b/mathics/builtin/compilation.py
@@ -210,7 +210,9 @@ class CompiledCodeBox(BoxConstruct):
"""
Used internally by CompileCode[].
"""
-
+ def output_cost(self):
+ return 12
+
def boxes_to_text(self, leaves=None, **options):
if leaves is None:
leaves = self._leaves
diff --git a/mathics/builtin/evaluation.py b/mathics/builtin/evaluation.py
index 708698fd4d..47a67a3d02 100644
--- a/mathics/builtin/evaluation.py
+++ b/mathics/builtin/evaluation.py
@@ -317,6 +317,58 @@ class Sequence(Builtin):
"""
+
+
+class OutputSizeLimit(Predefined):
+ """
+
+ - '$OutputSizeLimit'
+
- specifies the maximum amount of data output that gets
+ displayed before the output gets truncated. The amount of
+ output is measured as the number of bytes of MathML XML
+ that has been generated to represent the output data.
+
+ To set no limit on output size, use $OutputSizeLimit = Infinity.
+
+
+ >> $OutputSizeLimit = 50;
+
+ >> Table[i, {i, 1, 100}]
+ : Parts of this output were omitted (see <<71>>). To generate the whole output, please set $OutputSizeLimit = Infinity.
+ = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, <<71>>, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100}
+
+ #> Take[Range[1000], 1001]
+ : Cannot take positions 1 through 1001 in {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, <<976>>, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000}.
+ : Parts of this output were omitted (see <<976>>). To generate the whole output, please set $OutputSizeLimit = Infinity.
+ = Take[{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, <<976>>, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000}, 1001]
+
+ #> {}
+ = {}
+
+ #> $OutputSizeLimit = 100;
+
+ #> Table[Graphics[Table[Circle[],{10}]], {5}]
+ = {-Graphics-, -Graphics-, -Graphics-, -Graphics-, -Graphics-}
+
+ #> Quiet[ImageAvailable = SameQ[Head[Image[{{0, 1}, {1, 0}}] // ToBoxes], ImageBox]];
+ #> If[ImageAvailable, Table[Image[{{1, 0}, {0, 1}}], {5}], {"-Image-", "-Image-", "-Image-", "-Image-", "-Image-"}]
+ = {-Image-, -Image-, -Image-, -Image-, -Image-}
+
+ #> $OutputSizeLimit = Infinity;
+
+ """
+ attributes = ("Unprotected", )
+ name = '$OutputSizeLimit'
+ value = 1000
+
+ rules = {
+ '$OutputSizeLimit': str(value),
+ }
+
+ def evaluate(self, evaluation):
+ return Integer(self.value)
+
+
class Quit(Builtin):
"""
@@ -345,3 +397,4 @@ def apply(self, evaluation, n):
if isinstance(n, Integer):
exitcode = n.get_int_value()
raise SystemExit(exitcode)
+
diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py
index 4fd503607d..2b2df1a793 100644
--- a/mathics/builtin/inout.py
+++ b/mathics/builtin/inout.py
@@ -6,6 +6,7 @@
import re
import mpmath
+from itertools import chain
import typing
from typing import Any
@@ -37,8 +38,10 @@
PrecisionReal,
SymbolList,
SymbolMakeBoxes,
- SymbolRule
+ SymbolRule,
+ Omitted,
)
+
from mathics.core.numbers import (
dps,
convert_base,
@@ -130,11 +133,14 @@ def parenthesize(precedence, leaf, leaf_boxes, when_equal):
return leaf_boxes
-def make_boxes_infix(leaves, ops, precedence, grouping, form):
- result = []
- for index, leaf in enumerate(leaves):
- if index > 0:
- result.append(ops[index - 1])
+def make_boxes_infix(leaves, ops, precedence, grouping, form, evaluation):
+ def materialize(prefix, inner, suffix):
+ return Expression('RowBox', Expression('List', *list(chain(prefix, inner, suffix))))
+
+ def make_leaf(index):
+ leaf = leaves[index]
+ box = Expression('MakeBoxes', leaf, form)
+
parenthesized = False
if grouping == "System`NonAssociative":
parenthesized = True
@@ -143,11 +149,12 @@ def make_boxes_infix(leaves, ops, precedence, grouping, form):
elif grouping == "System`Right" and index == 0:
parenthesized = True
- leaf_boxes = MakeBoxes(leaf, form)
- leaf = parenthesize(precedence, leaf, leaf_boxes, parenthesized)
+ return parenthesize(precedence, leaf, box, parenthesized)
- result.append(leaf)
- return Expression("RowBox", Expression(SymbolList, *result))
+ return evaluation.make_boxes(
+ None, make_leaf, len(leaves),
+ None, None, ops,
+ materialize, form)
def real_to_s_exp(expr, n):
@@ -520,29 +527,24 @@ def apply_general(self, expr, f, evaluation):
# Parenthesize infix operators at the head of expressions,
# like (a + b)[x], but not f[a] in f[a][b].
- #
- head_boxes = parenthesize(670, head, MakeBoxes(head, f), False)
- result = [head_boxes, String(left)]
-
- if len(leaves) > 1:
- row = []
- if f_name in (
- "System`InputForm",
- "System`OutputForm",
- "System`FullForm",
- ):
- sep = ", "
- else:
- sep = ","
- for index, leaf in enumerate(leaves):
- if index > 0:
- row.append(String(sep))
- row.append(MakeBoxes(leaf, f))
- result.append(RowBox(Expression(SymbolList, *row)))
- elif len(leaves) == 1:
- result.append(MakeBoxes(leaves[0], f))
- result.append(String(right))
- return RowBox(Expression(SymbolList, *result))
+ prefix = parenthesize(670, head, Expression('MakeBoxes', head, f), False)
+
+ def make_leaf(i):
+ return Expression('MakeBoxes', leaves[i], f)
+
+ if f_name in ('System`InputForm', 'System`OutputForm',
+ 'System`FullForm'):
+ sep = ', '
+ else:
+ sep = ','
+
+ def materialize(prefix, inner, suffix):
+ if len(inner) > 1:
+ inner = [Expression('RowBox', Expression('List', *inner))]
+ return Expression('RowBox', Expression('List', *list(chain(prefix, inner, suffix))))
+
+ return evaluation.make_boxes(
+ prefix, make_leaf, len(leaves), String(left), String(right), String(sep), materialize, f)
def _apply_atom(self, x, f, evaluation):
"""MakeBoxes[x_?AtomQ,
@@ -609,7 +611,7 @@ def get_op(op):
ops = [get_op(op) for op in h.leaves]
else:
ops = [get_op(h)] * (len(leaves) - 1)
- return make_boxes_infix(leaves, ops, precedence, grouping, f)
+ return make_boxes_infix(leaves, ops, precedence, grouping, f, evaluation)
elif len(leaves) == 1:
return MakeBoxes(leaves[0], f)
else:
@@ -942,22 +944,37 @@ class Grid(Builtin):
options = GridBox.options
def apply_makeboxes(self, array, f, evaluation, options) -> Expression:
- """MakeBoxes[Grid[array_?MatrixQ, OptionsPattern[Grid]],
- f:StandardForm|TraditionalForm|OutputForm]"""
- return GridBox(
- Expression(
- "List",
- *(
- Expression(
- "List",
- *(Expression(SymbolMakeBoxes, item, f) for item in row.leaves)
- )
- for row in array.leaves
- )
- ),
- *options_to_rules(options)
- )
-
+ '''MakeBoxes[Grid[array_?MatrixQ, OptionsPattern[Grid]],
+ f:StandardForm|TraditionalForm|OutputForm]'''
+
+ lengths = [len(row._leaves) for row in array._leaves]
+ n_leaves = sum(lengths)
+
+ #def materialize(boxes):
+ def materialize(prefix, inner, suffix):
+ boxes = inner
+ if len(boxes) == n_leaves:
+ rows = []
+ i = 0
+ for l in lengths:
+ rows.append(Expression('List', *boxes[i:i + l]))
+ i += l
+ return Expression(
+ 'GridBox',
+ Expression('List', *rows),
+ *options_to_rules(options))
+ else: # too long
+ return Omitted('<<%d>>' % n_leaves)
+
+ flat = [item for row in array.leaves for item in row.leaves]
+
+ def make_leaf(i):
+ return Expression('MakeBoxes', flat[i], f)
+
+ return evaluation.make_boxes(
+ None, make_leaf, n_leaves,
+ None, None, None,
+ materialize, f)
# return Expression('GridBox',Expression('List', *(Expression('List', *(Expression('MakeBoxes', item, f) for item in row.leaves)) for row in array.leaves)), *options_to_rules(options))
@@ -1128,10 +1145,11 @@ class Subscript(Builtin):
def apply_makeboxes(self, x, y, f, evaluation) -> Expression:
"MakeBoxes[Subscript[x_, y__], f:StandardForm|TraditionalForm]"
- y = y.get_sequence()
- return Expression(
- "SubscriptBox", Expression(SymbolMakeBoxes, x, f), *list_boxes(y, f)
- )
+ def materialize(prefix, inner, suffix):
+ return Expression('SubscriptBox', *list(chain(prefix, inner, suffix)))
+
+ return list_boxes(
+ Expression('MakeBoxes', x, f), y.get_sequence(), materialize, f, evaluation)
class SubscriptBox(Builtin):
@@ -1308,6 +1326,12 @@ def apply_makeboxes(self, s, args, f, evaluation):
"""MakeBoxes[StringForm[s_String, args___],
f:StandardForm|TraditionalForm|OutputForm]"""
+ # StringForm does not call evaluation.make_boxes
+ # since we use it for messages and we never want
+ # to omit parts of the message. args are subject
+ # to MakeBoxes (see below) and thus can get parts
+ # omitted.
+
s = s.value
args = args.get_sequence()
result = []
@@ -1960,6 +1984,8 @@ class General(Builtin):
"syntax": "`1`",
"invalidargs": "Invalid arguments.",
"notboxes": "`1` is not a valid box structure.",
+ "omit": "Parts of this output were omitted (see `1`). " +
+ "To generate the whole output, please set $OutputSizeLimit = Infinity.",
"pyimport": '`1`[] is not available. Your Python installation misses the "`2`" module.',
}
diff --git a/mathics/builtin/lists.py b/mathics/builtin/lists.py
index 93df17d205..c966d8e1f6 100644
--- a/mathics/builtin/lists.py
+++ b/mathics/builtin/lists.py
@@ -281,9 +281,13 @@ def apply_makeboxes(self, items, f, evaluation):
f:StandardForm|TraditionalForm|OutputForm|InputForm]"""
items = items.get_sequence()
- return Expression(
- "RowBox", Expression(SymbolList, *list_boxes(items, f, "{", "}"))
- )
+
+ def materialize(prefix, inner, suffix):
+ return Expression(
+ "RowBox", Expression("List", *list(chain(prefix, inner, suffix)))
+ )
+
+ return list_boxes(None, items, materialize, f, evaluation, "{", "}")
class ListQ(Test):
@@ -317,25 +321,25 @@ def test(self, expr):
return expr.get_head_name() != "System`List"
-def list_boxes(items, f, open=None, close=None):
- result = [Expression(SymbolMakeBoxes, item, f) for item in items]
- if f.get_name() in ("System`OutputForm", "System`InputForm"):
+def list_boxes(prefix, items, materialize, f, evaluation, open=None, close=None):
+ if open is not None:
+ open = String(open)
+ if close is not None:
+ close = String(close)
+
+ if f in ("System`OutputForm", "System`InputForm"):
sep = ", "
else:
- sep = ","
- result = riffle(result, String(sep))
- if len(items) > 1:
- result = Expression("RowBox", Expression(SymbolList, *result))
- elif items:
- result = result[0]
- if result:
- result = [result]
- else:
- result = []
- if open is not None and close is not None:
- return [String(open)] + result + [String(close)]
- else:
- return result
+ sep = ", " # in the original, this was ",", but
+ # this broke several doctests...
+ # Let's restore it when we finish
+
+ def make_leaf(i):
+ return Expression("MakeBoxes", items[i], f)
+
+ return evaluation.make_boxes(
+ prefix, make_leaf, len(items), open, close, sep, materialize, f
+ )
class Length(Builtin):
@@ -1105,14 +1109,20 @@ def apply_makeboxes(self, list, i, f, evaluation):
f:StandardForm|TraditionalForm|OutputForm|InputForm]"""
i = i.get_sequence()
+
list = Expression(SymbolMakeBoxes, list, f)
+
if f.get_name() in ("System`OutputForm", "System`InputForm"):
open, close = "[[", "]]"
else:
open, close = "\u301a", "\u301b"
- indices = list_boxes(i, f, open, close)
- result = Expression("RowBox", Expression(SymbolList, list, *indices))
- return result
+
+ def materialize(prefix, inner, suffix):
+ return Expression(
+ "RowBox", Expression("List", *list(chain(prefix, inner, suffix)))
+ )
+
+ return list_boxes(list, i, materialize, f, evaluation, open, close)
def apply(self, list, i, evaluation):
"Part[list_, i___]"
@@ -5898,14 +5908,15 @@ def validate(exprs):
rules = rules.get_sequence()
if self.error_idx == 0 and validate(rules) is True:
expr = Expression(
- "RowBox", Expression(SymbolList, *list_boxes(rules, f, "<|", "|>"))
+ "RowBox",
+ Expression(SymbolList, *list_boxes(rules, f, "<|", "|>", evaluation)),
)
else:
self.error_idx += 1
symbol = Expression(SymbolMakeBoxes, SymbolAssociation, f)
expr = Expression(
"RowBox",
- Expression(SymbolList, symbol, *list_boxes(rules, f, "[", "]")),
+ Expression(SymbolList, symbol, *list_boxes(rules, f, "[", "]", evaluation)),
)
expr = expr.evaluate(evaluation)
diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py
index 5f183bd712..3cec0198c2 100644
--- a/mathics/core/evaluation.py
+++ b/mathics/core/evaluation.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-import pickle
from queue import Queue
import os
@@ -13,7 +12,15 @@
from mathics_scanner import TranslateError
from mathics import settings
-from mathics.core.expression import ensure_context, KeyComparable, SymbolAborted, SymbolList, SymbolNull
+from mathics.core.expression import (
+ ensure_context,
+ KeyComparable,
+ SymbolAborted,
+ SymbolList,
+ SymbolNull,
+ make_boxes_strategy,
+ Omissions,
+)
FORMATS = [
"StandardForm",
@@ -248,6 +255,7 @@ def __init__(
self.quiet_all = False
self.format = format
+ self.boxes_strategy = make_boxes_strategy(None, None, self)
self.catch_interrupt = catch_interrupt
self.SymbolNull = SymbolNull
@@ -428,7 +436,7 @@ def get_stored_result(self, eval_result):
def stop(self) -> None:
self.stopped = True
- def format_output(self, expr, format=None):
+ def format_output(self, expr, format=None, warn_about_omitted=True):
if format is None:
format = self.format
@@ -437,17 +445,48 @@ def format_output(self, expr, format=None):
from mathics.core.expression import Expression, BoxError
- if format == "text":
- result = expr.format(self, "System`OutputForm")
- elif format == "xml":
- result = Expression("StandardForm", expr).format(self, "System`MathMLForm")
- elif format == "tex":
- result = Expression("StandardForm", expr).format(self, "System`TeXForm")
- elif format == "unformatted":
- self.exc_result = None
- return expr
- else:
- raise ValueError
+ old_boxes_strategy = self.boxes_strategy
+ try:
+ capacity = self.definitions.get_config_value("System`$OutputSizeLimit")
+ omissions = Omissions()
+ self.boxes_strategy = make_boxes_strategy(capacity, omissions, self)
+
+ options = {}
+
+ # for xml/MathMLForm and tex/TexForm, output size limits are applied in the output
+ # form's apply methods (e.g. see MathMLForm.apply) and then passed through
+ # result.boxes_to_text which, in these two cases, must not apply any additional
+ # clipping (it would clip already clipped string material).
+
+ # for text/OutputForm, on the other hand, the call to result.boxes_to_text is the
+ # only place there is to apply output size limits, which has us resort to setting
+ # options['output_size_limit']. NOTE: disabled right now, causes problems with
+ # long message outputs (see test case Table[i, {i, 1, 100}] under OutputSizeLimit).
+
+ if format == "text":
+ result = expr.format(self, "System`OutputForm")
+ # options['output_size_limit'] = capacity
+ elif format == "xml":
+ result = Expression("StandardForm", expr).format(
+ self, "System`MathMLForm"
+ )
+ elif format == "tex":
+ result = Expression("StandardForm", expr).format(self, "System`TeXForm")
+ else:
+ raise ValueError
+
+ try:
+ boxes = result.boxes_to_text(evaluation=self, **options)
+ except BoxError:
+ self.message(
+ "General", "notboxes", Expression("FullForm", result).evaluate(self)
+ )
+ boxes = None
+
+ if warn_about_omitted:
+ omissions.warn(self)
+ finally:
+ self.boxes_strategy = old_boxes_strategy
try:
boxes = result.boxes_to_text(evaluation=self)
@@ -477,6 +516,13 @@ def get_quiet_messages(self):
return []
return value.leaves
+ def make_boxes(
+ self, prefix, make_leaf, n_leaves, left, right, sep, materialize, form
+ ):
+ return self.boxes_strategy.make(
+ prefix, make_leaf, n_leaves, left, right, sep, materialize, form
+ )
+
def message(self, symbol, tag, *args) -> None:
from mathics.core.expression import String, Symbol, Expression, from_python
@@ -510,7 +556,9 @@ def message(self, symbol, tag, *args) -> None:
text = String("Message %s::%s not found." % (symbol_shortname, tag))
text = self.format_output(
- Expression("StringForm", text, *(from_python(arg) for arg in args)), "text"
+ Expression("StringForm", text, *(from_python(arg) for arg in args)),
+ "text",
+ warn_about_omitted=False,
)
self.out.append(Message(symbol_shortname, tag, text))
diff --git a/mathics/core/expression.py b/mathics/core/expression.py
index 963c2e9a33..3061cac945 100644
--- a/mathics/core/expression.py
+++ b/mathics/core/expression.py
@@ -70,6 +70,16 @@ def system_symbols_dict(d):
return {ensure_context(k): v for k, v in d.items()}
+_layout_boxes = system_symbols(
+ "RowBox",
+ "SuperscriptBox",
+ "SubscriptBox",
+ "SubsuperscriptBox",
+ "FractionBox",
+ "SqrtBox",
+)
+
+
class BoxError(Exception):
def __init__(self, box, form) -> None:
super().__init__("Box %s cannot be formatted as %s" % (box, form))
@@ -518,6 +528,12 @@ def format(
result = Expression("MakeBoxes", expr, Symbol(form)).evaluate(evaluation)
return result
+ def output_cost(self):
+ # the cost of outputting this item, usually the number of
+ # characters without any formatting elements or whitespace;
+ # e.g. "a, b", would be counted as 3.
+ return 1 # fallback implementation: count as one character
+
def is_free(self, form, evaluation) -> bool:
from mathics.builtin.patterns import item_is_free
@@ -1508,6 +1524,7 @@ def boxes_to_text(self, **options) -> str:
is_style, options = self.process_style_box(options)
if is_style:
return self._leaves[0].boxes_to_text(**options)
+ head = self._head.get_name()
if self.has_form("RowBox", 1) and self._leaves[0].has_form( # nopep8
"List", None
):
@@ -1655,6 +1672,29 @@ def block(tex, only_subsup=False):
else:
raise BoxError(self, "tex")
+ def output_cost(self):
+ name = self.get_head_name()
+
+ if name in ("System`ImageBox", "System`GraphicsBox", "System`Graphics3DBox"):
+ return 1 # count as expensive as one character
+
+ leaves = self.leaves
+ cost_of_leaves = sum(leaf.output_cost() for leaf in leaves)
+
+ if name in _layout_boxes:
+ return cost_of_leaves
+ else:
+ separator_cost = 1 # i.e. ","
+ n_separators = max(len(leaves) - 1, 0)
+ total_cost = (
+ 2 + cost_of_leaves + separator_cost * n_separators
+ ) # {a, b, c}, [a, b, c]
+
+ if name != "System`List":
+ total_cost += self.head.output_cost()
+
+ return total_cost
+
def default_format(self, evaluation, form) -> str:
return "%s[%s]" % (
self._head.default_format(evaluation, form),
@@ -1975,6 +2015,9 @@ def __str__(self) -> str:
def do_copy(self) -> "Symbol":
return Symbol(self.name)
+ def output_cost(self):
+ return len(self.name)
+
def boxes_to_text(self, **options) -> str:
return str(self.name)
@@ -2130,6 +2173,7 @@ def from_mpmath(value, prec=None):
raise TypeError(type(value))
+
class Number(Atom):
def __str__(self) -> str:
return str(self.value)
@@ -2197,6 +2241,9 @@ def __init__(self, value) -> "Integer":
def boxes_to_text(self, **options) -> str:
return str(self.value)
+ def output_cost(self):
+ return len(str(self.value))
+
def boxes_to_mathml(self, **options) -> str:
return self.make_boxes("MathMLForm").boxes_to_mathml(**options)
@@ -2273,6 +2320,10 @@ def __new__(cls, numerator, denominator=1) -> "Rational":
self.value = sympy.Rational(numerator, denominator)
return self
+ def output_cost(self):
+ numer, denom = self.value.as_numer_denom()
+ return len(str(numer)) + len(str(denom))
+
def atom_to_boxes(self, f, evaluation):
return self.format(evaluation, f.get_name())
@@ -2386,6 +2437,9 @@ def __new__(cls, value, p=None) -> "Real":
else:
return PrecisionReal.__new__(PrecisionReal, value)
+ def output_cost(self):
+ return len(self.boxes_to_text())
+
def boxes_to_text(self, **options) -> str:
return self.make_boxes("System`OutputForm").boxes_to_text(**options)
@@ -2612,6 +2666,9 @@ def atom_to_boxes(self, f, evaluation):
def __str__(self) -> str:
return str(self.to_sympy())
+ def output_cost(self):
+ return self.real.output_cost() + self.imag.output_cost() + 2 # "+", "I"
+
def to_sympy(self, **kwargs):
return self.real.to_sympy() + sympy.I * self.imag.to_sympy()
@@ -2814,6 +2871,9 @@ def __new__(cls, value):
def __str__(self) -> str:
return '"%s"' % self.value
+ def output_cost(self):
+ return len(self.value)
+
def boxes_to_text(self, show_string_characters=False, **options) -> str:
value = self.value
@@ -3055,6 +3115,32 @@ def __getnewargs__(self):
return (self.value,)
+class Omitted(
+ String
+): # represents an omitted portion like <<42>> (itself not collapsible)
+ def __new__(cls, value, **kwargs):
+ return super(Omitted, cls).__new__(cls, value, **kwargs)
+
+ def boxes_to_text(self, **options):
+ new_options = dict(
+ (k, v) for k, v in options.items() if k != "output_size_limit"
+ )
+ return super(Omitted, self).boxes_to_text(**new_options)
+
+ def boxes_to_xml(self, **options):
+ new_options = dict(
+ (k, v) for k, v in options.items() if k != "output_size_limit"
+ )
+ s = super(Omitted, self).boxes_to_xml(**new_options)
+ return "%s" % s
+
+ def boxes_to_tex(self, **options):
+ new_options = dict(
+ (k, v) for k, v in options.items() if k != "output_size_limit"
+ )
+ return super(Omitted, self).boxes_to_tex(**new_options)
+
+
class StringFromPython(String):
def __new__(cls, value):
self = super().__new__(cls, value)
@@ -3098,6 +3184,240 @@ def print_parenthesizes(
)
+def _interleave(*gens): # interleaves over n generators of even or uneven lengths
+ active = [gen for gen in gens]
+ while len(active) > 0:
+ i = 0
+ while i < len(active):
+ try:
+ yield next(active[i])
+ i += 1
+ except StopIteration:
+ del active[i]
+
+
+class _MakeBoxesStrategy(object):
+ def capacity(self):
+ raise NotImplementedError()
+
+ def make(self, head, make_leaf, n_leaves, left, right, sep, materialize, form):
+ raise NotImplementedError()
+
+
+class Omissions:
+ def __init__(self):
+ self._omissions = []
+
+ def add(self, count):
+ n = len(self._omissions)
+ if n < 3:
+ self._omissions.append("<<%d>>" % count)
+ if n == 3:
+ self._omissions.append("...")
+
+ def warn(self, evaluation):
+ if self._omissions:
+ evaluation.message("General", "omit", ", ".join(self._omissions))
+
+
+def _riffle_separators(elements, separators):
+ yield elements[0]
+ for e, s in zip(elements[1:], separators):
+ yield s
+ yield e
+
+
+class _UnlimitedMakeBoxesStrategy(_MakeBoxesStrategy):
+ def __init__(self):
+ self.omissions_occured = False
+
+ def capacity(self):
+ return None
+
+ def make(self, head, make_leaf, n_leaves, left, right, sep, materialize, form):
+ prefix = []
+ if head is not None:
+ prefix.append(head)
+ if left is not None:
+ prefix.append(left)
+
+ inner = [make_leaf(i) for i in range(n_leaves)]
+
+ if sep is not None and n_leaves > 1:
+ if isinstance(sep, (list, tuple)):
+ inner = list(_riffle_separators(inner, sep))
+ else:
+ from mathics.builtin.lists import riffle
+
+ inner = riffle(inner, sep)
+
+ if right is not None:
+ suffix = [right]
+ else:
+ suffix = []
+
+ return materialize(prefix, inner, suffix)
+
+
+class _LimitedMakeBoxesState:
+ def __init__(self, capacity, side, both_sides, depth):
+ self.capacity = capacity # output size remaining
+ self.side = side # start from left side (<0) or right side (>0)?
+ self.both_sides = both_sides # always evaluate both sides?
+ self.depth = depth # stack depth of MakeBoxes evaluation
+ self.consumed = 0 # sum of costs consumed so far
+
+
+class _LimitedMakeBoxesStrategy(_MakeBoxesStrategy):
+ def __init__(self, capacity, omissions, evaluation):
+ self._capacity = capacity
+ self._evaluation = evaluation
+ self._state = _LimitedMakeBoxesState(self._capacity, 1, True, 1)
+ self._unlimited = _UnlimitedMakeBoxesStrategy()
+ self._omissions = omissions
+
+ def capacity(self):
+ return self._capacity
+
+ def make(self, head, make_leaf, n_leaves, left, right, sep, materialize, form):
+ state = self._state
+ capacity = state.capacity
+
+ if capacity is None or n_leaves <= 2:
+ return self._unlimited.make(
+ head, make_leaf, n_leaves, left, right, sep, materialize, form
+ )
+
+ left_leaves = []
+ right_leaves = []
+
+ middle = n_leaves // 2
+
+ # note that we use generator expressions, not list comprehensions, here, since we
+ # might quit early in the loop below, and copying all leaves might prove inefficient.
+ from_left = ((leaf, left_leaves.append) for leaf in range(0, middle))
+ from_right = (
+ (leaf, right_leaves.append) for leaf in reversed(range(middle, n_leaves))
+ )
+
+ # specify at which side to start making boxes (either from the left or from the right).
+ side = state.side
+ if side > 0:
+ from_sides = (from_left, from_right)
+ else:
+ from_sides = (from_right, from_left)
+
+ # usually we start on one side and make boxes until the capacity is exhausted.
+ # however, at the first real list (i.e. > 1 elements) we force evaluation of
+ # both sides in order to make sure the start and the end portion is visible.
+ both_sides = state.both_sides
+ if both_sides and n_leaves > 1:
+ delay_break = 1
+ both_sides = False # disable both_sides from this depth on
+ else:
+ delay_break = 0
+
+ depth = state.depth
+ sum_of_costs = 0
+
+ for i, (index, push) in enumerate(_interleave(*from_sides)):
+ # calling evaluate() here is a serious difference to the implementation
+ # without $OutputSizeLimit. here, we evaluate MakeBoxes bottom up, i.e.
+ # the leaves get evaluated first, since we need to estimate their size
+ # here.
+ #
+ # without $OutputSizeLimit, on the other hand, the expression
+ # gets evaluates from the top down, i.e. first MakeBoxes is wrapped around
+ # each expression, then we call evaluate on the root node. assuming that
+ # there are no rules like MakeBoxes[x_, MakeBoxes[y_]], both approaches
+ # should be identical.
+ #
+ # we could work around this difference by pushing the unevaluated
+ # expression here (see "push(box)" below), instead of the evaluated.
+ # this would be very inefficient though, since we would get quadratic
+ # runtime (quadratic in the depth of the tree).
+
+ box, cost = self._evaluate(
+ make_leaf,
+ index,
+ capacity=capacity // 2, # reserve rest half of capacity for other side
+ side=side * -((i % 2) * 2 - 1), # flip between side and -side
+ both_sides=both_sides, # force both-sides evaluation for deeper levels?
+ depth=depth + 1,
+ )
+
+ push(box)
+ state.consumed += cost
+
+ sum_of_costs += cost
+ if i >= delay_break:
+ capacity -= sum_of_costs
+ sum_of_costs = 0
+ if capacity <= 0:
+ break
+
+ ellipsis_size = n_leaves - (len(left_leaves) + len(right_leaves))
+ if ellipsis_size > 0 and self._omissions:
+ self._omissions.add(ellipsis_size)
+ ellipsis = [Omitted("<<%d>>" % ellipsis_size)] if ellipsis_size > 0 else []
+
+ # if segment is not None:
+ # if ellipsis_size > 0:
+ # segment.extend((True, len(left_leaves), len(items) - len(right_leaves)))
+ # else:
+ # segment.extend((False, 0, 0))
+
+ inner = list(chain(left_leaves, ellipsis, reversed(right_leaves)))
+
+ if sep is not None and n_leaves > 1:
+ if isinstance(sep, (list, tuple)):
+ # ellipsis item gets rightmost separator from ellipsed chunk
+ sep = sep[: len(left_leaves)] + sep[len(right_leaves) - 1 :]
+ inner = list(_riffle_separators(inner, sep))
+ else:
+ from mathics.builtin.lists import riffle
+
+ inner = riffle(inner, sep)
+
+ prefix = []
+ if head is not None:
+ prefix.append(head)
+ if left is not None:
+ prefix.append(left)
+
+ if right is not None:
+ suffix = [right]
+ else:
+ suffix = []
+
+ return materialize(prefix, inner, suffix)
+
+ def _evaluate(self, make_leaf, index, **kwargs):
+ old_state = self._state
+ try:
+ state = _LimitedMakeBoxesState(**kwargs)
+ self._state = state
+
+ box = make_leaf(index).evaluate(self._evaluation)
+
+ # estimate the cost of the output related to box. always calling boxes_to_xml here is
+ # the simple solution; the problem is that it's redundant, as for {{{a}, b}, c}, we'd
+ # call boxes_to_xml first on {a}, then on {{a}, b}, then on {{{a}, b}, c}. a good fix
+ # is not simple though, so let's keep it this way for now.
+ cost = box.output_cost()
+
+ return box, cost
+ finally:
+ self._state = old_state
+
+
+def make_boxes_strategy(capacity, omissions, evaluation):
+ if capacity is None:
+ return _UnlimitedMakeBoxesStrategy()
+ else:
+ return _LimitedMakeBoxesStrategy(capacity, omissions, evaluation)
+
+
def _is_neutral_symbol(symbol_name, cache, evaluation):
# a symbol is neutral if it does not invoke any rules, but is sure to make its Expression stay
# the way it is (e.g. List[1, 2, 3] will always stay List[1, 2, 3], so long as nobody defines