diff --git a/mathics/builtin/assignments/assignment.py b/mathics/builtin/assignments/assignment.py index cc85e83ca..f7aaae725 100644 --- a/mathics/builtin/assignments/assignment.py +++ b/mathics/builtin/assignments/assignment.py @@ -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): @@ -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.", } @@ -353,6 +353,7 @@ class UpSet(InfixOperator): """ attributes = A_HOLD_FIRST | A_PROTECTED | A_SEQUENCE_HOLD + has_side_effects = True grouping = "Right" summary_text = ( diff --git a/mathics/builtin/attributes.py b/mathics/builtin/attributes.py index a4dc27382..2bdfc40d3 100644 --- a/mathics/builtin/attributes.py +++ b/mathics/builtin/attributes.py @@ -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): @@ -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): @@ -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())}" } @@ -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): diff --git a/mathics/builtin/compilation.py b/mathics/builtin/compilation.py index b656fdf13..ac8ad3aca 100644 --- a/mathics/builtin/compilation.py +++ b/mathics/builtin/compilation.py @@ -12,7 +12,12 @@ from mathics.builtin.box.compilation import CompiledCodeBox from mathics.core.atoms import Complex, Integer, Rational, Real, 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 ( @@ -73,9 +78,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 *) - = 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 @@ -174,15 +181,23 @@ def to_sympy(self, *args, **kwargs): raise NotImplementedError def __hash__(self): + cfunc = self.cfunc + if cfunc is None: + hash( + ( + "CompiledCode", + None, + ) + ) # XXX hack try: - return hash(("CompiledCode", ctypes.addressof(self.cfunc))) # XXX hack + return hash(("CompiledCode", ctypes.addressof(cfunc))) # XXX hack except TypeError: return hash( ( - "CompiledCode", - self.cfunc, + "Pythonized-function", + cfunc, ) - ) # XXX hack + ) def atom_to_boxes(self, f, evaluation: Evaluation): return CompiledCodeBox(String(self.__str__()), evaluation=evaluation) @@ -198,7 +213,7 @@ class CompiledFunction(Builtin): >> sqr = Compile[{x}, x x] - = CompiledFunction[{x}, x ^ 2, ...] + = CompiledFunction[{x}, x x, ...] >> Head[sqr] = CompiledFunction >> sqr[2] @@ -206,6 +221,7 @@ class CompiledFunction(Builtin): """ + attributes = A_HOLD_ALL | A_PROTECTED | A_N_HOLD_ALL | A_READ_PROTECTED messages = { "argerr": "Invalid argument `1` should be Integer, Real, Complex or boolean.", "cfsa": "Argument `1` at position `2` should be a `3`.", diff --git a/mathics/builtin/procedural.py b/mathics/builtin/procedural.py index 6d4841c88..be420bfbd 100644 --- a/mathics/builtin/procedural.py +++ b/mathics/builtin/procedural.py @@ -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): @@ -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]", } @@ -487,6 +488,8 @@ class Interrupt(Builtin): = $Aborted """ + has_side_effects = True + # Set checking that the no arguments are allowed. # eval_error = Builtin.generic_argument_error # expected_args = 0 @@ -511,6 +514,7 @@ class Pause(Builtin): >> Pause[0.5] """ + has_side_effects = True messages = { "numnm": ( "Non-negative machine-sized number expected at " "position 1 in `1`." @@ -563,7 +567,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 @@ -604,7 +608,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. " @@ -659,7 +663,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.", } @@ -773,7 +777,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]", } diff --git a/mathics/builtin/scoping.py b/mathics/builtin/scoping.py index 6063788ca..1b50f9aeb 100644 --- a/mathics/builtin/scoping.py +++ b/mathics/builtin/scoping.py @@ -99,6 +99,7 @@ class Begin(Builtin): ## = Global`test` """ + has_side_effects = True rules = { "Begin[context_String]": """ Unprotect[System`Private`$ContextStack]; @@ -129,6 +130,7 @@ class BeginPackage(Builtin): ## = test` """ + has_side_effects = True messages = {"unimpl": "The second argument to BeginPackage is not yet implemented."} rules = { @@ -187,7 +189,7 @@ class Block(Builtin): """ attributes = A_HOLD_ALL | A_PROTECTED - + has_side_effects = True messages = { "lvsym": ( "Local variable specification contains `1`, " @@ -351,6 +353,7 @@ class End(Builtin): """ + has_side_effects = True messages = { "noctx": "No previous context defined.", } @@ -386,6 +389,7 @@ class EndPackage(Builtin): 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.", } @@ -450,7 +454,7 @@ class Module(Builtin): """ attributes = A_HOLD_ALL | A_PROTECTED - + has_side_effects = True messages = { "lvsym": ( "Local variable specification contains `1`, " @@ -710,7 +714,7 @@ class With(Builtin): """ attributes = A_HOLD_ALL | A_PROTECTED - + has_side_effects = True messages = { "lvsym": ( "Local variable specification contains `1`, " diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index f1784391e..37f0625f1 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -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] = {} @@ -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) @@ -1026,6 +1038,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: diff --git a/mathics/core/convert/function.py b/mathics/core/convert/function.py index 7c121dd88..c64b028c8 100644 --- a/mathics/core/convert/function.py +++ b/mathics/core/convert/function.py @@ -3,10 +3,7 @@ import numpy -from mathics.core.convert.lambdify import ( - CompileError as LambdifyCompileError, - lambdify_compile, -) +from mathics.core.definitions import SIDE_EFFECT_BUILTINS, Definition from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression, from_python from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue @@ -53,6 +50,33 @@ def __init__(self, var): self.var = var +def evaluate_without_side_effects( + 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( + 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_llvm( expr: Expression, args: Optional[list] = None, @@ -64,10 +88,36 @@ def expression_to_llvm( 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: return _compile(expr, args) if (USE_LLVM and args is not None) else None except CompileError: - return None + cfunc = None + + if cfunc is None: + if evaluation is None: + raise CompileError + try: + + def _pythonized_mathics_expr(*x): + from mathics.eval.scoping import dynamic_scoping + + inner_evaluation = Evaluation(definitions=evaluation.definitions) + 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 + + # TODO: check if we can use numba to compile this... + cfunc = _pythonized_mathics_expr + except Exception: + cfunc = None + return cfunc def expression_to_python_function( @@ -141,6 +191,11 @@ def expression_to_callable_and_args( expr: A Mathics Expression object vars: a list of Symbols or Mathics Lists of the form {Symbol, Type} """ + from mathics.core.convert.lambdify import ( + CompileError as LambdifyCompileError, + lambdify_compile, + ) + args = collect_args(vars) # If vectorize is requested, first, try to lambdify the expression: diff --git a/mathics/core/convert/lambdify.py b/mathics/core/convert/lambdify.py index 877052d6d..82db5a109 100644 --- a/mathics/core/convert/lambdify.py +++ b/mathics/core/convert/lambdify.py @@ -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, sympy_strip_context from mathics.core.util import print_expression_tree, print_sympy_tree @@ -73,7 +74,7 @@ def lambdify_compile(evaluation, expr, names, debug=0): # or `*Set*`. # try: - new_expr = expr.evaluate(evaluation) + new_expr = evaluate_without_side_effects(expr, evaluation) if new_expr: expr = new_expr except Exception: diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index b66f6e735..413e80d98 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -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. @@ -1093,7 +1099,7 @@ def load_builtin_definitions( mathics3_builtins_modules, ) from mathics.eval.files_io.files import get_file_time - from mathics.eval.pymathics import PyMathicsLoadException, load_pymathics_module + from mathics.eval.pymathics import load_pymathics_module from mathics.session import autoload_files loaded = False diff --git a/test/core/convert/test_function.py b/test/core/convert/test_function.py new file mode 100644 index 000000000..37a9df658 --- /dev/null +++ b/test/core/convert/test_function.py @@ -0,0 +1,32 @@ +""" +Conversion from expressions to functions +""" + +from test.helper import session + +from mathics.core.convert.function import evaluate_without_side_effects + + +def test_evaluate_without_side_effects(): + session.reset() + session.evaluate( + """ + F[x_]:=G[x]; + G[x_]:=(a=2^2*x;Do[a=x+2^2,{3}];a); + Q[x_]:=(a=2^2*x); + """ + ) + expr = session.parse("F[x]") + # Notice that since `CompoundExpression` has the attribute "HoldAll", + # subelements are not evaluated (`2^2`->`2^2`) + expect = session.parse("(a=2^2*x; Do[a=x+2^2,{3}];a)") + result = evaluate_without_side_effects(expr, session.evaluation) + assert result.sameQ(expect) + + expr = session.parse("Q[x]") + # If the head of the expression does not have the attribute "HoldAll", + # subelements are evaluated (`2^2`->`4`) + expect = session.parse("a=4*x") + result = evaluate_without_side_effects(expr, session.evaluation) + + assert result.sameQ(expect)