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)