diff --git a/src/Debugger.jl b/src/Debugger.jl index 2e385b8..d6cee72 100644 --- a/src/Debugger.jl +++ b/src/Debugger.jl @@ -42,6 +42,8 @@ Base.@kwdef mutable struct DebuggerState frame::Union{Nothing, Frame} level::Int = 1 broke_on_error::Bool = false + # The exception the session stopped for (see `break_on`), while `broke_on_error` + error_stopped_for = nothing watch_list::Vector = WATCH_LIST lowered_status::Bool = false interp::Interpreter = RecursiveInterpreter() diff --git a/src/commands.jl b/src/commands.jl index 561d37d..1ea13d4 100644 --- a/src/commands.jl +++ b/src/commands.jl @@ -11,6 +11,15 @@ function assert_allow_step(state) return true end +# Whether an exception thrown in `frame` is caught by it or by one of its callers +function is_caught_in_frames(frame::Union{Nothing,Frame}) + while frame !== nothing + isempty(frame.framedata.exception_frames) || return true + frame = caller(frame) + end + return false +end + invalid_command(state, cmd) = execute_command(state, Val(:_), cmd) # print error function show_breakpoint(io::IO, bp::BreakpointRef, state::DebuggerState) @@ -40,6 +49,15 @@ function execute_command(state::DebuggerState, v::Union{Val{:c},Val{:nc},Val{:n} kwargs = Dict(:line => line) end end + if state.broke_on_error && state.level == 1 && is_caught_in_frames(state.frame) + # Stopped for an exception that an interpreted frame catches (`break_on(:throw)`): + # resume by unwinding into the handler. `c` continues from there; the other + # commands stop at the start of the `catch` block. + state.frame = JuliaInterpreter.unwind_exception(state.frame, state.error_stopped_for) + state.broke_on_error = false + state.error_stopped_for = nothing + cmd == "c" || return true + end assert_allow_step(state) || return false cmd == "so" && (cmd = "finish") cmd == "u" && (cmd = "until") @@ -58,6 +76,7 @@ function execute_command(state::DebuggerState, v::Union{Val{:c},Val{:nc},Val{:n} end if pc.err !== nothing state.broke_on_error = true + state.error_stopped_for = pc.err end end if pc_expr(state.frame) === nothing # happens when stopping for @bp diff --git a/test/misc.jl b/test/misc.jl index 2c758c2..3e1bf92 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -501,3 +501,60 @@ end Debugger.set_highlight(prev_highlight) end end + +function f_caught_throws(xs) + n = 0 + for x in xs + v = try + parse(Int, x) + catch + -1 + end + n += v + end + return n +end +f_uncaught_throw(x) = parse(Int, x) + +# With `break_on(:throw)`, stopping for an exception that the code catches must +# not end the session: stepping resumes in the `catch` block +@testset "continue after breaking on a caught throw" begin + try + JuliaInterpreter.break_on(:throw) + state = dummy_state(JuliaInterpreter.enter_call(f_caught_throws, ["a", "1", "b"])) + execute_command(state, Val(:c), "c") + @test state.broke_on_error + @test occursin("'a'", sprint(showerror, state.error_stopped_for)) + # `c` continues past the handler to the next throw + @test execute_command(state, Val(:c), "c") + @test state.broke_on_error + @test occursin("'b'", sprint(showerror, state.error_stopped_for)) + # `n` stops in the `catch` block of the catching frame + @test execute_command(state, Val(:n), "n") + @test !state.broke_on_error + @test state.frame.framecode.scope.name === :f_caught_throws + @test JuliaInterpreter.caller(state.frame) === nothing + execute_command(state, Val(:c), "c") + @test state.frame === nothing + @test state.overall_result == -1 + finally + JuliaInterpreter.break_off(:throw) + end +end + +@testset "no stepping after breaking on an uncaught error" begin + try + JuliaInterpreter.break_on(:error) + state = dummy_state(JuliaInterpreter.enter_call(f_uncaught_throw, "a")) + execute_command(state, Val(:c), "c") + @test state.broke_on_error + frame = state.frame + @test !redirect_stderr(devnull) do + execute_command(state, Val(:c), "c") + end + @test state.broke_on_error + @test state.frame === frame + finally + JuliaInterpreter.break_off(:error) + end +end