From 0a5cab8b7ebb8f747b4dbe4ddb4b6fa9527c5543 Mon Sep 17 00:00:00 2001 From: Nicholas Bauer Date: Fri, 19 Nov 2021 14:26:25 -0500 Subject: [PATCH] Add `err` global to REPL to store most recent errors (#40642) --- NEWS.md | 3 +++ base/client.jl | 33 ++++++++++++++++---------- base/task.jl | 4 ++-- contrib/generate_precompile.jl | 4 +--- stdlib/REPL/src/REPL.jl | 6 ++++- stdlib/REPL/test/repl.jl | 43 +++++++++++++++++++++++++++++++--- 6 files changed, 72 insertions(+), 21 deletions(-) diff --git a/NEWS.md b/NEWS.md index f19ba0897f8cd3..3a4080cd5d303d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -111,6 +111,9 @@ Standard library changes argument have a type more specific than `Any`; use SHIFT-TAB instead of TAB to allow any compatible methods. +* New `err` global variable in `Main` set when an expression throws an exception, akin to `ans`. Typing `err` reprints + the exception information. + #### SparseArrays #### Dates diff --git a/base/client.jl b/base/client.jl index 375e41d3ee38fc..67264f96fae0ed 100644 --- a/base/client.jl +++ b/base/client.jl @@ -84,27 +84,33 @@ end function scrub_repl_backtrace(bt) if bt !== nothing && !(bt isa Vector{Any}) # ignore our sentinel value types - bt = stacktrace(bt) + bt = bt isa Vector{StackFrame} ? copy(bt) : stacktrace(bt) # remove REPL-related frames from interactive printing eval_ind = findlast(frame -> !frame.from_c && frame.func === :eval, bt) eval_ind === nothing || deleteat!(bt, eval_ind:length(bt)) end return bt end +scrub_repl_backtrace(stack::ExceptionStack) = + ExceptionStack(Any[(;x.exception, backtrace = scrub_repl_backtrace(x.backtrace)) for x in stack]) -function display_error(io::IO, er, bt) +istrivialerror(stack::ExceptionStack) = + length(stack) == 1 && length(stack[1].backtrace) ≤ 1 + # frame 1 = top level; assumes already went through scrub_repl_backtrace + +function display_error(io::IO, stack::ExceptionStack) printstyled(io, "ERROR: "; bold=true, color=Base.error_color()) - bt = scrub_repl_backtrace(bt) - showerror(IOContext(io, :limit => true), er, bt, backtrace = bt!==nothing) + show_exception_stack(IOContext(io, :limit => true), stack) println(io) end -function display_error(io::IO, stack::ExceptionStack) +display_error(stack::ExceptionStack) = display_error(stderr, stack) + +# these forms are depended on by packages outside Julia +function display_error(io::IO, exception, backtrace) 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) + showerror(IOContext(io, :limit => true), er, bt, backtrace = bt!==nothing) println(io) end -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) @@ -117,6 +123,8 @@ function eval_user_input(errio, @nospecialize(ast), show_value::Bool) print(color_normal) end if lasterr !== nothing + lasterr = scrub_repl_backtrace(lasterr) + istrivialerror(lasterr) || ccall(:jl_set_global, Cvoid, (Any, Any, Any), Main, :err, lasterr) invokelatest(display_error, errio, lasterr) errcount = 0 lasterr = nothing @@ -143,7 +151,8 @@ function eval_user_input(errio, @nospecialize(ast), show_value::Bool) @error "SYSTEM: display_error(errio, lasterr) caused an error" end errcount += 1 - lasterr = current_exceptions() + lasterr = scrub_repl_backtrace(current_exceptions()) + ccall(:jl_set_global, Cvoid, (Any, Any, Any), Main, :err, lasterr) if errcount > 2 @error "It is likely that something important is broken, and Julia will not be able to continue normally" errcount break @@ -260,7 +269,7 @@ function exec_options(opts) try load_julia_startup() catch - invokelatest(display_error, current_exceptions()) + invokelatest(display_error, scrub_repl_backtrace(current_exceptions())) !(repl || is_interactive::Bool) && exit(1) end end @@ -294,7 +303,7 @@ function exec_options(opts) try include(Main, PROGRAM_FILE) catch - invokelatest(display_error, current_exceptions()) + invokelatest(display_error, scrub_repl_backtrace(current_exceptions())) if !is_interactive::Bool exit(1) end @@ -496,7 +505,7 @@ function _start() try exec_options(JLOptions()) catch - invokelatest(display_error, current_exceptions()) + invokelatest(display_error, scrub_repl_backtrace(current_exceptions())) exit(1) end if is_interactive && get(stdout, :color, false) diff --git a/base/task.jl b/base/task.jl index 3f4c1d4ef0e64c..6f3deaaa0ec18b 100644 --- a/base/task.jl +++ b/base/task.jl @@ -489,13 +489,13 @@ function errormonitor(t::Task) try # try to display the failure atomically errio = IOContext(PipeBuffer(), errs::IO) emphasize(errio, "Unhandled Task ") - display_error(errio, current_exceptions(t)) + display_error(errio, scrub_repl_backtrace(current_exceptions(t))) write(errs, errio) catch try # try to display the secondary error atomically errio = IOContext(PipeBuffer(), errs::IO) print(errio, "\nSYSTEM: caught exception while trying to print a failed Task notice: ") - display_error(errio, current_exceptions()) + display_error(errio, scrub_repl_backtrace(current_exceptions())) write(errs, errio) flush(errs) # and then the actual error, as best we can diff --git a/contrib/generate_precompile.jl b/contrib/generate_precompile.jl index fbc4f837ccae4e..3c31c4c118c000 100644 --- a/contrib/generate_precompile.jl +++ b/contrib/generate_precompile.jl @@ -32,9 +32,7 @@ precompile(Tuple{typeof(Base.recursive_prefs_merge), Base.Dict{String, Any}}) precompile(Tuple{typeof(isassigned), Core.SimpleVector, Int}) precompile(Tuple{typeof(getindex), Core.SimpleVector, Int}) precompile(Tuple{typeof(Base.Experimental.register_error_hint), Any, Type}) -precompile(Tuple{typeof(Base.display_error), MethodError, Vector{Union{Ptr{Nothing}, Base.InterpreterIP}}}) -precompile(Tuple{typeof(Base.display_error), ErrorException}) -precompile(Tuple{typeof(Base.display_error), BoundsError}) +precompile(Tuple{typeof(Base.display_error), Base.ExceptionStack}) precompile(Tuple{Core.kwftype(typeof(Type)), NamedTuple{(:sizehint,), Tuple{Int}}, Type{IOBuffer}}) precompile(Base.CoreLogging.current_logger_for_env, (Base.CoreLogging.LogLevel, String, Module)) precompile(Base.CoreLogging.current_logger_for_env, (Base.CoreLogging.LogLevel, Symbol, Module)) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 9e67ad9c2d8ab9..a450e2eb7c1bd0 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -284,6 +284,8 @@ function print_response(errio::IO, response, show_value::Bool, have_color::Bool, try Base.sigatomic_end() if iserr + val = Base.scrub_repl_backtrace(val) + Base.istrivialerror(val) || ccall(:jl_set_global, Cvoid, (Any, Any, Any), Main, :err, val) Base.invokelatest(Base.display_error, errio, val) else if val !== nothing && show_value @@ -305,7 +307,9 @@ function print_response(errio::IO, response, show_value::Bool, have_color::Bool, 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, current_exceptions()) + excs = Base.scrub_repl_backtrace(current_exceptions()) + ccall(:jl_set_global, Cvoid, (Any, Any, Any), Main, :err, excs) + Base.invokelatest(Base.display_error, errio, excs) catch e # at this point, only print the name of the type as a Symbol to # minimize the possibility of further errors. diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index 3fbf6d8825bba8..e81b48f69f705f 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -885,13 +885,13 @@ end # Test containers in error messages are limited #18726 let io = IOBuffer() - Base.display_error(io, - try + Base.display_error(io, Base.ExceptionStack(Any[(exception = + (try [][trues(6000)] @assert false catch e e - end, []) + end), backtrace = [])])) @test length(String(take!(io))) < 1500 end @@ -1366,3 +1366,40 @@ end @test isempty(mods) end end + +# err should reprint error if deeper than top-level +fake_repl() do stdin_write, stdout_read, repl + repltask = @async begin + REPL.run_repl(repl) + end + # initialize `err` to `nothing` + write(stdin_write, "global err = nothing\n") + readline(stdout_read) + readline(stdout_read) == "\e[0m" + readuntil(stdout_read, "julia> ", keep=true) + # generate top-level error + write(stdin_write, "foobar\n") + readline(stdout_read) + @test readline(stdout_read) == "\e[0mERROR: UndefVarError: foobar not defined" + @test readline(stdout_read) == "" + readuntil(stdout_read, "julia> ", keep=true) + # check that top-level error did not change `err` + write(stdin_write, "err\n") + readline(stdout_read) + @test readline(stdout_read) == "\e[0m" + readuntil(stdout_read, "julia> ", keep=true) + # generate deeper error + write(stdin_write, "foo() = foobar\n") + readline(stdout_read) + readuntil(stdout_read, "julia> ", keep=true) + write(stdin_write, "foo()\n") + readline(stdout_read) + @test readline(stdout_read) == "\e[0mERROR: UndefVarError: foobar not defined" + readuntil(stdout_read, "julia> ", keep=true) + # check that deeper error did set `err` + write(stdin_write, "err\n") + readline(stdout_read) + @test readline(stdout_read) == "\e[0m1-element ExceptionStack:" + @test readline(stdout_read) == "UndefVarError: foobar not defined" + @test readline(stdout_read) == "Stacktrace:" +end