Skip to content

Commit

Permalink
Merge pull request #23 from oxinabox/ox/afinedebugger
Browse files Browse the repository at this point in the history
A fine debugger
  • Loading branch information
oxinabox authored Mar 20, 2019
2 parents 9e815bf + 6487da0 commit 68d8e2c
Show file tree
Hide file tree
Showing 27 changed files with 1,181 additions and 413 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
*.jl.cov
*.jl.*.cov
*.jl.mem
/dev
9 changes: 7 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
name = "MagneticReadHead"
uuid = "3f5657c2-1c21-11e9-2267-d379952df7a3"
authors = ["Lyndon White <[email protected]>"]
version = "0.1.0"

[deps]
Cassette = "7057c7e9-c182-5462-911a-8362d720325c"
CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
Mocking = "78c3b35d-d492-501b-9361-3d52fe80e533"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"

[compat]
CodeTracking = "0.4.0"
Revise = "2"
# Revise 1.1 works, except for setting breakpoints on files outside of packages

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

Expand Down
55 changes: 15 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,22 @@
When a breakpoint is hit, then you will be given an Iron REPL to work with-in,
allowing you to view/manipulate the arguments.

## Breakpoints

- `set_breakpoint(f)`: Set a breakpoint on call to the function `f`



[![asciicast](https://asciinema.org/a/PnffnrsqEkX8Oum71KY9sWMue.svg)](https://asciinema.org/a/PnffnrsqEkX8Oum71KY9sWMue)


## Other Julia Debuggers
With in this you can read (and write) variables,
- Step-Next: to move to the next IR statement
- Step-In: to debug in the next function call (assuming next is a function call)
- Step-Out: to debug from the next statement the function that called the current function
- Continue: proceed to next breakpoint
- Abort: terminate running the debugger.

MagneticReadHead is very early alpha.
THere are some otehr debugging projects going on,
and you can certainly mix and match within the same project depending on your needs.

The other projects are also alpha, but almost certainly more mature than MagneticReadHead.

### Rebugger
[Rebugger](https://github.com/timholy/Rebugger.jl) is the another debugging package for Julia.
Rebugger is a very nontraditional rebugger, MagneticReadHead is a lot closer to a traditional debugger.

Rebugger has has a user interface based on scooping the code of any function it steps into, into the REPL.
Then allow you to point your cursor at a function and step into that one.
Rebugger lets you edit the code at each step.
MagneticReadHead lets you run code to inspect variables or save data,
but you can not edit the code itself.

You can step backwards up the call-stack in Rebugger, MagneticReadHead does not support that (yet).
MagneticReadHead lets you set breakpoints (on function calls), Rebugger does not support that (yet).

MagneticReadHead currently only lets you examine the arguments going into a function.
Rebugger lets you manipulate the function body however you want (So you can add `@show` to examine the local variables.

Rebugger is based on Revise.jl,
MagneticReadHead is based on Cassette.jl and uses Revise.jl as a helper library.
## Breakpoints

- `set_breakpoint([function|method])`: Set a breakpoint on call to the argument
- `set_breakpoint(filename, line number)`: Set a breakpoint on the given line in the given function
- `set_nodubug([function|method|module])`: Disable debugging in the given function/method/module
- Not having debugging enabled for modules that are not between you and your breakpoints massively speeds up the running of your program.
- `list_breakpoints()`, `list_nodebugs()`: list all the breakpoints/nodebugs
- `rm_breakpoint(arg...)`, `rm_nodebug(args...)`: remove breakpoints/nodebugs. Takes same arguments as `set_...`.
- `clear_breakpoints()`, `clear_nodebugs()`: remove all breakpoints/nodebugs.

### ASTInterpreter2
[ASTInterpreter2](https://github.com/JuliaDebug/ASTInterpreter2.jl) is another debugger for julia.
Apparently it is working in 1.0. Who knew ? (not me :-P)
I don't think it has breakpoints yet,
but it's stepping is a lot more advanced/fine grained.

I need to check it out
[![asciicast](https://asciinema.org/a/DxgPaaLQQYVV5xXCMuwF5Aa36.svg)](https://asciinema.org/a/DxgPaaLQQYVV5xXCMuwF5Aa36)
32 changes: 16 additions & 16 deletions src/MagneticReadHead.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,40 @@ module MagneticReadHead
using Base: invokelatest
using Cassette
using MacroTools
using Mocking
using OrderedCollections
using Revise: get_method, sigex2sigts, get_signature
using InteractiveUtils
using CodeTracking
# We don't use Revise, but if it isn't loaded CodeTracking has issues
using Revise: Revise

export @iron_debug

export set_breakpoint, rm_breakpoint, @iron_debug
include("utils.jl")
include("method_utils.jl")

include("breakpoint_rules.jl")
include("core_control.jl")
include("pass.jl")

include("utils.jl")
include("inner_repl.jl")
include("breakpoints.jl")
include("break_action.jl")

include("locate.jl")
include("breakpoints.jl")

struct UserAbortedException <: Exception end

mutable struct MagneticMetadata
eval_module::Module
do_at_next_break_start::Any
stepping_mode::Bool
end
MagneticMetadata(eval_module) = MagneticMetadata(eval_module, ()->nothing, false)

macro iron_debug(body)
quote
ctx = Cassette.disablehooks(MagneticCtx(;metadata=MagneticMetadata($(__module__))))
ctx = HandEvalCtx($(__module__), StepContinue())
try
Cassette.recurse(ctx, ()->$(esc(body)))
return Cassette.recurse(ctx, ()->$(esc(body)))
catch err
@show err
err isa UserAbortedException || rethrow()
nothing
finally
disengage_stepping_mode!(ctx)
# Disable any stepping left-over
ctx.metadata.stepping_mode = StepContinue()
end

end
Expand Down
108 changes: 61 additions & 47 deletions src/break_action.jl
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
noact(ctx) = nothing

set_stepping_mode!(mode) = metadata->metadata.stepping_mode=mode()
const actions = OrderedDict([
:CC => (desc="Continue", before_break=noact, after_break=noact),
:SI => (desc="Step In", before_break=engage_stepping_mode!, after_break=noact),
:SN => (desc="Step Next", before_break=noact, after_break=engage_stepping_mode!),
:XX => (desc="Abort", before_break=ctx->throw(UserAbortedException()), after_break=noact),
:CC => (desc="Continue", act=set_stepping_mode!(StepContinue)),
:SI => (desc="Step In", act=set_stepping_mode!(StepIn)),
:SN => (desc="Step Next", act=set_stepping_mode!(StepNext)),
:SO => (desc="Step Out", act=set_stepping_mode!(StepOut)),
:XX => (desc="Abort", act=metadata->throw(UserAbortedException())),
])

function print_commands()
Expand All @@ -14,61 +14,75 @@ function print_commands()
end
################################

function breadcrumbs(f, args)
meth = methods(f, typeof.(args)) |> only
function breadcrumbs(meth, statement_ind)
printstyled("\nBreakpoint Hit: "; color=:blue)
printstyled(string(meth); color=:light_blue)
#TODO: Translate the statement_ind into a line number
line_num = statement_ind2src_linenum(meth, statement_ind)
breadcrumbs(string(meth.file), line_num)
println()
end

function iron_repl(f, args, eval_module)
@mock breadcrumbs(f, args)

name2arg = argnames(f, args)

printstyled("Args: "; color=:light_yellow)
println(join(keys(name2arg), ", "))
print_commands()
function breadcrumbs(file::AbstractString, line_num; nbefore=2, nafter=2)
return breadcrumbs(stdout, file, line_num; nbefore=nbefore, nafter=nafter)
end

local code_ast
while true
code_ast = get_user_input()
if haskey(actions, code_ast)
return code_ast # Send the codeword back
end
code_ast = subnames(name2arg, code_ast)
eval_and_display(code_ast, eval_module)
function breadcrumbs(io, file::AbstractString, line_num; nbefore=2, nafter=2)
@assert(nbefore >= 0)
@assert(nafter >= 0)

all_lines = readlines(file)
first_line_num = max(1, line_num - nbefore)
last_line_num = min(length(all_lines), line_num + nafter)

for ln in first_line_num:last_line_num
line = all_lines[ln]
if ln == line_num
line = "" * line
color = :cyan
else
line = " " * line
color = :light_green
if ln (first_line_num, last_line_num)
color = :light_black
end
end
printstyled(io, line, "\n"; color=color)
end
end

##############################


# this function exists only for mocking so we can test it.
breakpoint_hit(meth, statement_ind) = nothing

function break_action(ctx, f, args...)
# This is effectively Cassette.overdub
# It is called by all breakpoint overdubs
function iron_repl(metadata::HandEvalMeta, meth, statement_ind)
breakpoint_hit(meth, statement_ind)
breadcrumbs(meth, statement_ind)

ctx.metadata.do_at_next_break_start() # Do anything we have queued
printstyled("Vars: "; color=:light_yellow)
println(join(keys(metadata.variables), ", "))
print_commands()

eval_module = ctx.metadata.eval_module

start_code_word = iron_repl(f, args, eval_module)
actions[start_code_word].before_break(ctx)
run_repl(metadata.variables, metadata.eval_module)
end

ans = Base.invokelatest(Cassette.recurse, ctx, f, args...)

actions[start_code_word].after_break(ctx)

return ans
end
"""
break_action(metadata, meth, statement_ind)
function do_not_break_action(ctx, f, args...)
ctx.metadata.do_at_next_break_start() # Do anything we have queued

if f isa Core.IntrinsicFunction
f(args...)
else
Base.invokelatest(Cassette.recurse, ctx, f, args...)
This determines what we should do when we hit a potential point to break at.
We check if we should actually break here,
and if so open up a REPL.
if not, then we continue.
"""
function break_action(metadata, meth, statement_ind)
if !(metadata.stepping_mode isa StepNext
|| should_breakon(metadata.breakpoint_rules, meth, statement_ind)
)
# Only break on StepNext and actual breakpoints
return
end
end

code_word = iron_repl(metadata, meth, statement_ind)
actions[code_word].act(metadata)
end
74 changes: 74 additions & 0 deletions src/breakpoint_rules.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
struct Rule{V}
v::V
statement_ind::Int
end
# No matter if a Function or a Method, default to break at start
Rule(v) = Rule(v, 0)

##############################################################################
# instrumenting rules
match(rule::Rule{Method}, method) = method == rule.v
match(rule::Rule{Module}, method) = moduleof(method) == rule.v
function match(rule::Rule{F}, method) where F
# This one is for functions
return functiontypeof(method) <: F
end

#breakon rules
function match(rule::Rule, method, statement_ind)
return match(rule, method) && rule.statement_ind == statement_ind
end


## The overall rule object
"""
BreakpointRules
Holds information about breakpoints,
to allow the descision of which methods to instrument with potential breakpoints,
and to decide which potential breakpoints in instrumented code to actually break on.
(When not already in stepping mode)
"""
mutable struct BreakpointRules
no_instrument_rules::Vector{Rule}
breakon_rules::Vector{Rule}
end
BreakpointRules() = BreakpointRules(Rule[], Rule[])


"""
should_instrument(rules, method)
Returns true if according to the rules, this method should be instrumented
with potential breakpoints.
The default is to instrument everything.
"""
function should_instrument(rules::BreakpointRules, method)
# If we are going to break on it, then definately instrument it
for rule in rules.breakon_rules
match(rule, method) && return true
end
# otherwise:
# if we have a rule saying not to instrument it then don't
for rule in rules.no_instrument_rules
match(rule, method) && return false
end
# otherwise: no rules of relevence found, so we instrument it.
return true
end

# This is a Core.Builtin, it has no methods, do not try and instrument it
should_instrument(::BreakpointRules, ::Nothing) = false

"""
should_breakon(rules, method, statement_ind)
Returns true if according to the rules, this IR statement index within this method
should be broken on.
I.e. if this point in the code has a breakpoint set.
"""
function should_breakon(rules::BreakpointRules, method, statement_ind)
return any(rules.breakon_rules) do rule
match(rule, method, statement_ind)
end
end
Loading

0 comments on commit 68d8e2c

Please sign in to comment.