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
52 changes: 40 additions & 12 deletions mathics/builtin/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -176,9 +180,8 @@ class Evaluate(Builtin):
<url>:WMA link:https://reference.wolfram.com/language/ref/Evaluate.html</url>

<dl>
<dt>'Evaluate'[$expr$]
<dd>forces evaluation of $expr$, even if it occurs inside a
held argument or a 'Hold' form.
<dt>'Evaluate'[$expr$]
<dd>forces evaluation of $expr$, even if it occurs inside a held argument or a 'Hold' form.
</dl>

Create a function $f$ with a held argument:
Expand Down Expand Up @@ -211,32 +214,36 @@ class Unevaluated(Builtin):
<url>:WMA link:https://reference.wolfram.com/language/ref/Unevaluated.html</url>

<dl>
<dt>'Unevaluated'[$expr$]
<dd>temporarily leaves $expr$ in an unevaluated form when it
appears as a function argument.
<dt>'Unevaluated'[$expr$]
<dd>temporarily leaves $expr$ in an unevaluated form when it appears as a function argument.
</dl>

'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]]]

Expand All @@ -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):
"""
Expand Down
1 change: 0 additions & 1 deletion mathics/core/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
163 changes: 74 additions & 89 deletions mathics/core/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,27 +63,17 @@
)
from mathics.core.systemsymbols import (
SymbolAborted,
SymbolAlternatives,
SymbolBlank,
SymbolBlankNullSequence,
SymbolBlankSequence,
SymbolCondition,
SymbolDirectedInfinity,
SymbolFunction,
SymbolMinus,
SymbolOptional,
SymbolOptionsPattern,
SymbolOverflow,
SymbolPattern,
SymbolPatternTest,
SymbolPower,
SymbolSequence,
SymbolSin,
SymbolSlot,
SymbolSqrt,
SymbolSubtract,
SymbolUnevaluated,
SymbolVerbatim,
)
from mathics.eval.tracing import trace_evaluate

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
28 changes: 21 additions & 7 deletions test/builtin/test_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
],
)
Expand Down Expand Up @@ -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)
Loading