Skip to content

Commit 1ad8971

Browse files
committed
Revise Unevaluated[]
1 parent e757539 commit 1ad8971

File tree

4 files changed

+130
-102
lines changed

4 files changed

+130
-102
lines changed

mathics/builtin/evaluation.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
from mathics.core.atoms import Integer
1111
from mathics.core.attributes import A_HOLD_ALL, A_HOLD_ALL_COMPLETE, A_PROTECTED
1212
from mathics.core.builtin import Builtin, Predefined
13-
from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit
13+
from mathics.core.evaluation import (
14+
MAX_RECURSION_DEPTH,
15+
Evaluation,
16+
set_python_recursion_limit,
17+
)
1418

1519

1620
class RecursionLimit(Predefined):
@@ -211,32 +215,36 @@ class Unevaluated(Builtin):
211215
<url>:WMA link:https://reference.wolfram.com/language/ref/Unevaluated.html</url>
212216
213217
<dl>
214-
<dt>'Unevaluated'[$expr$]
215-
<dd>temporarily leaves $expr$ in an unevaluated form when it
216-
appears as a function argument.
218+
<dt>'Unevaluated'[$expr$]
219+
<dd>temporarily leaves $expr$ in an unevaluated form when it appears as a function argument.
217220
</dl>
218221
219-
'Unevaluated' is automatically removed when function arguments are
220-
evaluated:
222+
'Unevaluated' is automatically removed when function arguments are evaluated:
221223
>> Sqrt[Unevaluated[x]]
222224
= Sqrt[x]
223225
226+
In the following, the 'Length' value 4 because we do not evaluate the 'Plus':
224227
>> Length[Unevaluated[1+2+3+4]]
225228
= 4
229+
226230
'Unevaluated' has attribute 'HoldAllComplete':
227231
>> Attributes[Unevaluated]
228232
= {HoldAllComplete, Protected}
229233
230-
'Unevaluated' is maintained for arguments to non-executed functions:
234+
The 'Unevaluated[]' function call is kept in arguments of non-executed functions:
231235
>> f[Unevaluated[x]]
232236
= f[Unevaluated[x]]
233-
Likewise, its kept in flattened arguments and sequences:
237+
238+
In functions that have the 'Flat' property, 'Unevaluated[]' propagates into function's arguments:
234239
>> Attributes[f] = {Flat};
235240
>> f[a, Unevaluated[f[b, c]]]
236241
= f[a, Unevaluated[b], Unevaluated[c]]
242+
243+
In 'Sequences' containing 'Unevaluated' functions:
237244
>> g[a, Sequence[Unevaluated[b], Unevaluated[c]]]
238245
= g[a, Unevaluated[b], Unevaluated[c]]
239-
However, unevaluated sequences are kept:
246+
247+
However, when surrounding a 'Sequence' by 'Unevaluated', no proliferation of 'Unevaluated[]' function calls occurs:
240248
>> g[Unevaluated[Sequence[a, b, c]]]
241249
= g[Unevaluated[Sequence[a, b, c]]]
242250
@@ -245,6 +253,27 @@ class Unevaluated(Builtin):
245253
attributes = A_HOLD_ALL_COMPLETE | A_PROTECTED
246254
summary_text = "keep the element unevaluated, disregarding Hold attributes"
247255

256+
def eval(self, expr, evaluation: Evaluation):
257+
"Unevaluated[expr_]"
258+
# Note that because Unevaluated[] has the HoldAllComplete attribute, nothing
259+
# in expr should have been evaluated leading up to this call, leaving
260+
# methods like this to decide whether to evaluate or not. Of course, here
261+
# we don't want evaluation.
262+
263+
# Setting the "elements" property for Unevaluated[expr] (note,
264+
# not in the subexpression "expr") to be "fully evaluated"
265+
# will further keep anything under "expr" form getting evaluated.
266+
#
267+
# It may seem odd that in this case we are saying something as "fully evaluated" to mean
268+
# "don't evaluate". But you have a similar weirdness Unevaluated[5] means don't evaluate
269+
# 5 but, 5 is already fully evaluated.
270+
271+
# Note that this isn't complete. In any functions which call Unevaluated that do not
272+
# expect
273+
evaluation.current_expression.elements_properties.elements_fully_evaluated = (
274+
True
275+
)
276+
248277

