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

JuliaLang / julia / 1541

08 Dec 2025 04:29PM UTC coverage: 76.699% (-0.04%) from 76.74%
1541

push

buildkite

web-flow
codegen: implement `sret_union` ABI for pointer-ful types (#55045)

This effectively expands our existing `union` ABI to cover both of these
existing cases:
 - `sret`  ABI (which can stack-allocate a _single pointer-ful_ type)
 - `union` ABI (which can stack-allocate _many pointer-free_ types)

This provides some nice speed-ups for temporary "wrappers":
```julia
const v = Any[]
@noinline maybe_wrapped(i) = (i % 32 != 0) ? Some(v) : nothing
function foo()
    count = 0
    for i = 1:1_000_000
        count += (maybe_wrapped(i) !== nothing) ? 1 : 0
    end
    return count
end
```

On this PR this gives:
```julia
julia> @btime foo()
  1.675 ms (0 allocations: 0 bytes)
968750
```

compared to current master:
```julia
julia> @btime foo()
  6.877 ms (968750 allocations: 14.78 MiB)
968750
```

Co-authored-by: Gabriel Baraldi <baraldigabriel@gmail.com>
Co-authored-by: Jameson Nash <vtjnash@gmail.com>

62469 of 81447 relevant lines covered (76.7%)

22102469.65 hits per line

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

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

3
## AnnotatedIOBuffer
4

5
struct AnnotatedIOBuffer <: AbstractPipe
6
    io::IOBuffer
824✔
7
    annotations::Vector{RegionAnnotation}
8
end
9

10
AnnotatedIOBuffer(io::IOBuffer) = AnnotatedIOBuffer(io, Vector{RegionAnnotation}())
824✔
11
AnnotatedIOBuffer() = AnnotatedIOBuffer(IOBuffer())
824✔
12

13
function show(io::IO, aio::AnnotatedIOBuffer)
14
    show(io, AnnotatedIOBuffer)
2✔
15
    size = filesize(aio.io)
2✔
16
    print(io, '(', size, " byte", ifelse(size == 1, "", "s"), ", ",
2✔
17
          length(aio.annotations), " annotation", ifelse(length(aio.annotations) == 1, "", "s"), ")")
18
end
19

20
pipe_reader(io::AnnotatedIOBuffer) = io.io
574✔
21
pipe_writer(io::AnnotatedIOBuffer) = io.io
4,548✔
22

23
# Useful `IOBuffer` methods that we don't get from `AbstractPipe`
24
position(io::AnnotatedIOBuffer) = position(io.io)
697✔
25
seek(io::AnnotatedIOBuffer, n::Integer) = (seek(io.io, n); io)
1,006✔
26
seekend(io::AnnotatedIOBuffer) = (seekend(io.io); io)
×
27
skip(io::AnnotatedIOBuffer, n::Integer) = (skip(io.io, n); io)
2✔
28
copy(io::AnnotatedIOBuffer) = AnnotatedIOBuffer(copy(io.io), copy(io.annotations))
×
29

30
annotations(io::AnnotatedIOBuffer) = io.annotations
×
31

32
annotate!(io::AnnotatedIOBuffer, range::UnitRange{Int}, label::Symbol, @nospecialize(val::Any)) =
×
33
    (_annotate!(io.annotations, range, label, val); io)
×
34

35
function write(io::AnnotatedIOBuffer, astr::Union{AnnotatedString, SubString{<:AnnotatedString}})
28✔
36
    astr = AnnotatedString(astr)
282✔
37
    offset = position(io.io)
282✔
38
    eof(io) || _clear_annotations_in_region!(io.annotations, offset+1:offset+ncodeunits(astr))
290✔
39
    _insert_annotations!(io, astr.annotations)
282✔
40
    write(io.io, String(astr))
282✔
41
end
42

43
write(io::AnnotatedIOBuffer, c::AnnotatedChar) =
16✔
44
    write(io, AnnotatedString(string(c), [(region=1:ncodeunits(c), a...) for a in c.annotations]))
45
write(io::AnnotatedIOBuffer, x::AbstractString) = write(io.io, x)
×
46
write(io::AnnotatedIOBuffer, s::Union{SubString{String}, String}) = write(io.io, s)
5,324✔
47
write(io::AnnotatedIOBuffer, b::UInt8) = write(io.io, b)
80✔
48

49
function write(dest::AnnotatedIOBuffer, src::AnnotatedIOBuffer)
×
50
    destpos = position(dest)
×
51
    isappending = eof(dest)
×
52
    srcpos = position(src)
×
53
    nb = write(dest.io, src.io)
×
54
    isappending || _clear_annotations_in_region!(dest.annotations, destpos:destpos+nb)
×
55
    srcannots = [setindex(annot, max(1 + srcpos, first(annot.region)):last(annot.region), :region)
×
56
                 for annot in src.annotations if first(annot.region) >= srcpos]
57
    _insert_annotations!(dest, srcannots, destpos - srcpos)
×
58
    nb
×
59
end
60

61
# So that read/writes with `IOContext` (and any similar `AbstractPipe` wrappers)
62
# work as expected.
63
function write(io::AbstractPipe, s::Union{AnnotatedString, SubString{<:AnnotatedString}})
2✔
64
    if pipe_writer(io) isa AnnotatedIOBuffer
436✔
65
        write(pipe_writer(io), s)
2✔
66
    else
67
        invoke(write, Tuple{IO, typeof(s)}, io, s)
434✔
68
    end::Int
69
end
70

71
# Can't be part of the `Union` above because it introduces method ambiguities
72
function write(io::AbstractPipe, c::AnnotatedChar)
2✔
73
    if pipe_writer(io) isa AnnotatedIOBuffer
2✔
74
        write(pipe_writer(io), c)
2✔
75
    else
76
        invoke(write, Tuple{IO, typeof(c)}, io, c)
×
77
    end::Int
78
end
79

80
function read(io::AnnotatedIOBuffer, ::Type{AnnotatedString{T}}) where {T <: AbstractString}
5✔
81
    if (start = position(io)) == 0
5✔
82
        AnnotatedString(read(io.io, T), copy(io.annotations))
5✔
83
    else
84
        annots = [setindex(annot, UnitRange{Int}(max(1, first(annot.region) - start), last(annot.region)-start), :region)
×
85
                  for annot in io.annotations if last(annot.region) > start]
86
        AnnotatedString(read(io.io, T), annots)
×
87
    end
88
end
89
read(io::AnnotatedIOBuffer, ::Type{AnnotatedString{AbstractString}}) = read(io, AnnotatedString{String})
×
90
read(io::AnnotatedIOBuffer, ::Type{AnnotatedString}) = read(io, AnnotatedString{String})
432✔
91

92
function read(io::AnnotatedIOBuffer, ::Type{AnnotatedChar{T}}) where {T <: AbstractChar}
×
93
    pos = position(io)
×
94
    char = read(io.io, T)
×
95
    annots = [NamedTuple{(:label, :value)}(annot) for annot in io.annotations if pos+1 in annot.region]
×
96
    AnnotatedChar(char, annots)
×
97
end
98
read(io::AnnotatedIOBuffer, ::Type{AnnotatedChar{AbstractChar}}) = read(io, AnnotatedChar{Char})
×
99
read(io::AnnotatedIOBuffer, ::Type{AnnotatedChar}) = read(io, AnnotatedChar{Char})
234✔
100

101
function truncate(io::AnnotatedIOBuffer, size::Integer)
10✔
102
    truncate(io.io, size)
10✔
103
    filter!(ann -> first(ann.region) <= size, io.annotations)
28✔
104
    map!(ann -> setindex(ann, first(ann.region):min(size, last(ann.region)), :region),
20✔
105
         io.annotations, io.annotations)
106
    io
10✔
107
end
108

109
"""
110
    _clear_annotations_in_region!(annotations::Vector{$RegionAnnotation}, span::UnitRange{Int})
111

112
Erase the presence of `annotations` within a certain `span`.
113

114
This operates by removing all elements of `annotations` that are entirely
115
contained in `span`, truncating ranges that partially overlap, and splitting
116
annotations that subsume `span` to just exist either side of `span`.
117
"""
118
function _clear_annotations_in_region!(annotations::Vector{RegionAnnotation}, span::UnitRange{Int})
×
119
    # Clear out any overlapping pre-existing annotations.
120
    filter!(ann -> first(ann.region) < first(span) || last(ann.region) > last(span), annotations)
×
121
    extras = Tuple{Int, RegionAnnotation}[]
×
122
    for i in eachindex(annotations)
×
123
        annot = annotations[i]
×
124
        region = annot.region
×
125
        # Test for partial overlap
126
        if first(region) <= first(span) <= last(region) || first(region) <= last(span) <= last(region)
×
127
            annotations[i] =
×
128
                setindex(annot,
129
                         if first(region) < first(span)
130
                             first(region):first(span)-1
×
131
                         else
132
                             last(span)+1:last(region)
×
133
                         end,
134
                         :region)
135
            # If `span` fits exactly within `region`, then we've only copied over
136
            # the beginning overhang, but also need to conserve the end overhang.
137
            if first(region) < first(span) && last(span) < last(region)
×
138
                push!(extras, (i, setindex(annot, last(span)+1:last(region), :region)))
×
139
            end
140
        end
141
    end
×
142
    # Insert any extra entries in the appropriate position
143
    for (offset, (i, entry)) in enumerate(extras)
×
144
        insert!(annotations, i + offset, entry)
×
145
    end
×
146
    annotations
×
147
end
148

149
"""
150
    _insert_annotations!(io::AnnotatedIOBuffer, annotations::Vector{$RegionAnnotation}, offset::Int = position(io))
151

152
Register new `annotations` in `io`, applying an `offset` to their regions.
153

154
The largely consists of simply shifting the regions of `annotations` by `offset`
155
and pushing them onto `io`'s annotations. However, when it is possible to merge
156
the new annotations with recent annotations in accordance with the semantics
157
outlined in [`AnnotatedString`](@ref), we do so. More specifically, when there
158
is a run of the most recent annotations that are also present as the first
159
`annotations`, with the same value and adjacent regions, the new annotations are
160
merged into the existing recent annotations by simply extending their range.
161

162
This is implemented so that one can say write an `AnnotatedString` to an
163
`AnnotatedIOBuffer` one character at a time without needlessly producing a
164
new annotation for each character.
165
"""
166
function _insert_annotations!(annots::Vector{RegionAnnotation}, newannots::Vector{RegionAnnotation}, offset::Int = 0)
8✔
167
    run = 0
8✔
168
    if !isempty(annots) && last(last(annots).region) == offset
8✔
169
        for i in reverse(axes(newannots, 1))
4✔
170
            annot = newannots[i]
4✔
171
            first(annot.region) == 1 || continue
4✔
172
            i <= length(annots) || continue
4✔
173
            if annot.label == last(annots).label && annot.value == last(annots).value
4✔
174
                valid_run = true
×
175
                for runlen in 1:i
×
176
                    new = newannots[begin+runlen-1]
×
177
                    old = annots[end-i+runlen]
×
178
                    if last(old.region) != offset || first(new.region) != 1 || old.label != new.label || old.value != new.value
×
179
                        valid_run = false
×
180
                        break
×
181
                    end
182
                end
×
183
                if valid_run
×
184
                    run = i
×
185
                    break
×
186
                end
187
            end
188
        end
4✔
189
    end
190
    for runindex in 0:run-1
8✔
191
        old_index = lastindex(annots) - run + 1 + runindex
×
192
        old = annots[old_index]
×
193
        new = newannots[begin+runindex]
×
194
        extannot = (region = first(old.region):last(new.region)+offset,
×
195
                    label = old.label,
196
                    value = old.value)
197
        annots[old_index] = extannot
×
198
    end
×
199
    for index in run+1:lastindex(newannots)
8✔
200
        annot = newannots[index]
15✔
201
        start, stop = first(annot.region), last(annot.region)
15✔
202
        # REVIEW: For some reason, construction of `newannot`
203
        # can be a significant contributor to the overall runtime
204
        # of this function. For instance, executing:
205
        #
206
        #     replace(AnnotatedIOBuffer(), S"apple",
207
        #             'e' => S"{red:x}", 'p' => S"{green:y}")
208
        #
209
        # results in 3 calls to `_insert_annotations!`. It takes
210
        # ~570ns in total, compared to ~200ns if we push `annot`
211
        # instead of `newannot`. Commenting out the `_insert_annotations!`
212
        # line reduces the runtime to ~170ns, from which we can infer
213
        # that constructing `newannot` is somehow responsible for
214
        # a ~30ns -> ~400ns (~13x) increase in runtime!!
215
        # This also comes with a marginal increase in allocations
216
        # (compared to the commented out version) of 2 -> 14 (250b -> 720b).
217
        #
218
        # This seems quite strange, but I haven't dug into the generated
219
        # LLVM or ASM code. If anybody reading this is interested in checking
220
        # this out, that would be brilliant 🙏.
221
        #
222
        # What I have done is found that "direct tuple reconstruction"
223
        # (as below) is several times faster than using `setindex`.
224
        newannot = (region = start+offset:stop+offset,
15✔
225
                    label = annot.label,
226
                    value = annot.value)
227
        push!(annots, newannot)
30✔
228
    end
15✔
229
end
230

231
_insert_annotations!(io::AnnotatedIOBuffer, newannots::Vector{RegionAnnotation}, offset::Int = position(io)) =
564✔
232
    _insert_annotations!(io.annotations, newannots, offset)
233

234
# String replacement
235

236
# REVIEW: For some reason the `Core.kwcall` indirection seems to cause a
237
# substantial slowdown here. If we remove `; count` from the signature
238
# and run the sample code above in `_insert_annotations!`, the runtime
239
# drops from ~4400ns to ~580ns (~7x faster). I cannot guess why this is.
240
function replace(out::AnnotatedIOBuffer, str::AnnotatedString, pat_f::Pair...; count = typemax(Int))
564✔
241
    if count == 0 || isempty(pat_f)
564✔
242
        write(out, str)
×
243
        return out
×
244
    end
245
    e1, patterns, replacers, repspans, notfound = _replace_init(str.string, pat_f, count)
282✔
246
    if notfound
282✔
247
        foreach(_free_pat_replacer, patterns)
6✔
248
        write(out, str)
6✔
249
        return out
6✔
250
    end
251
    # Modelled after `Base.annotated_chartransform`, but needing
252
    # to handle a bit more complexity.
253
    isappending = eof(out)
276✔
254
    newannots = empty(out.annotations)
276✔
255
    bytepos = bytestart = firstindex(str.string)
276✔
256
    replacements = [(region = (bytestart - 1):(bytestart - 1), offset = position(out))]
276✔
257
    nrep = 1
276✔
258
    while nrep <= count
794✔
259
        repspans, ridx, xspan, newbytes, bytepos = @inline _replace_once(
1,302✔
260
            out.io, str.string, bytestart, e1, patterns, replacers, repspans, count, nrep, bytepos)
261
        first(xspan) >= e1 && break
776✔
262
        nrep += 1
518✔
263
        # NOTE: When the replaced pattern ends with a multi-codeunit character,
264
        # `xspan` only covers up to the start of that character. However,
265
        # for us to correctly account for the changes to the string we need
266
        # the /entire/ span of codeunits that were replaced.
267
        if !isempty(xspan) && codeunit(str.string, last(xspan)) > 0x80
518✔
268
            xspan = first(xspan):nextind(str.string, last(xspan))-1
10✔
269
        end
270
        drift = last(replacements).offset
518✔
271
        thisrep = (region = xspan, offset = drift + newbytes - length(xspan))
518✔
272
        destoff = first(xspan) - 1 + drift
518✔
273
        push!(replacements, thisrep)
518✔
274
        replacement = replacers[ridx]
518✔
275
        _isannotated(replacement) || continue
518✔
276
        annots = annotations(replacement)
106✔
277
        annots′ = if eltype(annots) == Annotation # When it's a char not a string
106✔
278
            region = 1:newbytes
18✔
279
            [@NamedTuple{region::UnitRange{Int}, label::Symbol, value}((region, label, value))
36✔
280
             for (; label, value) in annots]
281
        else
282
            annots
116✔
283
        end::Vector{RegionAnnotation}
284
        _insert_annotations!(newannots, annots′, destoff)
106✔
285
    end
518✔
286
    push!(replacements, (region = e1:(e1-1), offset = last(replacements).offset))
276✔
287
    foreach(_free_pat_replacer, patterns)
276✔
288
    write(out.io, SubString(str.string, bytepos))
498✔
289
    # NOTE: To enable more efficient annotation clearing,
290
    # we make use of the fact that `_replace_once` picks
291
    # replacements ordered by their match start position.
292
    # This means that the start of `.region`s in
293
    # `replacements` is monotonically increasing.
294
    isappending || _clear_annotations_in_region!(out.annotations, first(replacements).offset:position(out))
278✔
295
    for (; region, label, value) in str.annotations
276✔
296
        start, stop = first(region), last(region)
330✔
297
        prioridx = searchsortedlast(
330✔
298
            replacements, (region = start:start, offset = 0),
299
            by = r -> first(r.region))
1,504✔
300
        postidx = searchsortedfirst(
330✔
301
            replacements, (region = stop:stop, offset = 0),
302
            by = r -> first(r.region))
1,484✔
303
        priorrep, postrep = replacements[prioridx], replacements[postidx]
330✔
304
        if prioridx == postidx && start >= first(priorrep.region) && stop <= last(priorrep.region)
330✔
305
            # Region contained with a replacement
306
            continue
18✔
307
        elseif postidx - prioridx <= 1 && start > last(priorrep.region) && stop < first(postrep.region)
312✔
308
            # Lies between replacements
309
            shiftregion = (start + priorrep.offset):(stop + priorrep.offset)
80✔
310
            shiftann = (region = shiftregion, label, value)
78✔
311
            push!(out.annotations, shiftann)
78✔
312
        else
313
            # Split between replacements
314
            prevrep = replacements[max(begin, prioridx - 1)]
234✔
315
            for rep in @view replacements[max(begin, prioridx - 1):min(end, postidx + 1)]
234✔
316
                gap = max(start, last(prevrep.region)+1):min(stop, first(rep.region)-1)
1,532✔
317
                if !isempty(gap)
908✔
318
                    shiftregion = (first(gap) + prevrep.offset):(last(gap) + prevrep.offset)
284✔
319
                    shiftann = (; region = shiftregion, label, value)
284✔
320
                    push!(out.annotations, shiftann)
568✔
321
                end
322
                prevrep = rep
908✔
323
            end
908✔
324
        end
325
    end
330✔
326
    append!(out.annotations, newannots)
344✔
327
    out
276✔
328
end
329

330
replace(out::IO, str::AnnotatedString, pat_f::Pair...; count=typemax(Int)) =
6✔
331
    replace(out, str.string, pat_f...; count)
332

333
function replace(str::AnnotatedString, pat_f::Pair...; count=typemax(Int))
524✔
334
    isempty(pat_f) || iszero(count) && return str
262✔
335
    out = AnnotatedIOBuffer()
260✔
336
    replace(out, str, pat_f...; count)
260✔
337
    read(seekstart(out), AnnotatedString)
260✔
338
end
339

340
# Printing
341

342
function printstyled end
343

344
# NOTE: This is an interim solution to the invalidations caused
345
# by the split styled display implementation. This should be
346
# replaced by a more robust solution (such as a consolidation of
347
# the type and method definitions) in the near future.
348
module AnnotatedDisplay
349

350
using ..Base: IO, SubString, AnnotatedString, AnnotatedChar, AnnotatedIOBuffer
351
using ..Base: eachregion, invoke_in_world, tls_world_age
352

353
# Write
354

355
ansi_write(f::Function, io::IO, x::Any) = f(io, String(x))
×
356

357
ansi_write_(f::Function, io::IO, @nospecialize(x::Any)) =
12,660✔
358
    invoke_in_world(tls_world_age(), ansi_write, f, io, x)
359

360
Base.write(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}) =
522✔
361
    ansi_write_(write, io, s)::Int
