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

JuliaLang / julia / #38004

08 Feb 2025 04:15AM UTC coverage: 19.401% (-3.6%) from 23.008%
#38004

push

local

web-flow
Inference: propagate struct initialization info on `setfield!` (#57222)

When a variable has a field set with `setfield!(var, field, value)`,
inference now assumes that this specific field is defined and may for
example constant-propagate `isdefined(var, field)` as `true`.
`PartialStruct`, the lattice element used to encode this information,
still has a few limitations in terms of what it may represent (it cannot
represent mutable structs with non-contiguously defined fields yet),
further work on extending it would increase the impact of this change.

Consider the following function:
```julia
julia> function f()
           a = A(1)
           setfield!(a, :y, 2)
           invokelatest(identity, a)
           isdefined(a, :y) && return 1.0
           a
       end
f (generic function with 1 method)
```

Here is before on `master`:
```julia
julia> @code_typed f()
CodeInfo(
1 ─ %1 = %new(Main.A, 1)::A
│          builtin Main.setfield!(%1, :y, 2)::Int64
│        dynamic builtin (Core._call_latest)(identity, %1)::Any
│   %4 =   builtin Main.isdefined(%1, :y)::Bool
└──      goto #3 if not %4
2 ─      return 1.0
3 ─      return %1
) => Union{Float64, A}
```

And after this PR:
```julia
julia> @code_typed f()
CodeInfo(
1 ─ %1 = %new(Main.A, 1)::A
│          builtin Main.setfield!(%1, :y, 2)::Int64
│        dynamic builtin (Core._call_latest)(identity, %1)::Any
└──      return 1.0
) => Float64
```

---------

Co-authored-by: Cédric Belmant <cedric.belmant@juliahub.com>

9440 of 48658 relevant lines covered (19.4%)

95900.84 hits per line

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

33.54
/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, CodeInstance
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.CodeInstance, Core.CodeInfo, Nothing}`
25

26
  The Method, MethodInstance, CodeInstance, 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"
8✔
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 CodeInstance 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{Core.MethodInstance, Core.CodeInstance, 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

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)
×
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
×
89
end
90

91
function hash(frame::StackFrame, h::UInt)
×
92
    h += 0xf4fbda67fe20ce88 % UInt
×
93
    h = hash(frame.line, h)
×
94
    h = hash(frame.file, h)
×
95
    h = hash(frame.func, h)
×
96
    h = hash(frame.from_c, h)
×
97
    h = hash(frame.inlined, h)
×
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})
×
109
    infos = ccall(:jl_lookup_code_address, Any, (Ptr{Cvoid}, Cint), pointer, false)::Core.SimpleVector
×
110
    pointer = convert(UInt64, pointer)
×
111
    isempty(infos) && return [StackFrame(empty_sym, empty_sym, -1, nothing, true, false, pointer)] # this is equal to UNKNOWN
×
112
    res = Vector{StackFrame}(undef, length(infos))
×
113
    for i in 1:length(infos)
×
114
        info = infos[i]::Core.SimpleVector
×
115
        @assert(length(info) == 6)
×
116
        func = info[1]::Symbol
×
117
        file = info[2]::Symbol
×
118
        linenum = info[3]::Int
×
119
        linfo = info[4]
×
120
        res[i] = StackFrame(func, file, linenum, linfo, info[5]::Bool, info[6]::Bool, pointer)
×
121
    end
×
122
    return res
×
123
end
124

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

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

181
"""
182
    stacktrace([trace::Vector{Ptr{Cvoid}},] [c_funcs::Bool=false]) -> StackTrace
183

184
Return a stack trace in the form of a vector of `StackFrame`s. (By default stacktrace
185
doesn't return C functions, but this can be enabled.) When called without specifying a
186
trace, `stacktrace` first calls `backtrace`.
187
"""
188
Base.@constprop :none function stacktrace(trace::Vector{<:Union{Base.InterpreterIP,Ptr{Cvoid}}}, c_funcs::Bool=false)
189
    stack = StackTrace()
9✔
190
    for ip in trace
191
        for frame in lookup(ip)
192
            # Skip frames that come from C calls.
193
            if c_funcs || !frame.from_c
194
                push!(stack, frame)
195
            end
196
        end
197
    end
198
    return stack
199
end
200

201
Base.@constprop :none function stacktrace(c_funcs::Bool=false)
×
202
    stack = stacktrace(backtrace(), c_funcs)
×
203
    # Remove frame for this function (and any functions called by this function).
204
    remove_frames!(stack, :stacktrace)
×
205
    # also remove all of the non-Julia functions that led up to this point (if that list is non-empty)
206
    c_funcs && deleteat!(stack, 1:(something(findfirst(frame -> !frame.from_c, stack), 1) - 1))
×
207
    return stack
×
208
end
209

210
"""
211
    remove_frames!(stack::StackTrace, name::Symbol)
212

213
Takes a `StackTrace` (a vector of `StackFrames`) and a function name (a `Symbol`) and
214
removes the `StackFrame` specified by the function name from the `StackTrace` (also removing
215
all frames above the specified function). Primarily used to remove `StackTraces` functions
216
from the `StackTrace` prior to returning it.
217
"""
218
function remove_frames!(stack::StackTrace, name::Symbol)
×
219
    deleteat!(stack, 1:something(findlast(frame -> frame.func == name, stack), 0))
×
220
    return stack
×
221
end
222

223
function remove_frames!(stack::StackTrace, names::Vector{Symbol})
×
224
    deleteat!(stack, 1:something(findlast(frame -> frame.func in names, stack), 0))
×
225
    return stack
×
226
end
227

228
"""
229
    remove_frames!(stack::StackTrace, m::Module)
230

231
Return the `StackTrace` with all `StackFrame`s from the provided `Module` removed.
232
"""
233
function remove_frames!(stack::StackTrace, m::Module)
×
234
    filter!(f -> !from(f, m), stack)
×
235
    return stack
×
236
end
237

238
is_top_level_frame(f::StackFrame) = f.linfo isa CodeInfo || (f.linfo === nothing && f.func === top_level_scope_sym)
×
239

240
function frame_method_or_module(lkup::StackFrame)
241
    code = lkup.linfo
78✔
242
    code isa Method && return code
78✔
243
    code isa Module && return code
×
244
    mi = frame_mi(lkup)
78✔
245
    mi isa MethodInstance || return nothing
78✔
246
    return mi.def
78✔
247
end
248

249
function frame_mi(lkup::StackFrame)
250
    code = lkup.linfo
116✔
251
    code isa Core.CodeInstance && (code = code.def)
116✔
252
    code isa Core.ABIOverride && (code = code.def)
116✔
253
    code isa MethodInstance || return nothing
116✔
254
    return code
116✔
255
end
256

257
function show_spec_linfo(io::IO, frame::StackFrame)
55✔
258
    linfo = frame.linfo
55✔
259
    if linfo === nothing
55✔
260
        if frame.func === empty_sym
10✔
261
            print(io, "ip:0x", string(frame.pointer, base=16))
×
262
        elseif frame.func === top_level_scope_sym
10✔
263
            print(io, "top-level scope")
1✔
264
        else
265
            Base.print_within_stacktrace(io, Base.demangle_function_name(string(frame.func)), bold=true)
9✔
266
        end
267
    elseif linfo isa CodeInfo
45✔
268
        print(io, "top-level scope")
7✔
269
    elseif linfo isa Module
38✔
270
        Base.print_within_stacktrace(io, Base.demangle_function_name(string(frame.func)), bold=true)
×
271
    else
272
        if linfo isa Union{MethodInstance, CodeInstance}
38✔
273
            def = frame_method_or_module(frame)
76✔
274
            if def isa Module
38✔
275
                Base.show_mi(io, linfo, #=from_stackframe=#true)
×
276
            else
277
                show_spec_sig(io, def, frame_mi(frame).specTypes)
38✔
278
            end
279
        else
280
            m = linfo::Method
×
281
            show_spec_sig(io, m, m.sig)
×
282
        end
283
    end
284
end
285

286
function show_spec_sig(io::IO, m::Method, @nospecialize(sig::Type))
38✔
287
    if get(io, :limit, :false)::Bool
76✔
288
        if !haskey(io, :displaysize)
147✔
289
            io = IOContext(io, :displaysize => displaysize(io))
38✔
290
        end
291
    end
292
    argnames = Base.method_argnames(m)
76✔
293
    argnames = replace(argnames, :var"#unused#" => :var"")
38✔
294
    if m.nkw > 0
38✔
295
        # rearrange call kw_impl(kw_args..., func, pos_args...) to func(pos_args...; kw_args)
296
        kwarg_types = Any[ fieldtype(sig, i) for i = 2:(1+m.nkw) ]
13✔
297
        uw = Base.unwrap_unionall(sig)::DataType
3✔
298
        pos_sig = Base.rewrap_unionall(Tuple{uw.parameters[(m.nkw+2):end]...}, sig)
3✔
299
        kwnames = argnames[2:(m.nkw+1)]
6✔
300
        for i = 1:length(kwnames)
3✔
301
            str = string(kwnames[i])::String
13✔
302
            if endswith(str, "...")
13✔
303
                kwnames[i] = Symbol(str[1:end-3])
×
304
            end
305
        end
23✔
306
        Base.show_tuple_as_call(io, m.name, pos_sig;
3✔
307
                                demangle=true,
308
                                kwargs=zip(kwnames, kwarg_types),
309
                                argnames=argnames[m.nkw+2:end])
310
    else
311
        Base.show_tuple_as_call(io, m.name, sig; demangle=true, argnames)
35✔
312
    end
313
end
314

315
function show(io::IO, frame::StackFrame)
×
316
    show_spec_linfo(io, frame)
×
317
    if frame.file !== empty_sym
×
318
        file_info = basename(string(frame.file))
×
319
        print(io, " at ")
×
320
        print(io, file_info, ":")
×
321
        if frame.line >= 0
×
322
            print(io, frame.line)
×
323
        else
324
            print(io, "?")
×
325
        end
326
    end
327
    if frame.inlined
×
328
        print(io, " [inlined]")
×
329
    end
330
end
331

332
function Base.parentmodule(frame::StackFrame)
333
    linfo = frame.linfo
110✔
334
    if linfo isa CodeInstance
110✔
335
        linfo = linfo.def
8✔
336
        if isa(linfo, Core.ABIOverride)
8✔
337
            linfo = linfo.def
×
338
        end
339
    end
340
    if linfo isa MethodInstance
110✔
341
        def = linfo.def
76✔
342
        if def isa Module
76✔
343
            return def
×
344
        else
345
            return (def::Method).module
76✔
346
        end
347
    elseif linfo isa Method
34✔
348
        return linfo.module
×
349
    elseif linfo isa Module
34✔
350
        return linfo
×
351
    else
352
        # The module is not always available (common reasons include
353
        # frames arising from the interpreter)
354
        nothing
×
355
    end
356
end
357

358
"""
359
    from(frame::StackFrame, filter_mod::Module) -> Bool
360

361
Return whether the `frame` is from the provided `Module`
362
"""
363
function from(frame::StackFrame, m::Module)
×
364
    return parentmodule(frame) === m
×
365
end
366

367
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