Skip to content

Commit 7e2ce0e

Browse files
authored
RFC: Deprecate implicit scalar broadcasting in setindex! (#26347)
The current `setindex!` function (the `A[i] = b` syntax) does three very different operations: * Given an index `i` that refers to only one location (scalar indexing), `A[i] = b` modifies `A` in that location to hold `b`. * Given an index `I` that refers to more than one location (non-scalar indexing), `A[I] = b` can mean one of two things: * If `b` is an AbstractArray (multi-value), assign the values it contains to those locations `I` in `A`. That is, essentially, `[A[i] = bᵢ for (i, bᵢ) in zip(I, b)]`. * If `b` is not an AbstractArray (scalar-value), then broadcast its value to every location selected by `I` in `A`. These two different behaviors in the non-scalar indexing case basically make using this function in a generic way impossible. If you want to _always_ set the value `b` to many indices of `A`, you _cannot_ use this syntax because `b` might happen to be an array in some cases, radically changing the meaning of the expression. You need to manually iterate over the indices, using scalar setindex methods. But we now also have the new `broadcast!` syntax, `A[I] .= B`, which uses the usual broadcasting semantics to determine how `B` should fill into the values of `A`. This PR deprecates the implicit broadcasting of scalar values in non-scalar indexing in favor of an explicit `.=` broadcast syntax, leaving multi-value non-scalar indexing intact. This is the _exact opposite_ of PR #24368.
1 parent 5a062fa commit 7e2ce0e

33 files changed

+316
-246
lines changed

NEWS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,18 @@ Deprecated or removed
697697
`Matrix{Int}(undef, (2, 4))`, and `Array{Float32,3}(11, 13, 17)` is now
698698
`Array{Float32,3}(undef, 11, 13, 17)` ([#24781]).
699699

700+
* Previously `setindex!(A, x, I...)` (and the syntax `A[I...] = x`) supported two
701+
different modes of operation when supplied with a set of non-scalar indices `I`
702+
(e.g., at least one index is an `AbstractArray`) depending upon the value of `x`
703+
on the right hand side. If `x` is an `AbstractArray`, its _contents_ are copied
704+
elementwise into the locations in `A` selected by `I` and it must have the same
705+
number of elements as `I` selects locations. Otherwise, if `x` is not an
706+
`AbstractArray`, then its _value_ is implicitly broadcast to all locations to
707+
all locations in `A` selected by `I`. This latter behavior—implicitly broadcasting
708+
"scalar"-like values across many locations—is now deprecated in favor of explicitly
709+
using the broadcasted assignment syntax `A[I...] .= x` or `fill!(view(A, I...), x)`
710+
([#26347]).
711+
700712
* `LinAlg.fillslots!` has been renamed `LinAlg.fillstored!` ([#25030]).
701713

702714
* `fill!(A::Diagonal, x)` and `fill!(A::AbstractTriangular, x)` have been deprecated

base/abstractarray.jl

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,8 +1181,8 @@ function get!(X::AbstractArray{T}, A::AbstractArray, I::Union{AbstractRange,Abst
11811181
# Linear indexing
11821182
ind = findall(in(1:length(A)), I)
11831183
X[ind] = A[I[ind]]
1184-
X[1:first(ind)-1] = default
1185-
X[last(ind)+1:length(X)] = default
1184+
fill!(view(X, 1:first(ind)-1), default)
1185+
fill!(view(X, last(ind)+1:length(X)), default)
11861186
X
11871187
end
11881188

@@ -1380,7 +1380,11 @@ function _cat(A, shape::NTuple{N}, catdims, X...) where N
13801380
end
13811381
end
13821382
I::NTuple{N, UnitRange{Int}} = (inds...,)
1383-
A[I...] = x
1383+
if x isa AbstractArray
1384+
A[I...] = x
1385+
else
1386+
fill!(view(A, I...), x)
1387+
end
13841388
end
13851389
return A
13861390
end
@@ -1930,27 +1934,27 @@ function mapslices(f, A::AbstractArray, dims::AbstractVector)
19301934
ridx[d] = axes(R,d)
19311935
end
19321936

1933-
R[ridx...] = r1
1937+
concatenate_setindex!(R, r1, ridx...)
19341938

19351939
nidx = length(otherdims)
1936-
indices = Iterators.drop(CartesianIndices(itershape), 1)
1940+
indices = Iterators.drop(CartesianIndices(itershape), 1) # skip the first element, we already handled it
19371941
inner_mapslices!(safe_for_reuse, indices, nidx, idx, otherdims, ridx, Aslice, A, f, R)
19381942
end
19391943

19401944
@noinline function inner_mapslices!(safe_for_reuse, indices, nidx, idx, otherdims, ridx, Aslice, A, f, R)
19411945
if safe_for_reuse
19421946
# when f returns an array, R[ridx...] = f(Aslice) line copies elements,
19431947
# so we can reuse Aslice
1944-
for I in indices # skip the first element, we already handled it
1948+
for I in indices
19451949
replace_tuples!(nidx, idx, ridx, otherdims, I)
19461950
_unsafe_getindex!(Aslice, A, idx...)
1947-
R[ridx...] = f(Aslice)
1951+
concatenate_setindex!(R, f(Aslice), ridx...)
19481952
end
19491953
else
19501954
# we can't guarantee safety (#18524), so allocate new storage for each slice
19511955
for I in indices
19521956
replace_tuples!(nidx, idx, ridx, otherdims, I)
1953-
R[ridx...] = f(A[idx...])
1957+
concatenate_setindex!(R, f(A[idx...]), ridx...)
19541958
end
19551959
end
19561960

@@ -1963,6 +1967,8 @@ function replace_tuples!(nidx, idx, ridx, otherdims, I)
19631967
end
19641968
end
19651969

1970+
concatenate_setindex!(R, v, I...) = (R[I...] .= (v,); R)
1971+
concatenate_setindex!(R, X::AbstractArray, I...) = (R[I...] = X)
19661972

19671973
## 1 argument
19681974

base/abstractarraymath.jl

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -363,10 +363,6 @@ _rshps(shp, shp_i, sz, i, ::Tuple{}) =
363363
_reperr(s, n, N) = throw(ArgumentError("number of " * s * " repetitions " *
364364
"($n) cannot be less than number of dimensions of input ($N)"))
365365

366-
# We need special handling when repeating arrays of arrays
367-
cat_fill!(R, X, inds) = (R[inds...] = X)
368-
cat_fill!(R, X::AbstractArray, inds) = fill!(view(R, inds...), X)
369-
370366
@noinline function _repeat(A::AbstractArray, inner, outer)
371367
shape, inner_shape = rep_shapes(A, inner, outer)
372368

@@ -385,7 +381,7 @@ cat_fill!(R, X::AbstractArray, inds) = fill!(view(R, inds...), X)
385381
n = inner[i]
386382
inner_indices[i] = (1:n) .+ ((c[i] - 1) * n)
387383
end
388-
cat_fill!(R, A[c], inner_indices)
384+
fill!(view(R, inner_indices...), A[c])
389385
end
390386
end
391387

base/array.jl

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -706,29 +706,6 @@ function setindex! end
706706
@eval setindex!(A::Array{T}, x, i1::Int, i2::Int, I::Int...) where {T} =
707707
(@_inline_meta; arrayset($(Expr(:boundscheck)), A, convert(T,x)::T, i1, i2, I...))
708708

709-
# These are redundant with the abstract fallbacks but needed for bootstrap
710-
function setindex!(A::Array, x, I::AbstractVector{Int})
711-
@_propagate_inbounds_meta
712-
I′ = unalias(A, I)
713-
for i in I′
714-
A[i] = x
715-
end
716-
return A
717-
end
718-
function setindex!(A::Array, X::AbstractArray, I::AbstractVector{Int})
719-
@_propagate_inbounds_meta
720-
@boundscheck setindex_shape_check(X, length(I))
721-
X′ = unalias(A, X)
722-
I′ = unalias(A, I)
723-
count = 1
724-
for i in I′
725-
@inbounds x = X′[count]
726-
A[i] = x
727-
count += 1
728-
end
729-
return A
730-
end
731-
732709
# Faster contiguous setindex! with copyto!
733710
function setindex!(A::Array{T}, X::Array{T}, I::UnitRange{Int}) where T
734711
@_inline_meta
@@ -750,9 +727,6 @@ function setindex!(A::Array{T}, X::Array{T}, c::Colon) where T
750727
return A
751728
end
752729

753-
setindex!(A::Array, x::Number, ::Colon) = fill!(A, x)
754-
setindex!(A::Array{T, N}, x::Number, ::Vararg{Colon, N}) where {T, N} = fill!(A, x)
755-
756730
# efficiently grow an array
757731

758732
_growbeg!(a::Vector, delta::Integer) =

base/bitarray.jl

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ function gen_bitarray_from_itr(itr, st)
584584
end
585585
end
586586
if ind > 1
587-
@inbounds C[ind:bitcache_size] = false
587+
@inbounds C[ind:bitcache_size] .= false
588588
resize!(B, length(B) + ind - 1)
589589
dumpbitcache(Bc, cind, C)
590590
end
@@ -608,7 +608,7 @@ function fill_bitarray_from_itr!(B::BitArray, itr, st)
608608
end
609609
end
610610
if ind > 1
611-
@inbounds C[ind:bitcache_size] = false
611+
@inbounds C[ind:bitcache_size] .= false
612612
dumpbitcache(Bc, cind, C)
613613
end
614614
return B
@@ -653,44 +653,10 @@ end
653653
indexoffset(i) = first(i)-1
654654
indexoffset(::Colon) = 0
655655

656-
@inline function setindex!(B::BitArray, x, J0::Union{Colon,UnitRange{Int}})
657-
I0 = to_indices(B, (J0,))[1]
658-
@boundscheck checkbounds(B, I0)
659-
y = Bool(x)
660-
l0 = length(I0)
661-
l0 == 0 && return B
662-
f0 = indexoffset(I0)+1
663-
fill_chunks!(B.chunks, y, f0, l0)
664-
return B
665-
end
666656
@propagate_inbounds function setindex!(B::BitArray, X::AbstractArray, J0::Union{Colon,UnitRange{Int}})
667657
_setindex!(IndexStyle(B), B, X, to_indices(B, (J0,))[1])
668658
end
669659

670-
# logical indexing
671-
672-
# When indexing with a BitArray, we can operate whole chunks at a time for a ~100x gain
673-
@inline function setindex!(B::BitArray, x, I::BitArray)
674-
@boundscheck checkbounds(B, I)
675-
_unsafe_setindex!(B, x, I)
676-
end
677-
function _unsafe_setindex!(B::BitArray, x, I::BitArray)
678-
y = convert(Bool, x)
679-
Bc = B.chunks
680-
Ic = I.chunks
681-
length(Bc) == length(Ic) || throw_boundserror(B, I)
682-
@inbounds if y
683-
for i = 1:length(Bc)
684-
Bc[i] |= Ic[i]
685-
end
686-
else
687-
for i = 1:length(Bc)
688-
Bc[i] &= ~Ic[i]
689-
end
690-
end
691-
return B
692-
end
693-
694660
# Assigning an array of bools is more complicated, but we can still do some
695661
# work on chunks by combining X and I 64 bits at a time to improve perf by ~40%
696662
@inline function setindex!(B::BitArray, X::AbstractArray, I::BitArray)

base/bitset.jl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,16 @@ end
114114
@inline function _growend0!(b::Bits, nchunks::Int)
115115
len = length(b)
116116
_growend!(b, nchunks)
117-
@inbounds b[len+1:end] = CHK0 # resize! gives dirty memory
117+
for i in len+1:length(b)
118+
@inbounds b[i] = CHK0 # resize! gives dirty memory
119+
end
118120
end
119121

120122
@inline function _growbeg0!(b::Bits, nchunks::Int)
121123
_growbeg!(b, nchunks)
122-
@inbounds b[1:nchunks] = CHK0
124+
for i in 1:nchunks
125+
@inbounds b[i] = CHK0
126+
end
123127
end
124128

125129
function _matched_map!(f, s1::BitSet, s2::BitSet)

base/broadcast.jl

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,7 @@ end
832832
end
833833
end
834834
if ind > 1
835-
@inbounds tmp[ind:bitcache_size] = false
835+
@inbounds tmp[ind:bitcache_size] .= false
836836
dumpbitcache(destc, cind, tmp)
837837
end
838838
return dest
@@ -1108,6 +1108,39 @@ See [`broadcast_getindex`](@ref) for examples of the treatment of `inds`.
11081108
end
11091109
end
11101110

1111+
## In specific instances, we can broadcast masked BitArrays whole chunks at a time
1112+
# Very intentionally do not support much functionality here: scalar indexing would be O(n)
1113+
struct BitMaskedBitArray{N,M}
1114+
parent::BitArray{N}
1115+
mask::BitArray{M}
1116+
BitMaskedBitArray{N,M}(parent, mask) where {N,M} = new(parent, mask)
1117+
end
1118+
@inline function BitMaskedBitArray(parent::BitArray{N}, mask::BitArray{M}) where {N,M}
1119+
@boundscheck checkbounds(parent, mask)
1120+
BitMaskedBitArray{N,M}(parent, mask)
1121+
end
1122+
Base.@propagate_inbounds dotview(B::BitArray, i::BitArray) = BitMaskedBitArray(B, i)
1123+
Base.show(io::IO, B::BitMaskedBitArray) = foreach(arg->show(io, arg), (typeof(B), (B.parent, B.mask)))
1124+
# Override materialize! to prevent the BitMaskedBitArray from escaping to an overrideable method
1125+
@inline materialize!(B::BitMaskedBitArray, bc::Broadcasted{<:Any,<:Any,typeof(identity),Tuple{Bool}}) = fill!(B, bc.args[1])
1126+
@inline materialize!(B::BitMaskedBitArray, bc::Broadcasted{<:Any}) = materialize!(SubArray(B.parent, to_indices(B.parent, (B.mask,))), bc)
1127+
function Base.fill!(B::BitMaskedBitArray, b::Bool)
1128+
Bc = B.parent.chunks
1129+
Ic = B.mask.chunks
1130+
@inbounds if b
1131+
for i = 1:length(Bc)
1132+
Bc[i] |= Ic[i]
1133+
end
1134+
else
1135+
for i = 1:length(Bc)
1136+
Bc[i] &= ~Ic[i]
1137+
end
1138+
end
1139+
return B
1140+
end
1141+
1142+
1143+
11111144
############################################################
11121145

11131146
# x[...] .= f.(y...) ---> broadcast!(f, dotview(x, ...), y...).

base/char.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,10 @@ widen(::Type{T}) where {T<:AbstractChar} = T
215215
print(io::IO, c::Char) = (write(io, c); nothing)
216216
print(io::IO, c::AbstractChar) = print(io, Char(c)) # fallback: convert to output UTF-8
217217

218-
const hex_chars = UInt8['0':'9';'a':'z']
218+
const hex_chars = UInt8['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
219+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
220+
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
221+
's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
219222

220223
function show_invalid(io::IO, c::Char)
221224
write(io, 0x27)

base/compiler/ssair/ir.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,9 @@ function resize!(compact::IncrementalCompact, nnewnodes)
588588
resize!(compact.result_lines, nnewnodes)
589589
resize!(compact.result_flags, nnewnodes)
590590
resize!(compact.used_ssas, nnewnodes)
591-
compact.used_ssas[(old_length+1):nnewnodes] = 0
591+
for i in (old_length+1):nnewnodes
592+
compact.used_ssas[i] = 0
593+
end
592594
nothing
593595
end
594596

base/compiler/ssair/slot2ssa.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,8 @@ function domsort_ssa!(ir::IRCode, domtree::DomTree)
377377
end
378378
old_inst_range = ir.cfg.blocks[bb].stmts
379379
inst_range = (bb_start_off+1):(bb_start_off+length(old_inst_range))
380-
inst_rename[old_inst_range] = Any[SSAValue(x) for x in inst_range]
381380
for (nidx, idx) in zip(inst_range, old_inst_range)
381+
inst_rename[idx] = SSAValue(nidx)
382382
stmt = ir.stmts[idx]
383383
if isa(stmt, PhiNode)
384384
result_stmts[nidx] = rename_phinode_edges(stmt, bb, result_order, bb_rename)

base/deprecated.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,6 +1491,27 @@ function slicedim(A::AbstractVector, d::Integer, i::Number)
14911491
end
14921492
end
14931493

1494+
# PR #26347: Deprecate implicit scalar broadcasting in setindex!
1495+
_axes(::Ref) = ()
1496+
_axes(x) = axes(x)
1497+
function deprecate_scalar_setindex_broadcast_message(v, I...)
1498+
value = (_axes(Base.Broadcast.broadcastable(v)) == () ? "x" : "(x,)")
1499+
"using `A[I...] = x` to implicitly broadcast `x` across many locations is deprecated. Use `A[I...] .= $value` instead."
1500+
end
1501+
deprecate_scalar_setindex_broadcast_message(v, ::Colon, ::Vararg{Colon}) =
1502+
"using `A[:] = x` to implicitly broadcast `x` across many locations is deprecated. Use `fill!(A, x)` instead."
1503+
1504+
function _iterable(v, I...)
1505+
depwarn(deprecate_scalar_setindex_broadcast_message(v, I...), :setindex!)
1506+
Iterators.repeated(v)
1507+
end
1508+
function setindex!(B::BitArray, x, I0::Union{Colon,UnitRange{Int}}, I::Union{Int,UnitRange{Int},Colon}...)
1509+
depwarn(deprecate_scalar_setindex_broadcast_message(x, I0, I...), :setindex!)
1510+
B[I0, I...] .= (x,)
1511+
B
1512+
end
1513+
1514+
14941515
# PR #26283
14951516
@deprecate contains(haystack, needle) occursin(needle, haystack)
14961517
@deprecate contains(s::AbstractString, r::Regex, offset::Integer) occursin(r, s, offset=offset)

base/iobuffer.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function IOBuffer(;
117117
append=flags.append,
118118
truncate=flags.truncate,
119119
maxsize=maxsize)
120-
buf.data[:] = 0
120+
fill!(buf.data, 0)
121121
return buf
122122
end
123123

@@ -246,7 +246,7 @@ function truncate(io::GenericIOBuffer, n::Integer)
246246
if n > length(io.data)
247247
resize!(io.data, n)
248248
end
249-
io.data[io.size+1:n] = 0
249+
io.data[io.size+1:n] .= 0
250250
io.size = n
251251
io.ptr = min(io.ptr, n+1)
252252
ismarked(io) && io.mark > n && unmark(io)

0 commit comments

Comments
 (0)