Let’s say that I’d like to run foo()
, which calls bar
, but replace the bar
calls with calls to yummy()
. This is the standard use case for packages like Cassette and IRTools (are there others now?), but even after all these years, they still look like half-heartedly-maintained fringe packages filled with caveats (which is understandable given how complex the problem space is!) Meanwhile, JuliaInterpreter has
julia> bp = @breakpoint sum([1, 2]) any(x->x>4, a);
julia> frame, bpref = @interpret sum([1,2,5]) # should trigger breakpoint
(Frame for sum(a::AbstractArray; dims, kw...) in Base at reducedim.jl:889
c 1 889 1 ─ nothing
2 889 │ %2 = ($(QuoteNode(NamedTuple)))()
3 889 │ %3 = Base.pairs(%2)
⋮
a = [1, 2, 5], breakpoint(sum(a::AbstractArray; dims, kw...) in Base at reducedim.jl:889, line 889))
Would it be that unreasonable to use a breakpoint on bar
, then hack into the frame
to change the call stack and insert a call to yummy
?
The answer seems to be yes, with this code being the critical piece:
const JInterp = JuliaInterpreter
function return_and_continue_execution(fr::JInterp.Frame, return_value)
# This assume the `fr` is the _calling_ frame, for whatever call was breaked on, and indeed,
# this is the case for JInterp breakpoints.
# This code is a slimmed down version of `JInterp.maybe_reset_frame!`
@assert !JInterp.is_leaf(fr)
# First, abort the call to the breakpointed function
JInterp.recycle(fr.callee)
fr.callee = nothing
# Then assign the value and move on to the next instruction
JInterp.maybe_assign!(fr, return_value)
fr.pc += 1 # maybe_reset_frame! has more complicated logic, that I don't get, but git-blame
# suggests that it's for kwarg funs
return JInterp.finish_and_return!(fr) # continue execution
end
julia> using JuliaInterpreter
julia> sump7(x) = only(sum(x)) + 7
sump7 (generic function with 1 method)
julia> bp = @breakpoint sum([10,10]);
julia> frame, _ = @interpret sump7([100,100]);
julia> return_and_continue_execution(frame, 500)
507