Can I use hasmethod inside @generated function?

I thought the answer was no, since hasmethod is not pure. But unless I try very hard to break it (see struct B example below) I cannot invoke any suspicious bugs. From the example I wrote, it seems that using hasmethod inside @generated function is OK as long as the method checked by hasmethod is already defined before the first invocation of the method. Is it correct? Are there any other pitfalls? Am I relying on some implementation details of julia which can change in the future?

To avoid the XY problem here is the context: What I need in the end is a single type-stable Base.setindex-like method that works with immutable tuples/arrays such as StaticArrays and also mutable arrays such as standard Array. I thought setindex_static below would be the straightforward implementation if hasmethod were pure (but if hasmethod were pure we can’t program anything in Julia).

I wonder adding world age bounds to CodeInfo would solve this kind of issue.

module TestHasMethodHack

using Test

function setindex_on_copy(obj, val, indices...)
    clone = similar(obj, promote_type(eltype(obj), typeof(val)))
    copyto!(clone, obj)
    setindex!(clone, val, indices...)
    return clone
end


struct A
    x
end

Base.setindex(::A, x, ::Integer) = A(x)


@generated function setindex_static(obj, val, indices...)
    if hasmethod(Base.setindex, Tuple{obj, val, indices...})
        setter = Base.setindex
    else
        setter = setindex_on_copy
    end
    quote
        $setter(obj, val, indices...)
    end
end

@assert setindex_static([0], 1, 1) == [1]  # Having it here (or commenting it out) does not change anything.


struct B
    x
end

try
    setindex_static(B(1), 2, 1)
catch err
    @info "Ignoring" exception=err
end

Base.setindex(::B, x, ::Integer) = B(x)


struct C
    x
end

Base.setindex(::C, x, ::Integer) = C(x)


@testset begin
    @test setindex_static(A(0), 1, 1) == A(1)
    @test_broken setindex_static(B(0), 1, 1) == B(1)
    @test setindex_static(C(0), 1, 1) == C(1)
end

end  # module

which outputs:

┌ Info: Ignoring
│   exception =
│    MethodError: no method matching similar(::Main.TestHasMethodHack.B, ::Type{Any})
│    Closest candidates are:
│      similar(::Array{T,1}, ::Type) where T at array.jl:329
│      similar(::Array{T,2}, ::Type) where T at array.jl:330
│      similar(::Array, ::Type, ::Tuple{Vararg{Int64,N}}) where N at array.jl:332
└      ...
Test Summary: | Pass  Broken  Total
test set      |    2       1      3
2 Likes

Somewhat related: https://github.com/mauro3/SimpleTraits.jl/issues/40.

Thanks for the link! It was mentioned that hasmethod (method_exists) can be made inferable. Do you know what had happened in julia for such direction?

I think, nothing so far. I talked to Jameson at JuliaCon, and he thinks it is easy to do (for him). Although he was not sure of the merit of it. Note, that method comparison would may need to be inferrable too to make a “universal” system due to generic fall-back methods.

2 Likes

I see. Thanks. There are several occasions I wanted this feature. Too bad it does not exist…