249278
class ReleaseHold(Builtin):
250279
"""

mathics/core/element.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ class BaseElement(KeyComparable, ABC):
144144

145145
options: Optional[Dict[str, Any]]
146146
last_evaluated: Any
147-
unevaluated: bool
148147
# this variable holds a function defined in mathics.core.expression that creates an expression
149148
create_expression: Any
150149

mathics/core/expression.py

Lines changed: 71 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -63,27 +63,18 @@
6363
)
6464
from mathics.core.systemsymbols import (
6565
SymbolAborted,
66-
SymbolAlternatives,
67-
SymbolBlank,
68-
SymbolBlankNullSequence,
69-
SymbolBlankSequence,
70-
SymbolCondition,
7166
SymbolDirectedInfinity,
7267
SymbolFunction,
7368
SymbolMinus,
74-
SymbolOptional,
75-
SymbolOptionsPattern,
7669
SymbolOverflow,
77-
SymbolPattern,
78-
SymbolPatternTest,
7970
SymbolPower,
8071
SymbolSequence,
8172
SymbolSin,
8273
SymbolSlot,
8374
SymbolSqrt,
8475
SymbolSubtract,
76+
SymbolUndefined,
8577
SymbolUnevaluated,
86-
SymbolVerbatim,
8778
)
8879
from mathics.eval.tracing import trace_evaluate
8980

@@ -743,7 +734,7 @@ def flatten_with_respect_to_head(
743734
break
744735
if do_flatten:
745736
new_elements: List[BaseElement] = []
746-
for element in self._elements:
737+
for i, element in enumerate(self._elements):
747738
if (
748739
isinstance(element, Expression)
749740
and element.get_head().sameQ(head)
@@ -753,8 +744,9 @@ def flatten_with_respect_to_head(
753744
head, pattern_only, callback, level=sub_level
754745
)
755746
if callback is not None:
756-
callback(new_element._elements, element)
757-
new_elements.extend(new_element._elements)
747+
new_elements += callback(new_element._elements, i)
748+
else:
749+
new_elements.extend(new_element._elements)
758750
else:
759751
new_elements.append(element)
760752
return to_expression_with_specialization(self._head, *new_elements)
@@ -1119,25 +1111,51 @@ def rewrite_apply_eval_step(self, evaluation) -> Tuple[BaseElement, bool]:
11191111
assert self.elements_properties is not None
11201112

11211113
recompute_properties = False
1114+
unevaluated_pairs: Dict[int, BaseElement] = {}
11221115

11231116
# @timeit
11241117
def eval_elements():
11251118
# @timeit
1126-
def eval_range(indices):
1119+
def eval_range(indices: range):
1120+
"""
1121+
This is called to evaluate arguments of a function when the function
1122+
doesn't have some sort of Hold property for parameters named by "indices".
1123+
"""
11271124
nonlocal recompute_properties
11281125
recompute_properties = False
11291126
for index in indices:
11301127
element = elements[index]
1131-
if not (element.is_literal or element.has_form("Unevaluated", 1)):
1128+
if not element.is_literal:
11321129
if isinstance(element, EvalMixin):
11331130
new_value = element.evaluate(evaluation)
11341131
# We need id() because != by itself is too permissive
11351132
if id(element) != id(new_value):
1136-
recompute_properties = True
1133+
if (
1134+
hasattr(new_value, "head")
1135+
and new_value.head is SymbolUnevaluated
1136+
):
1137+
# Strip off Unevaluated[], but keep property of the expression inside
1138+
# Unevaluated[] to be "fully evaluated". (Or, rather, not
1139+
# needing further evaluation.)
1140+
# We also have to save the old value in case there is no function
1141+
# that gets applied.
1142+
new_value_first = new_value.elements[0]
1143+
1144+
# I don't understand why, but if we have Unevaluated[Sequnce[...]], that should
1145+
# not be changed.
1146+
if not (
1147+
hasattr(new_value_first, "head")
1148+
and new_value_first.head is SymbolSequence
1149+
):
1150+
new_value = new_value_first
1151+
unevaluated_pairs[index] = element
1152+
else:
1153+
recompute_properties = True
1154+
11371155
elements[index] = new_value
11381156

11391157
# @timeit
1140-
def rest_range(indices):
1158+
def rest_range(indices: range):
11411159
nonlocal recompute_properties
11421160
if not A_HOLD_ALL_COMPLETE & attributes:
11431161
if self._does_not_contain_symbol("System`Evaluate"):
@@ -1205,54 +1223,26 @@ def rest_range(indices):
12051223
new._build_elements_properties()
12061224
elements = new._elements
12071225

1208-
# comment @mmatera: I think this is wrong now, because alters
1209-
# singletons... (see PR #58) The idea is to mark which elements was
1210-
# marked as "Unevaluated" Also, this consumes time for long lists, and
1211-
# is useful just for a very unfrequent expressions, involving
1212-
# `Unevaluated` elements. Notice also that this behaviour is broken
1213-
# when the argument of "Unevaluated" is a symbol (see comment and tests
1214-
# in test/test_unevaluate.py)
1215-
1216-
for element in elements:
1217-
element.unevaluated = False
1218-
1219-
# If HoldAllComplete Attribute (flag ``A_HOLD_ALL_COMPLETE``) is not set,
1220-
# and the expression has elements of the form `Unevaluated[element]`
1221-
# change them to `element` and set a flag `unevaluated=True`
1222-
# If the evaluation fails, use this flag to restore back the initial form
1223-
# Unevaluated[element]
1224-
1225-
# comment @mmatera:
1226-
# what we need here is some way to track which elements are marked as
1227-
# Unevaluated, that propagates by flatten, and at the end,
1228-
# to recover a list of positions that (eventually)
1229-
# must be marked again as Unevaluated.
1230-
1231-
if not A_HOLD_ALL_COMPLETE & attributes:
1232-
dirty_elements = None
1233-
1234-
for index, element in enumerate(elements):
1235-
if element.has_form("Unevaluated", 1):
1236-
if dirty_elements is None:
1237-
dirty_elements = list(elements)
1238-
dirty_elements[index] = element.get_element(0)
1239-
dirty_elements[index].unevaluated = True
1240-
1241-
if dirty_elements:
1242-
new = Expression(head, *dirty_elements)
1243-
elements = dirty_elements
1244-
new._build_elements_properties()
1245-
1246-
# If the Attribute ``Flat`` (flag ``A_FLAT``) is set, calls
1247-
# flatten with a callback that set elements as unevaluated
1248-
# too.
1249-
def flatten_callback(new_elements, old):
1250-
for element in new_elements:
1251-
element.unevaluated = old.unevaluated
1226+
def flatten_callback_for_Unevaluated(new_elements: tuple, i: int) -> list:
1227+
"""If the Attribute ``Flat`` (flag ``A_FLAT``) is set, this
1228+
function is called to reinstate any Unevaluated[] stripping that
1229+
was performed earlier in parameter evalaution."""
1230+
if i in unevaluated_pairs.keys():
1231+
new_unevaluated_elements = []
1232+
for element in new_elements:
1233+
new_unevaluated_elements.append(
1234+
Expression(SymbolUnevaluated, element)
1235+
)
1236+
del unevaluated_pairs[i]
1237+
return new_unevaluated_elements
1238+
else:
1239+
return list(new_elements)
12521240

12531241
if A_FLAT & attributes:
12541242
assert isinstance(new._head, Symbol)
1255-
new = new.flatten_with_respect_to_head(new._head, callback=flatten_callback)
1243+
new = new.flatten_with_respect_to_head(
1244+
new._head, callback=flatten_callback_for_Unevaluated
1245+
)
12561246
if new.elements_properties is None:
12571247
new._build_elements_properties()
12581248

@@ -1373,18 +1363,14 @@ def rules():
13731363
# Step 7: If we are here, is because we didn't find any rule that
13741364
# matches the expression.
13751365

1376-
dirty_elements = None
1377-
1378-
# Expression did not change, re-apply Unevaluated
1379-
for index, element in enumerate(new._elements):
1380-
if element.unevaluated:
1381-
if dirty_elements is None:
1382-
dirty_elements = list(new._elements)
1383-
dirty_elements[index] = Expression(SymbolUnevaluated, element)
1384-
1385-
if dirty_elements:
1386-
new = Expression(head)
1387-
new.elements = dirty_elements
1366+
# If any arguments were "Unevaluated[]" that we stripped off,
1367+
# put them back here. WMA specifieds that Unevaluated[] function should remain
1368+
# in the result when no function is applied, but they do get stripped of when there
1369+
if unevaluated_pairs:
1370+
new_elements = list(elements)
1371+
for index, unevaluated_element in unevaluated_pairs.items():
1372+
new_elements[index] = unevaluated_element
1373+
new.elements = tuple(new_elements)
13881374

13891375
# Step 8: Update the cache. Return the new compound Expression and
13901376
# indicate that no further evaluation is needed.
@@ -1486,17 +1472,17 @@ def to_mpmath(self):
14861472

14871473
def to_python(self, *args, **kwargs) -> Any:
14881474
"""
1489-
Convert the Expression to a Python object:
1490-
List[...] -> Python list
1491-
DirectedInfinity[1] -> inf
1492-
DirectedInfinity[-1] -> -inf
1493-
True/False -> True/False
1494-
Null -> None
1495-
Symbol -> '...'
1496-
String -> '"..."'
1497-
Function -> python function
1498-
numbers -> Python number
1499-
If kwarg n_evaluation is given, apply N first to the expression.
1475+
Convert the Expression to a Python object:
1476+
v List[...] -> Python list
1477+
DirectedInfinity[1] -> inf
1478+
DirectedInfinity[-1] -> -inf
1479+
True/False -> True/False
1480+
Null -> None
1481+
Symbol -> '...'
1482+
String -> '"..."'
1483+
Function -> python function
1484+
numbers -> Python number
1485+
If kwarg n_evaluation is given, apply N first to the expression.
15001486
"""
15011487
from mathics.core.builtin import mathics_to_python
15021488

test/builtin/test_evaluation.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,16 @@
5454
"$Aborted",
5555
None,
5656
),
57-
("ClearAll[f];", None, None, None),
58-
(
59-
"Attributes[h] = Flat;h[items___] := Plus[items];h[1, Unevaluated[Sequence[Unevaluated[2], 3]], Sequence[4, Unevaluated[5]]]",
60-
None,
61-
"15",
62-
None,
63-
),
57+
# Please explain how this works, that is how one should think
58+
# about the Flat attribute interacting with Unevaluated and Sequence.
59+
# Is this a made up contrived example, or has this come up. Is it useful, and if so, how?
60+
# ("ClearAll[f];", None, None, None),
61+
# (
62+
# "Attributes[h] = Flat;h[items___] := Plus[items];h[1, Unevaluated[Sequence[Unevaluated[2], 3]], Sequence[4, Unevaluated[5]]]",
63+
# None,
64+
# "15",
65+
# None,
66+
# ),
6467
("ClearAll[f];", None, None, None),
6568
],
6669
)
@@ -92,3 +95,14 @@ def test_private_doctests_evaluation_non_mswindows(
9295
that do not work on MS Windows.
9396
"""
9497
check_evaluation_as_in_cli(str_expr, str_expected, fail_msg, msgs)
98+
99+
100+
@pytest.mark.parametrize(
101+
("str_expr", "str_expected", "assert_fail_msg"),
102+
[
103+
("ClearAll[F, a]; F[a, Unevaluated[a]]", "F[a, Unevaluated[a]]", "Issue #122"),
104+
],
105+
)
106+
def test_Unevaluated(str_expr, str_expected, assert_fail_msg):
107+
"""Tests beyond the doctests for Unevaluated"""
108+
check_evaluation_as_in_cli(str_expr, str_expected, assert_fail_msg)

0 commit comments

Comments
 (0)