How can I see what parameters are making my code erroring when broadcasting a function call?

Hi all!
I have a function that raises a MethodError when broadcasted to a large vector of Strings. The error simply says

julia> df_model_measure_master[!, dest_source_eq_sym] = has_dest_source_eq.(df_model_measure_master[:, instruction_sym])
ERROR: MethodError: Cannot `convert` an object of type Nothing to an object of type Bool
Closest candidates are:
  convert(::Type{T}, ::Ptr) where T<:Integer at pointer.jl:23
  convert(::Type{T}, ::DualNumbers.Dual) where T<:Union{Real, Complex} at ~/.julia/packages/DualNumbers/5knFX/src/dual.jl:24
  convert(::Type{T}, ::Gray24) where T<:Real at ~/.julia/packages/ColorTypes/1dGw6/src/conversions.jl:114
  ...
Stacktrace:
  [1] has_dest_source_eq(instruction::String)
    @ Main.Commons ~/onedrive/tesi/Julia_package/ARMCortexM4Model/src/Commons.jl:155
  [2] _broadcast_getindex_evalf
    @ ./broadcast.jl:670 [inlined]
  [3] _broadcast_getindex
    @ ./broadcast.jl:643 [inlined]
  [4] getindex
    @ ./broadcast.jl:597 [inlined]
  [5] macro expansion
    @ ./broadcast.jl:979 [inlined]
  [6] macro expansion
    @ ./simdloop.jl:77 [inlined]
  [7] copyto!(dest::BitVector, bc::Base.Broadcast.Broadcasted{Nothing, Tuple{Base.OneTo{Int64}}, typeof(has_dest_source_eq), Tuple{Vector{String}}})
    @ Base.Broadcast ./broadcast.jl:978
  [8] copyto!
    @ ./broadcast.jl:913 [inlined]
  [9] copy
    @ ./broadcast.jl:885 [inlined]
 [10] materialize(bc::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}, Nothing, typeof(has_dest_source_eq), Tuple{Vector{String}}})
    @ Base.Broadcast ./broadcast.jl:860
 [11] top-level scope
    @ REPL[54]:1

and as you can see it doesn’t tell which particular string made it panic. How can I see that?

A possible idea is to remove the type annotation from the function (which, btw, is this one):

function has_dest_source_eq(instruction::AbstractString)::Bool
    if startswith(instruction, Regex(join(mnemonic_source_source, "|")))
        # we return false, as there is no dest to begin with
        return false
    else
        # we have at least one dest
        match_itr = eachmatch(r"r\d+", instruction)
        
        if startswith(instruction, Regex(join(mnemonic_dest_dest_source_source, "|")))
            #we need 2 dests and 2 sources
            dest_regs = Iterators.take(match_itr, 2) |>
                collect .|> x -> x.match
            # here we assume all the instructions are well formed
            # TODO implement a check?
            source_regs = Iterators.drop(match_itr, 2) |>
                collect .|> x -> x.match
            return any(in(dest_regs).(source_regs))
        else
            # we have just 1 dest, and 1 to 3 sources
            (dest_reg, match_itr) = Iterators.peel(match_itr)
            dest_reg = dest_reg.match
            if startswith(instruction, Regex(join(mnemonic_dest_source, "|")))
                source_regs = Iterators.take(match_itr, 1) |>
                    collect .|> x -> x.match
                return dest_reg ∈ source_regs
            elseif startswith(instruction, Regex(join(mnemonic_dest_source_source, "|")))
                source_regs = Iterators.take(match_itr, 2) |>
                    collect .|> x -> x.match
                return dest_reg ∈ source_regs
            elseif startswith(instruction, Regex(join(mnemonic_dest_source_source_source, "|")))
                source_regs = Iterators.take(match_itr, 3) |>
                    collect .|> x -> x.match
                return dest_reg ∈ source_regs
            end
        end
    end
end

and then see what string produced a nothing, but it seems quite a sophistry…? Is there a more straightforward way? May Debugger.jl help here (without manually stepping through all the values)

Thanks!

I’m not sure this is what you are looking for, but if you want to determine the offending string, you can try each value of the list and see which one causes the error. First, get the index of the string that causes the error:

cause_error(str) = try (has_dest_source_eq(str); false) catch; true end
index_error = findfirst(cause_error, df_model_measure_master[:, instruction_sym])

and then check the string:

println(df_model_measure_master[index_error, instruction_sym])
2 Likes

Thanks, it works! However, it seems quite overcomplicated. Hasn’t Julia a implemented way to do this? May be worth opening a request on GH?

In julia, every expression returns some value, which is the last statement in that expression (a loop expression always returns nothing due to there not being one last expression of the block). For if, that means the last line in a branch is also the value if the whole if, meaning this is legal:

x = if false
   5
else
   3
end

And x is assigned the value 3. The innermost if/elseif chain doesn’t have an else, so if none of the conditions in the elseif match, what should the whole chain return? There is no value specified, and so it returns nothing. This then trickles up, because the innermost if/elseif chain is the last expression in the else branch it’s placed in, which is the last expression of the function. As a result, the whole function can return nothing! This is indeed the case, as your explicit ::Bool return type annotation tries to convert that nothing to a Bool, which can’t be done.

As for why there’s is not a builtin feature to detect this, that’s a fair question. One answer is that it can be hard to experiment with an API for this if it’s locked to the release cadence of julia itself. People usually recommend JET.jl for analyzing such statically inferrable errors, which should catch this and warn you about it.