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?

1 Like

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