If I get a vote, I'd say spill to the stack every time, but still keep the current behavior for when debugging is not enabled.
At a later date, we can implement some sort of deoptimizer that transform every function into a trampoline function that either calls the non-debugging-instrumented code, or lazily generates a debugging-instrumented code and calls that instead. This way we can reduce the overhead for everything that we're not interested in debugging (e.g. if we're running code normally without hitting any breakpoint, the debug_trap() function should not be emitted; as soon as a breakpoint is set, the deoptimized/debuggable version of that function is emitted and the debug_trap() gets to be called, and functions will be re-generated with debugging instrumentation lazily as we reach them.)
As an alternative to the trampoline (or a transform that essentially does if (!debugging) { original_code; } else { ensure_instrumented_version_exists; call_it_instead; }), a mechanism similar to a PLT could be used instead, but would require all calls to be indirect calls when running winch for debugging. With this mechanism, we'd have a table like so:
0 func_0_orig
1 func_1_orig
2 func_2_orig
And then emit calls as the target machine equivalent of table[index_constant_for_func_n](arg1, arg2, ...) instead of func_n(arg1, arg2, ...).
If a breakpoint were set on, say, func_2, we'd replace table[2] with func_2_debug after generating the code for it with all the calls to debug_trap() as one would expect. This can be done entirely in the Winch side.
To allow for stepping across functions, the *_orig functions can check if global_debug_trap_mask is not 0 (cheap!), ask Winch to generate an instrumented version, patch the respective table entry, and call that. It doesn't need to be a tail call at this point (although it potentially could to save a stack frame for unwinding reasons), because this will happen only the first time this point is reached; every other time the instrumented version will be called automatically. This of course needs to be done partially in the Winch side, and partially by the generated code.
This seems like a more elegant solution in my opinion, including making it reentrant if the table we patch is part of each Winch state. Suffice to say, this has to be implemented directly as part of the JIT-generated code rather than relying on indirect call mechanisms from Wasm itself.