Skip to content

Commit 9191196

Browse files
authored
Merge pull request #126 from JuliaDebug/teh/debugging
Implement core debug API here instead of Debugger.jl
2 parents 7508239 + ba1f4f5 commit 9191196

14 files changed

+535
-55
lines changed

docs/src/dev_reference.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ JuliaInterpreter.evaluate_call!
4343
JuliaInterpreter.evaluate_foreigncall
4444
JuliaInterpreter.maybe_evaluate_builtin
4545
JuliaInterpreter.maybe_next_call!
46+
JuliaInterpreter.next_line!
47+
JuliaInterpreter.next_call!
48+
JuliaInterpreter.maybe_reset_frame!
49+
JuliaInterpreter.maybe_step_through_wrapper!
4650
JuliaInterpreter.handle_err
51+
JuliaInterpreter.debug_command
4752
```
4853

4954
## Breakpoints

src/JuliaInterpreter.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ using InteractiveUtils
1313
using CodeTracking
1414

1515
export @interpret, Compiled, Frame, root, leaf,
16-
BreakpointRef, breakpoint, @breakpoint, breakpoints, enable, disable, remove
16+
BreakpointRef, breakpoint, @breakpoint, breakpoints, enable, disable, remove,
17+
debug_command
1718

1819
module CompiledCalls
1920
# This module is for handling intrinsics that must be compiled (llvmcall)
@@ -48,6 +49,8 @@ function set_compiled_methods()
4849
push!(compiled_methods, which(flush, Tuple{IOStream}))
4950
push!(compiled_methods, which(disable_sigint, Tuple{Function}))
5051
push!(compiled_methods, which(reenable_sigint, Tuple{Function}))
52+
# Signal-handling in the `print` dispatch hierarchy
53+
push!(compiled_methods, which(Base.unsafe_write, Tuple{Base.LibuvStream, Ptr{UInt8}, UInt}))
5154
end
5255

5356
function __init__()

src/breakpoints.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ function breakpoint(filename::AbstractString, line::Integer, args...)
217217
offset = line1 - method.line
218218
src = JuliaInterpreter.get_source(method)
219219
lastline = src.linetable[end]
220-
if lastline.line + offset >= line
220+
if getline(lastline) + offset >= line
221221
return breakpoint(method, line, args...)
222222
end
223223
end

src/commands.jl

Lines changed: 201 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""
2-
ret = finish!(recurse, frame, istoplevel=false)
3-
ret = finish!(frame, istoplevel=false)
2+
pc = finish!(recurse, frame, istoplevel=false)
3+
pc = finish!(frame, istoplevel=false)
44
5-
Run `frame` until execution terminates. `ret` is either `nothing` (if execution terminates
5+
Run `frame` until execution terminates. `pc` is either `nothing` (if execution terminates
66
when it hits a `return` statement) or a reference to a breakpoint.
77
In the latter case, `leaf(frame)` returns the frame in which it hit the breakpoint.
88
@@ -22,8 +22,8 @@ finish!(frame::Frame, istoplevel::Bool=false) = finish!(finish_and_return!, fram
2222
ret = finish_and_return!(recurse, frame, istoplevel::Bool=false)
2323
ret = finish_and_return!(frame, istoplevel::Bool=false)
2424
25-
Call [`JuliaInterpreter.finish!`](@ref) and pass back the return value. If execution
26-
pauses at a breakpoint, the reference to the breakpoint is returned.
25+
Call [`JuliaInterpreter.finish!`](@ref) and pass back the return value `ret`. If execution
26+
pauses at a breakpoint, `ret` is the reference to the breakpoint.
2727
"""
2828
function finish_and_return!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false)
2929
pc = finish!(recurse, frame, istoplevel)
@@ -33,26 +33,29 @@ end
3333
finish_and_return!(frame::Frame, istoplevel::Bool=false) = finish_and_return!(finish_and_return!, frame, istoplevel)
3434

3535
"""
36-
bpref = dummy_breakpoint(recurse, frame::Frame)
36+
bpref = dummy_breakpoint(recurse, frame::Frame, istoplevel)
3737
38-
Return a fake breakpoint. This can be useful as the `recurse` argument to `evaluate_call!`
39-
(or any of the higher-order commands) to ensure that you return immediately after stepping
40-
into a call.
38+
Return a fake breakpoint. `dummy_breakpoint` can be useful as the `recurse` argument to
39+
`evaluate_call!` (or any of the higher-order commands) to ensure that you return immediately
40+
after stepping into a call.
4141
"""
42-
dummy_breakpoint(@nospecialize(recurse), frame::Frame) = BreakpointRef(frame.framecode, 0)
42+
dummy_breakpoint(@nospecialize(recurse), frame::Frame, istoplevel) = BreakpointRef(frame.framecode, 0)
4343

4444
"""
45-
ret = finish_stack!(recurse, frame, istoplevel=false)
46-
ret = finish_stack!(frame, istoplevel=false)
45+
ret = finish_stack!(recurse, frame, rootistoplevel=false)
46+
ret = finish_stack!(frame, rootistoplevel=false)
4747
4848
Unwind the callees of `frame`, finishing each before returning to the caller.
49-
`frame` itself is also finished
50-
If execution hits a breakpoint, `ret` will be a reference to the breakpoint.
49+
`frame` itself is also finished. `rootistoplevel` should be true if the root frame is top-level.
50+
51+
`ret` is typically the returned value. If execution hits a breakpoint, `ret` will be a
52+
reference to the breakpoint.
5153
"""
52-
function finish_stack!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false)
54+
function finish_stack!(@nospecialize(recurse), frame::Frame, rootistoplevel::Bool=false)
5355
frame0 = frame
5456
frame = leaf(frame)
5557
while true
58+
istoplevel = rootistoplevel && frame.caller === nothing
5659
ret = finish_and_return!(recurse, frame, istoplevel)
5760
isa(ret, BreakpointRef) && return ret
5861
frame === frame0 && return ret
@@ -93,14 +96,22 @@ end
9396
next_until!(predicate, frame::Frame, istoplevel::Bool=false) =
9497
next_until!(predicate, finish_and_return!, frame, istoplevel)
9598

99+
"""
100+
pc = next_call!(recurse, frame, istoplevel=false)
101+
pc = next_call!(frame, istoplevel=false)
102+
103+
Execute the current statement. Continue stepping through `frame` until the next
104+
`:return` or `:call` expression.
105+
"""
96106
next_call!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false) =
97107
next_until!(is_call_or_return, recurse, frame, istoplevel)
98108
next_call!(frame::Frame, istoplevel::Bool=false) = next_call!(finish_and_return!, frame, istoplevel)
99109

100110
"""
101-
maybe_next_call!(predicate, frame, istoplevel=false)
111+
pc = maybe_next_call!(recurse, frame, istoplevel=false)
112+
pc = maybe_next_call!(frame, istoplevel=false)
102113
103-
Return the current statement of `frame` if it is a `:return` or `:call` expression.
114+
Return the current program counter of `frame` if it is a `:return` or `:call` expression.
104115
Otherwise, step through the statements of `frame` until the next `:return` or `:call` expression.
105116
"""
106117
function maybe_next_call!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false)
@@ -112,8 +123,8 @@ maybe_next_call!(frame::Frame, istoplevel::Bool=false) =
112123
maybe_next_call!(finish_and_return!, frame, istoplevel)
113124

114125
"""
115-
through_methoddef_or_done!(recurse, frame)
116-
through_methoddef_or_done!(frame)
126+
pc = through_methoddef_or_done!(recurse, frame)
127+
pc = through_methoddef_or_done!(frame)
117128
118129
Runs `frame` at top level until it either finishes (e.g., hits a `return` statement)
119130
or defines a new method.
@@ -144,11 +155,20 @@ function changed_line!(expr, line, fls)
144155
end
145156
end
146157

158+
"""
159+
pc = next_line!(recurse, frame, istoplevel=false)
160+
pc = next_line!(frame, istoplevel=false)
161+
162+
Execute until reaching the first call of the next line of the source code.
163+
Upon return, `pc` is either the new program counter, `nothing` if a `return` is reached,
164+
or a `BreakpointRef` if it encountered a wrapper call. In the latter case, call `leaf(frame)`
165+
to obtain the new execution frame.
166+
"""
147167
function next_line!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false)
148-
initial = linenumber(frame)
149-
first = true
150168
pc = frame.pc
151-
while linenumber(frame, pc) == initial
169+
initialline, initialfile = linenumber(frame, pc), getfile(frame, pc)
170+
first = true
171+
while linenumber(frame, pc) == initialline && getfile(frame, pc) == initialfile
152172
# If this is a return node, interrupt execution
153173
expr = pc_expr(frame, pc)
154174
(!first && isexpr(expr, :return)) && return pc
@@ -177,13 +197,170 @@ function next_line!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false
177197
expr = pc_expr(frame)
178198
end
179199
# Signal that we've switched frames
180-
switched && return BreakpointRef(frame.framecode, frame.pc)
200+
if switched
201+
pc = next_line!(recurse, frame, false)
202+
pc === nothing && error("confusing next_line!")
203+
lframe = leaf(frame)
204+
return isa(pc, BreakpointRef) ? pc : BreakpointRef(lframe.framecode, lframe.pc)
205+
end
181206
else
182207
pc = step_expr!(recurse, frame, istoplevel)
183208
(pc === nothing || isa(pc, BreakpointRef)) && return pc
184209
end
185210
shouldbreak(frame, pc) && return BreakpointRef(frame.framecode, pc)
186211
end
187-
maybe_next_call!(recurse, frame, pc)
212+
maybe_next_call!(recurse, frame, istoplevel)
188213
end
189214
next_line!(frame::Frame, istoplevel::Bool=false) = next_line!(finish_and_return!, frame, istoplevel)
215+
216+
"""
217+
cframe = maybe_step_through_wrapper!(recurse, frame)
218+
cframe = maybe_step_through_wrapper!(frame)
219+
220+
Return the new frame of execution, potentially stepping through "wrapper" methods like those
221+
that supply default positional arguments or handle keywords. `cframe` is the leaf frame from
222+
which execution should start.
223+
"""
224+
function maybe_step_through_wrapper!(@nospecialize(recurse), frame::Frame)
225+
code = frame.framecode
226+
stmts, scope = code.src.code, code.scope::Method
227+
length(stmts) < 2 && return frame
228+
last = stmts[end-1]
229+
isexpr(last, :(=)) && (last = last.args[2])
230+
is_kw = isa(scope, Method) && startswith(String(Base.unwrap_unionall(scope.sig).parameters[1].name.name), "#kw")
231+
if is_kw || isexpr(last, :call) && any(x->x==Core.SlotNumber(1), last.args)
232+
# If the last expr calls #self# or passes it to an implementation method,
233+
# this is a wrapper function that we might want to step through
234+
while frame.pc != length(stmts)-1
235+
pc = next_call!(recurse, frame, false) # since we're in a Method we're not at toplevel
236+
end
237+
ret = evaluate_call!(dummy_breakpoint, frame, last)
238+
@assert isa(ret, BreakpointRef)
239+
return maybe_step_through_wrapper!(recurse, callee(frame))
240+
end
241+
return frame
242+
end
243+
maybe_step_through_wrapper!(frame::Frame) = maybe_step_through_wrapper!(finish_and_return!, frame)
244+
245+
"""
246+
ret = maybe_reset_frame!(recurse, frame, pc, rootistoplevel)
247+
248+
Perform a return to the caller, or descend to the level of a breakpoint.
249+
`pc` is the return state from the previous command (e.g., `next_call!` or similar).
250+
`rootistoplevel` should be true if the root frame is top-level.
251+
252+
`ret` will be `nothing` if we have just completed a top-level frame. Otherwise,
253+
254+
cframe, cpc = ret
255+
256+
where `cframe` is the frame from which execution should continue and `cpc` is the state
257+
of `cframe` (the program counter, a `BreakpointRef`, or `nothing`).
258+
"""
259+
function maybe_reset_frame!(@nospecialize(recurse), frame::Frame, @nospecialize(pc), rootistoplevel::Bool)
260+
isa(pc, BreakpointRef) && return leaf(frame), pc
261+
if pc === nothing
262+
val = get_return(frame)
263+
recycle(frame)
264+
frame = caller(frame)
265+
frame === nothing && return nothing
266+
frame.callee = nothing
267+
maybe_assign!(frame, val)
268+
frame.pc += 1
269+
pc = maybe_next_call!(recurse, frame, rootistoplevel && frame.caller===nothing)
270+
return maybe_reset_frame!(recurse, frame, pc, rootistoplevel)
271+
end
272+
return frame, pc
273+
end
274+
275+
# Unwind the stack until an exc is eventually caught, thereby
276+
# returning the frame that caught the exception at the pc of the catch
277+
# or rethrow the error
278+
function unwind_exception(frame::Frame, exc)
279+
while frame !== nothing
280+
if !isempty(frame.framedata.exception_frames)
281+
# Exception caught
282+
frame.pc = frame.framedata.exception_frames[end]
283+
frame.framedata.last_exception[] = exc
284+
return frame
285+
end
286+
recycle(frame)
287+
frame = caller(frame)
288+
frame === nothing || (frame.callee = nothing)
289+
end
290+
rethrow(exc)
291+
end
292+
293+
"""
294+
ret = debug_command(recurse, frame, cmd, rootistoplevel=false)
295+
ret = debug_command(frame, cmd, rootistoplevel=false)
296+
297+
Perform one "debugger" command. `cmd` should be one of:
298+
299+
- "n": advance to the next line
300+
- "s": step into the next call
301+
- "c": continue execution until termination or reaching a breakpoint
302+
- "finish": finish the current frame and return to the parent
303+
304+
or one of the 'advanced' commands
305+
306+
- "nc": step forward to the next call
307+
- "se": execute a single statement
308+
- "si": execute a single statement, stepping in if it's a call
309+
- "sg": step into the generator of a generated function
310+
311+
`rootistoplevel` and `ret` are as described for [`JuliaInterpreter.maybe_reset_frame!`](@ref).
312+
"""
313+
function debug_command(@nospecialize(recurse), frame::Frame, cmd::AbstractString, rootistoplevel::Bool=false)
314+
istoplevel = rootistoplevel && frame.caller === nothing
315+
if cmd == "si"
316+
stmt = pc_expr(frame)
317+
cmd = is_call(stmt) ? "s" : "se"
318+
end
319+
try
320+
cmd == "nc" && return maybe_reset_frame!(recurse, frame, next_call!(recurse, frame, istoplevel), rootistoplevel)
321+
cmd == "n" && return maybe_reset_frame!(recurse, frame, next_line!(recurse, frame, istoplevel), rootistoplevel)
322+
cmd == "se" && return maybe_reset_frame!(recurse, frame, step_expr!(recurse, frame, istoplevel), rootistoplevel)
323+
324+
enter_generated = false
325+
if cmd == "sg"
326+
enter_generated = true
327+
cmd = "s"
328+
end
329+
if cmd == "s"
330+
pc = maybe_next_call!(recurse, frame, istoplevel)
331+
(isa(pc, BreakpointRef) || pc === nothing) && return maybe_reset_frame!(recurse, frame, pc, rootistoplevel)
332+
stmt0 = stmt = pc_expr(frame, pc)
333+
isexpr(stmt0, :return) && return maybe_reset_frame!(recurse, frame, nothing, rootistoplevel)
334+
if isexpr(stmt, :(=))
335+
stmt = stmt.args[2]
336+
end
337+
local ret
338+
try
339+
ret = evaluate_call!(dummy_breakpoint, frame, stmt; enter_generated=enter_generated)
340+
catch err
341+
ret = handle_err(recurse, frame, err)
342+
return isa(ret, BreakpointRef) ? (leaf(frame), ret) : ret
343+
end
344+
isa(ret, BreakpointRef) && return maybe_reset_frame!(recurse, frame, ret, rootistoplevel)
345+
maybe_assign!(frame, stmt0, ret)
346+
frame.pc += 1
347+
return frame, frame.pc
348+
end
349+
if cmd == "c"
350+
r = root(frame)
351+
ret = finish_stack!(recurse, r, rootistoplevel)
352+
return isa(ret, BreakpointRef) ? (leaf(r), ret) : nothing
353+
end
354+
cmd == "finish" && return maybe_reset_frame!(recurse, frame, finish!(recurse, frame, istoplevel), rootistoplevel)
355+
catch err
356+
frame = unwind_exception(frame, err)
357+
if cmd == "c"
358+
return debug_command(recurse, frame, "c", istoplevel)
359+
else
360+
return debug_command(recurse, frame, "nc", istoplevel)
361+
end
362+
end
363+
throw(ArgumentError("command $cmd not recognized"))
364+
end
365+
debug_command(frame::Frame, cmd::AbstractString, rootistoplevel::Bool=false) =
366+
debug_command(finish_and_return!, frame, cmd, rootistoplevel)

src/construct.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ rather than recursed into via the interpreter.
2020
"""
2121
const compiled_methods = Set{Method}()
2222

23-
const junk = FrameData[] # to allow re-use of allocated memory (this is otherwise a bottleneck)
24-
recycle(frame) = push!(junk, frame.framedata)
23+
const junk = Base.IdSet{FrameData}() # to allow re-use of allocated memory (this is otherwise a bottleneck)
24+
recycle(frame) = push!(junk, frame.framedata) # using an IdSet ensures that a frame can't be added twice
2525

2626
const empty_svec = Core.svec()
2727

@@ -238,7 +238,8 @@ function prepare_framedata(framecode, argvals::Vector{Any})
238238
ng = isa(ssavt, Int) ? ssavt : length(ssavt::Vector{Any})
239239
nargs = length(argvals)
240240
if !isempty(junk)
241-
olddata = pop!(junk)
241+
olddata = first(junk)
242+
delete!(junk, olddata)
242243
locals, ssavalues, sparams = olddata.locals, olddata.ssavalues, olddata.sparams
243244
exception_frames, last_reference = olddata.exception_frames, olddata.last_reference
244245
last_exception = olddata.last_exception

0 commit comments

Comments
 (0)