362

363
Base.write(io::IO, c::AnnotatedChar) =
18✔
364
    ansi_write_(write, io, c)::Int
365

366
function Base.write(io::IO, aio::AnnotatedIOBuffer)
4✔
367
    if get(io, :color, false) == true
4✔
368
        # This does introduce an overhead that technically
369
        # could be avoided, but I'm not sure that it's currently
370
        # worth the effort to implement an efficient version of
371
        # writing from an AnnotatedIOBuffer with style.
372
        # In the meantime, by converting to an `AnnotatedString` we can just
373
        # reuse all the work done to make that work.
374
        ansi_write_(write, io, read(aio, AnnotatedString))::Int
2✔
375
    else
376
        write(io, aio.io)
2✔
377
    end
378
end
379

380
# Print
381

382
Base.print(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}) =
11,644✔
383
    (ansi_write_(write, io, s); nothing)
2,438✔
384

385
Base.print(io::IO, s::AnnotatedChar) =
472✔
386
    (ansi_write_(write, io, s); nothing)
472✔
387

388
Base.print(io::AnnotatedIOBuffer, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}) =
230✔
389
    (write(io, s); nothing)
58✔
390

391
Base.print(io::AnnotatedIOBuffer, c::AnnotatedChar) =
×
392
    (write(io, c); nothing)
×
393

394
styled_print(io::AnnotatedIOBuffer, msg::Any, kwargs::Any) = print(io, msg...)
×
395

396
styled_print_(io::AnnotatedIOBuffer, @nospecialize(msg), @nospecialize(kwargs)) =
10✔
397
    invoke_in_world(tls_world_age(), styled_print, io, msg, kwargs)::Nothing
398

399
Base.printstyled(io::AnnotatedIOBuffer, msg...; kwargs...) =
20✔
400
    styled_print_(io, msg, kwargs)
401

402
# Escape
403

404
Base.escape_string(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}},
405
              esc = ""; keep = (), ascii::Bool=false, fullhex::Bool=false) =
4✔
406
    (ansi_write_((io, s) -> escape_string(io, s, esc; keep, ascii, fullhex), io, s); nothing)
4✔
407

408
# Show
409

410
show_annot(io::IO, ::Any) = nothing
×
411
show_annot(io::IO, ::MIME, ::Any) = nothing
×
412

413
show_annot_(io::IO, @nospecialize(x::Any)) =
×
414
    invoke_in_world(tls_world_age(), show_annot, io, x)::Nothing
415

416
show_annot_(io::IO, m::MIME, @nospecialize(x::Any)) =
4✔
417
    invoke_in_world(tls_world_age(), show_annot, io, m, x)::Nothing
418

419
Base.show(io::IO, m::MIME"text/html", s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}) =
4✔
420
    show_annot_(io, m, s)
421

422
Base.show(io::IO, m::MIME"text/html", c::AnnotatedChar) =
×
423
    show_annot_(io, m, c)
424

425
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