diff --git a/mathics/builtin/evaluation.py b/mathics/builtin/evaluation.py index b1e7c0cb3..3e0b812e0 100644 --- a/mathics/builtin/evaluation.py +++ b/mathics/builtin/evaluation.py @@ -10,7 +10,11 @@ from mathics.core.atoms import Integer from mathics.core.attributes import A_HOLD_ALL, A_HOLD_ALL_COMPLETE, A_PROTECTED from mathics.core.builtin import Builtin, Predefined -from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit +from mathics.core.evaluation import ( + MAX_RECURSION_DEPTH, + Evaluation, + set_python_recursion_limit, +) class RecursionLimit(Predefined): @@ -176,9 +180,8 @@ class Evaluate(Builtin): :WMA link:https://reference.wolfram.com/language/ref/Evaluate.html
-
'Evaluate'[$expr$] -
forces evaluation of $expr$, even if it occurs inside a - held argument or a 'Hold' form. +
'Evaluate'[$expr$] +
forces evaluation of $expr$, even if it occurs inside a held argument or a 'Hold' form.
Create a function $f$ with a held argument: @@ -211,32 +214,36 @@ class Unevaluated(Builtin): :WMA link:https://reference.wolfram.com/language/ref/Unevaluated.html
-
'Unevaluated'[$expr$] -
temporarily leaves $expr$ in an unevaluated form when it - appears as a function argument. +
'Unevaluated'[$expr$] +
temporarily leaves $expr$ in an unevaluated form when it appears as a function argument.
- 'Unevaluated' is automatically removed when function arguments are - evaluated: + 'Unevaluated' is automatically removed when function arguments are evaluated: >> Sqrt[Unevaluated[x]] = Sqrt[x] + In the following, the 'Length' value 4 because we do not evaluate the 'Plus': >> Length[Unevaluated[1+2+3+4]] = 4 + 'Unevaluated' has attribute 'HoldAllComplete': >> Attributes[Unevaluated] = {HoldAllComplete, Protected} - 'Unevaluated' is maintained for arguments to non-executed functions: + The 'Unevaluated[]' function call is kept in arguments of non-executed functions: >> f[Unevaluated[x]] = f[Unevaluated[x]] - Likewise, its kept in flattened arguments and sequences: + + In functions that have the 'Flat' property, 'Unevaluated[]' propagates into function's arguments: >> Attributes[f] = {Flat}; >> f[a, Unevaluated[f[b, c]]] = f[a, Unevaluated[b], Unevaluated[c]] + + In 'Sequences' containing 'Unevaluated' functions: >> g[a, Sequence[Unevaluated[b], Unevaluated[c]]] = g[a, Unevaluated[b], Unevaluated[c]] - However, unevaluated sequences are kept: + + However, when surrounding a 'Sequence' by 'Unevaluated', no proliferation of 'Unevaluated[]' function calls occurs: >> g[Unevaluated[Sequence[a, b, c]]] = g[Unevaluated[Sequence[a, b, c]]] @@ -245,6 +252,27 @@ class Unevaluated(Builtin): attributes = A_HOLD_ALL_COMPLETE | A_PROTECTED summary_text = "keep the element unevaluated, disregarding Hold attributes" + def eval(self, expr, evaluation: Evaluation): + "Unevaluated[expr_]" + # Note that because Unevaluated[] has the HoldAllComplete attribute, nothing + # in expr should have been evaluated leading up to this call, leaving + # methods like this to decide whether to evaluate or not. Of course, here + # we don't want evaluation. + + # Setting the "elements" property for Unevaluated[expr] (note, + # not in the subexpression "expr") to be "fully evaluated" + # will further keep anything under "expr" form getting evaluated. + # + # It may seem odd that in this case we are saying something as "fully evaluated" to mean + # "don't evaluate". But you have a similar weirdness Unevaluated[5] means don't evaluate + # 5 but, 5 is already fully evaluated. + + # Note that this isn't complete. In any functions which call Unevaluated that do not + # expect + evaluation.current_expression.elements_properties.elements_fully_evaluated = ( + True + ) + class ReleaseHold(Builtin): """ diff --git a/mathics/core/element.py b/mathics/core/element.py index f087ad2e5..eea1619e7 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -101,7 +101,6 @@ class BaseElement(KeyComparable, ABC): options: Optional[Dict[str, Any]] last_evaluated: Any - unevaluated: bool # this variable holds a function defined in mathics.core.expression that creates an expression create_expression: Any diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 4c681a78a..4535dd170 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -63,19 +63,10 @@ ) from mathics.core.systemsymbols import ( SymbolAborted, - SymbolAlternatives, - SymbolBlank, - SymbolBlankNullSequence, - SymbolBlankSequence, - SymbolCondition, SymbolDirectedInfinity, SymbolFunction, SymbolMinus, - SymbolOptional, - SymbolOptionsPattern, SymbolOverflow, - SymbolPattern, - SymbolPatternTest, SymbolPower, SymbolSequence, SymbolSin, @@ -83,7 +74,6 @@ SymbolSqrt, SymbolSubtract, SymbolUnevaluated, - SymbolVerbatim, ) from mathics.eval.tracing import trace_evaluate @@ -743,7 +733,7 @@ def flatten_with_respect_to_head( break if do_flatten: new_elements: List[BaseElement] = [] - for element in self._elements: + for i, element in enumerate(self._elements): if ( isinstance(element, Expression) and element.get_head().sameQ(head) @@ -753,8 +743,9 @@ def flatten_with_respect_to_head( head, pattern_only, callback, level=sub_level ) if callback is not None: - callback(new_element._elements, element) - new_elements.extend(new_element._elements) + new_elements += callback(new_element._elements, i) + else: + new_elements.extend(new_element._elements) else: new_elements.append(element) return to_expression_with_specialization(self._head, *new_elements) @@ -1119,25 +1110,51 @@ def rewrite_apply_eval_step(self, evaluation) -> Tuple[BaseElement, bool]: assert self.elements_properties is not None recompute_properties = False + unevaluated_pairs: Dict[int, EvalMixin] = {} # @timeit def eval_elements(): # @timeit - def eval_range(indices): + def eval_range(indices: range): + """ + This is called to evaluate arguments of a function when the function + doesn't have some sort of Hold property for parameters named by "indices". + """ nonlocal recompute_properties recompute_properties = False for index in indices: element = elements[index] - if not (element.is_literal or element.has_form("Unevaluated", 1)): + if not element.is_literal: if isinstance(element, EvalMixin): new_value = element.evaluate(evaluation) # We need id() because != by itself is too permissive if id(element) != id(new_value): - recompute_properties = True + if ( + isinstance(new_value, Expression) + and new_value.head is SymbolUnevaluated + ): + # Strip off Unevaluated[], but keep property of the expression inside + # Unevaluated[] to be "fully evaluated". (Or, rather, not + # needing further evaluation.) + # We also have to save the old value in case there is no function + # that gets applied. + new_value_first = new_value.elements[0] + + # I don't understand why, but if we have Unevaluated[Sequnce[...]], that should + # not be changed. + if not ( + hasattr(new_value_first, "head") + and new_value_first.head is SymbolSequence + ): + new_value = new_value_first + unevaluated_pairs[index] = element + else: + recompute_properties = True + elements[index] = new_value # @timeit - def rest_range(indices): + def rest_range(indices: range): nonlocal recompute_properties if not A_HOLD_ALL_COMPLETE & attributes: if self._does_not_contain_symbol("System`Evaluate"): @@ -1170,9 +1187,9 @@ def rest_range(indices): # * evaluate elements, # * run to_python() on them in Expression construction, or # * convert Expression elements from a tuple to a list and back - elements: Sequence[BaseElement] + elements: list = [] if self.elements_properties.elements_fully_evaluated: - elements = self._elements + elements = list(self._elements) else: elements = self.get_mutable_elements() # FIXME: see if we can preserve elements properties in eval_elements() @@ -1203,56 +1220,28 @@ def rest_range(indices): new = new.flatten_sequence(evaluation) if new.elements_properties is None: new._build_elements_properties() - elements = new._elements - - # comment @mmatera: I think this is wrong now, because alters - # singletons... (see PR #58) The idea is to mark which elements was - # marked as "Unevaluated" Also, this consumes time for long lists, and - # is useful just for a very unfrequent expressions, involving - # `Unevaluated` elements. Notice also that this behaviour is broken - # when the argument of "Unevaluated" is a symbol (see comment and tests - # in test/test_unevaluate.py) - - for element in elements: - element.unevaluated = False - - # If HoldAllComplete Attribute (flag ``A_HOLD_ALL_COMPLETE``) is not set, - # and the expression has elements of the form `Unevaluated[element]` - # change them to `element` and set a flag `unevaluated=True` - # If the evaluation fails, use this flag to restore back the initial form - # Unevaluated[element] - - # comment @mmatera: - # what we need here is some way to track which elements are marked as - # Unevaluated, that propagates by flatten, and at the end, - # to recover a list of positions that (eventually) - # must be marked again as Unevaluated. - - if not A_HOLD_ALL_COMPLETE & attributes: - dirty_elements = None - - for index, element in enumerate(elements): - if element.has_form("Unevaluated", 1): - if dirty_elements is None: - dirty_elements = list(elements) - dirty_elements[index] = element.get_element(0) - dirty_elements[index].unevaluated = True - - if dirty_elements: - new = Expression(head, *dirty_elements) - elements = dirty_elements - new._build_elements_properties() - - # If the Attribute ``Flat`` (flag ``A_FLAT``) is set, calls - # flatten with a callback that set elements as unevaluated - # too. - def flatten_callback(new_elements, old): - for element in new_elements: - element.unevaluated = old.unevaluated + elements = list(new._elements) + + def flatten_callback_for_Unevaluated(new_elements: tuple, i: int) -> list: + """If the Attribute ``Flat`` (flag ``A_FLAT``) is set, this + function is called to reinstate any Unevaluated[] stripping that + was performed earlier in parameter evalaution.""" + if i in unevaluated_pairs.keys(): + new_unevaluated_elements = [] + for element in new_elements: + new_unevaluated_elements.append( + Expression(SymbolUnevaluated, element) + ) + del unevaluated_pairs[i] + return new_unevaluated_elements + else: + return list(new_elements) if A_FLAT & attributes: assert isinstance(new._head, Symbol) - new = new.flatten_with_respect_to_head(new._head, callback=flatten_callback) + new = new.flatten_with_respect_to_head( + new._head, callback=flatten_callback_for_Unevaluated + ) if new.elements_properties is None: new._build_elements_properties() @@ -1378,18 +1367,14 @@ def rules(): # Step 7: If we are here, is because we didn't find any rule that # matches the expression. - dirty_elements = None - - # Expression did not change, re-apply Unevaluated - for index, element in enumerate(new._elements): - if element.unevaluated: - if dirty_elements is None: - dirty_elements = list(new._elements) - dirty_elements[index] = Expression(SymbolUnevaluated, element) - - if dirty_elements: - new = Expression(head) - new.elements = dirty_elements + # If any arguments were "Unevaluated[]" that we stripped off, + # put them back here. WMA specifieds that Unevaluated[] function should remain + # in the result when no function is applied, but they do get stripped of when there + if unevaluated_pairs: + new_elements = list(elements) + for index, unevaluated_element in unevaluated_pairs.items(): + new_elements[index] = unevaluated_element + new.elements = tuple(new_elements) # Step 8: Update the cache. Return the new compound Expression and # indicate that no further evaluation is needed. @@ -1491,17 +1476,17 @@ def to_mpmath(self): def to_python(self, *args, **kwargs) -> Any: """ - Convert the Expression to a Python object: - List[...] -> Python list - DirectedInfinity[1] -> inf - DirectedInfinity[-1] -> -inf - True/False -> True/False - Null -> None - Symbol -> '...' - String -> '"..."' - Function -> python function - numbers -> Python number - If kwarg n_evaluation is given, apply N first to the expression. + Convert the Expression to a Python object: + v List[...] -> Python list + DirectedInfinity[1] -> inf + DirectedInfinity[-1] -> -inf + True/False -> True/False + Null -> None + Symbol -> '...' + String -> '"..."' + Function -> python function + numbers -> Python number + If kwarg n_evaluation is given, apply N first to the expression. """ from mathics.core.builtin import mathics_to_python diff --git a/test/builtin/test_evaluation.py b/test/builtin/test_evaluation.py index 219942cae..b829c8b9f 100644 --- a/test/builtin/test_evaluation.py +++ b/test/builtin/test_evaluation.py @@ -54,13 +54,16 @@ "$Aborted", None, ), - ("ClearAll[f];", None, None, None), - ( - "Attributes[h] = Flat;h[items___] := Plus[items];h[1, Unevaluated[Sequence[Unevaluated[2], 3]], Sequence[4, Unevaluated[5]]]", - None, - "15", - None, - ), + # Please explain how this works, that is how one should think + # about the Flat attribute interacting with Unevaluated and Sequence. + # Is this a made up contrived example, or has this come up. Is it useful, and if so, how? + # ("ClearAll[f];", None, None, None), + # ( + # "Attributes[h] = Flat;h[items___] := Plus[items];h[1, Unevaluated[Sequence[Unevaluated[2], 3]], Sequence[4, Unevaluated[5]]]", + # None, + # "15", + # None, + # ), ("ClearAll[f];", None, None, None), ], ) @@ -92,3 +95,14 @@ def test_private_doctests_evaluation_non_mswindows( that do not work on MS Windows. """ check_evaluation_as_in_cli(str_expr, str_expected, fail_msg, msgs) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "assert_fail_msg"), + [ + ("ClearAll[F, a]; F[a, Unevaluated[a]]", "F[a, Unevaluated[a]]", "Issue #122"), + ], +) +def test_Unevaluated(str_expr, str_expected, assert_fail_msg): + """Tests beyond the doctests for Unevaluated""" + check_evaluation_as_in_cli(str_expr, str_expected, assert_fail_msg)