Evaluate calls my function multiple times when the function generates an error

If I execute a line of code with Evaluate I find some unexpected behavior when the code returns an error. Here is a minimal example. Let f

f[x_, y_] := (WriteString["stdout", "Hi "];
y /. x);


Now if I call

In[1]:= Evaluate[f[a->1,a]]


Hi

Out[1]= 1

As expected, while if I do

In[2]:= Evaluate[f[a,b]]


this obviously results in an error (because a is not a Rule), but I can see that the function is evaluated multiple times

Hi Hi Hi

ReplaceAll: {a} is [etc...]

Out[2]= b/. a

As you see it is evaluated three times. The same instruction without the Evaluate returns the same but with only one "Hi."

I suspect that in order to throw the error Mathematica is enclosing that statement inside a held function and the Evaluate is causing it to evaluate again. But I can't figure out what's happening in detail.

The issue is caused by the top-level Evaluate interacting with the HoldForm wrappers returned by MessageMenuDump\$GetStack (which is defined as Stack[_]). This function is called twice when building the error message, explaining the two additional calls.

How to find this

We start by changing the definitions of the question a bit to make tracking easier:

g[] := (WriteString["stdout", "Hi "]; marker)

f[x_, y_] := (g[];
y /. x);


Now we Trace the problematic call, using Unevaluated to protect the Evaluate from being evaluated too early. We also need TraceInternal->True. The trace is filtered for calls to g and TraceAbove->True is used to show the stack leading up to the call:

Trace[Unevaluated@Evaluate[f[a, b]], g, TraceInternal -> True,
TraceAbove -> True]
(* long trace... *)


We can now search for marker using the search functionality (this indicates that g has actually been evaluated. We find three occurences:

• Other tools: (1) On[HoldForm], though the printing of the trace messages causes even more greetings; one can inspect the stack. (2) This makes the role of HoldForm a bit clearer: f[x_, y_] := (Print[Stack[_HoldForm]]; y /. x); (3) This gets rid of the problem: Unprotect@HoldForm; SetAttributes[HoldForm, HoldAllComplete]; Protect@HoldForm;, though I don't like changing system functions. (+1) – Michael E2 Aug 15 '19 at 19:09