From 09a632badb6a62c901248c73c914a1e7b22f7686 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sun, 1 Oct 2017 06:53:23 -0500 Subject: [PATCH] Rework the broadcast API and document it (fixes #20740) --- base/broadcast.jl | 467 ++++++++++++++++++++++++---------- base/cartesian.jl | 2 +- base/deprecated.jl | 12 + base/exports.jl | 1 + base/sparse/higherorderfns.jl | 177 +++++-------- doc/src/manual/interfaces.md | 186 ++++++++++++++ doc/src/stdlib/arrays.md | 6 + test/broadcast.jl | 138 +++++++--- test/sparse/higherorderfns.jl | 16 +- 9 files changed, 719 insertions(+), 286 deletions(-) diff --git a/base/broadcast.jl b/base/broadcast.jl index 09f2b2c605aad..435c410bfccdf 100644 --- a/base/broadcast.jl +++ b/base/broadcast.jl @@ -3,63 +3,207 @@ module Broadcast using Base.Cartesian -using Base: linearindices, tail, OneTo, to_shape, +using Base: Bottom, Indices, OneTo, linearindices, tail, to_shape, _msk_end, unsafe_bitgetindex, bitcache_chunks, bitcache_size, dumpbitcache, nullable_returntype, null_safe_op, hasvalue, isoperator import Base: broadcast, broadcast! export broadcast_getindex, broadcast_setindex!, dotview, @__dot__ -const ScalarType = Union{Type{Any}, Type{Nullable}} +# Note: `indices` will be overridden below, thus you need to use +# Base.indices when you want the Base versions. + +## Types used by `rule` +# Unknown acts a bit like `Bottom`, in that it loses to everything. But by not having +# it be a subtype of every other type, we limit the need for ambiguity resolution. +abstract type Unknown end +# Objects that act like a scalar for purposes of broadcasting +abstract type Scalar end +# An AbstractArray type that "loses" in precedence comparisons to all other AbstractArrays. +# We will want to keep track of dimensionality, so we make it the first parameter. +abstract type BottomArray{N,T} <: AbstractArray{T,N} end +Bottom0d = BottomArray{0} +BottomVector = BottomArray{1} +BottomMatrix = BottomArray{2} +# When two or more AbstractArrays have specialized broadcasting, and no `rule` +# is defined to establish precedence, then we have a conflict +abstract type ArrayConflict{T,N} <: AbstractArray{T,N} end + + +""" + result = Broadcast.Result{ContainerType}() + result = Broadcast.Result{ContainerType,ElType}(inds::Indices) + +Create an object that specifies the type and (optionally) indices +of the result (output) of a broadcasting operation. + +Using a dedicated type for this information makes it possible to support +variants of [`broadcast`](@ref) that accept `result` as an argument; +it prevents an ambiguity of intent that would otherwise arise because +both types and indices-tuples are among the supported *input* +arguments to `broadcast`. For example, `parse.(Int, ("1", "2"))` is +equivalent to `broadcast(parse, Int, ("1", "2"))`, and as a consequence +it would would be ambiguous if result-type and output-indices information +were passed as positional arguments to `broadcast`. + +You can extract `inds` with `indices(result)`. +""" +struct Result{ContainerType,ElType,I<:Union{Void,Indices}} + indices::I +end +Result{ContainerType}() where ContainerType = + Result{ContainerType,Void,Void}(nothing) +Result{ContainerType,ElType}(inds::Indices) where {ContainerType,ElType} = + Result{ContainerType,ElType,typeof(inds)}(inds) +indices(r::Result) = r.indices +Base.indices(r::Result) = indices(r) +Base.eltype(r::Result{ContainerType,ElType}) where {ContainerType,ElType} = ElType + + +### User-extensible methods (see the Interfaces chapter of the manual) ### +## Computing the result (output) type +""" + Broadcast.rule(::Type{<:MyContainer}) = MyContainer + +Declare that objects of type `MyContainer` have a customized broadcast implementation. +If you define this method, you are responsible for defining the following method: + + Base.similar(f, r::Broadcast.Result{MyContainer}, As...) = ... + +where `f` is the function you're broadcasting, `r` is a [`Broadcast.Result`](@ref) +indicating the eltype and indices of the output container, and `As...` contains +the input arguments to `broadcast`. +""" +rule(::Type{Bottom}) = Unknown # ambiguity resolution +rule(::Type{<:Ptr}) = Scalar # Ptrs act like scalars, not like Ref +rule(::Type{T}) where T = Scalar # Types act like scalars (e.g. `parse.(Int, ["1", "2"])`) +rule(::Type{<:Nullable}) = Nullable +rule(::Type{<:Tuple}) = Tuple +rule(::Type{<:Ref}) = Bottom0d +rule(::Type{<:AbstractArray{T,N}}) where {T,N} = BottomArray{N} + +# Note that undeclared types act like scalars due to the Type{T} rule + +""" + Broadcast.rule(::Type{S}, ::Type{T}) where {S,T} = U + +Indicate how to resolve different broadcast `rule`s. For example, + + Broadcast.rule(::Type{Primary}, ::Type{Secondary}) = Primary + +would indicate that `Primary` has precedence over `Secondary`. +You do not have to (and generally should not) define both argument orders. +The result does not have to be one of the input arguments, it could be a third type. + +Please see the Interfaces chapter of the manual for more information. +""" +rule(::Type{T}, ::Type{T}) where T = T # homogeneous types preserved +# Fall back to Unknown. This is necessary to implement argument-swapping +rule(::Type{S}, ::Type{T}) where {S,T} = Unknown +# Unknown loses to everything +rule(::Type{Unknown}, ::Type{Unknown}) = Unknown +rule(::Type{T}, ::Type{Unknown}) where T = T +# Precedence rules. Where applicable, the higher-precedence argument is placed first. +# This reduces the likelihood of method ambiguities. +rule(::Type{Nullable}, ::Type{Scalar}) = Nullable +rule(::Type{Tuple}, ::Type{Scalar}) = Tuple +rule(::Type{Bottom0d}, ::Type{Tuple}) = BottomVector +rule(::Type{BottomArray{N}}, ::Type{Tuple}) where N = BottomArray{N} +rule(::Type{BottomArray{N}}, ::Type{Scalar}) where N = BottomArray{N} +rule(::Type{BottomArray{N}}, ::Type{BottomArray{N}}) where N = BottomArray{N} +rule(::Type{BottomArray{M}}, ::Type{BottomArray{N}}) where {M,N} = _ruleba(longest(Val(M),Val(N))) +# Any specific array type beats BottomArray. With these fallbacks the dimensionality is not used +rule(::Type{A}, ::Type{BottomArray{N}}) where {A<:AbstractArray,N} = A +rule(::Type{A}, ::Type{Tuple}) where A<:AbstractArray = A +rule(::Type{A}, ::Type{Scalar}) where A<:AbstractArray = A + +## Allocating the output container +""" + dest = similar(f, r::Broadcast.Result{ContainerType}, As...) + +Allocate an output object `dest`, of the type signaled by `ContainerType`, for [`broadcast`](@ref). +`f` is the broadcast operations, and `As...` are the arguments supplied to `broadcast`. +See [`Broadcast.Result`](@ref) and [`Broadcast.rule`](@ref). +""" +Base.similar(f, r::Result{BottomArray{N}}, As...) where N = similar(Array{eltype(r)}, indices(r)) +# In cases of conflict we fall back on Array +Base.similar(f, r::Result{ArrayConflict}, As...) = similar(Array{eltype(r)}, indices(r)) + +## Computing the result's indices. Most types probably won't need to specialize this. +indices() = () +indices(::Type{T}) where T = () +indices(A) = indices(combine_types(A), A) +indices(::Type{Scalar}, A) = () +# indices(::Type{Any}, A) = () +indices(::Type{Nullable}, A) = () +indices(::Type{Tuple}, A) = (OneTo(length(A)),) +indices(::Type{BottomArray{N}}, A::Ref) where N = () +indices(::Type{<:AbstractArray}, A) = Base.indices(A) + +### End of methods that users will typically have to specialize ### ## Broadcasting utilities ## -# fallbacks for some special cases -@inline broadcast(f, x::Number...) = f(x...) -@inline broadcast(f, t::NTuple{N,Any}, ts::Vararg{NTuple{N,Any}}) where {N} = map(f, t, ts...) -broadcast!(::typeof(identity), x::Array{T,N}, y::Array{S,N}) where {T,S,N} = - size(x) == size(y) ? copy!(x, y) : broadcast_c!(identity, Array, Array, x, y) +# special cases +broadcast(f, x::Number...) = f(x...) +broadcast(f, t::NTuple{N,Any}, ts::Vararg{NTuple{N,Any}}) where {N} = map(f, t, ts...) +broadcast!(::typeof(identity), x::AbstractArray{T,N}, y::AbstractArray{S,N}) where {T,S,N} = + Base.indices(x) == Base.indices(y) ? copy!(x, y) : _broadcast!(identity, x, y) # special cases for "X .= ..." (broadcast!) assignments broadcast!(::typeof(identity), X::AbstractArray, x::Number) = fill!(X, x) broadcast!(f, X::AbstractArray, x::Number...) = (@inbounds for I in eachindex(X); X[I] = f(x...); end; X) -# logic for deciding the resulting container type -_containertype(::Type) = Any -_containertype(::Type{<:Ptr}) = Any -_containertype(::Type{<:Tuple}) = Tuple -_containertype(::Type{<:Ref}) = Array -_containertype(::Type{<:AbstractArray}) = Array -_containertype(::Type{<:Nullable}) = Nullable -containertype(x) = _containertype(typeof(x)) -containertype(ct1, ct2) = promote_containertype(containertype(ct1), containertype(ct2)) -@inline containertype(ct1, ct2, cts...) = promote_containertype(containertype(ct1), containertype(ct2, cts...)) - -promote_containertype(::Type{Array}, ::Type{Array}) = Array -promote_containertype(::Type{Array}, ct) = Array -promote_containertype(ct, ::Type{Array}) = Array -promote_containertype(::Type{Tuple}, ::ScalarType) = Tuple -promote_containertype(::ScalarType, ::Type{Tuple}) = Tuple -promote_containertype(::Type{Any}, ::Type{Nullable}) = Nullable -promote_containertype(::Type{Nullable}, ::Type{Any}) = Nullable -promote_containertype(::Type{T}, ::Type{T}) where {T} = T - -## Calculate the broadcast indices of the arguments, or error if incompatible -# array inputs -broadcast_indices() = () -broadcast_indices(A) = broadcast_indices(containertype(A), A) -@inline broadcast_indices(A, B...) = broadcast_shape(broadcast_indices(A), broadcast_indices(B...)) -broadcast_indices(::ScalarType, A) = () -broadcast_indices(::Type{Tuple}, A) = (OneTo(length(A)),) -broadcast_indices(::Type{Array}, A::Ref) = () -broadcast_indices(::Type{Array}, A) = indices(A) +## logic for deciding the resulting container type +# BottomArray dimensionality: computing max(M,N) in the type domain so we preserve inferrability +_ruleba(::NTuple{N,Bool}) where N = BottomArray{N} +longest(V1::Val, V2::Val) = longest(ntuple(identity, V1), ntuple(identity, V2)) +longest(t1::Tuple, t2::Tuple) = (true, longest(Base.tail(t1), Base.tail(t2))...) +longest(::Tuple{}, t2::Tuple) = (true, longest((), Base.tail(t2))...) +longest(t1::Tuple, ::Tuple{}) = (true, longest(Base.tail(t1), ())...) +longest(::Tuple{}, ::Tuple{}) = () + +# combine_types operates on values (arbitrarily many) +combine_types(c) = result_type(rule(typeof(c))) +combine_types(c1, c2) = result_type(combine_types(c1), combine_types(c2)) +combine_types(c1, c2, cs...) = result_type(combine_types(c1), combine_types(c2, cs...)) + +# result_type works on types (singletons and pairs), and leverages `rule` +result_type(::Type{T}) where T = T +result_type(::Type{T}, ::Type{T}) where T = T +# Test both orders so users typically only have to declare one order +result_type(::Type{S}, ::Type{T}) where {S,T} = result_join(S, T, rule(S, T), rule(T, S)) + +# result_join is the final referee. Because `rule` for undeclared pairs results in Unknown, +# we defer to any case where the result of `rule` is known. +result_join(::Type{S}, ::Type{T}, ::Type{Unknown}, ::Type{Unknown}) where {S,T} = Unknown +result_join(::Type{S}, ::Type{T}, ::Type{Unknown}, ::Type{U}) where {S,T,U} = U +result_join(::Type{S}, ::Type{T}, ::Type{U}, ::Type{Unknown}) where {S,T,U} = U +# For AbstractArray types with specialized broadcasting and undefined precedence rules, +# we have to signal conflict. Because ArrayConflict is a subtype of AbstractArray, +# this will "poison" any future operations (if we instead returned `BottomArray`, then for +# 3-array broadcasting the returned type would depend on argument order). +result_join(::Type{<:AbstractArray}, ::Type{<:AbstractArray}, ::Type{Unknown}, ::Type{Unknown}) = + ArrayConflict +# Fallbacks in case users define `rule` for both argument-orders (not recommended) +result_join(::Type{S}, ::Type{T}, ::Type{U}, ::Type{U}) where {S,T,U} = U +@noinline function result_join(::Type{S}, ::Type{T}, ::Type{U}, ::Type{V}) where {S,T,U,V} + error("""conflicting broadcast rules defined + Broadcast.rule($S, $T) = $U + Broadcast.rule($T, $S) = $V +One of these should be undefined (and thus return Broadcast.Unknown).""") +end + +# Indices utilities +combine_indices(A, B...) = broadcast_shape(indices(A), combine_indices(B...)) +combine_indices(A) = indices(A) # shape (i.e., tuple-of-indices) inputs broadcast_shape(shape::Tuple) = shape -@inline broadcast_shape(shape::Tuple, shape1::Tuple, shapes::Tuple...) = broadcast_shape(_bcs(shape, shape1), shapes...) +broadcast_shape(shape::Tuple, shape1::Tuple, shapes::Tuple...) = broadcast_shape(_bcs(shape, shape1), shapes...) # _bcs consolidates two shapes into a single output shape _bcs(::Tuple{}, ::Tuple{}) = () -@inline _bcs(::Tuple{}, newshape::Tuple) = (newshape[1], _bcs((), tail(newshape))...) -@inline _bcs(shape::Tuple, ::Tuple{}) = (shape[1], _bcs(tail(shape), ())...) -@inline function _bcs(shape::Tuple, newshape::Tuple) +_bcs(::Tuple{}, newshape::Tuple) = (newshape[1], _bcs((), tail(newshape))...) +_bcs(shape::Tuple, ::Tuple{}) = (shape[1], _bcs(tail(shape), ())...) +function _bcs(shape::Tuple, newshape::Tuple) return (_bcs1(shape[1], newshape[1]), _bcs(tail(shape), tail(newshape))...) end # _bcs1 handles the logic for a single dimension @@ -82,7 +226,7 @@ function check_broadcast_shape(shp, Ashp::Tuple) _bcsm(shp[1], Ashp[1]) || throw(DimensionMismatch("array could not be broadcast to match destination")) check_broadcast_shape(tail(shp), tail(Ashp)) end -check_broadcast_indices(shp, A) = check_broadcast_shape(shp, broadcast_indices(A)) +check_broadcast_indices(shp, A) = check_broadcast_shape(shp, indices(A)) # comparing many inputs @inline function check_broadcast_indices(shp, A, As...) check_broadcast_indices(shp, A) @@ -95,6 +239,7 @@ end # is appropriate for a particular broadcast array/scalar. `keep` is a # NTuple{N,Bool}, where keep[d] == true means that one should preserve # I[d]; if false, replace it with Idefault[d]. +# If dot-broadcasting were already defined, this would be `ifelse.(keep, I, Idefault)`. @inline newindex(I::CartesianIndex, keep, Idefault) = CartesianIndex(_newindex(I.I, keep, Idefault)) @inline _newindex(I, keep, Idefault) = (ifelse(keep[1], I[1], Idefault[1]), _newindex(tail(I), tail(keep), tail(Idefault))...) @@ -102,9 +247,9 @@ end # newindexer(shape, A) generates `keep` and `Idefault` (for use by # `newindex` above) for a particular array `A`, given the -# broadcast_indices `shape` +# broadcast indices `shape` # `keep` is equivalent to map(==, indices(A), shape) (but see #17126) -@inline newindexer(shape, A) = shapeindexer(shape, broadcast_indices(A)) +@inline newindexer(shape, A) = shapeindexer(shape, indices(A)) @inline shapeindexer(shape, indsA::Tuple{}) = (), () @inline function shapeindexer(shape, indsA::Tuple) ind1 = indsA[1] @@ -126,10 +271,11 @@ end (keep, keeps...), (Idefault, Idefaults...) end -Base.@propagate_inbounds _broadcast_getindex(A, I) = _broadcast_getindex(containertype(A), A, I) -Base.@propagate_inbounds _broadcast_getindex(::Type{Array}, A::Ref, I) = A[] -Base.@propagate_inbounds _broadcast_getindex(::ScalarType, A, I) = A -Base.@propagate_inbounds _broadcast_getindex(::Any, A, I) = A[I] +Base.@propagate_inbounds _broadcast_getindex(::Type{T}, I) where T = T +Base.@propagate_inbounds _broadcast_getindex(A, I) = _broadcast_getindex(combine_types(A), A, I) +Base.@propagate_inbounds _broadcast_getindex(::Type{Bottom0d}, A::Ref, I) = A[] +Base.@propagate_inbounds _broadcast_getindex(::Union{Type{Unknown},Type{Scalar},Type{Nullable}}, A, I) = A +Base.@propagate_inbounds _broadcast_getindex(::Type, A, I) = A[I] ## Broadcasting core # nargs encodes the number of As arguments (which matches the number @@ -201,8 +347,11 @@ arguments to `f` unless it is also listed in the `As`, as in `broadcast!(f, A, A, B)` to perform `A[:] = broadcast(f, A, B)`. """ @inline broadcast!(f, C::AbstractArray, A, Bs::Vararg{Any,N}) where {N} = - broadcast_c!(f, containertype(C), containertype(A, Bs...), C, A, Bs...) -@inline function broadcast_c!(f, ::Type, ::Type, C, A, Bs::Vararg{Any,N}) where N + _broadcast!(f, C, A, Bs...) + +# This indirection allows size-dependent implementations (e.g., see the copying `identity` +# specialization above) +@inline function _broadcast!(f, C, A, Bs::Vararg{Any,N}) where N shape = indices(C) @boundscheck check_broadcast_indices(shape, A, Bs...) keeps, Idefaults = map_newindexer(shape, A, Bs) @@ -211,7 +360,9 @@ as in `broadcast!(f, A, A, B)` to perform `A[:] = broadcast(f, A, B)`. return C end -# broadcast with computed element type +# broadcast with element type adjusted on-the-fly. This widens the element type of +# B as needed (allocating a new container and copying previously-computed values) to +# accomodate any incompatible new elements. @generated function _broadcast!(f, B::AbstractArray, keeps::K, Idefaults::ID, As::AT, ::Val{nargs}, iter, st, count) where {K,ID,AT,nargs} quote $(Expr(:meta, :noinline)) @@ -232,13 +383,14 @@ end if S <: eltype(B) @inbounds B[I] = V else - R = typejoin(eltype(B), S) - new = similar(B, R) + # This element type doesn't fit in B. Allocate a new B with wider eltype, + # copy over old values, and continue + newB = Base.similar(B, typejoin(eltype(B), S)) for II in Iterators.take(iter, count) - new[II] = B[II] + newB[II] = B[II] end - new[I] = V - return _broadcast!(f, new, keeps, Idefaults, As, Val(nargs), iter, st, count+1) + newB[I] = V + return _broadcast!(f, newB, keeps, Idefaults, As, Val(nargs), iter, st, count+1) end count += 1 end @@ -246,104 +398,31 @@ end end end -# broadcast methods that dispatch on the type found by inference -function broadcast_t(f, ::Type{Any}, shape, iter, As...) - nargs = length(As) - keeps, Idefaults = map_newindexer(shape, As) - st = start(iter) - I, st = next(iter, st) - val = f([ _broadcast_getindex(As[i], newindex(I, keeps[i], Idefaults[i])) for i=1:nargs ]...) - if val isa Bool - B = similar(BitArray, shape) - else - B = similar(Array{typeof(val)}, shape) - end - B[I] = val - return _broadcast!(f, B, keeps, Idefaults, As, Val(nargs), iter, st, 1) -end -@inline function broadcast_t(f, T, shape, iter, A, Bs::Vararg{Any,N}) where N - C = similar(Array{T}, shape) - keeps, Idefaults = map_newindexer(shape, A, Bs) - _broadcast!(f, C, keeps, Idefaults, A, Bs, Val(N), iter) - return C -end - -# default to BitArray for broadcast operations producing Bool, to save 8x space -# in the common case where this is used for logical array indexing; in -# performance-critical cases where Array{Bool} is desired, one can always -# use broadcast! instead. -@inline function broadcast_t(f, ::Type{Bool}, shape, iter, A, Bs::Vararg{Any,N}) where N - C = similar(BitArray, shape) - keeps, Idefaults = map_newindexer(shape, A, Bs) - _broadcast!(f, C, keeps, Idefaults, A, Bs, Val(N), iter) - return C -end - maptoTuple(f) = Tuple{} maptoTuple(f, a, b...) = Tuple{f(a), maptoTuple(f, b...).types...} # An element type satisfying for all A: # broadcast_getindex( -# containertype(A), -# A, broadcast_indices(A) +# combine_types(A), +# A, indices(A) # )::_broadcast_getindex_eltype(A) -_broadcast_getindex_eltype(A) = _broadcast_getindex_eltype(containertype(A), A) -_broadcast_getindex_eltype(::ScalarType, T::Type) = Type{T} -_broadcast_getindex_eltype(::ScalarType, A) = typeof(A) -_broadcast_getindex_eltype(::Any, A) = eltype(A) # Tuple, Array, etc. +_broadcast_getindex_eltype(A) = _broadcast_getindex_eltype(combine_types(A), A) +_broadcast_getindex_eltype(::Type{Scalar}, ::Type{T}) where T = Type{T} +_broadcast_getindex_eltype(::Union{Type{Unknown},Type{Scalar},Type{Nullable}}, A) = typeof(A) +_broadcast_getindex_eltype(::Type, A) = eltype(A) # Tuple, Array, etc. # An element type satisfying for all A: # unsafe_get(A)::unsafe_get_eltype(A) _unsafe_get_eltype(x::Nullable) = eltype(x) -_unsafe_get_eltype(T::Type) = Type{T} +_unsafe_get_eltype(::Type{T}) where T = Type{T} _unsafe_get_eltype(x) = typeof(x) # Inferred eltype of result of broadcast(f, xs...) -_broadcast_eltype(f, A, As...) = +combine_eltypes(f, A, As...) = Base._return_type(f, maptoTuple(_broadcast_getindex_eltype, A, As...)) _nullable_eltype(f, A, As...) = Base._return_type(f, maptoTuple(_unsafe_get_eltype, A, As...)) -# broadcast methods that dispatch on the type of the final container -@inline function broadcast_c(f, ::Type{Array}, A, Bs...) - T = _broadcast_eltype(f, A, Bs...) - shape = broadcast_indices(A, Bs...) - iter = CartesianRange(shape) - if Base._isleaftype(T) - return broadcast_t(f, T, shape, iter, A, Bs...) - end - if isempty(iter) - return similar(Array{T}, shape) - end - return broadcast_t(f, Any, shape, iter, A, Bs...) -end -@inline function broadcast_c(f, ::Type{Nullable}, a...) - nonnull = all(hasvalue, a) - S = _nullable_eltype(f, a...) - if Base._isleaftype(S) && null_safe_op(f, maptoTuple(_unsafe_get_eltype, - a...).types...) - Nullable{S}(f(map(unsafe_get, a)...), nonnull) - else - if nonnull - Nullable(f(map(unsafe_get, a)...)) - else - Nullable{nullable_returntype(S)}() - end - end -end -@inline broadcast_c(f, ::Type{Any}, a...) = f(a...) -@inline broadcast_c(f, ::Type{Tuple}, A, Bs...) = - tuplebroadcast(f, first_tuple(A, Bs...), A, Bs...) -@inline tuplebroadcast(f, ::NTuple{N,Any}, As...) where {N} = - ntuple(k -> f(tuplebroadcast_getargs(As, k)...), Val(N)) -@inline tuplebroadcast(f, ::NTuple{N,Any}, ::Type{T}, As...) where {N,T} = - ntuple(k -> f(T, tuplebroadcast_getargs(As, k)...), Val(N)) -first_tuple(A::Tuple, Bs...) = A -@inline first_tuple(A, Bs...) = first_tuple(Bs...) -tuplebroadcast_getargs(::Tuple{}, k) = () -@inline tuplebroadcast_getargs(As, k) = - (_broadcast_getindex(first(As), k), tuplebroadcast_getargs(tail(As), k)...) - """ broadcast(f, As...) @@ -431,7 +510,109 @@ julia> (1 + im) ./ Nullable{Int}() Nullable{Complex{Float64}}() ``` """ -@inline broadcast(f, A, Bs...) = broadcast_c(f, containertype(A, Bs...), A, Bs...) +broadcast(f, A, Bs...) = broadcast(f, + Result{combine_types(A, Bs...)}(), + A, Bs...) + +""" + broadcast(f, Broadcast.Result{ContainerType}(), As...) + +Specify the container-type of the output of a broadcasting operation. +You can specialize such calls as + + function Broadcast.broadcast(f, ::Broadcast.Result{ContainerType,Void,Void}, As...) where ContainerType + ... + end +""" +function broadcast(f, ::Result{ContainerType,Void,Void}, A, Bs...) where ContainerType + ElType = combine_eltypes(f, A, Bs...) + broadcast(f, + Result{ContainerType,ElType}(combine_indices(A, Bs...)), + A, Bs...) +end + +""" + broadcast(f, Broadcast.Result{ContainerType,ElType}(indices), As...) + +Specify the container-type, element-type, and indices of the output +of a broadcasting operation. You can specialize such calls as + + function Broadcast.broadcast(f, r::Broadcast.Result{ContainerType,ElType,<:Tuple}, As...) where {ContainerType,ElType} + ... + end + +This variant might be the most convenient specialization for container types +that don't support [`setindex!`](@ref) and therefore can't use [`broadcast!`](@ref). +""" +function broadcast(f, result::Result{ContainerType,ElType,<:Indices}, As...) where {ContainerType,ElType} + if !Base._isleaftype(ElType) + return broadcast_nonleaf(f, result, As...) + end + dest = similar(f, result, As...) + broadcast!(f, dest, As...) +end + +# default to BitArray for broadcast operations producing Bool, to save 8x space +# in the common case where this is used for logical array indexing; in +# performance-critical cases where Array{Bool} is desired, one can always +# use broadcast! instead. +function broadcast(f, r::Result{BottomArray{N},Bool}, As...) where N + dest = Base.similar(BitArray, indices(r)) + broadcast!(f, dest, As...) +end + +# When ElType is not concrete, use narrowing. Use the first element of each input to determine +# the starting output eltype; the _broadcast! method will widen `dest` as needed to +# accomodate later values. +function broadcast_nonleaf(f, r::Result{BottomArray{N},ElType,<:Indices}, As...) where {N,ElType} + nargs = length(As) + shape = indices(r) + iter = CartesianRange(shape) + if isempty(iter) + return Base.similar(Array{ElType}, shape) + end + keeps, Idefaults = map_newindexer(shape, As) + st = start(iter) + I, st = next(iter, st) + val = f([ _broadcast_getindex(As[i], newindex(I, keeps[i], Idefaults[i])) for i=1:nargs ]...) + if val isa Bool + dest = Base.similar(BitArray, shape) + else + dest = Base.similar(Array{typeof(val)}, shape) + end + dest[I] = val + return _broadcast!(f, dest, keeps, Idefaults, As, Val(nargs), iter, st, 1) +end + +@inline function broadcast(f, r::Result{<:Nullable,Void,Void}, a...) + nonnull = all(hasvalue, a) + S = _nullable_eltype(f, a...) + if Base._isleaftype(S) && null_safe_op(f, maptoTuple(_unsafe_get_eltype, + a...).types...) + Nullable{S}(f(map(unsafe_get, a)...), nonnull) + else + if nonnull + Nullable(f(map(unsafe_get, a)...)) + else + Nullable{nullable_returntype(S)}() + end + end +end + +broadcast(f, ::Result{<:Union{Scalar,Unknown},Void,Void}, a...) = f(a...) + +broadcast(f, ::Result{Tuple,Void,Void}, A, Bs...) = + tuplebroadcast(f, first_tuple(A, Bs...), A, Bs...) +tuplebroadcast(f, ::NTuple{N,Any}, As...) where {N} = + ntuple(k -> f(tuplebroadcast_getargs(As, k)...), Val(N)) +tuplebroadcast(f, ::NTuple{N,Any}, ::Type{T}, As...) where {N,T} = + ntuple(k -> f(T, tuplebroadcast_getargs(As, k)...), Val(N)) +first_tuple(A::Tuple, Bs...) = A +first_tuple(A, Bs...) = first_tuple(Bs...) +tuplebroadcast_getargs(::Tuple{}, k) = () +tuplebroadcast_getargs(As, k) = + (_broadcast_getindex(first(As), k), tuplebroadcast_getargs(tail(As), k)...) + """ broadcast_getindex(A, inds...) @@ -473,7 +654,11 @@ julia> broadcast_getindex(C,[1,2,10]) 15 ``` """ -broadcast_getindex(src::AbstractArray, I::AbstractArray...) = broadcast_getindex!(similar(Array{eltype(src)}, broadcast_indices(I...)), src, I...) +broadcast_getindex(src::AbstractArray, I::AbstractArray...) = + broadcast_getindex!(Base.similar(Array{eltype(src)}, combine_indices(I...)), + src, + I...) + @generated function broadcast_getindex!(dest::AbstractArray, src::AbstractArray, I::AbstractArray...) N = length(I) Isplat = Expr[:(I[$d]) for d = 1:N] @@ -481,7 +666,7 @@ broadcast_getindex(src::AbstractArray, I::AbstractArray...) = broadcast_getindex @nexprs $N d->(I_d = I[d]) check_broadcast_indices(indices(dest), $(Isplat...)) # unnecessary if this function is never called directly checkbounds(src, $(Isplat...)) - @nexprs $N d->(@nexprs $N k->(Ibcast_d_k = indices(I_k, d) == OneTo(1))) + @nexprs $N d->(@nexprs $N k->(Ibcast_d_k = Base.indices(I_k, d) == OneTo(1))) @nloops $N i dest d->(@nexprs $N k->(j_d_k = Ibcast_d_k ? 1 : i_d)) begin @nexprs $N k->(@inbounds J_k = @nref $N I_k d->j_d_k) @inbounds (@nref $N dest i) = (@nref $N src J) @@ -502,9 +687,9 @@ position in `X` at the indices in `A` given by the same positions in `inds`. quote @nexprs $N d->(I_d = I[d]) checkbounds(A, $(Isplat...)) - shape = broadcast_indices($(Isplat...)) + shape = combine_indices($(Isplat...)) @nextract $N shape d->(length(shape) < d ? OneTo(1) : shape[d]) - @nexprs $N d->(@nexprs $N k->(Ibcast_d_k = indices(I_k, d) == 1:1)) + @nexprs $N d->(@nexprs $N k->(Ibcast_d_k = Base.indices(I_k, d) == 1:1)) if !isa(x, AbstractArray) xA = convert(eltype(A), x) @nloops $N i d->shape_d d->(@nexprs $N k->(j_d_k = Ibcast_d_k ? 1 : i_d)) begin diff --git a/base/cartesian.jl b/base/cartesian.jl index b944617726345..2eb59e71b6722 100644 --- a/base/cartesian.jl +++ b/base/cartesian.jl @@ -41,7 +41,7 @@ end function _nloops(N::Int, itersym::Symbol, arraysym::Symbol, args::Expr...) @gensym d - _nloops(N, itersym, :($d->indices($arraysym, $d)), args...) + _nloops(N, itersym, :($d->Base.indices($arraysym, $d)), args...) end function _nloops(N::Int, itersym::Symbol, rangeexpr::Expr, args::Expr...) diff --git a/base/deprecated.jl b/base/deprecated.jl index ceb7378ca6a02..1cf6524c7978d 100644 --- a/base/deprecated.jl +++ b/base/deprecated.jl @@ -1867,6 +1867,18 @@ end # also remove deprecation warnings in find* functions in array.jl, sparse/sparsematrix.jl, # and sparse/sparsevector.jl. +# Broadcast extension API (#23939) +@eval Broadcast begin + Base.@deprecate_binding containertype combine_types false + Base.@deprecate_binding _containertype rule false + Base.@deprecate_binding promote_containertype rule false + Base.@deprecate_binding broadcast_indices combine_indices false + Base.@deprecate_binding broadcast_c! broadcast! false ", broadcast_c!(f, ::Type, ::Type, C, As...) should become broadcast!(f, C, As...)" + Base.@deprecate_binding broadcast_c broadcast false ", `broadcast_c(f, ::Type{C}, As...)` should become `broadcast(f, Broadcast.Result{C}(), As...))`" + Base.@deprecate_binding broadcast_t broadcast false ", broadcast_t(f, ::Type{ElType}, shape, iter, As...)` should become `broadcast(f, Result{BottomArray,ElType,<:Tuple}(shape), As...))`" +end + + # END 0.7 deprecations # BEGIN 1.0 deprecations diff --git a/base/exports.jl b/base/exports.jl index 09fd49d701fb2..794534cdcf505 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -20,6 +20,7 @@ export Threads, Iterators, Distributed, + Broadcast, # Types AbstractChannel, diff --git a/base/sparse/higherorderfns.jl b/base/sparse/higherorderfns.jl index c7431a0408730..7c1e7b1776d43 100644 --- a/base/sparse/higherorderfns.jl +++ b/base/sparse/higherorderfns.jl @@ -5,8 +5,6 @@ module HigherOrderFns # This module provides higher order functions specialized for sparse arrays, # particularly map[!]/broadcast[!] for SparseVectors and SparseMatrixCSCs at present. import Base: map, map!, broadcast, broadcast! -import Base.Broadcast: _containertype, promote_containertype, - broadcast_indices, broadcast_c, broadcast_c! using Base: front, tail, to_shape using ..SparseArrays: SparseVector, SparseMatrixCSC, AbstractSparseVector, @@ -23,10 +21,10 @@ using ..SparseArrays: SparseVector, SparseMatrixCSC, AbstractSparseVector, # (7) Define _broadcast_[not]zeropres! specialized for a single (input) sparse vector/matrix. # (8) Define _broadcast_[not]zeropres! specialized for a pair of (input) sparse vectors/matrices. # (9) Define general _broadcast_[not]zeropres! capable of handling >2 (input) sparse vectors/matrices. -# (10) Define (broadcast[!]) methods handling combinations of broadcast scalars and sparse vectors/matrices. -# (11) Define (broadcast[!]) methods handling combinations of scalars, sparse vectors/matrices, +# (10) Define broadcast methods handling combinations of broadcast scalars and sparse vectors/matrices. +# (11) Define broadcast[!] methods handling combinations of scalars, sparse vectors/matrices, # structured matrices, and one- and two-dimensional Arrays. -# (12) Define (map[!]) methods handling combinations of sparse and structured matrices. +# (12) Define map[!] methods handling combinations of sparse and structured matrices. # (1) The definitions below provide a common interface to sparse vectors and matrices @@ -85,7 +83,7 @@ function _noshapecheck_map(f::Tf, A::SparseVecOrMat, Bs::Vararg{SparseVecOrMat,N fofzeros = f(_zeros_eltypes(A, Bs...)...) fpreszeros = _iszero(fofzeros) maxnnzC = fpreszeros ? min(length(A), _sumnnzs(A, Bs...)) : length(A) - entrytypeC = Base.Broadcast._broadcast_eltype(f, A, Bs...) + entrytypeC = Base.Broadcast.combine_eltypes(f, A, Bs...) indextypeC = _promote_indtype(A, Bs...) C = _allocres(size(A), indextypeC, entrytypeC, maxnnzC) return fpreszeros ? _map_zeropres!(f, C, A, Bs...) : @@ -126,8 +124,8 @@ function _diffshape_broadcast(f::Tf, A::SparseVecOrMat, Bs::Vararg{SparseVecOrMa fofzeros = f(_zeros_eltypes(A, Bs...)...) fpreszeros = _iszero(fofzeros) indextypeC = _promote_indtype(A, Bs...) - entrytypeC = Base.Broadcast._broadcast_eltype(f, A, Bs...) - shapeC = to_shape(Base.Broadcast.broadcast_indices(A, Bs...)) + entrytypeC = Base.Broadcast.combine_eltypes(f, A, Bs...) + shapeC = to_shape(Base.Broadcast.combine_indices(A, Bs...)) maxnnzC = fpreszeros ? _checked_maxnnzbcres(shapeC, A, Bs...) : _densennz(shapeC) C = _allocres(shapeC, indextypeC, entrytypeC, maxnnzC) return fpreszeros ? _broadcast_zeropres!(f, C, A, Bs...) : @@ -897,29 +895,25 @@ end end -# (10) broadcast[!] over combinations of broadcast scalars and sparse vectors/matrices +# (10) broadcast over combinations of broadcast scalars and sparse vectors/matrices -# broadcast shape promotion for combinations of sparse arrays and other types -broadcast_indices(::Type{AbstractSparseArray}, A) = indices(A) # broadcast container type promotion for combinations of sparse arrays and other types -_containertype(::Type{<:SparseVecOrMat}) = AbstractSparseArray -# combinations of sparse arrays with broadcast scalars should yield sparse arrays -promote_containertype(::Type{Any}, ::Type{AbstractSparseArray}) = AbstractSparseArray -promote_containertype(::Type{AbstractSparseArray}, ::Type{Any}) = AbstractSparseArray -# combinations of sparse arrays with tuples should divert to the generic AbstractArray broadcast code -# (we handle combinations involving dense vectors/matrices below) -promote_containertype(::Type{Tuple}, ::Type{AbstractSparseArray}) = Array -promote_containertype(::Type{AbstractSparseArray}, ::Type{Tuple}) = Array +# inference has a hard time with Union type returns, so define a specific "signal" type +abstract type SPVM end +# Because it's not a subtype of AbstractArray, we have to define Broadcast.indices +Broadcast.indices(::Type{SPVM}, A) = Base.indices(A) +Broadcast.rule(::Type{<:SparseVector}) = SPVM +Broadcast.rule(::Type{<:SparseMatrixCSC}) = SPVM +# Scalars lose to SPVM +Broadcast.rule(::Type{SPVM}, ::Type{Broadcast.Scalar}) = SPVM -# broadcast[!] entry points for combinations of sparse arrays and other (scalar) types -@inline function broadcast_c(f, ::Type{AbstractSparseArray}, mixedargs::Vararg{Any,N}) where N +# broadcast entry points for combinations of sparse arrays and other (scalar) types +function broadcast(f, r::Broadcast.Result{SPVM,Void,Void}, mixedargs::Vararg{Any,N}) where N parevalf, passedargstup = capturescalars(f, mixedargs) return broadcast(parevalf, passedargstup...) end -@inline function broadcast_c!(f, ::Type{AbstractSparseArray}, dest::SparseVecOrMat, mixedsrcargs::Vararg{Any,N}) where N - parevalf, passedsrcargstup = capturescalars(f, mixedsrcargs) - return broadcast!(parevalf, dest, passedsrcargstup...) -end +# for broadcast! see (11) + # capturescalars takes a function (f) and a tuple of mixed sparse vectors/matrices and # broadcast scalar arguments (mixedargs), and returns a function (parevalf, i.e. partially # evaluated f) and a reduced argument tuple (passedargstup) containing only the sparse @@ -969,99 +963,68 @@ broadcast(f::Tf, A::SparseMatrixCSC, ::Type{T}) where {Tf,T} = broadcast(x -> f( # for combinations involving only scalars, sparse arrays, structured matrices, and dense # vectors/matrices, promote all structured matrices and dense vectors/matrices to sparse # and rebroadcast. otherwise, divert to generic AbstractArray broadcast code. -# -# this requires three steps: segregate combinations to promote to sparse via Broadcast's -# containertype promotion and dispatch layer (broadcast_c[!], containertype, -# promote_containertype), separate ambiguous cases from the preceding dispatch -# layer in sparse broadcast's internal containertype promotion and dispatch layer -# (spbroadcast_c[!], spcontainertype, promote_spcontainertype), and then promote -# arguments to sparse as appropriate and rebroadcast. - -# first (Broadcast containertype) dispatch layer's promotion logic -struct PromoteToSparse end +# combinations of sparse arrays, tuples, and arrays of dimensionality 0-2 should yield sparse arrays +abstract type PromoteToSparse end +# Since we're not making PromoteToSparse a subtype of AbstractArray, we need to define indices +Broadcast.indices(::Type{PromoteToSparse}, A) = Base.indices(A) -# broadcast containertype definitions for structured matrices StructuredMatrix = Union{Diagonal,Bidiagonal,Tridiagonal,SymTridiagonal} -_containertype(::Type{<:StructuredMatrix}) = PromoteToSparse -broadcast_indices(::Type{PromoteToSparse}, A) = indices(A) - -# combinations explicitly involving Tuples and PromoteToSparse collections -# divert to the generic AbstractArray broadcast code -promote_containertype(::Type{PromoteToSparse}, ::Type{Tuple}) = Array -promote_containertype(::Type{Tuple}, ::Type{PromoteToSparse}) = Array -# combinations involving scalars and PromoteToSparse collections continue in the promote-to-sparse funnel -promote_containertype(::Type{PromoteToSparse}, ::Type{Any}) = PromoteToSparse -promote_containertype(::Type{Any}, ::Type{PromoteToSparse}) = PromoteToSparse -# combinations involving sparse arrays and PromoteToSparse collections continue in the promote-to-sparse funnel -promote_containertype(::Type{PromoteToSparse}, ::Type{AbstractSparseArray}) = PromoteToSparse -promote_containertype(::Type{AbstractSparseArray}, ::Type{PromoteToSparse}) = PromoteToSparse -# combinations involving Arrays and PromoteToSparse collections continue in the promote-to-sparse funnel -promote_containertype(::Type{PromoteToSparse}, ::Type{Array}) = PromoteToSparse -promote_containertype(::Type{Array}, ::Type{PromoteToSparse}) = PromoteToSparse -# combinations involving Arrays and sparse arrays continue in the promote-to-sparse funnel -promote_containertype(::Type{AbstractSparseArray}, ::Type{Array}) = PromoteToSparse -promote_containertype(::Type{Array}, ::Type{AbstractSparseArray}) = PromoteToSparse - -# second (internal sparse broadcast containertype) dispatch layer's promotion logic -# mostly just disambiguates Array from the main containertype promotion mechanism -# AbstractArray serves as a marker to shunt to the generic AbstractArray broadcast code -_spcontainertype(x) = _containertype(x) -_spcontainertype(::Type{<:Vector}) = Vector -_spcontainertype(::Type{<:Matrix}) = Matrix -_spcontainertype(::Type{<:RowVector}) = Matrix -_spcontainertype(::Type{<:Ref}) = AbstractArray -_spcontainertype(::Type{<:AbstractArray}) = AbstractArray -# need the following two methods to override the immediately preceding method -_spcontainertype(::Type{<:StructuredMatrix}) = PromoteToSparse -_spcontainertype(::Type{<:SparseVecOrMat}) = AbstractSparseArray -spcontainertype(x) = _spcontainertype(typeof(x)) -spcontainertype(ct1, ct2) = promote_spcontainertype(spcontainertype(ct1), spcontainertype(ct2)) -@inline spcontainertype(ct1, ct2, cts...) = promote_spcontainertype(spcontainertype(ct1), spcontainertype(ct2, cts...)) +Broadcast.rule(::Type{<:StructuredMatrix}) = PromoteToSparse -promote_spcontainertype(::Type{T}, ::Type{T}) where {T} = T -# combinations involving AbstractArrays and/or Tuples divert to the generic AbstractArray broadcast code -DivertToAbsArrayBC = Union{Type{AbstractArray},Type{Tuple}} -promote_spcontainertype(::DivertToAbsArrayBC, ct) = AbstractArray -promote_spcontainertype(ct, ::DivertToAbsArrayBC) = AbstractArray -promote_spcontainertype(::DivertToAbsArrayBC, ::DivertToAbsArrayBC) = AbstractArray -# combinations involving scalars, sparse arrays, structured matrices (PromoteToSparse), -# dense vectors/matrices, and PromoteToSparse collections continue in the promote-to-sparse funnel -FunnelToSparseBC = Union{Type{Any},Type{Vector},Type{Matrix},Type{PromoteToSparse},Type{AbstractSparseArray}} -promote_spcontainertype(::FunnelToSparseBC, ::FunnelToSparseBC) = PromoteToSparse +Broadcast.rule(::Type{SPVM}, ::Type{Tuple}) = PromoteToSparse +Broadcast.rule(::Type{SPVM}, ::Type{Broadcast.Bottom0d}) = PromoteToSparse +Broadcast.rule(::Type{SPVM}, ::Type{Broadcast.BottomVector}) = PromoteToSparse +Broadcast.rule(::Type{SPVM}, ::Type{Broadcast.BottomMatrix}) = PromoteToSparse +Broadcast.rule(::Type{PromoteToSparse}, ::Type{Broadcast.Scalar}) = PromoteToSparse +Broadcast.rule(::Type{PromoteToSparse}, ::Type{Tuple}) = PromoteToSparse +Broadcast.rule(::Type{PromoteToSparse}, ::Type{Broadcast.Bottom0d}) = PromoteToSparse +Broadcast.rule(::Type{PromoteToSparse}, ::Type{Broadcast.BottomVector}) = PromoteToSparse +Broadcast.rule(::Type{PromoteToSparse}, ::Type{Broadcast.BottomMatrix}) = PromoteToSparse +Broadcast.rule(::Type{PromoteToSparse}, ::Type{SPVM}) = PromoteToSparse +# Combinations of sparse arrays and higher-dimensional arrays default to the generic infrastructure. +# In this case it's best to keep the same argument order as above, so that we don't get ambiguities +# or conflicting rules +Broadcast.rule(::Type{SPVM}, ::Type{Broadcast.BottomArray{N}}) where N = Broadcast.BottomArray{N} +Broadcast.rule(::Type{PromoteToSparse}, ::Type{Broadcast.BottomArray{N}}) where N = Broadcast.BottomArray{N} +broadcast(f, r::Broadcast.Result{PromoteToSparse,Void,Void}, As::Vararg{Any,N}) where {N} = + broadcast(f, map(_sparsifystructured, As)...) -# first (Broadcast containertype) dispatch layer -# (broadcast_c[!], containertype, promote_containertype) -@inline broadcast_c(f, ::Type{PromoteToSparse}, As::Vararg{Any,N}) where {N} = - spbroadcast_c(f, spcontainertype(As...), As...) -@inline broadcast_c!(f, ::Type{AbstractSparseArray}, ::Type{PromoteToSparse}, C, B, As::Vararg{Any,N}) where {N} = - spbroadcast_c!(f, AbstractSparseArray, spcontainertype(B, As...), C, B, As...) -# where destination C is not an AbstractSparseArray, divert to generic AbstractArray broadcast code -@inline broadcast_c!(f, CT::Type, ::Type{PromoteToSparse}, C, B, As::Vararg{Any,N}) where {N} = - broadcast_c!(f, CT, Array, C, B, As...) +# ambiguity resolution +broadcast!(::typeof(identity), dest::SparseVecOrMat, x::Number) = + fill!(dest, x) +broadcast!(f, dest::SparseVecOrMat, x::Number...) = + spbroadcast_args!(f, dest, SPVM, mixedsrcargs...) -# second (internal sparse broadcast containertype) dispatch layer -# (spbroadcast_c[!], spcontainertype, promote_spcontainertype) -@inline spbroadcast_c(f, ::Type{PromoteToSparse}, As::Vararg{Any,N}) where {N} = - broadcast(f, map(_sparsifystructured, As)...) -@inline spbroadcast_c(f, ::Type{AbstractArray}, As::Vararg{Any,N}) where {N} = - broadcast_c(f, Array, As...) -@inline spbroadcast_c!(f, ::Type{AbstractSparseArray}, ::Type{PromoteToSparse}, C, B, As::Vararg{Any,N}) where {N} = - broadcast!(f, C, _sparsifystructured(B), map(_sparsifystructured, As)...) -@inline spbroadcast_c!(f, ::Type{AbstractSparseArray}, ::Type{AbstractArray}, C, B, As::Vararg{Any,N}) where {N} = - broadcast_c!(f, Array, Array, C, B, As...) +# For broadcast! with ::Any inputs, we need a layer of indirection to determine whether +# the inputs can be promoted to SparseVecOrMat. If it's just SparseVecOrMat and scalars, +# we can handle it here, otherwise see below for the promotion machinery. +broadcast!(f, dest::SparseVecOrMat, mixedsrcargs::Vararg{Any,N}) where N = + spbroadcast_args!(f, dest, Broadcast.combine_types(mixedsrcargs...), mixedsrcargs...) +function spbroadcast_args!(f, dest, ::Type{SPVM}, mixedsrcargs::Vararg{Any,N}) where N + # mixedsrcargs contains nothing but SparseVecOrMat and scalars + parevalf, passedsrcargstup = capturescalars(f, mixedsrcargs) + return broadcast!(parevalf, dest, passedsrcargstup...) +end +function spbroadcast_args!(f, dest, ::Type{PromoteToSparse}, mixedsrcargs::Vararg{Any,N}) where N + broadcast!(f, dest, map(_sparsifystructured, mixedsrcargs)...) +end +function spbroadcast_args!(f, dest, ::Type, mixedsrcargs::Vararg{Any,N}) where N + # Fallback. From a performance perspective would it be best to densify? + Broadcast._broadcast!(f, dest, mixedsrcargs...) +end -@inline _sparsifystructured(M::AbstractMatrix) = SparseMatrixCSC(M) -@inline _sparsifystructured(V::AbstractVector) = SparseVector(V) -@inline _sparsifystructured(M::AbstractSparseMatrix) = SparseMatrixCSC(M) -@inline _sparsifystructured(V::AbstractSparseVector) = SparseVector(V) -@inline _sparsifystructured(S::SparseVecOrMat) = S -@inline _sparsifystructured(x) = x +_sparsifystructured(M::AbstractMatrix) = SparseMatrixCSC(M) +_sparsifystructured(V::AbstractVector) = SparseVector(V) +_sparsifystructured(P::AbstractArray{T,0}) where T = SparseVector(reshape(P, 1)) +_sparsifystructured(M::AbstractSparseMatrix) = SparseMatrixCSC(M) +_sparsifystructured(V::AbstractSparseVector) = SparseVector(V) +_sparsifystructured(S::SparseVecOrMat) = S +_sparsifystructured(x) = x # (12) map[!] over combinations of sparse and structured matrices -StructuredMatrix = Union{Diagonal,Bidiagonal,Tridiagonal,SymTridiagonal} SparseOrStructuredMatrix = Union{SparseMatrixCSC,StructuredMatrix} map(f::Tf, A::StructuredMatrix) where {Tf} = _noshapecheck_map(f, _sparsifystructured(A)) map(f::Tf, A::SparseOrStructuredMatrix, Bs::Vararg{SparseOrStructuredMatrix,N}) where {Tf,N} = diff --git a/doc/src/manual/interfaces.md b/doc/src/manual/interfaces.md index 8576bffa4d9b7..c4a12e6c68d2b 100644 --- a/doc/src/manual/interfaces.md +++ b/doc/src/manual/interfaces.md @@ -371,3 +371,189 @@ If you are defining an array type that allows non-traditional indexing (indices something other than 1), you should specialize `indices`. You should also specialize [`similar`](@ref) so that the `dims` argument (ordinarily a `Dims` size-tuple) can accept `AbstractUnitRange` objects, perhaps range-types `Ind` of your own design. For more information, see [Arrays with custom indices](@ref). + +## Specializing broadcasting + +| Methods to implement | Brief description | +|:-------------------- |:----------------- | +| `Broadcast.rule(::Type{SrcType}) = ContainerType` | Output type produced by broadcasting `SrcType` | +| `similar(f, r::Broadcast.Result{ContainerType}, As...)` | Allocation of output container | +| **Optional methods** | | | +| `Broadcast.rule(::Type{ContainerType1}, ::Type{ContainerType2}) = ContainerType` | Precedence rules for output type | +| `Broadcast.indices(::Type, A)` | Declaration of the indices of `A` for broadcasting purposes (for AbstractArrays, defaults to `Base.indices(A)`) | +| **Bypassing default machinery** | | +| `broadcast(f, As...)` | Complete bypass of broadcasting machinery | +| `broadcast(f, r::Broadcast.Result{ContainerType,Void,Void}, As...)` | Bypass after container type is computed | +| `broadcast(f, r::Broadcast.Result{ContainerType,ElType,<:Tuple}, As...)` | Bypass after container type, eltype, and indices are computed | + +[Broadcasting](@ref) is triggered by an explicit call to `broadcast` or `broadcast!`, or implicitly by +"dot" operations like `A .+ b`. Any `AbstractArray` type supports broadcasting, +but the default result (output) type is `Array`. To specialize the result for specific input type(s), +the main task is the allocation of an appropriate result object. +(This is not an issue for `broadcast!`, where +the result object is passed as an argument.) This process is split into two stages: computation +of the type from the arguments ([`Broadcast.rule`](@ref)), and allocation of the object +given the resulting type with a broadcast-specific ([`similar`](@ref)). + +`Broadcast.rule` is somewhat analogous to [`promote_rule`](@ref), except that you +may only need to define a unary variant. The unary variant simply states that you intend to +handle broadcasting for this type, and do not wish to rely on the default fallback. Most +implementations will be simple: + +```julia +Broadcast.rule(::Type{<:MyType}) = MyType +``` +where unary `rule` should typically discard type parameters so that any binary `rule` methods +can be concrete (without using `<:` for type arguments). + +For `AbstractArray` types, this prevents the fallback choice, `Broadcast.BottomArray`, +which is an `AbstractArray` type that "loses" to every other `AbstractArray` type in a binary call +`Broadcast.rule(S, T)` for two types `S` and `T`. +You do not need to write a binary `rule` unless you want to establish precedence for +two or more non-`BottomArray` types. If you do write a binary rule, you do not need to +supply the types in both orders, as internal machinery will try both. For more detail, +see [below](@ref writing-binary-broadcasting-rules). + +The actual allocation of the result array is handled by specialized implementations of `similar`: + +```julia +Base.similar(f, r::Broadcast.Result{ContainerType}, As...) +``` + +`f` is the operation being performed, `ContainerType` signals the resulting container type +(e.g., `Broadcast.BottomArray`, `Tuple`, etc.). +`eltype(r)` returns the element type, and `indices(r)` the object's indices. +`As...` is the list of input objects. You may not need to use `f` or `As...` +unless they help you build the appropriate object; the fallback definition is + +```julia +Base.similar(f, r::Broadcast.Result{BottomArray}, As...) = similar(Array{eltype(r)}, indices(r)) +``` + +However, if needed you can specialize on any or all of these arguments. + +For a complete example, let's say you have created a type, `ArrayAndChar`, that stores an +array and a single character: + +```jldoctest +struct ArrayAndChar{T,N} <: AbstractArray{T,N} + data::Array{T,N} + char::Char +end +Base.size(A::ArrayAndChar) = size(A.data) +Base.getindex(A::ArrayAndChar{T,N}, inds::Vararg{Int,N}) where {T,N} = A.data[inds...] +Base.setindex!(A::ArrayAndChar{T,N}, val, inds::Vararg{Int,N}) where {T,N} = A.data[inds...] = val +Base.showarg(io::IO, A::ArrayAndChar, toplevel) = print(io, typeof(A), " with char '", A.char, "'") +``` + +You might want broadcasting to preserve the `char` "metadata." First we define + +```jldoctest +Broadcast.rule(::Type{AC}) where AC<:ArrayAndChar = ArrayAndChar +``` + +This forces us to also define a `similar` method: +```jldoctest +function Base.similar(f, r::Broadcast.Result{ArrayAndChar}, As...) + # Scan the inputs for the ArrayAndChar: + A = find_aac(As...) + # Use the char field of A to create the output + ArrayAndChar(similar(Array{eltype(r)}, indices(r)), A.char) +end + +"`A = find_aac(As...)` returns the first ArrayAndChar among the arguments." +find_aac(A::ArrayAndChar, B...) = A +find_aac(A, B...) = find_aac(B...) +``` + +From these definitions, one obtains the following behavior: +```jldoctest +julia> a = ArrayAndChar([1 2; 3 4], 'x') +2×2 ArrayAndChar{Int64,2} with char 'x': + 1 2 + 3 4 + +julia> a .+ 1 +2×2 ArrayAndChar{Int64,2} with char 'x': + 2 3 + 4 5 + +julia> a .+ [5,10] +2×2 ArrayAndChar{Int64,2} with char 'x': + 6 7 + 13 14 +``` + +Finally, it's worth noting that sometimes it's easier simply to bypass the machinery for +computing result types and container sizes, and just do everything manually. For example, +you can convert a `UnitRange{Int}` `rng` to a `UnitRange{BigInt}` with `big.(rng)`; the definition +of this method is approximately + +```julia +Broadcast.broadcast(::typeof(big), rng::UnitRange) = big(first(rng)):big(last(rng)) +``` + +This exploits Julia's ability to dispatch on a particular function type. (This kind of +explicit definition can indeed be necessary if the output container does not support `setindex!`.) +You can optionally choose to implement the actual broadcasting yourself, but allow +the internal machinery to compute the container type, element type, and indices by specializing + +```julia +Broadcast.broadcast(::typeof(somefunction), r::Broadcast.Result{ContainerType,ElType,<:Tuple}, As...) +``` + +### [Writing binary broadcasting rules](@id writing-binary-broadcasting-rules) + +Binary rules look something like this: + + Broadcast.rule(::Type{Primary}, ::Type{Secondary}) = Primary + +This would indicate that `Primary` has precedence over `Secondary`. +Generally you should only define one argument order, because internal machinery will test +both orders. +The result does not have to be one of the input arguments, it could be a third type. +For example, you could imagine defining + + Broadcast.rule(::Type{Ref}, ::Type{Tuple}) = Vector + +so that a `Ref` and a `Tuple` broadcast to a `Vector`. (`Ref` is handled a bit +differently than this by internal machinery, so this is just for the purposes +of illustration.) + +While there are exceptions, in general you may find it simpler if the arguments avoid +subtyping, e.g., in binary `rule`s use `Type{Primary}` rather than `Type{<:Primary}`. +The main motivation for this advice is that subtyping can lead to ambiguities or conflicts +for types that are subtypes of other types. +As a consequence, if you define binary `rule` methods, when defining corresponding unary +`rule` methods if possible you should discard type parameters, i.e., + + Broadcast.rule{<:Primary} = Primary + +rather than + + Broadcast.rule{P} where P<:Primary = P + +`Broadcast` defines several internal types that can assist in writing binary rules: +- `Broadcast.Scalar` for objects that act like scalars +- `Broadcast.BottomArray{N}` for array types that haven't declared specialized broadcast implementations + +`BottomArray` stores the dimensionality as a type parameter (thus violating the advice above) +to support specialized array types that have fixed dimensionality +requirements. `BottomArray` "loses" to other array types that have specialized broadcasting +rules because of the following method: + + Broadcast.rule(::Type{<:AbstractArray{T,N}}) where {T,N} = BottomArray{N} + Broadcast.rule(::Type{A}, ::Type{BottomArray{N}}) where {A<:AbstractArray,N} = A + +If you want to leverage this `AbstractArray` types, it makes sense to have the unary `rule` return an + +As an example of how to leverage the dimensionality argument of `BottomArray`, +the sparse array code contains rules something like the following: + + Broadcast.rule(::Type{SparseVecOrMat}, ::Type{Broadcast.BottomVector}) = SparseVecOrMat + Broadcast.rule(::Type{SparseVecOrMat}, ::Type{Broadcast.BottomMatrix}) = SparseVecOrMat + Broadcast.rule(::Type{SparseVecOrMat}, ::Type{Broadcast.BottomArray{N}}) where N = + Broadcast.BottomArray{N} + +These rules allow broadcasting to keep the sparse representation for operations that result +in one or two dimensional outputs, but produce an `Array` for any other dimensionality. diff --git a/doc/src/stdlib/arrays.md b/doc/src/stdlib/arrays.md index 86bcddb6fa1d1..ab72667dbe15f 100644 --- a/doc/src/stdlib/arrays.md +++ b/doc/src/stdlib/arrays.md @@ -65,6 +65,12 @@ Base.Broadcast.broadcast_getindex Base.Broadcast.broadcast_setindex! ``` +For specializing broadcast on custom types, see +```@docs +Base.Broadcast.Result +Base.Broadcast.rule +``` + ## Indexing and assignment ```@docs diff --git a/test/broadcast.jl b/test/broadcast.jl index e88f5a0403d1d..669948a9c89f6 100644 --- a/test/broadcast.jl +++ b/test/broadcast.jl @@ -2,8 +2,7 @@ module TestBroadcastInternals -using Base.Broadcast: broadcast_indices, check_broadcast_indices, - check_broadcast_shape, newindex, _bcs +using Base.Broadcast: check_broadcast_indices, check_broadcast_shape, newindex, _bcs using Base: OneTo using Test @@ -20,10 +19,10 @@ using Test @test_throws DimensionMismatch _bcs((-1:1, 2:6), (-1:1, 2:5)) @test_throws DimensionMismatch _bcs((-1:1, 2:5), (2, 2:5)) -@test @inferred(broadcast_indices(zeros(3,4), zeros(3,4))) == (OneTo(3),OneTo(4)) -@test @inferred(broadcast_indices(zeros(3,4), zeros(3))) == (OneTo(3),OneTo(4)) -@test @inferred(broadcast_indices(zeros(3), zeros(3,4))) == (OneTo(3),OneTo(4)) -@test @inferred(broadcast_indices(zeros(3), zeros(1,4), zeros(1))) == (OneTo(3),OneTo(4)) +@test @inferred(Broadcast.combine_indices(zeros(3,4), zeros(3,4))) == (OneTo(3),OneTo(4)) +@test @inferred(Broadcast.combine_indices(zeros(3,4), zeros(3))) == (OneTo(3),OneTo(4)) +@test @inferred(Broadcast.combine_indices(zeros(3), zeros(3,4))) == (OneTo(3),OneTo(4)) +@test @inferred(Broadcast.combine_indices(zeros(3), zeros(1,4), zeros(1))) == (OneTo(3),OneTo(4)) check_broadcast_indices((OneTo(3),OneTo(5)), zeros(3,5)) check_broadcast_indices((OneTo(3),OneTo(5)), zeros(3,1)) @@ -404,7 +403,7 @@ StrangeType18623(x,y) = (x,y) let f(A, n) = broadcast(x -> +(x, n), A) @test @inferred(f([1.0], 1)) == [2.0] - g() = (a = 1; Base.Broadcast._broadcast_eltype(x -> x + a, 1.0)) + g() = (a = 1; Broadcast.combine_eltypes(x -> x + a, 1.0)) @test @inferred(g()) === Float64 end @@ -419,34 +418,83 @@ end @test let z = 1; A = broadcast!(() -> z += 1, zeros(2)); A[1] != A[2]; end @test let z = 1; A = broadcast!(x -> z += x, zeros(2), 1); A[1] != A[2]; end -# broadcasting for custom AbstractArray -struct Array19745{T,N} <: AbstractArray{T,N} +## broadcasting for custom AbstractArray +abstract type ArrayData{T,N} <: AbstractArray{T,N} end +Base.getindex(A::ArrayData, i::Integer...) = A.data[i...] +Base.setindex!(A::ArrayData, v::Any, i::Integer...) = setindex!(A.data, v, i...) +Base.size(A::ArrayData) = size(A.data) + +struct Array19745{T,N} <: ArrayData{T,N} data::Array{T,N} end -Base.getindex(A::Array19745, i::Integer...) = A.data[i...] -Base.setindex!(A::Array19745, v::Any, i::Integer...) = setindex!(A.data, v, i...) -Base.size(A::Array19745) = size(A.data) +Broadcast.rule(::Type{T}) where {T<:Array19745} = Array19745 +Base.similar(f, r::Broadcast.Result{Array19745}, As...) = + Array19745(Array{eltype(r)}(indices(r))) -Base.Broadcast._containertype(::Type{T}) where {T<:Array19745} = Array19745 +# Two specialized broadcast rules with no declared precedence +struct AD1{T,N} <: ArrayData{T,N} + data::Array{T,N} +end +Broadcast.rule(::Type{T}) where {T<:AD1} = AD1 +Base.similar(f, r::Broadcast.Result{AD1}, As...) = + AD1(Array{eltype(r)}(indices(r))) +struct AD2{T,N} <: ArrayData{T,N} + data::Array{T,N} +end +Broadcast.rule(::Type{T}) where {T<:AD2} = AD2 +Base.similar(f, r::Broadcast.Result{AD2}, As...) = + AD2(Array{eltype(r)}(indices(r))) -Base.Broadcast.promote_containertype(::Type{Array19745}, ::Type{Array19745}) = Array19745 -Base.Broadcast.promote_containertype(::Type{Array19745}, ::Type{Array}) = Array19745 -Base.Broadcast.promote_containertype(::Type{Array19745}, ct) = Array19745 -Base.Broadcast.promote_containertype(::Type{Array}, ::Type{Array19745}) = Array19745 -Base.Broadcast.promote_containertype(ct, ::Type{Array19745}) = Array19745 +# Two specialized broadcast rules with explicit precedence +struct AD1P{T,N} <: ArrayData{T,N} + data::Array{T,N} +end +Broadcast.rule(::Type{T}) where {T<:AD1P} = AD1P +Base.similar(f, r::Broadcast.Result{AD1P}, As...) = + AD1P(Array{eltype(r)}(indices(r))) +struct AD2P{T,N} <: ArrayData{T,N} + data::Array{T,N} +end +Broadcast.rule(::Type{T}) where {T<:AD2P} = AD2P +Base.similar(f, r::Broadcast.Result{AD2P}, As...) = + AD2P(Array{eltype(r)}(indices(r))) -Base.Broadcast.broadcast_indices(::Type{Array19745}, A) = indices(A) -Base.Broadcast.broadcast_indices(::Type{Array19745}, A::Ref) = () +Broadcast.rule(::Type{AD1P}, ::Type{AD2P}) = AD1P -getfield19745(x::Array19745) = x.data -getfield19745(x) = x +# Two specialized broadcast rules where users unnecessarily +# define `rule` for both argument orders (but do so consistently) +struct AD1B{T,N} <: ArrayData{T,N} + data::Array{T,N} +end +Broadcast.rule(::Type{T}) where {T<:AD1B} = AD1B +Base.similar(f, r::Broadcast.Result{AD1B}, As...) = + AD1B(Array{eltype(r)}(indices(r))) +struct AD2B{T,N} <: ArrayData{T,N} + data::Array{T,N} +end +Broadcast.rule(::Type{T}) where {T<:AD2B} = AD2B +Base.similar(f, r::Broadcast.Result{AD2B}, As...) = + AD2B(Array{eltype(r)}(indices(r))) -function Base.Broadcast.broadcast_c(f, ::Type{Array19745}, A, Bs...) - T = Base.Broadcast._broadcast_eltype(f, A, Bs...) - shape = Base.Broadcast.broadcast_indices(A, Bs...) - dest = Array19745(Array{T}(Base.index_lengths(shape...))) - return broadcast!(f, dest, A, Bs...) +Broadcast.rule(::Type{AD1B}, ::Type{AD2B}) = AD1B +Broadcast.rule(::Type{AD2B}, ::Type{AD1B}) = AD1B + +# Two specialized broadcast rules with conflicting precedence +struct AD1C{T,N} <: ArrayData{T,N} + data::Array{T,N} end +Broadcast.rule(::Type{T}) where {T<:AD1C} = AD1C +Base.similar(f, r::Broadcast.Result{AD1C}, As...) = + AD1C(Array{eltype(r)}(indices(r))) +struct AD2C{T,N} <: ArrayData{T,N} + data::Array{T,N} +end +Broadcast.rule(::Type{T}) where {T<:AD2C} = AD2C +Base.similar(f, r::Broadcast.Result{AD2C}, As...) = + AD2C(Array{eltype(r)}(indices(r))) + +Broadcast.rule(::Type{AD1C}, ::Type{AD2C}) = AD1C +Broadcast.rule(::Type{AD2C}, ::Type{AD1C}) = AD2C @testset "broadcasting for custom AbstractArray" begin a = randn(10) @@ -455,6 +503,35 @@ end @test a .* a' == @inferred(aa .* aa') @test isa(aa .+ 1, Array19745) @test isa(aa .* aa', Array19745) + a1 = AD1(rand(2,3)) + a2 = AD2(rand(2)) + @test a1 .+ 1 isa AD1 + @test a2 .+ 1 isa AD2 + @test a1 .+ a2 isa Array + @test a2 .+ a1 isa Array + @test a1 .+ a2 .+ a1 isa Array + @test a1 .+ a2 .+ a2 isa Array + a1 = AD1P(rand(2,3)) + a2 = AD2P(rand(2)) + @test a1 .+ 1 isa AD1P + @test a2 .+ 1 isa AD2P + @test a1 .+ a2 isa AD1P + @test a2 .+ a1 isa AD1P + @test a1 .+ a2 .+ a1 isa AD1P + @test a1 .+ a2 .+ a2 isa AD1P + a1 = AD1B(rand(2,3)) + a2 = AD2B(rand(2)) + @test a1 .+ 1 isa AD1B + @test a2 .+ 1 isa AD2B + @test a1 .+ a2 isa AD1B + @test a2 .+ a1 isa AD1B + @test a1 .+ a2 .+ a1 isa AD1B + @test a1 .+ a2 .+ a2 isa AD1B + a1 = AD1C(rand(2,3)) + a2 = AD2C(rand(2)) + @test a1 .+ 1 isa AD1C + @test a2 .+ 1 isa AD2C + @test_throws ErrorException a1 .+ a2 end # broadcast should only "peel off" one container layer @@ -466,7 +543,7 @@ end # Test that broadcast's promotion mechanism handles closures accepting more than one argument. # (See issue #19641 and referenced issues and pull requests.) -let f() = (a = 1; Base.Broadcast._broadcast_eltype((x, y) -> x + y + a, 1.0, 1.0)) +let f() = (a = 1; Broadcast.combine_eltypes((x, y) -> x + y + a, 1.0, 1.0)) @test @inferred(f()) == Float64 end @@ -485,7 +562,7 @@ end # Test that broadcast treats type arguments as scalars, i.e. containertype yields Any, # even for subtypes of abstract array. (https://github.com/JuliaStats/DataArrays.jl/issues/229) @testset "treat type arguments as scalars, DataArrays issue 229" begin - @test Base.Broadcast.containertype(AbstractArray) == Any + @test Broadcast.combine_types(AbstractArray) == Broadcast.Scalar @test broadcast(==, [1], AbstractArray) == BitArray([false]) @test broadcast(==, 1, AbstractArray) == false end @@ -525,3 +602,6 @@ let t = (0, 1, 2) o = 1 @test @inferred(broadcast(+, t, o)) == (1, 2, 3) end + +# Issue #22180 +@test isequal(convert.(Nullable, [1,2]), [Nullable(1), Nullable(2)]) diff --git a/test/sparse/higherorderfns.jl b/test/sparse/higherorderfns.jl index e0d613ffdfb72..323825e3482b3 100644 --- a/test/sparse/higherorderfns.jl +++ b/test/sparse/higherorderfns.jl @@ -118,7 +118,7 @@ end @test broadcast!(cos, Z, X) == sparse(broadcast!(cos, fZ, fX)) # --> test shape checks for broadcast! entry point # TODO strengthen this test, avoiding dependence on checking whether - # broadcast_indices throws to determine whether sparse broadcast should throw + # check_broadcast_indices throws to determine whether sparse broadcast should throw try Base.Broadcast.check_broadcast_indices(indices(Z), spzeros((shapeX .- 1)...)) catch @@ -142,7 +142,7 @@ end @test broadcast!(cos, V, X) == sparse(broadcast!(cos, fV, fX)) # --> test shape checks for broadcast! entry point # TODO strengthen this test, avoiding dependence on checking whether - # broadcast_indices throws to determine whether sparse broadcast should throw + # check_broadcast_indices throws to determine whether sparse broadcast should throw try Base.Broadcast.check_broadcast_indices(indices(V), spzeros((shapeX .- 1)...)) catch @@ -176,9 +176,9 @@ end @test broadcast(*, X, Y) == sparse(broadcast(*, fX, fY)) @test broadcast(f, X, Y) == sparse(broadcast(f, fX, fY)) # TODO strengthen this test, avoiding dependence on checking whether - # broadcast_indices throws to determine whether sparse broadcast should throw + # check_broadcast_indices throws to determine whether sparse broadcast should throw try - Base.Broadcast.broadcast_indices(spzeros((shapeX .- 1)...), Y) + Base.Broadcast.combine_indices(spzeros((shapeX .- 1)...), Y) catch @test_throws DimensionMismatch broadcast(+, spzeros((shapeX .- 1)...), Y) end @@ -199,7 +199,7 @@ end @test broadcast!(f, Z, X, Y) == sparse(broadcast!(f, fZ, fX, fY)) # --> test shape checks for both broadcast and broadcast! entry points # TODO strengthen this test, avoiding dependence on checking whether - # broadcast_indices throws to determine whether sparse broadcast should throw + # check_broadcast_indices throws to determine whether sparse broadcast should throw try Base.Broadcast.check_broadcast_indices(indices(Z), spzeros((shapeX .- 1)...), Y) catch @@ -226,9 +226,9 @@ end @test broadcast(*, X, Y, Z) == sparse(broadcast(*, fX, fY, fZ)) @test broadcast(f, X, Y, Z) == sparse(broadcast(f, fX, fY, fZ)) # TODO strengthen this test, avoiding dependence on checking whether - # broadcast_indices throws to determine whether sparse broadcast should throw + # check_broadcast_indices throws to determine whether sparse broadcast should throw try - Base.Broadcast.broadcast_indices(spzeros((shapeX .- 1)...), Y, Z) + Base.Broadcast.combine_indices(spzeros((shapeX .- 1)...), Y, Z) catch @test_throws DimensionMismatch broadcast(+, spzeros((shapeX .- 1)...), Y, Z) end @@ -256,7 +256,7 @@ end @test broadcast!(f, Q, X, Y, Z) == sparse(broadcast!(f, fQ, fX, fY, fZ)) # --> test shape checks for both broadcast and broadcast! entry points # TODO strengthen this test, avoiding dependence on checking whether - # broadcast_indices throws to determine whether sparse broadcast should throw + # check_broadcast_indices throws to determine whether sparse broadcast should throw try Base.Broadcast.check_broadcast_indices(indices(Q), spzeros((shapeX .- 1)...), Y, Z) catch