Skip to content

Commit

Permalink
Simplify backtrace (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkf authored Jan 7, 2022
1 parent 970e03c commit 709ee9d
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 2 deletions.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,68 @@ julia> Try.unwrap_err(result)
KeyError(:b)
```

### Error trace

Consider an example where an error "bubbles up" from a deep stack of function
calls:

```JULIA
julia> using Try

julia> f1(x) = x ? Ok(nothing) : Err(KeyError(:b));

julia> f2(x) = f1(x);

julia> f3(x) = f2(x);
```

Since Try.jl represents an error simply as a Julia value, there is no
information on the source this error:

```JULIA
julia> f3(false)
Try.Err: KeyError: key :b not found
```

We can enable the stacktrace recording of the error by calling
`Try.enable_errortrace()`.

```JULIA
julia> Try.enable_errortrace();

julia> y = f3(false)
Try.Err: KeyError: key :b not found
Stacktrace:
[1] f1
@ ./REPL[2]:1 [inlined]
[2] f2
@ ./REPL[3]:1 [inlined]
[3] f3(x::Bool)
@ Main ./REPL[4]:1
[4] top-level scope
@ REPL[7]:1

julia> Try.disable_errortrace();
```

Note that `f3` didn't throw an exception. It returned a value of type `Err`:

```JULIA
julia> Try.iserr(y)
true

julia> Try.unwrap_err(y)
KeyError(:b)
```

That is to say, the stacktrace is simply attached as "metadata" and
`Try.enable_errortrace()` does not alter how `Err` values behave.

**Limitation/implementation details**: To eliminate the cost of stacktrace
capturing when it is not used, `Try.enable_errortrace()` is implemented using
method invalidation. Thus, error trace cannot be enabled for `Task`s that have
been already started.

### EAFP

```julia
Expand Down
54 changes: 53 additions & 1 deletion src/errortrace.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,58 @@ end

maybe_backtrace() = include_backtrace() ? backtrace() : nothing

function common_suffix(bt, here = backtrace())
a = lastindex(bt)
b = lastindex(here)
while true
a < firstindex(bt) && return a # unreachable?
b < firstindex(bt) && break
bt[a] != here[b] && break
a -= 1
b -= 1
end
return a
end

function simplify_backtrace_impl(bt)
a = common_suffix(bt)
no_common_suffix = a == lastindex(bt)
bt = Base.process_backtrace(bt[1:a])

j = lastindex(bt)
if no_common_suffix
# No common suffix. Evaluated in REPL's display?
fr, = bt[end]
if fr.func === :_start && basename(string(fr.file)) == "client.jl"
j = findlast(((fr, _),) -> !fr.from_c && fr.func === :eval, bt)
j = max(firstindex(bt), j - 1)
end
end

i = firstindex(bt)
if bt[i][1].func === :maybe_backtrace
i += 1
end
if bt[i][1].func === :Err
i += 1
end

return bt[i:j]
end

function simplify_backtrace(bt)
try
return simplify_backtrace_impl(bt)
catch err
@error(
"Fail to simplify backtrace. Fallback to plain backtrace.",
exception = (err, catch_backtrace()),
maxlog = 5,
)
return bt
end
end

struct ErrorTrace <: Exception
exception::Exception
backtrace::typeof(Base.backtrace())
Expand All @@ -31,7 +83,7 @@ function Base.showerror(io::IO, errtrace::ErrorTrace)

# TODO: remove common prefix?
buffer = IOBuffer()
Base.show_backtrace(IOContext(buffer, io), errtrace.backtrace)
Base.show_backtrace(IOContext(buffer, io), simplify_backtrace(errtrace.backtrace))
seekstart(buffer)
println(io, "┌ Original: stacktrace")
for ln in eachline(buffer)
Expand Down
2 changes: 1 addition & 1 deletion src/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ function Base.show(io::IO, ::MIME"text/plain", err::Err)
if backtrace === nothing
showerror(io, ex)
else
showerror(io, ex, err.backtrace)
showerror(io, ex, simplify_backtrace(err.backtrace))
end
end
1 change: 1 addition & 0 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function define_docstrings()
include_dependency(path)
doc = read(path, String)
doc = replace(doc, r"^```julia"m => "```jldoctest $name")
doc = replace(doc, r"^```JULIA"m => "```julia-repl")
doc = replace(doc, "<kbd>TAB</kbd>" => "_TAB_")
@eval Try $Base.@doc $doc $name
end
Expand Down

0 comments on commit 709ee9d

Please sign in to comment.