Skip to content

Conversation

@mmatera
Copy link
Contributor

@mmatera mmatera commented Dec 4, 2025

This PR adds a helper function for evaluating expressions, keeping unevaluated those expressions that could have side effects, like changing the value of a variable. This could be useful to convert (compile) expressions into Python functions.
Let' s consider the following WL code:

In[1]:= F[x_,y_]:=(a=x; Do[a=a+i,{y}];a)

In[2]:= Plot3D[F[x,y],{x,0,1},{y,0,1}]

F[x,y] is a convoluted way to write (x+IntegerPart[y]), but I imagine more complicated cases where this kind of construction is needed.
If we use the vectorized version of Plot3D, F[x,y] is compiled to a vectorized Python function through the functionplot_compile.

plot_compile(evaluation,expr, ...) tryies to convert F[x,y] into a Sympy expression. To do that, it needs to evaluate first F[x,y] into an expression involving just x,y, and built-in symbols. In the current implementation, this is done by first evaluating the expression expr (expr = expr.evaluate(evaluation)), and then trying to convert it to sympy. But in the case of the example, evaluating F[x,y] results in x, and leaves assigned the symbol a with the symbol x, which is an undesired side effect.

This PR should avoid the evaluation of Do, Set, and CompoundExpression. This makes the conversion to sympy fail, forcing the evaluation to use the standard method.
In another round, we could also provide suitable conversions to SymPy for these symbols, allowing a proper vectorized implementation.

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.

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.

Copy link
Member

Choose a reason for hiding this comment

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

The two obvious places to start alternate representations or approximate computations of an expression (where a symbol is considered to be an expression) are either inside the compound Expression object or inside a definitions entry.

I suspect we'll need to make a pass over the definitions table at some point. As with so many other things, it is one of those dark areas where, I suspect, things are a bit unconventional from both the conventional compiler standpoint as well as how WMA thinks of scoping.

= 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.

