From 38f8b280682863343871f7418c40833b0761baaf Mon Sep 17 00:00:00 2001 From: mmatera Date: Wed, 12 May 2021 08:58:13 -0300 Subject: [PATCH 1/3] rebase 605 --- mathics/builtin/evaluation.py | 53 ++++++ mathics/builtin/inout.py | 134 ++++++++------ mathics/builtin/lists.py | 59 ++++--- mathics/core/evaluation.py | 78 +++++++-- mathics/core/expression.py | 320 ++++++++++++++++++++++++++++++++++ 5 files changed, 550 insertions(+), 94 deletions(-) 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..6b2d4e6970 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,23 @@ 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 = ", " + + 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 +1107,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 +5906,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 From e98fa03cee350e05318fa0ee4952c88f0f25de06 Mon Sep 17 00:00:00 2001 From: mmatera Date: Wed, 12 May 2021 09:07:37 -0300 Subject: [PATCH 2/3] comment the noop --- mathics/builtin/lists.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/lists.py b/mathics/builtin/lists.py index 6b2d4e6970..c966d8e1f6 100644 --- a/mathics/builtin/lists.py +++ b/mathics/builtin/lists.py @@ -330,7 +330,9 @@ def list_boxes(prefix, items, materialize, f, evaluation, open=None, close=None) if f in ("System`OutputForm", "System`InputForm"): sep = ", " else: - sep = ", " + 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) From 1b35633a9b10a7c90a06b91713938139008125e3 Mon Sep 17 00:00:00 2001 From: mmatera Date: Wed, 12 May 2021 09:25:33 -0300 Subject: [PATCH 3/3] adding cost for compiledcodebox --- mathics/builtin/compilation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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