fredrikekre / Literate.jl

Simple package for literate programming in Julia
https://fredrikekre.github.io/Literate.jl
Other
547 stars 64 forks source link

WIP/RFC: print stacktrace when eval fails #160

Open mortenpi opened 3 years ago

mortenpi commented 3 years ago

Currently, suppose I have a file like

function foo(x)
  error("error on line $(@__LINE__)")
end

#-

foo(42)

I get an error which does not actually show a stacktrace of where the exception was raised

julia> Literate.notebook("stacktrace.jl")
[ Info: generating notebook from `~/Julia/Literate/stacktrace.jl`
[ Info: executing notebook `stacktrace.ipynb`
┌ Error: error when executing notebook based on input file: `~/Julia/Literate/stacktrace.jl`
└ @ Literate ~/Julia/Literate/src/Literate.jl:667
ERROR: LoadError: error on line 2
in expression starting at string:1
when executing the following code block in file `~/Julia/Literate/stacktrace.jl`

```julia
foo(42)

Stacktrace: [1] error(s::String) @ Base ./error.jl:33 [2] execute_block(sb::Module, block::String; inputfile::String) @ Literate ~/Julia/Literate/src/Literate.jl:785 [3] execute_notebook(nb::Dict{Any, Any}; inputfile::String) @ Literate ~/Julia/Literate/src/Literate.jl:683 [4] (::Literate.var"#36#38"{Dict{String, Any}})() @ Literate ~/Julia/Literate/src/Literate.jl:664 [5] cd(f::Literate.var"#36#38"{Dict{String, Any}}, dir::String) @ Base.Filesystem ./file.jl:106 [6] jupyter_notebook(chunks::Vector{Literate.Chunk}, config::Dict{String, Any}) @ Literate ~/Julia/Literate/src/Literate.jl:663 [7] notebook(inputfile::String, outputdir::String; config::Dict{Any, Any}, kwargs::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}) @ Literate ~/Julia/Literate/src/Literate.jl:600 [8] notebook (repeats 2 times) @ ~/Julia/Literate/src/Literate.jl:596 [inlined] [9] top-level scope @ REPL[6]:1


The stacktrace traces back to this `error` call: https://github.com/fredrikekre/Literate.jl/blob/42f39411cd5ebc22fc934161447c42548fe29e6f/src/Literate.jl#L785-L792

With this patch you get something like the following (which could be much longer if the error thrown is deep in some dependency, which is the information you would want):

julia> Literate.notebook("stacktrace.jl") [ Info: generating notebook from ~/Julia/Literate/stacktrace.jl [ Info: executing notebook stacktrace.ipynb ┌ Error: error when executing notebook based on input file: ~/Julia/Literate/stacktrace.jl └ @ Literate ~/Julia/Literate/src/Literate.jl:667 ERROR: Literate.EvalException: LoadError when executing code in: ~/Julia/Literate/stacktrace.jl LoadError: error on line 2 Stacktrace: [1] error(s::String) @ Base ./error.jl:33 [2] foo(x::Int64) @ Main.##257 ./string:2 [3] top-level scope @ string:1 [4] eval @ ./boot.jl:360 [inlined] [5] include_string(mapexpr::typeof(identity), mod::Module, code::String, filename::String) @ Base ./loading.jl:1094 [6] include_string (repeats 2 times) @ ./loading.jl:1104 [inlined] [7] #42 @ ~/Julia/Literate/src/Literate.jl:781 [inlined] [8] (::IOCapture.var"#3#5"{Core.TypeofBottom, Literate.var"#42#43"{Module, String}, Task, Pipe, Base.TTY, Base.TTY})() @ IOCapture ~/.julia/packages/IOCapture/g8FZl/src/IOCapture.jl:105 [9] with_logstate(f::Function, logstate::Any) @ Base.CoreLogging ./logging.jl:491 [10] with_logger @ ./logging.jl:603 [inlined] [11] capture(f::Literate.var"#42#43"{Module, String}; rethrow::Type, color::Bool) @ IOCapture ~/.julia/packages/IOCapture/g8FZl/src/IOCapture.jl:103 [12] execute_block(sb::Module, block::String; inputfile::String) @ Literate ~/Julia/Literate/src/Literate.jl:780 in expression starting at string:1

Stacktrace: [1] execute_block(sb::Module, block::String; inputfile::String) @ Literate ~/Julia/Literate/src/Literate.jl:801 [2] execute_notebook(nb::Dict{Any, Any}; inputfile::String) @ Literate ~/Julia/Literate/src/Literate.jl:683 [3] (::Literate.var"#36#38"{Dict{String, Any}})() @ Literate ~/Julia/Literate/src/Literate.jl:664 [4] cd(f::Literate.var"#36#38"{Dict{String, Any}}, dir::String) @ Base.Filesystem ./file.jl:106 [5] jupyter_notebook(chunks::Vector{Literate.Chunk}, config::Dict{String, Any}) @ Literate ~/Julia/Literate/src/Literate.jl:663 [6] notebook(inputfile::String, outputdir::String; config::Dict{Any, Any}, kwargs::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}) @ Literate ~/Julia/Literate/src/Literate.jl:600 [7] notebook (repeats 2 times) @ ~/Julia/Literate/src/Literate.jl:596 [inlined] [8] top-level scope @ REPL[2]:1



Now, I don't yet quite have a good idea of how to do this well, so opening this for discussion. This was just a side tangent when working on a notebook. A few notes though:

* The simple option (least amount of code modified) would actually be to `sprintf` the backtrace into `error`. However, returning a custom exception object seems like the more correct thing to do.
* The `@error` from the parent function seems actually redundant.
* I noticed that by using `include_string`, any error I think gets wrapped in `LoadError` (but this does not seem to be documented). It also adds a bunch of things into the stacktrace which are hard to get rid of.
* It's sorta confusing that we print two stacktraces.. but it's sorta the right thing to do. Unless we can somehow do some `rethrow`-type magic..