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