# This file is a part of Julia. License is MIT: https://julialang.org/license """ Tools for collecting and manipulating stack traces. Mainly used for building errors. """ module StackTraces import Base: hash, ==, show using Base.Printf: @printf using Base: something export StackTrace, StackFrame, stacktrace """ StackFrame Stack information representing execution context, with the following fields: - `func::Symbol` The name of the function containing the execution context. - `linfo::Union{Core.MethodInstance, CodeInfo, Nothing}` The MethodInstance containing the execution context (if it could be found). - `file::Symbol` The path to the file containing the execution context. - `line::Int` The line number in the file containing the execution context. - `from_c::Bool` True if the code is from C. - `inlined::Bool` True if the code is from an inlined frame. - `pointer::UInt64` Representation of the pointer to the execution context as returned by `backtrace`. """ struct StackFrame # this type should be kept platform-agnostic so that profiles can be dumped on one machine and read on another "the name of the function containing the execution context" func::Symbol "the path to the file containing the execution context" file::Symbol "the line number in the file containing the execution context" line::Int "the MethodInstance or CodeInfo containing the execution context (if it could be found)" linfo::Union{Core.MethodInstance, Core.CodeInfo, Nothing} "true if the code is from C" from_c::Bool "true if the code is from an inlined frame" inlined::Bool "representation of the pointer to the execution context as returned by `backtrace`" pointer::UInt64 # Large enough to be read losslessly on 32- and 64-bit machines. end StackFrame(func, file, line) = StackFrame(Symbol(func), Symbol(file), line, nothing, false, false, 0) """ StackTrace An alias for `Vector{StackFrame}` provided for convenience; returned by calls to `stacktrace`. """ const StackTrace = Vector{StackFrame} const empty_sym = Symbol("") const UNKNOWN = StackFrame(empty_sym, empty_sym, -1, nothing, true, false, 0) # === lookup(C_NULL) #= If the StackFrame has function and line information, we consider two of them the same if they share the same function/line information. =# function ==(a::StackFrame, b::StackFrame) return a.line == b.line && a.from_c == b.from_c && a.func == b.func && a.file == b.file && a.inlined == b.inlined # excluding linfo and pointer end function hash(frame::StackFrame, h::UInt) h += 0xf4fbda67fe20ce88 % UInt h = hash(frame.line, h) h = hash(frame.file, h) h = hash(frame.func, h) h = hash(frame.from_c, h) h = hash(frame.inlined, h) return h end """ lookup(pointer::Union{Ptr{Cvoid}, UInt}) -> Vector{StackFrame} Given a pointer to an execution context (usually generated by a call to `backtrace`), looks up stack frame context information. Returns an array of frame information for all functions inlined at that point, innermost function first. """ function lookup(pointer::Ptr{Cvoid}) infos = ccall(:jl_lookup_code_address, Any, (Ptr{Cvoid}, Cint), pointer - 1, false) isempty(infos) && return [StackFrame(empty_sym, empty_sym, -1, nothing, true, false, convert(UInt64, pointer))] res = Vector{StackFrame}(undef, length(infos)) for i in 1:length(infos) info = infos[i] @assert(length(info) == 7) res[i] = StackFrame(info[1], info[2], info[3], info[4], info[5], info[6], info[7]) end return res end lookup(pointer::UInt) = lookup(convert(Ptr{Cvoid}, pointer)) const top_level_scope_sym = Symbol("top-level scope") using Base.Meta is_loc_meta(expr, kind) = isexpr(expr, :meta) && length(expr.args) >= 1 && expr.args[1] === kind function lookup(ip::Base.InterpreterIP) if ip.code isa Core.MethodInstance && ip.code.def isa Method codeinfo = ip.code.inferred func = ip.code.def.name file = ip.code.def.file line = ip.code.def.line elseif ip.code === nothing # interpreted top-level expression with no CodeInfo return [StackFrame(top_level_scope_sym, empty_sym, 0, nothing, false, false, 0)] elseif ip.code isa Symbol # top-level expression during macro expansion pass return [StackFrame(top_level_scope_sym, ip.code, ip.stmt, nothing, false, false, 0)] else @assert ip.code isa Core.CodeInfo codeinfo = ip.code func = top_level_scope_sym file = empty_sym line = 0 end i = max(ip.stmt+1, 1) # ip.stmt is 0-indexed if i > length(codeinfo.codelocs) || codeinfo.codelocs[i] == 0 return [StackFrame(func, file, line, ip.code, false, false, 0)] end lineinfo = codeinfo.linetable[codeinfo.codelocs[i]] scopes = StackFrame[] while true push!(scopes, StackFrame(lineinfo.method, lineinfo.file, lineinfo.line, ip.code, false, false, 0)) if lineinfo.inlined_at == 0 break end lineinfo = codeinfo.linetable[lineinfo.inlined_at] end return scopes end # allow lookup on already-looked-up data for easier handling of pre-processed frames lookup(s::StackFrame) = StackFrame[s] lookup(s::Tuple{StackFrame,Int}) = StackFrame[s[1]] """ backtrace() Get a backtrace object for the current program point. """ function Base.backtrace() bt, bt2 = ccall(:jl_backtrace_from_here, Any, (Int32,), false) if length(bt) > 2 # remove frames for jl_backtrace_from_here and backtrace() if bt[2] == Ptr{Cvoid}(-1%UInt) # backtrace() is interpreted # Note: win32 is missing the top frame (see https://bugs.chromium.org/p/crashpad/issues/detail?id=53) @static if Base.Sys.iswindows() && Int === Int32 deleteat!(bt, 1:2) else deleteat!(bt, 1:3) end pushfirst!(bt2) else @static if Base.Sys.iswindows() && Int === Int32 deleteat!(bt, 1) else deleteat!(bt, 1:2) end end end return Base._reformat_bt(bt, bt2) end """ stacktrace([trace::Vector{Ptr{Cvoid}},] [c_funcs::Bool=false]) -> StackTrace Returns a stack trace in the form of a vector of `StackFrame`s. (By default stacktrace doesn't return C functions, but this can be enabled.) When called without specifying a trace, `stacktrace` first calls `backtrace`. """ function stacktrace(trace::Vector{<:Union{Base.InterpreterIP,Ptr{Cvoid}}}, c_funcs::Bool=false) stack = vcat(StackTrace(), map(lookup, trace)...)::StackTrace # Remove frames that come from C calls. if !c_funcs filter!(frame -> !frame.from_c, stack) end # Remove frame for this function (and any functions called by this function). remove_frames!(stack, :stacktrace) # is there a better way? the func symbol has a number suffix which changes. # it's possible that no test is needed and we could just popfirst! all the time. # this line was added to PR #16213 because otherwise stacktrace() != stacktrace(false). # not sure why. possibly b/c of re-ordering of base/sysimg.jl !isempty(stack) && startswith(string(stack[1].func),"jlcall_stacktrace") && popfirst!(stack) stack end stacktrace(c_funcs::Bool=false) = stacktrace(backtrace(), c_funcs) """ remove_frames!(stack::StackTrace, name::Symbol) Takes a `StackTrace` (a vector of `StackFrames`) and a function name (a `Symbol`) and removes the `StackFrame` specified by the function name from the `StackTrace` (also removing all frames above the specified function). Primarily used to remove `StackTraces` functions from the `StackTrace` prior to returning it. """ function remove_frames!(stack::StackTrace, name::Symbol) splice!(stack, 1:something(findlast(frame -> frame.func == name, stack), 0)) return stack end function remove_frames!(stack::StackTrace, names::Vector{Symbol}) splice!(stack, 1:something(findlast(frame -> frame.func in names, stack), 0)) return stack end """ remove_frames!(stack::StackTrace, m::Module) Returns the `StackTrace` with all `StackFrame`s from the provided `Module` removed. """ function remove_frames!(stack::StackTrace, m::Module) filter!(f -> !from(f, m), stack) return stack end is_top_level_frame(f::StackFrame) = f.linfo isa Core.CodeInfo || (f.linfo === nothing && f.func === top_level_scope_sym) function show_spec_linfo(io::IO, frame::StackFrame) if frame.linfo === nothing if frame.func === empty_sym @printf(io, "ip:%#x", frame.pointer) elseif frame.func === top_level_scope_sym print(io, "top-level scope") else color = get(io, :color, false) && get(io, :backtrace, false) ? Base.stackframe_function_color() : :nothing printstyled(io, string(frame.func), color=color) end elseif frame.linfo isa Core.MethodInstance if isa(frame.linfo.def, Method) Base.show_tuple_as_call(io, frame.linfo.def.name, frame.linfo.specTypes) else Base.show(io, frame.linfo) end elseif frame.linfo isa Core.CodeInfo print(io, "top-level scope") end end function show(io::IO, frame::StackFrame; full_path::Bool=false) show_spec_linfo(io, frame) if frame.file !== empty_sym file_info = full_path ? string(frame.file) : basename(string(frame.file)) print(io, " at ") Base.with_output_color(get(io, :color, false) && get(io, :backtrace, false) ? Base.stackframe_lineinfo_color() : :nothing, io) do io print(io, file_info, ":") if frame.line >= 0 print(io, frame.line) else print(io, "?") end end end if frame.inlined print(io, " [inlined]") end end """ from(frame::StackFrame, filter_mod::Module) -> Bool Returns whether the `frame` is from the provided `Module` """ function from(frame::StackFrame, m::Module) finfo = frame.linfo result = false if finfo isa Core.MethodInstance frame_m = finfo.def isa(frame_m, Method) && (frame_m = frame_m.module) result = nameof(frame_m) === nameof(m) end return result end end