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_breakpoint
break_action
do?¶@eval
things into Main
scope, set_breakpoint
worksStepNext
by changing debugger statebreak_action(...)
need ?¶quote
@code_lowered
@code_typed
@code_llvm
@code_native
SSAValue(index)
GlobalRef(mod, func)
@generated
function can return a Expr
or a CodeInfo
CodeInfo
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
@eval
Base.deletemethod
Cassette.@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.CodeInfo
Reflection
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 lineinfo
lineinfo
: 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
¶method
s 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