"""

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.

@mmatera mmatera marked this pull request as ready for review December 4, 2025 18:53
@rocky
Copy link
Member

rocky commented Dec 14, 2025

I have this sinking gut feeling that this kind of piecemeal approach will not serve us well in the long term.

How we handle scope and variables is probably something that needs more thought. I know how complation is handled definitely needs more thought.

So, probably we should come back to this after the dust settles with vectorizing for graphs.

@bdlucas1
Copy link
Collaborator

How we handle scope and variables is probably something that needs more thought.

In that vein, isn't it the case that what needs to be banned here is not all side effects, but side effects at global scope? Side effects within a Module should be fine, right? Since we don't support Modules in plot_compile at this point banning all side effects doesn't lose us anything, from the perspective of Plot.

It would be (low priority) cool if someday we could do something like this example that ChatGPT helped me come up with when I asked for a cool demo that uses procedural programming to generate a plot. This creates fractal-ish landscapes by superimposing sine waves. I had it working in a hacky demo-level sort of way in an early prototype of plot_compile, but I jettisoned it when I found out about lambdify. This would need some kind of hybrid approach.

Manipulate[
    Plot3D[
        Module[
            {z = 0, i, freq, amp},
            freq = 1;
            amp = 1;
            For[i = 1, i <= n, i++,
                z += amp * Sin[freq x] * Cos[freq y];
                freq *= fmul;
                amp /= adiv;
            ];
            z
        ],
        {x, -3, 3}, {y, -3, 3}
    ],
    {{n,6}, 1, 10, 1},
    {{fmul,2}, 1, 3, 0.1},
    {{adiv,2}, 1, 3, 0.1}
]

@mmatera
Copy link
Contributor Author

mmatera commented Dec 14, 2025

In that vein, isn't it the case that what needs to be banned here is not all side effects, but side effects at global scope? Side effects within a Module should be fine, right? Since we don't support Modules in plot_compile at this point banning all side effects doesn't lose us anything, from the perspective of Plot.

What I am trying to address here is the problem that all these side effects should not be handled in the evaluation previous to a sympy conversion. Then, there is another problem related with how to handle these side effects at the level of sympy. Conditional expressions can be handled using sympy.Piecewise, but I do not know if it is possible to handle assignments and loops in a similar way. For this kind of instruction, I guess that a true compilation scheme for WL expressions would be needed. By now, if they appear in an expression, we can fall back into pythonizing the expression, i.e. generate a function that evaluates the expression in a special context (like we do in Compile).

@rocky
Copy link
Member

rocky commented Dec 14, 2025

In that vein, isn't it the case that what needs to be banned here is not all side effects, but side effects at global scope? Side effects within a Module should be fine, right? Since we don't support Modules in plot_compile at this point banning all side effects doesn't lose us anything, from the perspective of Plot.

What I am trying to address here is the problem that all these side effects should not be handled in the evaluation previous to a sympy conversion. Then, there is another problem related with how to handle these side effects at the level of sympy. Conditional expressions can be handled using sympy.Piecewise, but I do not know if it is possible to handle assignments and loops in a similar way. For this kind of instruction, I guess that a true compilation scheme for WL expressions would be needed. By now, if they appear in an expression, we can fall back into pythonizing the expression, i.e. generate a function that evaluates the expression in a special context (like we do in Compile).

I get that. What I suggest then is to study how this kind of thing is done in a compiler using symbol tables and scopes instead of trying to reinvent it through writing some code.

While I know, first hand, it can be fun to learn stuff this way through trial and error, using a large shared codebase to do it on is distracting and can be painful for others.

@rocky
Copy link
Member

rocky commented Dec 14, 2025

How we handle scope and variables is probably something that needs more thought.

In that vein, isn't it the case that what needs to be banned here is not all side effects, but side effects at global scope? Side effects within a Module should be fine, right? Since we don't support Modules in plot_compile at this point banning all side effects doesn't lose us anything, from the perspective of Plot.

The problem is understanding when there is a side effect, which scope it is at, and how this changes things aliased via rewrite rules.

My own experience with this code base is that I don't trust that it has been thought out carefully. And WMA probably adds its own idiosyncrasies, because, well, it can. There's no written-down standard that WMA has to follow, and if there's a deviation from standard practice, it can declare however it coded things as "standard" without even needing to explain its implementation.

@rocky
Copy link
Member

rocky commented Dec 14, 2025

This would need some kind of hybrid approach.

I don't know if it will help, but right now NumericArray[] is not exposed to Mathics3 users. If it were, that might facilitate hybrid approaches.

@mmatera
Copy link
Contributor Author

mmatera commented Dec 15, 2025

Sorry for the noise. It seems I screw up my black install, and it started making trivial changes. Now that tests are passing, I leave this as a draft for a future review.

@mmatera mmatera marked this pull request as draft December 15, 2025 11:10
@bdlucas1
Copy link
Collaborator

I take it back - it turns out that the above example with a Module as the argument to Plot can be compiled. This is because the initial evaluation in lambdify_compile is able to expand the Module into a non-procedural expression. I think this is safe because all side effects are confined to the Module and there are no global side effects, correct?

To take a simpler example, this function

Module[{sum=0, i}, For[i=1, i<=3, i++,  sum += Sin[i #]]; sum]&

after the initial evaluation has been expanded into the non-procedural expression

System`Plus[
    System`Sin[Global`x],
    System`Sin[System`Times[2, Global`x]],
    System`Sin[System`Times[3, Global`x]]
]

which converts to the sympy expression

Add
    sin
        Symbol(x)
    sin
        Mul
            Integer(2)
            Symbol(x)
    sin
        Mul
            Integer(3)
            Symbol(x)

and compiles to the Python function

def _lambdifygenerated(x):
    return sin(x) + sin(2*x) + sin(3*x)

(This works in the Plot example because the function is compiled every time the slider is moved, so the loop limit n has been bound by the Manipulate before compilation is done. This is slightly expensive, a couple tens of ms, but that still gives acceptable interactive response time. An optimization would be to compile the function once in Manipulate if possible, and then to try again in Plot if Manipulate couldn't compile it.)

If you want to try it for yourself, in https://github.com/bdlucas1/mathics-m3d

python m3d.py test/test-module.m3d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants