A Magentic Read Head sits above a cassette tape, and reads the content off, allowing it to be presented to the user.
lines in src including comments and white space
MRH: 855 lines
Only 200 of them are doing deep Cassette stuff
Debugger.jl: 1285 lines
JuliaInterpreter: 3780 lines
function summer(A)
s = zero(eltype(A))
for a in A
s += a
end
return s
end

julia> foo() = Complex.(rand(1,2), rand(1,2)) * rand(Int, 2,1);
julia> @btime foo();
297.770 ns (9 allocations: 720 bytes)
julia> @btime Debugger.@run foo();
15.472 ms (46982 allocations: 1.78 MiB)
julia> @time MagneticReadHead.@run foo()
#== Sits there compiling for over 30 minutes before I give up ==#
Original Code Lowered Size: $$O(n_{statements})\qquad \qquad \mathrm{eg:}\quad 160$$
MRH Instrumented Size: $$O(n_{slots} \times n_{statements}) \qquad \qquad \mathrm{eg:}\quad 25,230$$
Debugger.jl: Significant runtime overhead 🏃⏲️
MagneticReadHead.jl: Significant compile-time overhead 💻⏲️
should_break(...)break_action(...)Insert at points:
if should_break(current_location)
break_action(current_location, current_variables)
end
should_break(...) do?¶current_location against a set of breakpoint rulesset_breakpointbreak_action do?¶@eval things into Main scope, set_breakpoint worksStepNext by changing debugger statebreak_action(...) need ?¶quote@code_lowered@code_typed@code_llvm@code_nativeSSAValue(index)GlobalRef(mod, func)@generated function can return a Expr or a CodeInfoCodeInfo based on a modified version of one for a function argument. Its a bit like a macro with dynamic scope.call_and_print(f, args...) = (println(f, " ", args); f(args...))
@generated function rewritten(f)
ci = deepcopy(@code_lowered f.instance())
for ii in eachindex(ci.code)
if ci.code[ii] isa Expr && ci.code[ii].head==:call
func = GlobalRef(Main, :call_and_print)
ci.code[ii] = Expr(:call, func, ci.code[ii].args...)
end
end
return ci
end
julia> foo() = 2*(1+1);
julia> rewritten(foo)
+ (1, 1)
* (2, 2)
4
call_and_print:¶function overdub(f, args...)
println(f, " ", args)
rewritten(f, args...)
end
This is how Cassette (and IRTools, and Arborist) work.
using Cassette
Cassette.@context Concept1
function Cassette.overdub(ctx::Concept1, f::typeof(+), args...)
method = @which f(args...)
iprintstyled("Breakpont Hit", :red)
iprintstyled(method, :green);
iprintstyled("Args: ", args, :blue);
println("...press enter to continue...")
#readline()
Cassette.recurse(ctx, f, args...)
end
function foo(a)
b = a+1
c= 2b
return b+c
end
Cassette.recurse(Concept1(), ()->foo(4))
Breakpont Hit
+(x::T, y::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} in Base at int.jl:53
Args: (4, 1)
Breakpont Hit
+(x::T, y::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} in Base at int.jl:53
Args: (5, 10)
...press enter to continue... ...press enter to continue...
15
@evalBase.deletemethodCassette.@context Concept2
function set_breakpoint(ctx::Concept2, f::F) where F
@eval function Cassette.overdub(ctx::Concept2, f::$F, args...)
method = @which f(args...)
iprintstyled("Breakpont Hit", :red)
iprintstyled(method, :green);
iprintstyled("Args: ", args, :blue);
println("...press enter to continue...")
#readline()
if Cassette.canrecurse(ctx, f, args...)
Cassette.recurse(ctx, f, args...)
else
Cassette.fallback(ctx, f, args...)
end
end
end
set_breakpoint (generic function with 1 method)
using Revise: get_method, sigex2sigts, get_signature
macro undeclare(expr)
quote
sig = get_signature($(Expr(:quote, expr)))
sigt = only(sigex2sigts(@__MODULE__, sig))
meth = get_method(sigt)
if meth == nothing
@info "Method not found, thus not removed."
else
Base.delete_method(meth)
end
end
end
function rm_breakpoint(f::F) where F
@undeclare function Cassette.overdub(ctx::MagneticCtx, fi::$(F), zargs...)
end
end
bar(x) = x + blarg(x)
blarg(x) = 5
set_breakpoint(Concept2(), blarg)
Cassette.@overdub Concept2() bar(1)
Breakpont Hit
blarg(x) in Main at In[5]:2
Args: (1,)
...press enter to continue...
6
Lots of bits of a debugger are not too hard.
an ok one can be written in 12
function run_repl(name2var)
while true
code_ast = Meta.parse(readline()) # READ
code_ast == nothing && break
code_ast = substitute_vars(name2var, code_ast)
try
result = eval(code_ast) # EVAL
display(result) # PRINT
catch err
showerror(stdout, err)
end
end # LOOP
end
lineinfo and codelocs¶Contain all you need to go from Line to IR statement index
function statement_ind2src_linenum(ir, statement_ind)
code_loc = ir.codelocs[statement_ind]
return ir.linetable[code_loc].line
end
function src_line2ir_statement_ind(ir, src_line)
linetable_ind = findlast(ir.linetable) do lineinfo
lineinfo.line == src_line
end
statement_ind = findlast(isequal(linetable_ind), ir.codelocs)
return statement_ind
end
And/Or CodeTracking.jl
It is basically a state machine:
On the way is easy:
Out is harder:
This can all be controlled from the overdub

