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

JuliaLang / julia / #37658

20 Oct 2023 08:24PM UTC coverage: 87.459% (-0.5%) from 87.929%
#37658

push

local

web-flow
fix unicode indexing in parse(Complex, string) (#51758)

This fixes a string-indexing bug introduced in #24713 (Julia 0.7).
Sometimes, this would cause `parse(Complex{T}, string)` to throw a
`StringIndexError` rather than an `ArgumentError`, e.g. for
`parse(ComplexF64, "3 β+ 4im")` or `parse(ComplexF64, "3 + 4αm")`. (As
far as I can tell, it can never cause parsing to fail for valid
strings.)

The source of the error is that if `i` is the index of an ASCII
character in a string `s`, then `s[i+1]` is valid (even if the next
character is non-ASCII) but `s[i-1]` is invalid if the previous
character is non-ASCII.

5 of 5 new or added lines in 1 file covered. (100.0%)

73572 of 84122 relevant lines covered (87.46%)

11577017.06 hits per line

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

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

3
"""
4
Gives a reinterpreted view (of element type T) of the underlying array (of element type S).
5
If the size of `T` differs from the size of `S`, the array will be compressed/expanded in
6
the first dimension. The variant `reinterpret(reshape, T, a)` instead adds or consumes the first dimension
7
depending on the ratio of element sizes.
8
"""
9
struct ReinterpretArray{T,N,S,A<:AbstractArray{S},IsReshaped} <: AbstractArray{T, N}
10
    parent::A
11
    readable::Bool
12
    writable::Bool
13

14
    function throwbits(S::Type, T::Type, U::Type)
9✔
15
        @noinline
9✔
16
        throw(ArgumentError("cannot reinterpret `$(S)` as `$(T)`, type `$(U)` is not a bits type"))
9✔
17
    end
18
    function throwsize0(S::Type, T::Type, msg)
5✔
19
        @noinline
5✔
20
        throw(ArgumentError("cannot reinterpret a zero-dimensional `$(S)` array to `$(T)` which is of a $msg size"))
5✔
21
    end
22
    function throwsingleton(S::Type, T::Type)
5✔
23
        @noinline
5✔
24
        throw(ArgumentError("cannot reinterpret a `$(S)` array to `$(T)` which is a singleton type"))
5✔
25
    end
26

27
    global reinterpret
28

29
    @doc """
30
        reinterpret(T::DataType, A::AbstractArray)
31

32
    Construct a view of the array with the same binary data as the given
33
    array, but with `T` as element type.
34

35
    This function also works on "lazy" array whose elements are not computed until they are explicitly retrieved.
36
    For instance, `reinterpret` on the range `1:6` works similarly as on the dense vector `collect(1:6)`:
37

38
    ```jldoctest
39
    julia> reinterpret(Float32, UInt32[1 2 3 4 5])
40
    1×5 reinterpret(Float32, ::Matrix{UInt32}):
41
     1.0f-45  3.0f-45  4.0f-45  6.0f-45  7.0f-45
42

43
    julia> reinterpret(Complex{Int}, 1:6)
44
    3-element reinterpret(Complex{$Int}, ::UnitRange{$Int}):
45
     1 + 2im
46
     3 + 4im
47
     5 + 6im
48
    ```
49
    """
50
    function reinterpret(::Type{T}, a::A) where {T,N,S,A<:AbstractArray{S, N}}
16,270✔
51
        function thrownonint(S::Type, T::Type, dim)
15,550✔
52
            @noinline
2✔
53
            throw(ArgumentError("""
2✔
54
                cannot reinterpret an `$(S)` array to `$(T)` whose first dimension has size `$(dim)`.
55
                The resulting array would have non-integral first dimension.
56
                """))
57
        end
58
        function throwaxes1(S::Type, T::Type, ax1)
15,550✔
59
            @noinline
2✔
60
            throw(ArgumentError("cannot reinterpret a `$(S)` array to `$(T)` when the first axis is $ax1. Try reshaping first."))
2✔
61
        end
62
        isbitstype(T) || throwbits(S, T, T)
15,548✔
63
        isbitstype(S) || throwbits(S, T, S)
15,545✔
64
        (N != 0 || sizeof(T) == sizeof(S)) || throwsize0(S, T, "different")
15,542✔
65
        if N != 0 && sizeof(S) != sizeof(T)
15,538✔
66
            ax1 = axes(a)[1]
128,425✔
67
            dim = length(ax1)
15,124✔
68
            if issingletontype(T)
15,124✔
69
                issingletontype(S) || throwsingleton(S, T)
2✔
70
            else
71
                rem(dim*sizeof(S),sizeof(T)) == 0 || thrownonint(S, T, dim)
128,425✔
72
            end
73
            first(ax1) == 1 || throwaxes1(S, T, ax1)
128,423✔
74
        end
75
        readable = array_subpadding(T, S)
15,532✔
76
        writable = array_subpadding(S, T)
15,532✔
77
        new{T, N, S, A, false}(a, readable, writable)
130,889✔
78
    end
79
    reinterpret(::Type{T}, a::AbstractArray{T}) where {T} = a
130✔
80

81
    # With reshaping
82
    function reinterpret(::typeof(reshape), ::Type{T}, a::A) where {T,S,A<:AbstractArray{S}}
649✔
83
        function throwintmult(S::Type, T::Type)
650✔
84
            @noinline
1✔
85
            throw(ArgumentError("`reinterpret(reshape, T, a)` requires that one of `sizeof(T)` (got $(sizeof(T))) and `sizeof(eltype(a))` (got $(sizeof(S))) be an integer multiple of the other"))
1✔
86
        end
87
        function throwsize1(a::AbstractArray, T::Type)
651✔
88
            @noinline
2✔
89
            throw(ArgumentError("`reinterpret(reshape, $T, a)` where `eltype(a)` is $(eltype(a)) requires that `axes(a, 1)` (got $(axes(a, 1))) be equal to 1:$(sizeof(T) ÷ sizeof(eltype(a))) (from the ratio of element sizes)"))
2✔
90
        end
91
        function throwfromsingleton(S, T)
652✔
92
            @noinline
3✔
93
            throw(ArgumentError("`reinterpret(reshape, $T, a)` where `eltype(a)` is $S requires that $T be a singleton type, since $S is one"))
3✔
94
        end
95
        isbitstype(T) || throwbits(S, T, T)
649✔
96
        isbitstype(S) || throwbits(S, T, S)
647✔
97
        if sizeof(S) == sizeof(T)
646✔
98
            N = ndims(a)
44✔
99
        elseif sizeof(S) > sizeof(T)
602✔
100
            issingletontype(T) && throwsingleton(S, T)
558✔
101
            rem(sizeof(S), sizeof(T)) == 0 || throwintmult(S, T)
555✔
102
            N = ndims(a) + 1
555✔
103
        else
104
            issingletontype(S) && throwfromsingleton(S, T)
44✔
105
            rem(sizeof(T), sizeof(S)) == 0 || throwintmult(S, T)
41✔
106
            N = ndims(a) - 1
40✔
107
            N > -1 || throwsize0(S, T, "larger")
40✔
108
            axes(a, 1) == OneTo(sizeof(T) ÷ sizeof(S)) || throwsize1(a, T)
41✔
109
        end
110
        readable = array_subpadding(T, S)
636✔
111
        writable = array_subpadding(S, T)
636✔
112
        new{T, N, S, A, true}(a, readable, writable)
636✔
113
    end
114
    reinterpret(::typeof(reshape), ::Type{T}, a::AbstractArray{T}) where {T} = a
1✔
115
end
116

117
ReshapedReinterpretArray{T,N,S,A<:AbstractArray{S}} = ReinterpretArray{T,N,S,A,true}
118
NonReshapedReinterpretArray{T,N,S,A<:AbstractArray{S, N}} = ReinterpretArray{T,N,S,A,false}
119

120
"""
121
    reinterpret(reshape, T, A::AbstractArray{S}) -> B
122

123
Change the type-interpretation of `A` while consuming or adding a "channel dimension."
124

125
If `sizeof(T) = n*sizeof(S)` for `n>1`, `A`'s first dimension must be
126
of size `n` and `B` lacks `A`'s first dimension. Conversely, if `sizeof(S) = n*sizeof(T)` for `n>1`,
127
`B` gets a new first dimension of size `n`. The dimensionality is unchanged if `sizeof(T) == sizeof(S)`.
128

129
!!! compat "Julia 1.6"
130
    This method requires at least Julia 1.6.
131

132
# Examples
133

134
```jldoctest
135
julia> A = [1 2; 3 4]
136
2×2 Matrix{$Int}:
137
 1  2
138
 3  4
139

140
julia> reinterpret(reshape, Complex{Int}, A)    # the result is a vector
141
2-element reinterpret(reshape, Complex{$Int}, ::Matrix{$Int}) with eltype Complex{$Int}:
142
 1 + 3im
143
 2 + 4im
144

145
julia> a = [(1,2,3), (4,5,6)]
146
2-element Vector{Tuple{$Int, $Int, $Int}}:
147
 (1, 2, 3)
148
 (4, 5, 6)
149

150
julia> reinterpret(reshape, Int, a)             # the result is a matrix
151
3×2 reinterpret(reshape, $Int, ::Vector{Tuple{$Int, $Int, $Int}}) with eltype $Int:
152
 1  4
153
 2  5
154
 3  6
155
```
156
"""
157
reinterpret(::typeof(reshape), T::Type, a::AbstractArray)
158

159
reinterpret(::Type{T}, a::NonReshapedReinterpretArray) where {T} = reinterpret(T, a.parent)
131✔
160
reinterpret(::typeof(reshape), ::Type{T}, a::ReshapedReinterpretArray) where {T} = reinterpret(reshape, T, a.parent)
1✔
161

162
# Definition of StridedArray
163
StridedFastContiguousSubArray{T,N,A<:DenseArray} = FastContiguousSubArray{T,N,A}
164
StridedReinterpretArray{T,N,A<:Union{DenseArray,StridedFastContiguousSubArray},IsReshaped} = ReinterpretArray{T,N,S,A,IsReshaped} where S
165
StridedReshapedArray{T,N,A<:Union{DenseArray,StridedFastContiguousSubArray,StridedReinterpretArray}} = ReshapedArray{T,N,A}
166
StridedSubArray{T,N,A<:Union{DenseArray,StridedReshapedArray,StridedReinterpretArray},
167
    I<:Tuple{Vararg{Union{RangeIndex, ReshapedUnitRange, AbstractCartesianIndex}}}} = SubArray{T,N,A,I}
168
StridedArray{T,N} = Union{DenseArray{T,N}, StridedSubArray{T,N}, StridedReshapedArray{T,N}, StridedReinterpretArray{T,N}}
169
StridedVector{T} = StridedArray{T,1}
170
StridedMatrix{T} = StridedArray{T,2}
171
StridedVecOrMat{T} = Union{StridedVector{T}, StridedMatrix{T}}
172

173
strides(a::Union{DenseArray,StridedReshapedArray,StridedReinterpretArray}) = size_to_strides(1, size(a)...)
747,777✔
174
stride(A::Union{DenseArray,StridedReshapedArray,StridedReinterpretArray}, k::Integer) =
218,644✔
175
    k ≤ ndims(A) ? strides(A)[k] : length(A)
176

177
function strides(a::ReinterpretArray{T,<:Any,S,<:AbstractArray{S},IsReshaped}) where {T,S,IsReshaped}
3,031✔
178
    _checkcontiguous(Bool, a) && return size_to_strides(1, size(a)...)
3,031✔
179
    stp = strides(parent(a))
3,010✔
180
    els, elp = sizeof(T), sizeof(S)
3,010✔
181
    els == elp && return stp # 0dim parent is also handled here.
3,010✔
182
    IsReshaped && els < elp && return (1, _checked_strides(stp, els, elp)...)
1,880✔
183
    stp[1] == 1 || throw(ArgumentError("Parent must be contiguous in the 1st dimension!"))
670✔
184
    st′ = _checked_strides(tail(stp), els, elp)
645✔
185
    return IsReshaped ? st′ : (1, st′...)
607✔
186
end
187

188
@inline function _checked_strides(stp::Tuple, els::Integer, elp::Integer)
1,858✔
189
    if elp > els && rem(elp, els) == 0
1,858✔
190
        N = div(elp, els)
1,701✔
191
        return map(i -> N * i, stp)
5,866✔
192
    end
193
    drs = map(i -> divrem(elp * i, els), stp)
378✔
194
    all(i->iszero(i[2]), drs) ||
393✔
195
        throw(ArgumentError("Parent's strides could not be exactly divided!"))
196
    map(first, drs)
138✔
197
end
198

199
_checkcontiguous(::Type{Bool}, A::ReinterpretArray) = _checkcontiguous(Bool, parent(A))
3,099✔
200

201
similar(a::ReinterpretArray, T::Type, d::Dims) = similar(a.parent, T, d)
122,614✔
202

203
function check_readable(a::ReinterpretArray{T, N, S} where N) where {T,S}
40,487,934✔
204
    # See comment in check_writable
205
    if !a.readable && !array_subpadding(T, S)
40,487,934✔
206
        throw(PaddingError(T, S))
40,487,934✔
207
    end
208
end
209

210
function check_writable(a::ReinterpretArray{T, N, S} where N) where {T,S}
40,539,584✔
211
    # `array_subpadding` is relatively expensive (compared to a simple arrayref),
212
    # so it is cached in the array. However, it is computable at compile time if,
213
    # inference has the types available. By using this form of the check, we can
214
    # get the best of both worlds for the success case. If the types were not
215
    # available to inference, we simply need to check the field (relatively cheap)
216
    # and if they were we should be able to fold this check away entirely.
217
    if !a.writable && !array_subpadding(S, T)
40,539,584✔
218
        throw(PaddingError(T, S))
40,539,584✔
219
    end
220
end
221

222
## IndexStyle specializations
223

224
# For `reinterpret(reshape, T, a)` where we're adding a channel dimension and with
225
# `IndexStyle(a) == IndexLinear()`, it's advantageous to retain pseudo-linear indexing.
226
struct IndexSCartesian2{K} <: IndexStyle end   # K = sizeof(S) ÷ sizeof(T), a static-sized 2d cartesian iterator
1,148✔
227

228
IndexStyle(::Type{ReinterpretArray{T,N,S,A,false}}) where {T,N,S,A<:AbstractArray{S,N}} = IndexStyle(A)
26,586✔
229
function IndexStyle(::Type{ReinterpretArray{T,N,S,A,true}}) where {T,N,S,A<:AbstractArray{S}}
3,105✔
230
    if sizeof(T) < sizeof(S)
3,105✔
231
        IndexStyle(A) === IndexLinear() && return IndexSCartesian2{sizeof(S) ÷ sizeof(T)}()
2,313✔
232
        return IndexCartesian()
1,168✔
233
    end
234
    return IndexStyle(A)
792✔
235
end
236
IndexStyle(::IndexSCartesian2{K}, ::IndexSCartesian2{K}) where {K} = IndexSCartesian2{K}()
2✔
237

238
struct SCartesianIndex2{K}   # can't make <:AbstractCartesianIndex without N, and 2 would be a bit misleading
239
    i::Int
4,114✔
240
    j::Int
241
end
242
to_index(i::SCartesianIndex2) = i
24✔
243

244
struct SCartesianIndices2{K,R<:AbstractUnitRange{Int}} <: AbstractMatrix{SCartesianIndex2{K}}
245
    indices2::R
15✔
246
end
247
SCartesianIndices2{K}(indices2::AbstractUnitRange{Int}) where {K} = (@assert K::Int > 1; SCartesianIndices2{K,typeof(indices2)}(indices2))
30✔
248

249
eachindex(::IndexSCartesian2{K}, A::ReshapedReinterpretArray) where {K} = SCartesianIndices2{K}(eachindex(IndexLinear(), parent(A)))
13✔
250
@inline function eachindex(style::IndexSCartesian2{K}, A::AbstractArray, B::AbstractArray...) where {K}
2✔
251
    iter = eachindex(style, A)
2✔
252
    _all_match_first(C->eachindex(style, C), iter, B...) || throw_eachindex_mismatch_indices(IndexSCartesian2{K}(), axes(A), axes.(B)...)
4✔
253
    return iter
2✔
254
end
255

256
size(iter::SCartesianIndices2{K}) where K = (K, length(iter.indices2))
2✔
257
axes(iter::SCartesianIndices2{K}) where K = (OneTo(K), iter.indices2)
11✔
258

259
first(iter::SCartesianIndices2{K}) where {K} = SCartesianIndex2{K}(1, first(iter.indices2))
2✔
260
last(iter::SCartesianIndices2{K}) where {K}  = SCartesianIndex2{K}(K, last(iter.indices2))
3✔
261

262
@inline function getindex(iter::SCartesianIndices2{K}, i::Int, j::Int) where {K}
×
263
    @boundscheck checkbounds(iter, i, j)
×
264
    return SCartesianIndex2{K}(i, iter.indices2[j])
×
265
end
266

267
function iterate(iter::SCartesianIndices2{K}) where {K}
14✔
268
    ret = iterate(iter.indices2)
28✔
269
    ret === nothing && return nothing
14✔
270
    item2, state2 = ret
14✔
271
    return SCartesianIndex2{K}(1, item2), (1, item2, state2)
14✔
272
end
273

274
function iterate(iter::SCartesianIndices2{K}, (state1, item2, state2)) where {K}
96✔
275
    if state1 < K
96✔
276
        item1 = state1 + 1
61✔
277
        return SCartesianIndex2{K}(item1, item2), (item1, item2, state2)
61✔
278
    end
279
    ret = iterate(iter.indices2, state2)
62✔
280
    ret === nothing && return nothing
35✔
281
    item2, state2 = ret
27✔
282
    return SCartesianIndex2{K}(1, item2), (1, item2, state2)
27✔
283
end
284

285
SimdLoop.simd_outer_range(iter::SCartesianIndices2) = iter.indices2
×
286
SimdLoop.simd_inner_length(::SCartesianIndices2{K}, ::Any) where K = K
×
287
@inline function SimdLoop.simd_index(::SCartesianIndices2{K}, Ilast::Int, I1::Int) where {K}
×
288
    SCartesianIndex2{K}(I1+1, Ilast)
×
289
end
290

291
_maybe_reshape(::IndexSCartesian2, A::ReshapedReinterpretArray, I...) = A
×
292

293
# fallbacks
294
function _getindex(::IndexSCartesian2, A::AbstractArray{T,N}, I::Vararg{Int, N}) where {T,N}
18✔
295
    @_propagate_inbounds_meta
18✔
296
    getindex(A, I...)
18✔
297
end
298
function _setindex!(::IndexSCartesian2, A::AbstractArray{T,N}, v, I::Vararg{Int, N}) where {T,N}
×
299
    @_propagate_inbounds_meta
×
300
    setindex!(A, v, I...)
×
301
end
302
# fallbacks for array types that use "pass-through" indexing (e.g., `IndexStyle(A) = IndexStyle(parent(A))`)
303
# but which don't handle SCartesianIndex2
304
function _getindex(::IndexSCartesian2, A::AbstractArray{T,N}, ind::SCartesianIndex2) where {T,N}
6✔
305
    @_propagate_inbounds_meta
6✔
306
    J = _ind2sub(tail(axes(A)), ind.j)
6✔
307
    getindex(A, ind.i, J...)
6✔
308
end
309
function _setindex!(::IndexSCartesian2, A::AbstractArray{T,N}, v, ind::SCartesianIndex2) where {T,N}
18✔
310
    @_propagate_inbounds_meta
18✔
311
    J = _ind2sub(tail(axes(A)), ind.j)
18✔
312
    setindex!(A, v, ind.i, J...)
18✔
313
end
314
eachindex(style::IndexSCartesian2, A::AbstractArray) = eachindex(style, parent(A))
6✔
315

316
## AbstractArray interface
317

318
parent(a::ReinterpretArray) = a.parent
6,124✔
319
dataids(a::ReinterpretArray) = dataids(a.parent)
3,734✔
320
unaliascopy(a::NonReshapedReinterpretArray{T}) where {T} = reinterpret(T, unaliascopy(a.parent))
2✔
321
unaliascopy(a::ReshapedReinterpretArray{T}) where {T} = reinterpret(reshape, T, unaliascopy(a.parent))
1✔
322

323
function size(a::NonReshapedReinterpretArray{T,N,S} where {N}) where {T,S}
29,505✔
324
    psize = size(a.parent)
29,505✔
325
    size1 = issingletontype(T) ? psize[1] : div(psize[1]*sizeof(S), sizeof(T))
29,505✔
326
    tuple(size1, tail(psize)...)
29,505✔
327
end
328
function size(a::ReshapedReinterpretArray{T,N,S} where {N}) where {T,S}
2,401✔
329
    psize = size(a.parent)
2,401✔
330
    sizeof(S) > sizeof(T) && return (div(sizeof(S), sizeof(T)), psize...)
2,401✔
331
    sizeof(S) < sizeof(T) && return tail(psize)
263✔
332
    return psize
98✔
333
end
334
size(a::NonReshapedReinterpretArray{T,0}) where {T} = ()
18✔
335

336
function axes(a::NonReshapedReinterpretArray{T,N,S} where {N}) where {T,S}
79,056,334✔
337
    paxs = axes(a.parent)
79,237,967✔
338
    f, l = first(paxs[1]), length(paxs[1])
79,056,334✔
339
    size1 = issingletontype(T) ? l : div(l*sizeof(S), sizeof(T))
79,237,967✔
340
    tuple(oftype(paxs[1], f:f+size1-1), tail(paxs)...)
79,238,256✔
341
end
342
function axes(a::ReshapedReinterpretArray{T,N,S} where {N}) where {T,S}
9,907✔
343
    paxs = axes(a.parent)
9,907✔
344
    sizeof(S) > sizeof(T) && return (OneTo(div(sizeof(S), sizeof(T))), paxs...)
9,907✔
345
    sizeof(S) < sizeof(T) && return tail(paxs)
2,061✔
346
    return paxs
1,760✔
347
end
348
axes(a::NonReshapedReinterpretArray{T,0}) where {T} = ()
4✔
349

350
has_offset_axes(a::ReinterpretArray) = has_offset_axes(a.parent)
4,156✔
351

352
elsize(::Type{<:ReinterpretArray{T}}) where {T} = sizeof(T)
3,812✔
353
unsafe_convert(::Type{Ptr{T}}, a::ReinterpretArray{T,N,S} where N) where {T,S} = Ptr{T}(unsafe_convert(Ptr{S},a.parent))
8,411✔
354

355
@inline @propagate_inbounds function getindex(a::NonReshapedReinterpretArray{T,0,S}) where {T,S}
3✔
356
    if isprimitivetype(T) && isprimitivetype(S)
3✔
357
        reinterpret(T, a.parent[])
1✔
358
    else
359
        a[firstindex(a)]
2✔
360
    end
361
end
362

363
@inline @propagate_inbounds getindex(a::ReinterpretArray) = a[firstindex(a)]
5✔
364

365
@inline @propagate_inbounds function getindex(a::ReinterpretArray{T,N,S}, inds::Vararg{Int, N}) where {T,N,S}
42,738,125✔
366
    check_readable(a)
40,744,644✔
367
    _getindex_ra(a, inds[1], tail(inds))
87,891,396✔
368
end
369

370
@inline @propagate_inbounds function getindex(a::ReinterpretArray{T,N,S}, i::Int) where {T,N,S}
9,314✔
371
    check_readable(a)
9,314✔
372
    if isa(IndexStyle(a), IndexLinear)
9,314✔
373
        return _getindex_ra(a, i, ())
11,032✔
374
    end
375
    # Convert to full indices here, to avoid needing multiple conversions in
376
    # the loop in _getindex_ra
377
    inds = _to_subscript_indices(a, i)
2,742✔
378
    isempty(inds) ? _getindex_ra(a, 1, ()) : _getindex_ra(a, inds[1], tail(inds))
2,822✔
379
end
380

381
@inline @propagate_inbounds function getindex(a::ReshapedReinterpretArray{T,N,S}, ind::SCartesianIndex2) where {T,N,S}
4,014✔
382
    check_readable(a)
4,014✔
383
    s = Ref{S}(a.parent[ind.j])
4,014✔
384
    GC.@preserve s begin
4,014✔
385
        tptr = Ptr{T}(unsafe_convert(Ref{S}, s))
4,014✔
386
        return unsafe_load(tptr, ind.i)
4,014✔
387
    end
388
end
389

390
@inline @propagate_inbounds function _getindex_ra(a::NonReshapedReinterpretArray{T,N,S}, i1::Int, tailinds::TT) where {T,N,S,TT}
40,752,004✔
391
    # Make sure to match the scalar reinterpret if that is applicable
392
    if sizeof(T) == sizeof(S) && (fieldcount(T) + fieldcount(S)) == 0
40,752,004✔
393
        if issingletontype(T) # singleton types
598,029✔
394
            @boundscheck checkbounds(a, i1, tailinds...)
85✔
395
            return T.instance
79✔
396
        end
397
        return reinterpret(T, a.parent[i1, tailinds...])
7,472,867✔
398
    else
399
        @boundscheck checkbounds(a, i1, tailinds...)
41,451,031✔
400
        ind_start, sidx = divrem((i1-1)*sizeof(T), sizeof(S))
41,451,007✔
401
        # Optimizations that avoid branches
402
        if sizeof(T) % sizeof(S) == 0
40,153,963✔
403
            # T is bigger than S and contains an integer number of them
404
            n = sizeof(T) ÷ sizeof(S)
38,886,304✔
405
            t = Ref{T}()
38,951,840✔
406
            GC.@preserve t begin
38,951,840✔
407
                sptr = Ptr{S}(unsafe_convert(Ref{T}, t))
38,951,840✔
408
                for i = 1:n
39,017,376✔
409
                     s = a.parent[ind_start + i, tailinds...]
77,930,948✔
410
                     unsafe_store!(sptr, s, i)
77,930,940✔
411
                end
116,910,040✔
412
            end
413
            return t[]
38,951,840✔
414
        elseif sizeof(S) % sizeof(T) == 0
1,267,659✔
415
            # S is bigger than T and contains an integer number of them
416
            s = Ref{S}(a.parent[ind_start + 1, tailinds...])
2,499,195✔
417
            GC.@preserve s begin
2,499,135✔
418
                tptr = Ptr{T}(unsafe_convert(Ref{S}, s))
2,499,135✔
419
                return unsafe_load(tptr + sidx)
2,499,135✔
420
            end
421
        else
422
            i = 1
32✔
423
            nbytes_copied = 0
32✔
424
            # This is a bit complicated to deal with partial elements
425
            # at both the start and the end. LLVM will fold as appropriate,
426
            # once it knows the data layout
427
            s = Ref{S}()
32✔
428
            t = Ref{T}()
32✔
429
            GC.@preserve s t begin
32✔
430
                sptr = Ptr{S}(unsafe_convert(Ref{S}, s))
32✔
431
                tptr = Ptr{T}(unsafe_convert(Ref{T}, t))
32✔
432
                while nbytes_copied < sizeof(T)
104✔
433
                    s[] = a.parent[ind_start + i, tailinds...]
76✔
434
                    nb = min(sizeof(S) - sidx, sizeof(T)-nbytes_copied)
72✔
435
                    memcpy(tptr + nbytes_copied, sptr + sidx, nb)
72✔
436
                    nbytes_copied += nb
72✔
437
                    sidx = 0
72✔
438
                    i += 1
72✔
439
                end
102✔
440
            end
441
            return t[]
32✔
442
        end
443
    end
444
end
445

446
@inline @propagate_inbounds function _getindex_ra(a::ReshapedReinterpretArray{T,N,S}, i1::Int, tailinds::TT) where {T,N,S,TT}
1,953✔
447
    # Make sure to match the scalar reinterpret if that is applicable
448
    if sizeof(T) == sizeof(S) && (fieldcount(T) + fieldcount(S)) == 0
1,953✔
449
        if issingletontype(T) # singleton types
656✔
450
            @boundscheck checkbounds(a, i1, tailinds...)
64✔
451
            return T.instance
62✔
452
        end
453
        return reinterpret(T, a.parent[i1, tailinds...])
601✔
454
    end
455
    @boundscheck checkbounds(a, i1, tailinds...)
1,297✔
456
    if sizeof(T) >= sizeof(S)
1,297✔
457
        t = Ref{T}()
113✔
458
        GC.@preserve t begin
113✔
459
            sptr = Ptr{S}(unsafe_convert(Ref{T}, t))
113✔
460
            if sizeof(T) > sizeof(S)
113✔
461
                # Extra dimension in the parent array
462
                n = sizeof(T) ÷ sizeof(S)
110✔
463
                if isempty(tailinds) && IndexStyle(a.parent) === IndexLinear()
110✔
464
                    offset = n * (i1 - firstindex(a))
57✔
465
                    for i = 1:n
57✔
466
                        s = a.parent[i + offset]
170✔
467
                        unsafe_store!(sptr, s, i)
170✔
468
                    end
283✔
469
                else
470
                    for i = 1:n
53✔
471
                        s = a.parent[i, i1, tailinds...]
112✔
472
                        unsafe_store!(sptr, s, i)
106✔
473
                    end
159✔
474
                end
475
            else
476
                # No extra dimension
477
                s = a.parent[i1, tailinds...]
3✔
478
                unsafe_store!(sptr, s)
113✔
479
            end
480
        end
481
        return t[]
113✔
482
    end
483
    # S is bigger than T and contains an integer number of them
484
    # n = sizeof(S) ÷ sizeof(T)
485
    s = Ref{S}()
1,184✔
486
    GC.@preserve s begin
1,184✔
487
        tptr = Ptr{T}(unsafe_convert(Ref{S}, s))
1,184✔
488
        s[] = a.parent[tailinds...]
1,190✔
489
        return unsafe_load(tptr, i1)
1,184✔
490
    end
491
end
492

493
@inline @propagate_inbounds function setindex!(a::NonReshapedReinterpretArray{T,0,S}, v) where {T,S}
4✔
494
    if isprimitivetype(S) && isprimitivetype(T)
4✔
495
        a.parent[] = reinterpret(S, v)
1✔
496
        return a
1✔
497
    end
498
    setindex!(a, v, firstindex(a))
3✔
499
end
500

501
@inline @propagate_inbounds setindex!(a::ReinterpretArray, v) = setindex!(a, v, firstindex(a))
4✔
502

503
@inline @propagate_inbounds function setindex!(a::ReinterpretArray{T,N,S}, v, inds::Vararg{Int, N}) where {T,N,S}
40,660,906✔
504
    check_writable(a)
40,660,872✔
505
    _setindex_ra!(a, v, inds[1], tail(inds))
79,882,190✔
506
end
507

508
@inline @propagate_inbounds function setindex!(a::ReinterpretArray{T,N,S}, v, i::Int) where {T,N,S}
72✔
509
    check_writable(a)
72✔
510
    if isa(IndexStyle(a), IndexLinear)
72✔
511
        return _setindex_ra!(a, v, i, ())
64✔
512
    end
513
    inds = _to_subscript_indices(a, i)
9✔
514
    _setindex_ra!(a, v, inds[1], tail(inds))
9✔
515
end
516

517
@inline @propagate_inbounds function setindex!(a::ReshapedReinterpretArray{T,N,S}, v, ind::SCartesianIndex2) where {T,N,S}
1✔
518
    check_writable(a)
1✔
519
    v = convert(T, v)::T
1✔
520
    s = Ref{S}(a.parent[ind.j])
1✔
521
    GC.@preserve s begin
1✔
522
        tptr = Ptr{T}(unsafe_convert(Ref{S}, s))
1✔
523
        unsafe_store!(tptr, v, ind.i)
1✔
524
    end
525
    a.parent[ind.j] = s[]
1✔
526
    return a
1✔
527
end
528

529
@inline @propagate_inbounds function _setindex_ra!(a::NonReshapedReinterpretArray{T,N,S}, v, i1::Int, tailinds::TT) where {T,N,S,TT}
40,660,909✔
530
    v = convert(T, v)::T
40,660,909✔
531
    # Make sure to match the scalar reinterpret if that is applicable
532
    if sizeof(T) == sizeof(S) && (fieldcount(T) + fieldcount(S)) == 0
40,660,907✔
533
        if issingletontype(T) # singleton types
1,883,770✔
534
            @boundscheck checkbounds(a, i1, tailinds...)
6✔
535
            # setindex! is a noop except for the index check
536
        else
537
            setindex!(a.parent, reinterpret(S, v), i1, tailinds...)
2,328,750✔
538
        end
539
    else
540
        @boundscheck checkbounds(a, i1, tailinds...)
38,777,149✔
541
        ind_start, sidx = divrem((i1-1)*sizeof(T), sizeof(S))
38,777,125✔
542
        # Optimizations that avoid branches
543
        if sizeof(T) % sizeof(S) == 0
38,777,125✔
544
            # T is bigger than S and contains an integer number of them
545
            t = Ref{T}(v)
38,776,338✔
546
            GC.@preserve t begin
38,776,338✔
547
                sptr = Ptr{S}(unsafe_convert(Ref{T}, t))
38,776,338✔
548
                n = sizeof(T) ÷ sizeof(S)
38,776,338✔
549
                for i = 1:n
38,776,338✔
550
                    s = unsafe_load(sptr, i)
77,552,679✔
551
                    a.parent[ind_start + i, tailinds...] = s
77,552,679✔
552
                end
116,329,020✔
553
            end
554
        elseif sizeof(S) % sizeof(T) == 0
787✔
555
            # S is bigger than T and contains an integer number of them
556
            s = Ref{S}(a.parent[ind_start + 1, tailinds...])
783✔
557
            GC.@preserve s begin
783✔
558
                tptr = Ptr{T}(unsafe_convert(Ref{S}, s))
783✔
559
                unsafe_store!(tptr + sidx, v)
783✔
560
                a.parent[ind_start + 1, tailinds...] = s[]
783✔
561
            end
562
        else
563
            t = Ref{T}(v)
4✔
564
            s = Ref{S}()
4✔
565
            GC.@preserve t s begin
4✔
566
                tptr = Ptr{UInt8}(unsafe_convert(Ref{T}, t))
4✔
567
                sptr = Ptr{UInt8}(unsafe_convert(Ref{S}, s))
4✔
568
                nbytes_copied = 0
4✔
569
                i = 1
4✔
570
                # Deal with any partial elements at the start. We'll have to copy in the
571
                # element from the original array and overwrite the relevant parts
572
                if sidx != 0
4✔
573
                    s[] = a.parent[ind_start + i, tailinds...]
3✔
574
                    nb = min((sizeof(S) - sidx) % UInt, sizeof(T) % UInt)
2✔
575
                    memcpy(sptr + sidx, tptr, nb)
2✔
576
                    nbytes_copied += nb
2✔
577
                    a.parent[ind_start + i, tailinds...] = s[]
2✔
578
                    i += 1
2✔
579
                    sidx = 0
2✔
580
                end
581
                # Deal with the main body of elements
582
                while nbytes_copied < sizeof(T) && (sizeof(T) - nbytes_copied) > sizeof(S)
8✔
583
                    nb = min(sizeof(S), sizeof(T) - nbytes_copied)
2✔
584
                    memcpy(sptr, tptr + nbytes_copied, nb)
2✔
585
                    nbytes_copied += nb
2✔
586
                    a.parent[ind_start + i, tailinds...] = s[]
2✔
587
                    i += 1
2✔
588
                end
2✔
589
                # Deal with trailing partial elements
590
                if nbytes_copied < sizeof(T)
6✔
591
                    s[] = a.parent[ind_start + i, tailinds...]
6✔
592
                    nb = min(sizeof(S), sizeof(T) - nbytes_copied)
6✔
593
                    memcpy(sptr, tptr + nbytes_copied, nb)
6✔
594
                    a.parent[ind_start + i, tailinds...] = s[]
4✔
595
                end
596
            end
597
        end
598
    end
599
    return a
40,660,881✔
600
end
601

602
@inline @propagate_inbounds function _setindex_ra!(a::ReshapedReinterpretArray{T,N,S}, v, i1::Int, tailinds::TT) where {T,N,S,TT}
34✔
603
    v = convert(T, v)::T
34✔
604
    # Make sure to match the scalar reinterpret if that is applicable
605
    if sizeof(T) == sizeof(S) && (fieldcount(T) + fieldcount(S)) == 0
33✔
606
        if issingletontype(T) # singleton types
7✔
607
            @boundscheck checkbounds(a, i1, tailinds...)
3✔
608
            # setindex! is a noop except for the index check
609
        else
610
            setindex!(a.parent, reinterpret(S, v), i1, tailinds...)
5✔
611
        end
612
    end
613
    @boundscheck checkbounds(a, i1, tailinds...)
32✔
614
    if sizeof(T) >= sizeof(S)
32✔
615
        t = Ref{T}(v)
11✔
616
        GC.@preserve t begin
11✔
617
            sptr = Ptr{S}(unsafe_convert(Ref{T}, t))
11✔
618
            if sizeof(T) > sizeof(S)
11✔
619
                # Extra dimension in the parent array
620
                n = sizeof(T) ÷ sizeof(S)
3✔
621
                if isempty(tailinds) && IndexStyle(a.parent) === IndexLinear()
3✔
622
                    offset = n * (i1 - firstindex(a))
2✔
623
                    for i = 1:n
2✔
624
                        s = unsafe_load(sptr, i)
4✔
625
                        a.parent[i + offset] = s
4✔
626
                    end
6✔
627
                else
628
                    for i = 1:n
1✔
629
                        s = unsafe_load(sptr, i)
2✔
630
                        a.parent[i, i1, tailinds...] = s
2✔
631
                    end
3✔
632
                end
633
            else # sizeof(T) == sizeof(S)
634
                # No extra dimension
635
                s = unsafe_load(sptr)
8✔
636
                a.parent[i1, tailinds...] = s
11✔
637
            end
638
        end
639
    else
640
        # S is bigger than T and contains an integer number of them
641
        s = Ref{S}()
21✔
642
        GC.@preserve s begin
21✔
643
            tptr = Ptr{T}(unsafe_convert(Ref{S}, s))
21✔
644
            s[] = a.parent[tailinds...]
23✔
645
            unsafe_store!(tptr, v, i1)
21✔
646
            a.parent[tailinds...] = s[]
21✔
647
        end
648
    end
649
    return a
32✔
650
end
651

652
# Padding
653
struct Padding
654
    offset::Int # 0-indexed offset of the next valid byte; sizeof(T) indicates trailing padding
82✔
655
    size::Int   # bytes of padding before a valid byte
656
end
657
function intersect(p1::Padding, p2::Padding)
16✔
658
    start = max(p1.offset, p2.offset)
16✔
659
    stop = min(p1.offset + p1.size, p2.offset + p2.size)
16✔
660
    Padding(start, max(0, stop-start))
16✔
661
end
662

663
struct PaddingError <: Exception
664
    S::Type
2✔
665
    T::Type
666
end
667

668
function showerror(io::IO, p::PaddingError)
×
669
    print(io, "Padding of type $(p.S) is not compatible with type $(p.T).")
×
670
end
671

672
"""
673
    CyclePadding(padding, total_size)
674

675
Cycles an iterator of `Padding` structs, restarting the padding at `total_size`.
676
E.g. if `padding` is all the padding in a struct and `total_size` is the total
677
aligned size of that array, `CyclePadding` will correspond to the padding in an
678
infinite vector of such structs.
679
"""
680
struct CyclePadding{P}
681
    padding::P
107✔
682
    total_size::Int
683
end
684
eltype(::Type{<:CyclePadding}) = Padding
×
685
IteratorSize(::Type{<:CyclePadding}) = IsInfinite()
×
686
isempty(cp::CyclePadding) = isempty(cp.padding)
×
687
function iterate(cp::CyclePadding)
363✔
688
    y = iterate(cp.padding)
383✔
689
    y === nothing && return nothing
363✔
690
    y[1], (0, y[2])
20✔
691
end
692
function iterate(cp::CyclePadding, state::Tuple)
32✔
693
    y = iterate(cp.padding, tail(state)...)
48✔
694
    y === nothing && return iterate(cp, (state[1]+cp.total_size,))
32✔
695
    Padding(y[1].offset+state[1], y[1].size), (state[1], tail(y)...)
16✔
696
end
697

698
"""
699
    Compute the location of padding in an isbits datatype. Recursive over the fields of that type.
700
"""
701
@assume_effects :foldable function padding(T::DataType, baseoffset::Int = 0)
1,253✔
702
    pads = Padding[]
1,328✔
703
    last_end::Int = baseoffset
×
704
    for i = 1:fieldcount(T)
1,286✔
705
        offset = baseoffset + Int(fieldoffset(T, i))
714✔
706
        fT = fieldtype(T, i)
714✔
707
        append!(pads, padding(fT, offset))
714✔
708
        if offset != last_end
714✔
709
            push!(pads, Padding(offset, offset-last_end))
42✔
710
        end
711
        last_end = offset + sizeof(fT)
714✔
712
    end
1,213✔
713
    if 0 < last_end - baseoffset < sizeof(T)
1,071✔
714
        push!(pads, Padding(baseoffset + sizeof(T), sizeof(T) - last_end + baseoffset))
4✔
715
    end
716
    return Core.svec(pads...)
1,146✔
717
end
718

719
function CyclePadding(T::DataType)
16✔
720
    a, s = datatype_alignment(T), sizeof(T)
16✔
721
    as = s + (a - (s % a)) % a
16✔
722
    pad = padding(T)
16✔
723
    if s != as
16✔
724
        pad = Core.svec(pad..., Padding(s, as - s))
×
725
    end
726
    CyclePadding(pad, as)
16✔
727
end
728

729
@assume_effects :total function array_subpadding(S, T)
8✔
730
    lcm_size = lcm(sizeof(S), sizeof(T))
8✔
731
    s, t = CyclePadding(S), CyclePadding(T)
8✔
732
    checked_size = 0
8✔
733
    # use of Stateful harms inference and makes this vulnerable to invalidation
734
    (pad, tstate) = let
×
735
        it = iterate(t)
16✔
736
        it === nothing && return true
8✔
737
        it
8✔
738
    end
739
    (ps, sstate) = let
×
740
        it = iterate(s)
16✔
741
        it === nothing && return false
8✔
742
        it
8✔
743
    end
744
    while checked_size < lcm_size
16✔
745
        while true
12✔
746
            # See if there's corresponding padding in S
747
            ps.offset > pad.offset && return false
40✔
748
            intersect(ps, pad) == pad && break
16✔
749
            ps, sstate = iterate(s, sstate)
8✔
750
        end
8✔
751
        checked_size = pad.offset + pad.size
8✔
752
        pad, tstate = iterate(t, tstate)
8✔
753
    end
8✔
754
    return true
4✔
755
end
756

757
@assume_effects :foldable function struct_subpadding(::Type{Out}, ::Type{In}) where {Out, In}
82✔
758
    padding(Out) == padding(In)
82✔
759
end
760

761
@assume_effects :foldable function packedsize(::Type{T}) where T
2✔
762
    pads = padding(T)
2✔
763
    return sizeof(T) - sum((p.size for p ∈ pads), init = 0)
2✔
764
end
765

766
@assume_effects :foldable ispacked(::Type{T}) where T = isempty(padding(T))
×
767

768
function _copytopacked!(ptr_out::Ptr{Out}, ptr_in::Ptr{In}) where {Out, In}
4✔
769
    writeoffset = 0
4✔
770
    for i ∈ 1:fieldcount(In)
4✔
771
        readoffset = fieldoffset(In, i)
13✔
772
        fT = fieldtype(In, i)
13✔
773
        if ispacked(fT)
15✔
774
            readsize = sizeof(fT)
14✔
775
            memcpy(ptr_out + writeoffset, ptr_in + readoffset, readsize)
12✔
776
            writeoffset += readsize
12✔
777
        else # nested padded type
778
            _copytopacked!(ptr_out + writeoffset, Ptr{fT}(ptr_in + readoffset))
2✔
779
            writeoffset += packedsize(fT)
1✔
780
        end
781
    end
14✔
782
end
783

784
function _copyfrompacked!(ptr_out::Ptr{Out}, ptr_in::Ptr{In}) where {Out, In}
4✔
785
    readoffset = 0
4✔
786
    for i ∈ 1:fieldcount(Out)
4✔
787
        writeoffset = fieldoffset(Out, i)
13✔
788
        fT = fieldtype(Out, i)
13✔
789
        if ispacked(fT)
15✔
790
            writesize = sizeof(fT)
14✔
791
            memcpy(ptr_out + writeoffset, ptr_in + readoffset, writesize)
12✔
792
            readoffset += writesize
12✔
793
        else # nested padded type
794
            _copyfrompacked!(Ptr{fT}(ptr_out + writeoffset), ptr_in + readoffset)
2✔
795
            readoffset += packedsize(fT)
1✔
796
        end
797
    end
14✔
798
end
799

800
@inline function _reinterpret(::Type{Out}, x::In) where {Out, In}
3,648✔
801
    # handle non-primitive types
802
    isbitstype(Out) || throw(ArgumentError("Target type for `reinterpret` must be isbits"))
3,648✔
803
    isbitstype(In) || throw(ArgumentError("Source type for `reinterpret` must be isbits"))
3,647✔
804
    inpackedsize = packedsize(In)
3,646✔
805
    outpackedsize = packedsize(Out)
3,646✔
806
    inpackedsize == outpackedsize ||
3,646✔
807
        throw(ArgumentError("Packed sizes of types $Out and $In do not match; got $outpackedsize \
808
            and $inpackedsize, respectively."))
809
    in = Ref{In}(x)
3,642✔
810
    out = Ref{Out}()
3,642✔
811
    if struct_subpadding(Out, In)
3,642✔
812
        # if packed the same, just copy
813
        GC.@preserve in out begin
3,639✔
814
            ptr_in = unsafe_convert(Ptr{In}, in)
3,639✔
815
            ptr_out = unsafe_convert(Ptr{Out}, out)
3,639✔
816
            memcpy(ptr_out, ptr_in, sizeof(Out))
3,639✔
817
        end
818
        return out[]
3,639✔
819
    else
820
        # mismatched padding
821
        GC.@preserve in out begin
3✔
822
            ptr_in = unsafe_convert(Ptr{In}, in)
3✔
823
            ptr_out = unsafe_convert(Ptr{Out}, out)
3✔
824

825
            if fieldcount(In) > 0 && ispacked(Out)
3✔
826
                _copytopacked!(ptr_out, ptr_in)
×
827
            elseif fieldcount(Out) > 0 && ispacked(In)
3✔
828
                _copyfrompacked!(ptr_out, ptr_in)
×
829
            else
830
                packed = Ref{NTuple{inpackedsize, UInt8}}()
3✔
831
                GC.@preserve packed begin
3✔
832
                    ptr_packed = unsafe_convert(Ptr{NTuple{inpackedsize, UInt8}}, packed)
3✔
833
                    _copytopacked!(ptr_packed, ptr_in)
3✔
834
                    _copyfrompacked!(ptr_out, ptr_packed)
3✔
835
                end
836
            end
837
        end
838
        return out[]
3✔
839
    end
840
end
841

842

843
# Reductions with IndexSCartesian2
844

845
function _mapreduce(f::F, op::OP, style::IndexSCartesian2{K}, A::AbstractArrayOrBroadcasted) where {F,OP,K}
2✔
846
    inds = eachindex(style, A)
2✔
847
    n = size(inds)[2]
2✔
848
    if n == 0
2✔
849
        return mapreduce_empty_iter(f, op, A, IteratorEltype(A))
×
850
    else
851
        return mapreduce_impl(f, op, A, first(inds), last(inds))
2✔
852
    end
853
end
854

855
@noinline function mapreduce_impl(f::F, op::OP, A::AbstractArrayOrBroadcasted,
4✔
856
                                  ifirst::SCI, ilast::SCI, blksize::Int) where {F,OP,SCI<:SCartesianIndex2{K}} where K
857
    if ilast.j - ifirst.j < blksize
4✔
858
        # sequential portion
859
        @inbounds a1 = A[ifirst]
3✔
860
        @inbounds a2 = A[SCI(2,ifirst.j)]
3✔
861
        v = op(f(a1), f(a2))
3✔
862
        @simd for i = ifirst.i + 2 : K
3✔
863
            @inbounds ai = A[SCI(i,ifirst.j)]
3✔
864
            v = op(v, f(ai))
3✔
865
        end
×
866
        # Remaining columns
867
        for j = ifirst.j+1 : ilast.j
6✔
868
            @simd for i = 1:K
1,333✔
869
                @inbounds ai = A[SCI(i,j)]
3,999✔
870
                v = op(v, f(ai))
3,999✔
871
            end
×
872
        end
2,663✔
873
        return v
3✔
874
    else
875
        # pairwise portion
876
        jmid = ifirst.j + (ilast.j - ifirst.j) >> 1
1✔
877
        v1 = mapreduce_impl(f, op, A, ifirst, SCI(K,jmid), blksize)
1✔
878
        v2 = mapreduce_impl(f, op, A, SCI(1,jmid+1), ilast, blksize)
1✔
879
        return op(v1, v2)
1✔
880
    end
881
end
882

883
mapreduce_impl(f::F, op::OP, A::AbstractArrayOrBroadcasted, ifirst::SCartesianIndex2, ilast::SCartesianIndex2) where {F,OP} =
2✔
884
    mapreduce_impl(f, op, A, ifirst, ilast, pairwise_blocksize(f, op))
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

© 2025 Coveralls, Inc