Skip to content

Commit 70a4c95

Browse files
authored
Merge pull request #127 from JuliaDebug/kc/unwind_exc
Debug commands: implement unwinding of stack on thrown exception
2 parents b7542d2 + dd9b7e5 commit 70a4c95

File tree

3 files changed

+111
-27
lines changed

3 files changed

+111
-27
lines changed

src/commands.jl

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,24 @@ function maybe_reset_frame!(@nospecialize(recurse), frame::Frame, @nospecialize(
272272
return frame, pc
273273
end
274274

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+
275293
"""
276294
ret = debug_command(recurse, frame, cmd, rootistoplevel=false)
277295
ret = debug_command(frame, cmd, rootistoplevel=false)
@@ -294,40 +312,48 @@ or one of the 'advanced' commands
294312
Unlike other commands, the default setting for `recurse` is `Compiled()`.
295313
"""
296314
function debug_command(@nospecialize(recurse), frame::Frame, cmd::AbstractString, rootistoplevel::Bool=false)
297-
# ::Union{Val{:nc},Val{:n},Val{:se}},
298-
# Need try/catch
299315
istoplevel = rootistoplevel && frame.caller === nothing
300-
cmd == "nc" && return maybe_reset_frame!(recurse, frame, next_call!(recurse, frame, istoplevel), rootistoplevel)
301-
cmd == "n" && return maybe_reset_frame!(recurse, frame, next_line!(recurse, frame, istoplevel), rootistoplevel)
302316
if cmd == "si"
303317
stmt = pc_expr(frame)
304318
cmd = is_call(stmt) ? "s" : "se"
305319
end
306-
cmd == "se" && return maybe_reset_frame!(recurse, frame, step_expr!(recurse, frame, istoplevel), rootistoplevel)
307-
enter_generated = false
308-
if cmd == "sg"
309-
enter_generated = true
310-
cmd = "s"
311-
end
312-
if cmd == "s"
313-
pc = maybe_next_call!(recurse, frame, istoplevel)
314-
(isa(pc, BreakpointRef) || pc === nothing) && return maybe_reset_frame!(recurse, frame, pc, rootistoplevel)
315-
stmt0 = stmt = pc_expr(frame, pc)
316-
if isexpr(stmt, :(=))
317-
stmt = stmt.args[2]
320+
try
321+
cmd == "nc" && return maybe_reset_frame!(recurse, frame, next_call!(recurse, frame, istoplevel), rootistoplevel)
322+
cmd == "n" && return maybe_reset_frame!(recurse, frame, next_line!(recurse, frame, istoplevel), rootistoplevel)
323+
cmd == "se" && return maybe_reset_frame!(recurse, frame, step_expr!(recurse, frame, istoplevel), rootistoplevel)
324+
325+
enter_generated = false
326+
if cmd == "sg"
327+
enter_generated = true
328+
cmd = "s"
329+
end
330+
if cmd == "s"
331+
pc = maybe_next_call!(recurse, frame, istoplevel)
332+
(isa(pc, BreakpointRef) || pc === nothing) && return maybe_reset_frame!(recurse, frame, pc, rootistoplevel)
333+
stmt0 = stmt = pc_expr(frame, pc)
334+
if isexpr(stmt, :(=))
335+
stmt = stmt.args[2]
336+
end
337+
ret = evaluate_call!(dummy_breakpoint, frame, stmt; enter_generated=enter_generated)
338+
isa(ret, BreakpointRef) && return maybe_reset_frame!(recurse, frame, ret, rootistoplevel)
339+
maybe_assign!(frame, stmt0, ret)
340+
frame.pc = ret + 1
341+
return frame, frame.pc
342+
end
343+
if cmd == "c"
344+
r = root(frame)
345+
ret = finish_stack!(recurse, r, rootistoplevel)
346+
return isa(ret, BreakpointRef) ? (leaf(r), ret) : nothing
347+
end
348+
cmd == "finish" && return maybe_reset_frame!(recurse, frame, finish!(recurse, frame, istoplevel), rootistoplevel)
349+
catch err
350+
frame = unwind_exception(frame, err)
351+
if cmd == "c"
352+
return debug_command(recurse, frame, "c", istoplevel)
353+
else
354+
return debug_command(recurse, frame, "nc", istoplevel)
318355
end
319-
ret = evaluate_call!(dummy_breakpoint, frame, stmt; enter_generated=enter_generated)
320-
isa(ret, BreakpointRef) && return maybe_reset_frame!(recurse, frame, ret, rootistoplevel)
321-
maybe_assign!(frame, stmt0, ret)
322-
frame.pc = ret + 1
323-
return frame, frame.pc
324-
end
325-
if cmd == "c"
326-
r = root(frame)
327-
ret = finish_stack!(recurse, r, rootistoplevel)
328-
return isa(ret, BreakpointRef) ? (leaf(r), ret) : nothing
329356
end
330-
cmd == "finish" && return maybe_reset_frame!(recurse, frame, finish!(recurse, frame, istoplevel), rootistoplevel)
331357
throw(ArgumentError("command $cmd not recognized"))
332358
end
333359
debug_command(frame::Frame, cmd::AbstractString, rootistoplevel::Bool=false) =

test/debug.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,52 @@ end
128128
@test get_return(frame) == 3
129129
end
130130

131+
@testset "Exceptions" begin
132+
# Don't break on caught exceptions
133+
err_caught = Any[nothing]
134+
function f_exc_outer()
135+
try
136+
f_exc_inner()
137+
catch err;
138+
err_caught[1] = err
139+
end
140+
x = 1 + 1
141+
return x
142+
end
143+
f_exc_inner() = error()
144+
fr = JuliaInterpreter.enter_call(f_exc_outer)
145+
fr, pc = debug_command(fr, "s")
146+
fr, pc = debug_command(fr, "n")
147+
fr, pc = debug_command(fr, "n")
148+
debug_command(fr, "finish")
149+
@test get_return(fr) == 2
150+
@test first(err_caught) isa ErrorException
151+
@test stacklength(fr) == 1
152+
153+
err_caught = Any[nothing]
154+
fr = JuliaInterpreter.enter_call(f_exc_outer)
155+
fr, pc = debug_command(fr, "s")
156+
debug_command(fr, "c")
157+
@test get_return(root(fr)) == 2
158+
@test first(err_caught) isa ErrorException
159+
@test stacklength(root(fr)) == 1
160+
161+
# Rethrow on uncaught exceptions
162+
f_outer() = g_inner()
163+
g_inner() = error()
164+
fr = JuliaInterpreter.enter_call(f_outer)
165+
@test_throws ErrorException debug_command(fr, "finish")
166+
@test stacklength(fr) == 1
167+
168+
# Break on error
169+
try
170+
JuliaInterpreter.break_on_error[] = true
171+
fr = JuliaInterpreter.enter_call(f_outer)
172+
fr, pc = debug_command(JuliaInterpreter.finish_and_return!, fr, "finish")
173+
@test fr.framecode.scope.name == :error
174+
finally
175+
JuliaInterpreter.break_on_error[] = false
176+
end
177+
end
178+
131179
# end

test/utils.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ using JuliaInterpreter: finish_and_return!, evaluate_call!, step_expr!, shouldbr
66
using Base.Meta: isexpr
77
using Test, Random, SHA
88

9+
function stacklength(frame)
10+
n = 1
11+
frame = frame.callee
12+
while frame !== nothing
13+
n += 1
14+
frame = frame.callee
15+
end
16+
return n
17+
end
18+
919
# Execute a frame using Julia's regular compiled-code dispatch for any :call expressions
1020
runframe(frame) = Some{Any}(finish_and_return!(Compiled(), frame))
1121

0 commit comments

Comments
 (0)