Skip to content

Commit

Permalink
Exception stack API refinements
Browse files Browse the repository at this point in the history
* Rename `catch_stack()` to the more descriptive name `current_exceptions()`.
* Introduce an ExceptionStack as the return type for the function.

Having ExceptionStack gives us a place to integrate exception printing
in a natural way. In the same way this should be useful for dispatch in
other areas of the ecosystem which want to dispatch on exception stacks.
  • Loading branch information
c42f committed Dec 13, 2019
1 parent 7bc3742 commit ff25fee
Show file tree
Hide file tree
Showing 16 changed files with 108 additions and 85 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ Standard library changes

* Sets are now displayed less compactly in the REPL, as a column of elements, like vectors
and dictionaries ([#33300]).
* The experimental function `Base.catch_stack()` has been renamed to
`current_exceptions()` and is now exported from Base. It has also been given
a more precise return type.

#### Libdl

Expand Down
10 changes: 5 additions & 5 deletions base/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,12 @@ function display_error(io::IO, er, bt)
showerror(IOContext(io, :limit => true), er, bt, backtrace = bt!==nothing)
println(io)
end
function display_error(io::IO, stack::Vector)
function display_error(io::IO, stack::ExceptionStack)
printstyled(io, "ERROR: "; bold=true, color=Base.error_color())
bt = Any[ (x[1], scrub_repl_backtrace(x[2])) for x in stack ]
show_exception_stack(IOContext(io, :limit => true), bt)
end
display_error(stack::Vector) = display_error(stderr, stack)
display_error(stack::ExceptionStack) = display_error(stderr, stack)
display_error(er, bt=nothing) = display_error(stderr, er, bt)

function eval_user_input(errio, @nospecialize(ast), show_value::Bool)
Expand Down Expand Up @@ -141,7 +141,7 @@ function eval_user_input(errio, @nospecialize(ast), show_value::Bool)
@error "SYSTEM: display_error(errio, lasterr) caused an error"
end
errcount += 1
lasterr = catch_stack()
lasterr = current_exceptions()
if errcount > 2
@error "It is likely that something important is broken, and Julia will not be able to continue normally" errcount
break
Expand Down Expand Up @@ -287,7 +287,7 @@ function exec_options(opts)
try
include(Main, PROGRAM_FILE)
catch
invokelatest(display_error, catch_stack())
invokelatest(display_error, current_exceptions())
if !is_interactive
exit(1)
end
Expand Down Expand Up @@ -483,7 +483,7 @@ function _start()
try
exec_options(JLOptions())
catch
invokelatest(display_error, catch_stack())
invokelatest(display_error, current_exceptions())
exit(1)
end
if is_interactive && have_color
Expand Down
2 changes: 2 additions & 0 deletions base/deprecated.jl
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,6 @@ MPFR.BigFloat(x::Real, prec::Int, rounding::RoundingMode) = BigFloat(x, rounding
Base.@deprecate_binding Mutex ReentrantLock
end

@deprecate catch_stack(task=current_task(); include_bt=true) current_exceptions(task; backtrace=include_bt) false

# END 1.3 deprecations
34 changes: 20 additions & 14 deletions base/error.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ exception will continue propagation as if it had not been caught.
the program state at the time of the error so you're encouraged to instead
throw a new exception using `throw(e)`. In Julia 1.1 and above, using
`throw(e)` will preserve the root cause exception on the stack, as
described in [`catch_stack`](@ref).
described in [`current_exceptions`](@ref).
"""
rethrow() = ccall(:jl_rethrow, Bottom, ())
rethrow(e) = ccall(:jl_rethrow_other, Bottom, (Any,), e)
Expand Down Expand Up @@ -123,32 +123,38 @@ function catch_backtrace()
return _reformat_bt(bt::Vector{Ptr{Cvoid}}, bt2::Vector{Any})
end

struct ExceptionStack <: AbstractArray{Any,1}
stack
end

"""
catch_stack(task=current_task(); [inclue_bt=true])
current_exceptions(task=current_task(); [inclue_bt=true])
Get the stack of exceptions currently being handled. For nested catch blocks
there may be more than one current exception in which case the most recently
thrown exception is last in the stack. The stack is returned as a Vector of
`(exception,backtrace)` pairs, or a Vector of exceptions if `include_bt` is
false.
thrown exception is last in the stack. The stack is returned as an
`ExceptionStack` which is an AbstractVector of named tuples
`(exception,backtrace)`. If `backtrace` is false, the backtrace in each pair
will be set to `nothing`.
Explicitly passing `task` will return the current exception stack on an
arbitrary task. This is useful for inspecting tasks which have failed due to
uncaught exceptions.
!!! compat "Julia 1.1"
This function is experimental in Julia 1.1 and will likely be renamed in a
future release (see https://github.com/JuliaLang/julia/pull/29901).
!!! compat "Julia 1.4"
This function went by the experiemental name `catch_stack()` in Julia
1.1–1.3, and had a plain Vector as a return type.
"""
function catch_stack(task=current_task(); include_bt=true)
raw = ccall(:jl_get_excstack, Any, (Any,Cint,Cint), task, include_bt, typemax(Cint))
function current_exceptions(task=current_task(); backtrace=true)
raw = ccall(:jl_get_excstack, Any, (Any,Cint,Cint), task, backtrace, typemax(Cint))
formatted = Any[]
stride = include_bt ? 3 : 1
stride = backtrace ? 3 : 1
for i = reverse(1:stride:length(raw))
e = raw[i]
push!(formatted, include_bt ? (e,Base._reformat_bt(raw[i+1],raw[i+2])) : e)
exc = raw[i]
bt = backtrace ? Base._reformat_bt(raw[i+1],raw[i+2]) : nothing
push!(formatted, (exception=exc,backtrace=bt))
end
formatted
ExceptionStack(formatted)
end

## keyword arg lowering generates calls to this ##
Expand Down
12 changes: 11 additions & 1 deletion base/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ function process_backtrace(t::Vector, limit::Int=typemax(Int); skipC = true)
return ret
end

function show_exception_stack(io::IO, stack::Vector)
function show_exception_stack(io::IO, stack)
# Display exception stack with the top of the stack first. This ordering
# means that the user doesn't have to scroll up in the REPL to discover the
# root cause.
Expand All @@ -717,3 +717,13 @@ function show(io::IO, ip::InterpreterIP)
end
end

size(s::ExceptionStack) = size(s.stack)
getindex(s::ExceptionStack, i::Int) = s.stack[i]

function show(io::IO, ::MIME"text/plain", stack::ExceptionStack)
nexc = length(stack)
printstyled(io, nexc, "-element ExceptionStack", nexc == 0 ? "" : ":\n")
show_exception_stack(io, stack)
end
show(io::IO, stack::ExceptionStack) = show(io, MIME("text/plain"), stack)

1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,7 @@ export
# errors
backtrace,
catch_backtrace,
current_exceptions,
error,
rethrow,
retry,
Expand Down
2 changes: 1 addition & 1 deletion doc/src/base/base.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ Core.throw
Base.rethrow
Base.backtrace
Base.catch_backtrace
Base.catch_stack
Base.current_exceptions
Base.@assert
Base.ArgumentError
Base.AssertionError
Expand Down
2 changes: 1 addition & 1 deletion doc/src/manual/control-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ The power of the `try/catch` construct lies in the ability to unwind a deeply ne
immediately to a much higher level in the stack of calling functions. There are situations where
no error has occurred, but the ability to unwind the stack and pass a value to a higher level
is desirable. Julia provides the [`rethrow`](@ref), [`backtrace`](@ref), [`catch_backtrace`](@ref)
and [`Base.catch_stack`](@ref) functions for more advanced error handling.
and [`current_exceptions`](@ref) functions for more advanced error handling.

### `finally` Clauses

Expand Down
8 changes: 4 additions & 4 deletions doc/src/manual/stacktraces.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ ERROR: Whoops!
[...]
```

## Exception stacks and `catch_stack`
## Exception stacks and [`current_exceptions`](@ref)

!!! compat "Julia 1.1"
Exception stacks requires at least Julia 1.1.
Expand All @@ -197,7 +197,7 @@ identify the root cause of a problem. The julia runtime supports this by pushing
*exception stack* as it occurs. When the code exits a `catch` normally, any exceptions which were pushed onto the stack
in the associated `try` are considered to be successfully handled and are removed from the stack.

The stack of current exceptions can be accessed using the experimental [`Base.catch_stack`](@ref) function. For example,
The stack of current exceptions can be accessed using the [`current_exceptions`](@ref) function. For example,

```julia-repl
julia> try
Expand All @@ -206,7 +206,7 @@ julia> try
try
error("(B) An exception while handling the exception")
catch
for (exc, bt) in Base.catch_stack()
for (exc, bt) in current_exceptions()
showerror(stdout, exc, bt)
println()
end
Expand Down Expand Up @@ -235,7 +235,7 @@ exiting both catch blocks normally (i.e., without throwing a further exception)
and are no longer accessible.

The exception stack is stored on the `Task` where the exceptions occurred. When a task fails with uncaught exceptions,
`catch_stack(task)` may be used to inspect the exception stack for that task.
`current_exceptions(task)` may be used to inspect the exception stack for that task.

## Comparison with [`backtrace`](@ref)

Expand Down
2 changes: 1 addition & 1 deletion src/stackwalk.c
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ JL_DLLEXPORT jl_value_t *jl_get_backtrace(void)
// interleaved.
JL_DLLEXPORT jl_value_t *jl_get_excstack(jl_task_t* task, int include_bt, int max_entries)
{
JL_TYPECHK(catch_stack, task, (jl_value_t*)task);
JL_TYPECHK(current_exceptions, task, (jl_value_t*)task);
jl_ptls_t ptls = jl_get_ptls_states();
if (task != ptls->current_task &&
task->state != failed_sym && task->state != done_sym) {
Expand Down
13 changes: 6 additions & 7 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import Base:
display,
show,
AnyDict,
==,
catch_stack
==


include("Terminals.jl")
Expand Down Expand Up @@ -95,7 +94,7 @@ function eval_user_input(@nospecialize(ast), backend::REPLBackend)
println("SYSTEM ERROR: Failed to report error to REPL frontend")
println(err)
end
lasterr = catch_stack()
lasterr = current_exceptions()
end
end
Base.sigatomic_end()
Expand Down Expand Up @@ -174,7 +173,7 @@ function print_response(errio::IO, @nospecialize(response), show_value::Bool, ha
println(errio) # an error during printing is likely to leave us mid-line
println(errio, "SYSTEM (REPL): showing an error caused an error")
try
Base.invokelatest(Base.display_error, errio, catch_stack())
Base.invokelatest(Base.display_error, errio, current_exceptions())
catch e
# at this point, only print the name of the type as a Symbol to
# minimize the possibility of further errors.
Expand All @@ -184,7 +183,7 @@ function print_response(errio::IO, @nospecialize(response), show_value::Bool, ha
end
break
end
val = catch_stack()
val = current_exceptions()
iserr = true
end
end
Expand Down Expand Up @@ -724,7 +723,7 @@ function respond(f, repl, main; pass_empty = false)
ast = Base.invokelatest(f, line)
response = eval_with_backend(ast, backend(repl))
catch
response = (catch_stack(), true)
response = (current_exceptions(), true)
end
print_response(repl, response, !ends_with_semicolon(line), Base.have_color)
end
Expand Down Expand Up @@ -873,7 +872,7 @@ function setup_interface(
end
hist_from_file(hp, f, hist_path)
catch
print_response(repl, (catch_stack(),true), true, Base.have_color)
print_response(repl, (current_exceptions(),true), true, Base.have_color)
println(outstream(repl))
@info "Disabling history file for this session"
repl.history_file = false
Expand Down
2 changes: 1 addition & 1 deletion stdlib/REPL/test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ mutable struct Error19864 <: Exception; end
function test19864()
@eval Base.showerror(io::IO, e::Error19864) = print(io, "correct19864")
buf = IOBuffer()
fake_response = (Any[(Error19864(), Ptr{Cvoid}[])], true)
fake_response = (Base.ExceptionStack([(exception=Error19864(),backtrace=Ptr{Cvoid}[])]),true)
REPL.print_response(buf, fake_response, false, false, nothing)
return String(take!(buf))
end
Expand Down
6 changes: 3 additions & 3 deletions stdlib/Test/src/Test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ function get_test_result(ex, source)
$testret
catch _e
_e isa InterruptException && rethrow()
Threw(_e, Base.catch_stack(), $(QuoteNode(source)))
Threw(_e, Base.current_exceptions(), $(QuoteNode(source)))
end
end
Base.remove_linenums!(result)
Expand Down Expand Up @@ -1118,7 +1118,7 @@ function testset_beginend(args, tests, source)
err isa InterruptException && rethrow()
# something in the test block threw an error. Count that as an
# error in this test set
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.catch_stack(), $(QuoteNode(source))))
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source))))
finally
copy!(RNG, oldrng)
end
Expand Down Expand Up @@ -1191,7 +1191,7 @@ function testset_forloop(args, testloop, source)
err isa InterruptException && rethrow()
# Something in the test block threw an error. Count that as an
# error in this test set
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.catch_stack(), $(QuoteNode(source))))
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source))))
end
end
quote
Expand Down
2 changes: 1 addition & 1 deletion stdlib/Test/src/logging.jl
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ macro test_logs(exs...)
$(QuoteNode(exs[1:end-1])), logs)
end
catch e
testres = Error(:test_error, $orig_expr, e, Base.catch_stack(), $sourceloc)
testres = Error(:test_error, $orig_expr, e, Base.current_exceptions(), $sourceloc)
end
Test.record(Test.get_testset(), testres)
value
Expand Down
2 changes: 1 addition & 1 deletion test/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ nested_error_pattern = r"""
err_str = try
eval(nested_error_expr)
catch
excs = Base.catch_stack()
excs = Base.current_exceptions()
@test typeof.(first.(excs)) == [UndefVarError, DivideError]
sprint(Base.display_error, excs)
end
Expand Down
Loading

0 comments on commit ff25fee

Please sign in to comment.