Make ` Array{T}(undef, ...) ` fill with a value for debugging?

Is there a way to force array allocation to fill with a specific value, eg NaN? I often find myself debugging bugs that don’t appear when the array happens to be filled with zeros, so this would make these bugs reproducible.

I was thinking just overloading the Array constructors might work, but perhaps there’s already an established way to do this?

Why not just use fill(NaN, ...)?

Because I’m debugging code that calls Array, possibly in other packages.

Ah, now I see your problem. You could perhaps write a pass with IRTools.jl

That looks too complicated for me to figure out on my own…

struct DebugUndef x end
const undef = DebugUndef(NaN)

Base.Array(u::DebugUndef, args...) = fill!(Array(Base.undef, args...), u.x)
Base.Array{T}(u::DebugUndef, args...) where T = fill!(Array{T}(Base.undef, args...), u.x)
Base.Array{T,N}(u::DebugUndef, args...) where {T,N} = fill!(Array{T,N}(Base.undef, args...), u.x)
julia> Array{Float64, 2}(undef, 2, 3)
2×3 Array{Float64,2}:
 NaN  NaN  NaN
 NaN  NaN  NaN

julia> Vector{Float64}(undef, 4)
4-element Array{Float64,1}:
 NaN
 NaN
 NaN
 NaN
2 Likes

That is still locally scoped though so it won’t work in e.g.

You could try this solution with IRTools:

using IRTools: @dynamo, IR, recurse!, xcall
using IRTools.Inner: Variable, Statement

_nan(T::Type{<:AbstractFloat}) = T(NaN)
_nan(T) = T(false)

@dynamo function debug_array(a...)
    ir = IR(a...)
    ir === nothing && return
    for (_, st) in ir
        if Meta.isexpr(st.expr, :call)
            x = st.expr.args[1]
            if x isa Variable && haskey(ir, x)
                func = ir[x].expr
                if Meta.isexpr(func, :call) &&
                    func.args[1] === GlobalRef(Core, :apply_type) &&
                    func.args[2] isa GlobalRef && func.args[2].name === :Array

                    T = func.args[3]
                    ir[x] = Statement(Expr(:call, GlobalRef(@__MODULE__, :_nan), T))
                    st.expr.args[1] = GlobalRef(Base, :fill)
                    st.expr.args[2] = x
                end
            end
        end
    end
    for (x, st) in ir
        if Meta.isexpr(st.expr, :call) &&
            st.expr.args[1] !== GlobalRef(Base, :fill)
            ir[x] = xcall(debug_array, st.expr.args...)
        end
    end
    return ir
end

You can then call it like this:

julia> debug_array() do
           a = Array{Float32}(undef, 1, 2, 3)
           print(a[1, 2, 1])
       end
NaN
1 Like

How do I make Array{T,N} constructors also work, e.g.:

julia> debug_array() do
       Matrix{Float64}(undef,10,10)
       end
10×10 Array{Float64,2}:
 2.31914e-314  2.27727e-314  2.27195e-314  …  2.27201e-314  0.0
 2.27194e-314  2.27727e-314  2.272e-314       2.27201e-314  0.0
 2.272e-314    2.27727e-314  2.27727e-314     2.27735e-314  0.0
 2.27194e-314  2.27727e-314  2.27727e-314     2.27195e-314  0.0
 2.27727e-314  2.31914e-314  2.272e-314       2.27735e-314  0.0
 2.31914e-314  2.27195e-314  2.272e-314    …  2.27735e-314  2.28897e-314
 2.272e-314    2.27195e-314  2.272e-314       2.27195e-314  2.28897e-314
 2.272e-314    2.27195e-314  2.272e-314       2.27735e-314  0.0
 2.27194e-314  2.27195e-314  2.27727e-314     0.0           0.0
 2.27726e-314  2.27195e-314  2.272e-314       0.0           0.0

Perhaps not the nicest solution, but this should do the trick:

using IRTools: @dynamo, IR, recurse!, xcall
using IRTools.Inner: Variable, Statement

_nan(T::Type{<:AbstractFloat}) = T(NaN)
_nan(T) = T(false)

