Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
5 changes: 3 additions & 2 deletions mathics/builtin/assignments/assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class Set(InfixOperator):
"setraw": "Cannot assign to raw object `1`.",
"shape": "Lists `1` and `2` are not the same shape.",
}

has_side_effects = True
summary_text = "assign a value"

def eval(self, lhs, rhs, evaluation):
Expand Down Expand Up @@ -258,7 +258,7 @@ class TagSet(Builtin):
"""

attributes = A_HOLD_ALL | A_PROTECTED | A_SEQUENCE_HOLD

has_side_effects = True
messages = {
"tagnfd": "Tag `1` not found or too deep for an assigned rule.",
}
Expand Down Expand Up @@ -353,6 +353,7 @@ class UpSet(InfixOperator):
"""

attributes = A_HOLD_FIRST | A_PROTECTED | A_SEQUENCE_HOLD
has_side_effects = True
grouping = "Right"

summary_text = (
Expand Down
5 changes: 4 additions & 1 deletion mathics/builtin/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class ClearAttributes(Builtin):
"""

attributes = A_HOLD_FIRST | A_PROTECTED
has_side_effects = True
summary_text = "clear the attributes of a symbol"

def eval(self, symbols, attributes, evaluation):
Expand Down Expand Up @@ -526,6 +527,7 @@ class Protect(Builtin):
"""

attributes = A_HOLD_ALL | A_PROTECTED
has_side_effects = True
summary_text = "protect a symbol against redefinitions"

def eval(self, symbols, evaluation):
Expand Down Expand Up @@ -696,7 +698,7 @@ class SetAttributes(Builtin):
"""

attributes = A_HOLD_FIRST | A_PROTECTED

has_side_effects = True
messages = {
"unknownattr": f"`1` should be one of {', '.join(attribute_string_to_number.keys())}"
}
Expand Down Expand Up @@ -752,6 +754,7 @@ class Unprotect(Builtin):
"""

attributes = A_HOLD_ALL | A_PROTECTED
has_side_effects = True
summary_text = "remove protection against redefinitions"

def eval(self, symbols, evaluation):
Expand Down
34 changes: 29 additions & 5 deletions mathics/builtin/compilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@

from mathics.builtin.box.compilation import CompiledCodeBox
from mathics.core.atoms import Integer, String
from mathics.core.attributes import A_HOLD_ALL, A_PROTECTED
from mathics.core.attributes import (
A_HOLD_ALL,
A_N_HOLD_ALL,
A_PROTECTED,
A_READ_PROTECTED,
)
from mathics.core.builtin import Builtin
from mathics.core.convert.expression import to_mathics_list
from mathics.core.convert.function import (
Expand Down Expand Up @@ -65,9 +70,11 @@ class Compile(Builtin):
>> cf[3.5, 2]
= 2.18888

Loops and variable assignments are supported usinv Python builtin "compile" function:
>> Compile[{{a, _Integer}, {b, _Integer}}, While[b != 0, {a, b} = {b, Mod[a, b]}]; a] (* GCD of a, b *)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was wrong in many ways. On the one hand, this expression shows the following warning in WMA:

Compile::argset: 
   The assignment to a is illegal; it is not valid to assign a value to an
     argument.

Then, if we try to evaluate the compiled function, we get more errors, and an unfinished loop.

The expected result is also wrong in another way: it expects the wrong behaviour, where the expression is fully evaluated before compiling, to get a. The new test is something that actually works in WMA, and produces the expected behavior.

= CompiledFunction[{a, b}, a, -PythonizedCode-]
Loops and variable assignments are supported using Python builtin "compile" function:
>> gdc = Compile[{{a, _Integer}, {b, _Integer}}, Module[{x=a, y=b}, While[y != 0, {x, y} = {y, Mod[x, y]}]; x]] (* GCD of a, b *)
= CompiledFunction[{a, b}, Module[{x = a, y = b}, While[y != 0, {x, y} = {y, Mod[x, y]}] ; x], -PythonizedCode-]
>> gdc[18, 81]
= 9.
"""

attributes = A_HOLD_ALL | A_PROTECTED
Expand Down Expand Up @@ -167,7 +174,23 @@ def to_sympy(self, *args, **kwargs):
raise NotImplementedError

def __hash__(self):
return hash(("CompiledCode", ctypes.addressof(self.cfunc))) # XXX hack
cfunc = self.cfunc
if cfunc is None:
hash(
(
"CompiledCode",
None,
)
) # XXX hack
try:
return hash(("CompiledCode", ctypes.addressof(cfunc))) # XXX hack
except TypeError:
return hash(
(
"Pythonized-function",
cfunc,
)
)

def atom_to_boxes(self, f, evaluation: Evaluation):
return CompiledCodeBox(String(self.__str__()), evaluation=evaluation)
Expand All @@ -191,6 +214,7 @@ class CompiledFunction(Builtin):

"""

attributes = A_HOLD_ALL | A_PROTECTED | A_N_HOLD_ALL | A_READ_PROTECTED
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These attributes are very important: they prevent the evaluation of the original compiled expression.

messages = {"argerr": "Invalid argument `1` should be Integer, Real or boolean."}
summary_text = "A CompiledFunction object."

Expand Down
13 changes: 8 additions & 5 deletions mathics/builtin/procedural.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ class CompoundExpression(InfixOperator):
"""

attributes = A_HOLD_ALL | A_PROTECTED | A_READ_PROTECTED

has_side_effects = True
summary_text = "execute expressions in sequence"

def eval(self, expr, evaluation):
Expand Down Expand Up @@ -346,6 +346,7 @@ class For(Builtin):
"""

attributes = A_HOLD_REST | A_PROTECTED
has_side_effects = True
rules = {
"For[start_, test_, incr_]": "For[start, test, incr, Null]",
}
Expand Down Expand Up @@ -486,6 +487,7 @@ class Interrupt(Builtin):
| a
= $Aborted
"""
has_side_effects = True

# Set checking that the no arguments are allowed.
# eval_error = Builtin.generic_argument_error
Expand All @@ -511,6 +513,7 @@ class Pause(Builtin):
>> Pause[0.5]
"""

has_side_effects = True
messages = {
"numnm": (
"Non-negative machine-sized number expected at " "position 1 in `1`."
Expand Down Expand Up @@ -563,7 +566,7 @@ class Return(Builtin):
rules = {
"Return[]": "Return[Null]",
}

has_side_effects = True
summary_text = "return from a function"

def eval(self, expr, evaluation: Evaluation): # pylint: disable=unused-argument
Expand Down Expand Up @@ -604,7 +607,7 @@ class Switch(Builtin):

summary_text = "switch based on a value, with patterns allowed"
attributes = A_HOLD_REST | A_PROTECTED

has_side_effects = True
messages = {
"argct": (
"Switch called with `2` arguments. "
Expand Down Expand Up @@ -659,7 +662,7 @@ class Throw(Builtin):
# Set checking that the number of arguments required is one or two. WMA uses 1..3.
eval_error = Builtin.generic_argument_error
expected_args = (1, 2)

has_side_effects = True
messages = {
"nocatch": "Uncaught `1` returned to top level.",
}
Expand Down Expand Up @@ -773,7 +776,7 @@ class While(Builtin):
# Set checking that the number of arguments required is one.
eval_error = Builtin.generic_argument_error
expected_args = (1, 2)

has_side_effects = True
rules = {
"While[test_]": "While[test, Null]",
}
Expand Down
12 changes: 7 additions & 5 deletions mathics/builtin/scoping.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class Begin(Builtin):
## = Global`test`
"""

has_side_effects = True
rules = {
"Begin[context_String]": """
Unprotect[System`Private`$ContextStack];
Expand Down Expand Up @@ -128,7 +129,7 @@ class BeginPackage(Builtin):
## >> BeginPackage["test`"]
## = test`
"""

has_side_effects = True
messages = {"unimpl": "The second argument to BeginPackage is not yet implemented."}

rules = {
Expand Down Expand Up @@ -187,7 +188,7 @@ class Block(Builtin):
"""

attributes = A_HOLD_ALL | A_PROTECTED

has_side_effects = True
messages = {
"lvsym": (
"Local variable specification contains `1`, "
Expand Down Expand Up @@ -351,6 +352,7 @@ class End(Builtin):
</dl>
"""

has_side_effects = True
messages = {
"noctx": "No previous context defined.",
}
Expand Down Expand Up @@ -385,7 +387,7 @@ class EndPackage(Builtin):
After 'EndPackage', the values of '\$Context' and '\$ContextPath' at the \
time of the 'BeginPackage' call are restored, with the new package\'s context prepended to '\$ContextPath'.
"""

has_side_effects = True
messages = {
"noctx": "No previous context defined.",
}
Expand Down Expand Up @@ -450,7 +452,7 @@ class Module(Builtin):
"""

attributes = A_HOLD_ALL | A_PROTECTED

has_side_effects = True
messages = {
"lvsym": (
"Local variable specification contains `1`, "
Expand Down Expand Up @@ -710,7 +712,7 @@ class With(Builtin):
"""

attributes = A_HOLD_ALL | A_PROTECTED

has_side_effects = True
messages = {
"lvsym": (
"Local variable specification contains `1`, "
Expand Down
13 changes: 13 additions & 0 deletions mathics/core/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ def eval_with_options(x, evaluation: Evaluation, options: dict):
expected_args: Union[int, Tuple[int, int], range] = -1

formats: Dict[str, Any] = {}

# If the symbol include rules involving loops,
# setting values, or creating/changing contexts.
has_side_effects: bool = False

messages: Dict[str, Any] = {}
name: Optional[str] = None
options: Dict[str, Any] = {}
Expand Down Expand Up @@ -434,6 +439,13 @@ def contextify_form_name(f):
else:
definitions.builtin[name] = definition

# If the definition has side effects, store it in a
# dictionary.
if self.has_side_effects:
from mathics.core.definitions import SIDE_EFFECT_BUILTINS

SIDE_EFFECT_BUILTINS[name] = definition

makeboxes_def = definitions.builtin["System`MakeBoxes"]
for rule in box_rules:
makeboxes_def.add_rule(rule)
Expand Down Expand Up @@ -1024,6 +1036,7 @@ def get_name(cls, short=False) -> str:
class IterationFunction(Builtin, ABC):
attributes = A_HOLD_ALL | A_PROTECTED
allow_loopcontrol = False
has_side_effects = True
throw_iterb = True

def get_result(self, elements, is_uniform=False) -> Expression:
Expand Down
41 changes: 38 additions & 3 deletions mathics/core/convert/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import Callable, Optional, Tuple

from mathics.core.definitions import SIDE_EFFECT_BUILTINS, Definition
from mathics.core.element import BaseElement, EvalMixin
from mathics.core.evaluation import Evaluation
from mathics.core.expression import Expression, from_python
from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue
Expand Down Expand Up @@ -44,6 +46,33 @@ def __init__(self, var):
self.var = var


def evaluate_without_side_effects(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the right place for this function.

expr: Expression, evaluation: Evaluation
) -> Expression:
"""
Evaluate an expression leaving unevaluated subexpressions
related with side-effects (assignments, loops).
"""
definitions = evaluation.definitions
# Temporarily remove the builtin definitions
# of symbols with side effects
for name, defin in SIDE_EFFECT_BUILTINS.items():
# Change the definition by a temporal definition setting
# just the name and the attributes.
definitions.builtin[name] = Definition(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe these definitions can be stored in some place, and then each time we compile something, bring them up, instead of constructing them each time.

name, attributes=defin.attributes, builtin=defin.builtin
)
definitions.clear_cache(name)
try:
result = expr.evaluate(evaluation)
finally:
# Restore the definitions
for name, defin in SIDE_EFFECT_BUILTINS.items():
definitions.builtin[name] = defin
definitions.clear_cache(name)
return result if result is not None else expr


def expression_to_callable(
expr: Expression,
args: Optional[list] = None,
Expand All @@ -56,6 +85,9 @@ def expression_to_callable(
args: a list of CompileArg elements
evaluation: an Evaluation object used if the llvm compilation fails
"""
if evaluation is not None:
expr = evaluate_without_side_effects(expr, evaluation)

try:
cfunc = _compile(expr, args) if (use_llvm and args is not None) else None
except CompileError:
Expand All @@ -67,10 +99,13 @@ def expression_to_callable(
try:

def _pythonized_mathics_expr(*x):
from mathics.eval.scoping import dynamic_scoping

inner_evaluation = Evaluation(definitions=evaluation.definitions)
x_mathics = (from_python(u) for u in x[: len(args)])
vars = dict(list(zip([a.name for a in args], x_mathics)))
pyexpr = expr.replace_vars(vars)
vars = {a.name: from_python(u) for a, u in zip(args, x[: len(args)])}
pyexpr = dynamic_scoping(
lambda ev: expr.evaluate(ev), vars, inner_evaluation
)
pyexpr = eval_N(pyexpr, inner_evaluation)
res = pyexpr.to_python()
return res
Expand Down
6 changes: 6 additions & 0 deletions mathics/core/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ def __repr__(self) -> str:
return repr_str


# Dictionary of builtin definitions involving side effects
# like setting values, changing contexts or running loops:

SIDE_EFFECT_BUILTINS: Dict[str, Definition] = {}


class Definitions:
"""The state of one instance of the Mathics3 interpreter is stored in this object.

Expand Down
3 changes: 2 additions & 1 deletion mathics/eval/drawing/plot_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import scipy
import sympy

from mathics.core.convert.function import evaluate_without_side_effects
from mathics.core.convert.sympy import SympyExpression
from mathics.core.symbols import strip_context
from mathics.core.util import print_expression_tree, print_sympy_tree
Expand Down Expand Up @@ -54,7 +55,7 @@ def plot_compile(evaluation, expr, names, debug=0):
# because some functions are not themselves sympy-enabled
# if they always get rewritten to one that is.
try:
new_expr = expr.evaluate(evaluation)
new_expr = evaluate_without_side_effects(expr, evaluation)
if new_expr:
expr = new_expr
except Exception:
Expand Down
1 change: 1 addition & 0 deletions mathics/eval/scoping.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def dynamic_scoping(func, vars, evaluation: Evaluation):
and evaluates func(evaluation)
"""
original_definitions = {}

for var_name, new_def in vars.items():
assert fully_qualified_symbol_name(var_name)
original_definitions[var_name] = evaluation.definitions.get_user_definition(
Expand Down
Loading