This is a snake wearing a hat. It is clearly having a break also.
That is everything I know about this snake.
Reflection object.CodeInfoReflection input):¶method (+ signature + static_params)code_info: (copied) We are going to edit thiscode: Array of linearized expressions (untyped IR)codelocs: maps from IR statement index to entry in lineinfolineinfo: tells you a position in linenumber + filenamslotnames: Array of names for all the variablescall_expr¶function call_expr(mod::Module, func::Symbol, args...)
Expr(:call, Expr(:nooverdub, GlobalRef(mod, func)), args...)
end
Without the :nooverdub Cassette will try and recurse into this
In julia:
if (should_break(method, original_statement_index)
variables_names=Symbol[]
variables=Any[]
# Do stuff for each slot to
# capture all variables that are defined
#...
break_action(method, original_statement_index, variables_names, variables)
end
$original_statement
In untyped IR:
call_expr(MagneticReadHead, :should_break, method, orig_ind),
Expr(:gotoifnot, SSAValue(ind), next_statement_ind)),
call_expr(Base, :getindex, GlobalRef(Core, :Symbol)),
call_expr(Base, :getindex, GlobalRef(Core, :Any)),
# Do stuff for each slot to
# capture all variables that are defined
#...
call_expr(MagneticReadHead, :break_action,
method, orig_ind, SSAValue(ind+2), SSAValue(ind+3)
)
end
original_statement
In julia:
if @isdefined(slotname)
push!(variable_names, slotname)
push!(variables, slot)
end
In untyped IR:
Expr(:isdefined, slot) # cur_ind
Expr(:gotoifnot, Core.SSAValue(cur_ind), cur_ind + 4)
call_expr(Base, :push!, names_ssa, QuoteNode(slotname)
call_expr(Base, :push!, values_ssa, slot)
function fun_times()
y=2
x=2
y=x+y
end
Cassette.@overdub Concept31(metadata=Ref(0), pass=pass31n) fun_times()
Index 1 #self# getfield(Main, Symbol("##74#75"))()
fun_times() in Main at In[45]:2
Index 1 #self# fun_times
Index 11 #self# fun_times
Index 11 y 2
Index 21 #self# fun_times
Index 21 x 2
Index 21 y 2
+(x::T, y::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} in Base at int.jl:53
Index 1 #self# +
Index 1 x 2
Index 1 y 2
Index 11 #self# +
Index 11 x 2
Index 11 y 2
Index 31 #self# fun_times
Index 31 x 2
Index 31 y 2
Index 41 #self# fun_times
Index 41 x 2
Index 41 y 4
Index 5 #self# getfield(Main, Symbol("##74#75"))()
4
isdefined whenever you want¶function danger19()
y=2
function inner()
h=y
y=12
return h
end
inner()
end
Cassette.@overdub Concept31(metadata=Ref(0), pass=pass31n) danger19()

Basically need to make sure variables are declared before you check if they are defined.
Reliable way is to just ban any slot that was touched by NewvarNode.
but then you don't get all the variables.
There is no hot loop as hot as literally every single statement in your code
Almost as hot: Literally every function call in your code
StepNext, Continue were singleton types.Dict with references to all variables¶Dict stored in the Context:isdefined stuff.setindex every single assignment. Many allocations.methods to get the Method¶methods takes whole microseconds.@nospecialize¶Huge performance gain from
@inline function Cassette.overdub(ctx::HandEvalCtx, f, args...)
Original Code Lowered Size: $$O(n_{statements})\qquad \qquad \mathrm{eg:}\quad 160$$
MRH Instrumented Size: $$O(n_{slots} \times n_{statements}) \qquad \qquad \mathrm{eg:}\quad 25,230$$
set_uninstrumented!isdefined before it has been usedisdefined after it has been used