@dynamo function debug_array(a...)
    ir = IR(a...)
    ir === nothing && return
    for (_, st) in ir
        if Meta.isexpr(st.expr, :call)
            x = st.expr.args[1]
            if x isa Variable && haskey(ir, x)
                func = ir[x].expr
                if Meta.isexpr(func, :call) &&
                    func.args[1] === GlobalRef(Core, :apply_type) &&
                    func.args[2] isa GlobalRef && func.args[2].name in (:Array, :Vector, :Matrix)

                    T = func.args[3]
                    ir[x] = Statement(Expr(:call, GlobalRef(@__MODULE__, :_nan), T))
                    st.expr.args[1] = GlobalRef(Base, :fill)
                    st.expr.args[2] = x
                end
            end
        end
    end
    for (x, st) in ir
        if Meta.isexpr(st.expr, :call) &&
            st.expr.args[1] !== GlobalRef(Base, :fill)
            ir[x] = xcall(debug_array, st.expr.args...)
        end
    end
    return ir
end
1 Like

Cool! Seems to work pretty well even with other packages:

julia> debug_array() do
       BlockBandedMatrix{Float64}(undef, 1:3,1:3, (1,1))
       end
3×3-blocked 6×6 BlockSkylineMatrix{Float64,Array{Float64,1},BlockBandedMatrices.BlockSkylineSizes{Tuple{BlockArrays.BlockedUnitRange{Array{Int64,1}},BlockArrays.BlockedUnitRange{Array{Int64,1}}},Fill{Int64,1,Tuple{Base.OneTo{Int64}}},Fill{Int64,1,Tuple{Base.OneTo{Int64}}},BandedMatrix{Int64,Array{Int64,2},Base.OneTo{Int64}},Array{Int64,1}}}:
 NaN    │  NaN  NaN  │     ⋅      ⋅      ⋅ 
 ───────┼────────────┼─────────────────────
 NaN    │  NaN  NaN  │  NaN    NaN    NaN  
 NaN    │  NaN  NaN  │  NaN    NaN    NaN  
 ───────┼────────────┼─────────────────────
    ⋅   │  NaN  NaN  │  NaN    NaN    NaN  
    ⋅   │  NaN  NaN  │  NaN    NaN    NaN  
    ⋅   │  NaN  NaN  │  NaN    NaN    NaN  

Note that not every case is caught:

julia> debug_array() do
       BandedMatrix{Float64}(undef, (3,3), (1,1))
       end
3×3 BandedMatrix{Float64,Array{Float64,2},Base.OneTo{Int64}}:
 0.0  0.0        ⋅ 
 0.0  5.0e-324  5.0e-324
  ⋅   0.0       0.0

Though that’s due to an arguably bad design in BandedMatrices.jl:

BandedMatrix{T, C}(::UndefInitializer, (n,m)::NTuple{2,Integer}, (a,b)::NTuple{2,Integer}) where {T<:BlasFloat, C<:AbstractMatrix{T}} =
    _BandedMatrix(C(undef,max(0,b+a+1),m), n, a, b)

OK, I feel really dumb. This can be written much more concisely like this:

using IRTools: @dynamo, IR, recurse!, xcall
using IRTools.Inner: Variable, Statement

_nan(T::Type{<:AbstractFloat}) = T(NaN)
_nan(T) = T(false)

debug_array(::Type{<:Array{T}}, ::UndefInitializer, dims...) where T = fill(_nan(T), dims...)

@dynamo function debug_array(a...)
    ir = IR(a...)
    ir === nothing && return
    recurse!(ir)
    return ir
end

It even works in the BandedMatrix case

5 Likes

Beautiful!

julia> debug_array() do
       BandedMatrix{Float64}(undef, (3,3), (1,1))
       end
3×3 BandedMatrix{Float64,Array{Float64,2},Base.OneTo{Int64}}:
 NaN    NaN     ⋅ 
 NaN    NaN  NaN
    ⋅   NaN  NaN

julia> debug_array() do
       similar(randn(3), 3)
       end
3-element Array{Float64,1}:
 NaN
 NaN
 NaN