diff --git a/mathics/builtin/assignments/assignment.py b/mathics/builtin/assignments/assignment.py index dd626685f..248340bbf 100644 --- a/mathics/builtin/assignments/assignment.py +++ b/mathics/builtin/assignments/assignment.py @@ -138,7 +138,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): @@ -262,6 +262,7 @@ class TagSet(Builtin): """ attributes = A_HOLD_ALL | A_PROTECTED | A_SEQUENCE_HOLD + has_side_effects = True eval_error = Builtin.generic_argument_error expected_args = range(2, sys.maxsize) @@ -359,6 +360,7 @@ class UpSet(InfixOperator): """ attributes = A_HOLD_FIRST | A_PROTECTED | A_SEQUENCE_HOLD + has_side_effects = True eval_error = Builtin.generic_argument_error expected_args = range(1, sys.maxsize) grouping = "Right" diff --git a/mathics/builtin/attributes.py b/mathics/builtin/attributes.py index a8dd833bd..29bbf6558 100644 --- a/mathics/builtin/attributes.py +++ b/mathics/builtin/attributes.py @@ -119,6 +119,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): @@ -519,6 +520,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): @@ -689,7 +691,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())}" } @@ -745,6 +747,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 b56271fdd..8ab8cf675 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 4ffafbc20..01588ec2f 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`." @@ -564,7 +568,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 @@ -605,7 +609,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. " @@ -660,7 +664,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.", } @@ -774,7 +778,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 91d574d58..ba6f33a27 100644 --- a/mathics/builtin/scoping.py +++ b/mathics/builtin/scoping.py @@ -100,6 +100,7 @@ class Begin(Builtin): = Global`test` """ + has_side_effects = True messages = { "ctx": "Invalid context specified at position 1 in \\`Begin[`1`]\\`. A context must consist of valid symbol names separated by and ending with \\`.", } @@ -141,7 +142,8 @@ class BeginPackage(Builtin): ## = test` """ - messages = { + has_side_effects = True + messages = {"unimpl": "The second argument to BeginPackage is not yet implemented.", "ctx": "Invalid context specified at position 1 in \\`BeginPackage[`1`,...]\\`. A context must consist of valid symbol names separated by and ending with \\`.", "cxls": "Context or non-empty list of contexts expected at position 2 in \\`BeginPackage[`1`, `2`]\\`.", } @@ -216,7 +218,7 @@ class Block(Builtin): """ attributes = A_HOLD_ALL | A_PROTECTED - + has_side_effects = True messages = { "lvsym": ( "Local variable specification contains `1`, " @@ -391,6 +393,7 @@ class End(Builtin): """ + has_side_effects = True messages = { "noctx": "No previous context defined.", } @@ -426,6 +429,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.", } @@ -491,7 +495,7 @@ class Module(Builtin): """ attributes = A_HOLD_ALL | A_PROTECTED - + has_side_effects = True messages = { "lvsym": ( "Local variable specification contains `1`, " @@ -751,7 +755,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 853f26b09..203dc86ef 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -199,6 +199,12 @@ def eval_with_options(x, evaluation: Evaluation, options: dict): # Number of arguments expected. -1 is used for an arbitrary number. 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 + formats: dict[str, Any] = {} messages: dict[str, Any] = {} name: Optional[str] = None @@ -426,6 +432,17 @@ 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) + # This method is used to produce generic argument mismatch errors # (tags: "argx", "argr", "argrx", "argt", or "argtu") for builtin # functions that define this as an eval method. e.g. For example @@ -1075,6 +1092,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 a849893f7..c0ede7c07 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 Mathics3 Expression object vars: a list of Symbols or Mathics3 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 234ab7194..9337ecf22 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -129,6 +129,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. 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)