• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #37770

05 May 2024 01:26AM UTC coverage: 85.802% (-1.6%) from 87.442%
#37770

push

local

web-flow
A better mechanism for coordinating internal breaking changes. (#53849)

This was origiginally supposed to be an issue, but I just started
writing out the whole code in the issue text to explain what I want all
the behavior to be, so instead, here's the actual implementation of it,
with the motativation in the commit message, and the details of the
actual behavior in the code change ;)

Sometimes packages rely on Julia internals. This is in general
discouraged, but of course for some packages, there isn't really any
other option. If your packages needs to hook the julia internals in a
deep way or is specifically about introspecting the way that julia
itself works, then some amount of reliance on internals is inevitable.
In general, we're happy to let people touch the internals, as long as
they (and their users) are aware that things will break and it's on them
to fix things.

That said, I think we've been a little bit too *caveat emptor* on this
entire business. There's a number of really key packages that rely on
internals (I'm thinking in particular of Revise, Cthulhu and its
dependency stacks) that if they're broken, it's really hard to even
develop julia itself. In particular, these packages have been broken on
Julia master for a more than a week now (following #52415) and there has
been much frustration.

I think one of the biggest issues is that we're generally relying on
`VERSION` checks for these kinds of things. This isn't really a problem
when updating a package between released major versions, but for closely
coupled packages like the above you run into two problems:

1. Since the VERSION number of a package is not known ahead of time,
some breaking changes cannot be made atomically, i.e. we need to merge
the base PR (which bumps people's nightly) in order to get the version
number, which we then need to plug into the various PRs in all the
various packages. If something goes wrong in this process (as it did... (continued)

0 of 3 new or added lines in 1 file covered. (0.0%)

1453 existing lines in 67 files now uncovered.

74896 of 87289 relevant lines covered (85.8%)

14448147.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.36
/base/stacktraces.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
"""
4
Tools for collecting and manipulating stack traces. Mainly used for building errors.
5
"""
6
module StackTraces
7

8

9
import Base: hash, ==, show
10
import Core: CodeInfo, MethodInstance
11
using Base.IRShow: normalize_method_name, append_scopes!, LineInfoNode
12

13
export StackTrace, StackFrame, stacktrace
14

15
"""
16
    StackFrame
17

18
Stack information representing execution context, with the following fields:
19

20
- `func::Symbol`
21

22
  The name of the function containing the execution context.
23

24
- `linfo::Union{Method, Core.MethodInstance, Core.CodeInfo, Nothing}`
25

26
  The Method, MethodInstance, or CodeInfo containing the execution context (if it could be found), \
27
     or nothing (for example, if the inlining was a result of macro expansion).
28

29
- `file::Symbol`
30

31
  The path to the file containing the execution context.
32

33
- `line::Int`
34

35
  The line number in the file containing the execution context.
36

37
- `from_c::Bool`
38

39
  True if the code is from C.
40

41
- `inlined::Bool`
42

43
  True if the code is from an inlined frame.
44

45
- `pointer::UInt64`
46

47
  Representation of the pointer to the execution context as returned by `backtrace`.
48

49
"""
50
struct StackFrame # this type should be kept platform-agnostic so that profiles can be dumped on one machine and read on another
51
    "the name of the function containing the execution context"
35,768✔
52
    func::Symbol
53
    "the path to the file containing the execution context"
54
    file::Symbol
55
    "the line number in the file containing the execution context"
56
    line::Int
57
    "the MethodInstance or CodeInfo containing the execution context (if it could be found), \
58
     or nothing (for example, if the inlining was a result of macro expansion)."
59
    linfo::Union{MethodInstance, Method, CodeInfo, Nothing}
60
    "true if the code is from C"
61
    from_c::Bool
62
    "true if the code is from an inlined frame"
63
    inlined::Bool
64
    "representation of the pointer to the execution context as returned by `backtrace`"
65
    pointer::UInt64  # Large enough to be read losslessly on 32- and 64-bit machines.
66
end
67

UNCOV
68
StackFrame(func, file, line) = StackFrame(Symbol(func), Symbol(file), line,
×
69
                                          nothing, false, false, 0)
70

71
"""
72
    StackTrace
73

74
An alias for `Vector{StackFrame}` provided for convenience; returned by calls to
75
`stacktrace`.
76
"""
77
const StackTrace = Vector{StackFrame}
78

79
const empty_sym = Symbol("")
80
const UNKNOWN = StackFrame(empty_sym, empty_sym, -1, nothing, true, false, 0) # === lookup(C_NULL)
81

82

83
#=
84
If the StackFrame has function and line information, we consider two of them the same if
85
they share the same function/line information.
86
=#
87
function ==(a::StackFrame, b::StackFrame)
1✔
88
    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
77,282✔
89
end
90

91
function hash(frame::StackFrame, h::UInt)
UNCOV
92
    h += 0xf4fbda67fe20ce88 % UInt
×
93
    h = hash(frame.line, h)
1,364,606✔
94
    h = hash(frame.file, h)
1,364,606✔
95
    h = hash(frame.func, h)
1,364,606✔
96
    h = hash(frame.from_c, h)
1,364,606✔
97
    h = hash(frame.inlined, h)
1,364,606✔
UNCOV
98
    return h
×
99
end
100

101
"""
102
    lookup(pointer::Ptr{Cvoid}) -> Vector{StackFrame}
103

104
Given a pointer to an execution context (usually generated by a call to `backtrace`), looks
105
up stack frame context information. Returns an array of frame information for all functions
106
inlined at that point, innermost function first.
107
"""
108
Base.@constprop :none function lookup(pointer::Ptr{Cvoid})
18,564✔
109
    infos = ccall(:jl_lookup_code_address, Any, (Ptr{Cvoid}, Cint), pointer, false)::Core.SimpleVector
18,564✔
110
    pointer = convert(UInt64, pointer)
18,564✔
111
    isempty(infos) && return [StackFrame(empty_sym, empty_sym, -1, nothing, true, false, pointer)] # this is equal to UNKNOWN
18,564✔
112
    res = Vector{StackFrame}(undef, length(infos))
37,128✔
113
    for i in 1:length(infos)
18,564✔
114
        info = infos[i]::Core.SimpleVector
34,648✔
115
        @assert(length(info) == 6)
34,648✔
116
        func = info[1]::Symbol
34,648✔
117
        file = info[2]::Symbol
34,648✔
118
        linenum = info[3]::Int
34,648✔
119
        linfo = info[4]
34,648✔
120
        res[i] = StackFrame(func, file, linenum, linfo, info[5]::Bool, info[6]::Bool, pointer)
34,648✔
121
    end
50,732✔
122
    return res
18,564✔
123
end
124

125
const top_level_scope_sym = Symbol("top-level scope")
126

127
function lookup(ip::Union{Base.InterpreterIP,Core.Compiler.InterpreterIP})
216✔
128
    code = ip.code
216✔
129
    if code === nothing
216✔
130
        # interpreted top-level expression with no CodeInfo
131
        return [StackFrame(top_level_scope_sym, empty_sym, 0, nothing, false, false, 0)]
2✔
132
    end
133
    codeinfo = (code isa MethodInstance ? code.uninferred : code)::CodeInfo
428✔
134
    # prepare approximate code info
135
    if code isa MethodInstance && (meth = code.def; meth isa Method)
214✔
136
        func = meth.name
×
137
        file = meth.file
×
138
        line = meth.line
×
139
    else
140
        func = top_level_scope_sym
3✔
141
        file = empty_sym
3✔
142
        line = Int32(0)
3✔
143
    end
144
    def = (code isa MethodInstance ? code : StackTraces) # Module just used as a token for top-level code
214✔
145
    pc::Int = max(ip.stmt + 1, 0) # n.b. ip.stmt is 0-indexed
214✔
146
    scopes = LineInfoNode[]
214✔
147
    append_scopes!(scopes, pc, codeinfo.debuginfo, def)
214✔
148
    if isempty(scopes)
214✔
149
        return [StackFrame(func, file, line, code, false, false, 0)]
×
150
    end
151
    inlined = false
214✔
152
    scopes = map(scopes) do lno
214✔
153
        if inlined
670✔
154
            def = lno.method
456✔
155
            def isa Union{Method,MethodInstance} || (def = nothing)
912✔
156
        else
157
            def = codeinfo
214✔
158
        end
159
        sf = StackFrame(normalize_method_name(lno.method), lno.file, lno.line, def, false, inlined, 0)
670✔
160
        inlined = true
670✔
161
        return sf
670✔
162
    end
163
    return scopes
214✔
164
end
165

166
"""
167
    stacktrace([trace::Vector{Ptr{Cvoid}},] [c_funcs::Bool=false]) -> StackTrace
168

169
Return a stack trace in the form of a vector of `StackFrame`s. (By default stacktrace
170
doesn't return C functions, but this can be enabled.) When called without specifying a
171
trace, `stacktrace` first calls `backtrace`.
172
"""
173
Base.@constprop :none function stacktrace(trace::Vector{<:Union{Base.InterpreterIP,Core.Compiler.InterpreterIP,Ptr{Cvoid}}}, c_funcs::Bool=false)
55✔
174
    stack = StackTrace()
97✔
175
    for ip in trace
45✔
176
        for frame in lookup(ip)
1,084✔
177
            # Skip frames that come from C calls.
178
            if c_funcs || !frame.from_c
2,785✔
179
                push!(stack, frame)
633✔
180
            end
181
        end
1,445✔
182
    end
1,084✔
183
    return stack
45✔
184
end
185

UNCOV
186
Base.@constprop :none function stacktrace(c_funcs::Bool=false)
×
UNCOV
187
    stack = stacktrace(backtrace(), c_funcs)
×
188
    # Remove frame for this function (and any functions called by this function).
UNCOV
189
    remove_frames!(stack, :stacktrace)
×
190
    # also remove all of the non-Julia functions that led up to this point (if that list is non-empty)
UNCOV
191
    c_funcs && deleteat!(stack, 1:(something(findfirst(frame -> !frame.from_c, stack), 1) - 1))
×
UNCOV
192
    return stack
×
193
end
194

195
"""
196
    remove_frames!(stack::StackTrace, name::Symbol)
197

198
Takes a `StackTrace` (a vector of `StackFrames`) and a function name (a `Symbol`) and
199
removes the `StackFrame` specified by the function name from the `StackTrace` (also removing
200
all frames above the specified function). Primarily used to remove `StackTraces` functions
201
from the `StackTrace` prior to returning it.
202
"""
UNCOV
203
function remove_frames!(stack::StackTrace, name::Symbol)
×
UNCOV
204
    deleteat!(stack, 1:something(findlast(frame -> frame.func == name, stack), 0))
×
UNCOV
205
    return stack
×
206
end
207

UNCOV
208
function remove_frames!(stack::StackTrace, names::Vector{Symbol})
×
UNCOV
209
    deleteat!(stack, 1:something(findlast(frame -> frame.func in names, stack), 0))
×
UNCOV
210
    return stack
×
211
end
212

213
"""
214
    remove_frames!(stack::StackTrace, m::Module)
215

216
Return the `StackTrace` with all `StackFrame`s from the provided `Module` removed.
217
"""
UNCOV
218
function remove_frames!(stack::StackTrace, m::Module)
×
UNCOV
219
    filter!(f -> !from(f, m), stack)
×
UNCOV
220
    return stack
×
221
end
222

223
is_top_level_frame(f::StackFrame) = f.linfo isa CodeInfo || (f.linfo === nothing && f.func === top_level_scope_sym)
5✔
224

225
function show_spec_linfo(io::IO, frame::StackFrame)
2,492✔
226
    linfo = frame.linfo
2,492✔
227
    if linfo === nothing
2,492✔
228
        if frame.func === empty_sym
697✔
229
            print(io, "ip:0x", string(frame.pointer, base=16))
×
230
        elseif frame.func === top_level_scope_sym
697✔
231
            print(io, "top-level scope")
19✔
232
        else
233
            Base.print_within_stacktrace(io, Base.demangle_function_name(string(frame.func)), bold=true)
678✔
234
        end
235
    elseif linfo isa CodeInfo
1,795✔
236
        print(io, "top-level scope")
98✔
237
    elseif linfo isa Module
598✔
238
        Base.print_within_stacktrace(io, Base.demangle_function_name(string(frame.func)), bold=true)
×
239
    elseif linfo isa MethodInstance
1,697✔
240
        def = linfo.def
1,697✔
241
        if def isa Module
1,697✔
UNCOV
242
            Base.show_mi(io, linfo, #=from_stackframe=#true)
×
243
        else
244
            show_spec_sig(io, def, linfo.specTypes)
1,697✔
245
        end
246
    else
247
        m = linfo::Method
×
248
        show_spec_sig(io, m, m.sig)
×
249
    end
250
end
251

252
function show_spec_sig(io::IO, m::Method, @nospecialize(sig::Type))
1,697✔
253
    if get(io, :limit, :false)::Bool
1,236✔
254
        if !haskey(io, :displaysize)
532✔
255
            io = IOContext(io, :displaysize => displaysize(io))
110✔
256
        end
257
    end
258
    argnames = Base.method_argnames(m)
3,394✔
259
    argnames = replace(argnames, :var"#unused#" => :var"")
3,394✔
260
    if m.nkw > 0
1,697✔
261
        # rearrange call kw_impl(kw_args..., func, pos_args...) to func(pos_args...; kw_args)
262
        kwarg_types = Any[ fieldtype(sig, i) for i = 2:(1+m.nkw) ]
371✔
263
        uw = Base.unwrap_unionall(sig)::DataType
306✔
264
        pos_sig = Base.rewrap_unionall(Tuple{uw.parameters[(m.nkw+2):end]...}, sig)
306✔
265
        kwnames = argnames[2:(m.nkw+1)]
612✔
266
        for i = 1:length(kwnames)
306✔
267
            str = string(kwnames[i])::String
371✔
268
            if endswith(str, "...")
371✔
269
                kwnames[i] = Symbol(str[1:end-3])
×
270
            end
271
        end
436✔
272
        Base.show_tuple_as_call(io, m.name, pos_sig;
306✔
273
                                demangle=true,
274
                                kwargs=zip(kwnames, kwarg_types),
275
                                argnames=argnames[m.nkw+2:end])
276
    else
277
        Base.show_tuple_as_call(io, m.name, sig; demangle=true, argnames)
1,391✔
278
    end
279
end
280

281
function show(io::IO, frame::StackFrame)
148✔
282
    show_spec_linfo(io, frame)
148✔
283
    if frame.file !== empty_sym
148✔
284
        file_info = basename(string(frame.file))
141✔
285
        print(io, " at ")
141✔
286
        print(io, file_info, ":")
141✔
287
        if frame.line >= 0
141✔
288
            print(io, frame.line)
141✔
289
        else
290
            print(io, "?")
×
291
        end
292
    end
293
    if frame.inlined
148✔
294
        print(io, " [inlined]")
38✔
295
    end
296
end
297

298
function Base.parentmodule(frame::StackFrame)
299
    linfo = frame.linfo
4,010✔
300
    if linfo isa MethodInstance
4,010✔
301
        def = linfo.def
1,834✔
302
        if def isa Module
1,834✔
303
            return def
×
304
        else
305
            return (def::Method).module
1,834✔
306
        end
307
    elseif linfo isa Method
2,176✔
308
        return linfo.module
×
309
    elseif linfo isa Module
×
310
        return linfo
×
311
    else
312
        # The module is not always available (common reasons include
313
        # frames arising from the interpreter)
314
        nothing
315
    end
316
end
317

318
"""
319
    from(frame::StackFrame, filter_mod::Module) -> Bool
320

321
Return whether the `frame` is from the provided `Module`
322
"""
UNCOV
323
function from(frame::StackFrame, m::Module)
×
UNCOV
324
    return parentmodule(frame) === m
×
325
end
326

327
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc