Skip to content

Commit

Permalink
Merge pull request JuliaLang#48639 from JuliaLang/jn/applicable-hasme…
Browse files Browse the repository at this point in the history
…thod-tfuncs

define tfuncs for applicable and hasmethod
  • Loading branch information
vtjnash committed Feb 15, 2023
2 parents 1270b34 + f9e1c6c commit 95e0da1
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 14 deletions.
14 changes: 13 additions & 1 deletion base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,24 @@ include("options.jl")
function Core.kwcall(kwargs, ::typeof(invoke), f, T, args...)
@inline
# prepend kwargs and f to the invoked from the user
T = rewrap_unionall(Tuple{Any, Core.Typeof(f), (unwrap_unionall(T)::DataType).parameters...}, T)
T = rewrap_unionall(Tuple{Core.Typeof(kwargs), Core.Typeof(f), (unwrap_unionall(T)::DataType).parameters...}, T)
return invoke(Core.kwcall, T, kwargs, f, args...)
end
# invoke does not have its own call cache, but kwcall for invoke does
setfield!(typeof(invoke).name.mt, :max_args, 3, :monotonic) # invoke, f, T, args...

# define applicable(f, T, args...; kwargs...), without kwargs wrapping
# to forward to applicable
function Core.kwcall(kwargs, ::typeof(applicable), @nospecialize(args...))
@inline
return applicable(Core.kwcall, kwargs, args...)
end
function Core._hasmethod(@nospecialize(f), @nospecialize(t)) # this function has a special tfunc (TODO: make this a Builtin instead like applicable)
tt = rewrap_unionall(Tuple{Core.Typeof(f), (unwrap_unionall(t)::DataType).parameters...}, t)
return Core._hasmethod(tt)
end


# core operations & types
include("promotion.jl")
include("tuple.jl")
Expand Down
5 changes: 5 additions & 0 deletions base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -850,4 +850,9 @@ struct Pair{A, B}
end
end

function _hasmethod(@nospecialize(tt)) # this function has a special tfunc
world = ccall(:jl_get_tls_world_age, UInt, ())
return Intrinsics.not_int(ccall(:jl_gf_invoke_lookup, Any, (Any, Any, UInt), tt, nothing, world) === nothing)
end

ccall(:jl_set_istopmod, Cvoid, (Any, Bool), Core, true)
6 changes: 5 additions & 1 deletion base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1889,7 +1889,7 @@ function abstract_invoke(interp::AbstractInterpreter, (; fargs, argtypes)::ArgIn
(types, isexact, isconcrete, istype) = instanceof_tfunc(argtype_by_index(argtypes, 3))
isexact || return CallMeta(Any, Effects(), NoCallInfo())
unwrapped = unwrap_unionall(types)
if types === Bottom || types === Any || !(unwrapped isa DataType)
if types === Bottom || !(unwrapped isa DataType) || unwrapped.name !== Tuple.name
return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo())
end
argtype = argtypes_to_type(argtype_tail(argtypes, 4))
Expand Down Expand Up @@ -1971,6 +1971,8 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
return abstract_modifyfield!(interp, argtypes, si, sv)
elseif f === Core.finalizer
return abstract_finalizer(interp, argtypes, sv)
elseif f === applicable
return abstract_applicable(interp, argtypes, sv, max_methods)
end
rt = abstract_call_builtin(interp, f, arginfo, sv, max_methods)
effects = builtin_effects(𝕃ᵢ, f, arginfo, rt)
Expand Down Expand Up @@ -2059,6 +2061,8 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
val = _pure_eval_call(f, arginfo)
return CallMeta(val === nothing ? Type : val, EFFECTS_TOTAL, MethodResultPure())
end
elseif f === Core._hasmethod
return _hasmethod_tfunc(interp, argtypes, sv)
end
atype = argtypes_to_type(argtypes)
return abstract_call_gf_by_type(interp, f, arginfo, si, atype, sv, max_methods)
Expand Down
90 changes: 88 additions & 2 deletions base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,6 @@ end
return true
end
add_tfunc(Core._typevar, 3, 3, typevar_tfunc, 100)
add_tfunc(applicable, 1, INT_INF, @nospecs((𝕃::AbstractLattice, f, args...)->Bool), 100)

@nospecs function arraysize_tfunc(𝕃::AbstractLattice, ary, dim)
hasintersect(widenconst(ary), Array) || return Bottom
Expand Down Expand Up @@ -2107,7 +2106,7 @@ end
end

# known to be always effect-free (in particular nothrow)
const _PURE_BUILTINS = Any[tuple, svec, ===, typeof, nfields]
const _PURE_BUILTINS = Any[tuple, svec, ===, typeof, nfields, applicable]

# known to be effect-free (but not necessarily nothrow)
const _EFFECT_FREE_BUILTINS = [
Expand Down Expand Up @@ -2539,6 +2538,93 @@ function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, s
return CallMeta(Type, EFFECTS_THROWS, NoCallInfo())
end

# a simplified model of abstract_call_gf_by_type for applicable
function abstract_applicable(interp::AbstractInterpreter, argtypes::Vector{Any},
sv::InferenceState, max_methods::Int)
length(argtypes) < 2 && return CallMeta(Union{}, EFFECTS_UNKNOWN, NoCallInfo())
isvarargtype(argtypes[2]) && return CallMeta(Bool, EFFECTS_UNKNOWN, NoCallInfo())
argtypes = argtypes[2:end]
atype = argtypes_to_type(argtypes)
matches = find_matching_methods(argtypes, atype, method_table(interp),
InferenceParams(interp).max_union_splitting, max_methods)
if isa(matches, FailedMethodMatch)
rt = Bool # too many matches to analyze
else
(; valid_worlds, applicable) = matches
update_valid_age!(sv, valid_worlds)

# also need an edge to the method table in case something gets
# added that did not intersect with any existing method
if isa(matches, MethodMatches)
matches.fullmatch || add_mt_backedge!(sv, matches.mt, atype)
else
for (thisfullmatch, mt) in zip(matches.fullmatches, matches.mts)
thisfullmatch || add_mt_backedge!(sv, mt, atype)
end
end

napplicable = length(applicable)
if napplicable == 0
rt = Const(false) # never any matches
else
rt = Const(true) # has applicable matches
for i in 1:napplicable
match = applicable[i]::MethodMatch
edge = specialize_method(match)
add_backedge!(sv, edge)
end

if isa(matches, MethodMatches) ? (!matches.fullmatch || any_ambig(matches)) :
(!all(matches.fullmatches) || any_ambig(matches))
# Account for the fact that we may encounter a MethodError with a non-covered or ambiguous signature.
rt = Bool
end
end
end
return CallMeta(rt, EFFECTS_TOTAL, NoCallInfo())
end
add_tfunc(applicable, 1, INT_INF, @nospecs((𝕃::AbstractLattice, f, args...)->Bool), 40)

# a simplified model of abstract_invoke for Core._hasmethod
function _hasmethod_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, sv::InferenceState)
if length(argtypes) == 3 && !isvarargtype(argtypes[3])
ft′ = argtype_by_index(argtypes, 2)
ft = widenconst(ft′)
ft === Bottom && return CallMeta(Bool, EFFECTS_THROWS, NoCallInfo())
typeidx = 3
elseif length(argtypes) == 2 && !isvarargtype(argtypes[2])
typeidx = 2
else
return CallMeta(Any, Effects(), NoCallInfo())
end
(types, isexact, isconcrete, istype) = instanceof_tfunc(argtype_by_index(argtypes, typeidx))
isexact || return CallMeta(Bool, Effects(), NoCallInfo())
unwrapped = unwrap_unionall(types)
if types === Bottom || !(unwrapped isa DataType) || unwrapped.name !== Tuple.name
return CallMeta(Bool, EFFECTS_THROWS, NoCallInfo())
end
if typeidx == 3
isdispatchelem(ft) || return CallMeta(Bool, Effects(), NoCallInfo()) # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below
types = rewrap_unionall(Tuple{ft, unwrapped.parameters...}, types)::Type
end
mt = ccall(:jl_method_table_for, Any, (Any,), types)
if !isa(mt, Core.MethodTable)
return CallMeta(Bool, EFFECTS_THROWS, NoCallInfo())
end
match, valid_worlds, overlayed = findsup(types, method_table(interp))
update_valid_age!(sv, valid_worlds)
if match === nothing
rt = Const(false)
add_mt_backedge!(sv, mt, types) # this should actually be an invoke-type backedge
else
rt = Const(true)
edge = specialize_method(match)
add_invoke_backedge!(sv, types, edge)
end
return CallMeta(rt, EFFECTS_TOTAL, NoCallInfo())
end


# N.B.: typename maps type equivalence classes to a single value
function typename_static(@nospecialize(t))
t isa Const && return _typename(t.val)
Expand Down
5 changes: 4 additions & 1 deletion base/methodshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function kwarg_decl(m::Method, kwtype = nothing)
push!(kws, kws[i])
deleteat!(kws, i)
end
isempty(kws) && push!(kws, :var"...")
return kws
end
end
Expand Down Expand Up @@ -194,7 +195,9 @@ end

function sym_to_string(sym)
s = String(sym)
if endswith(s, "...")
if s === :var"..."
return "..."
elseif endswith(s, "...")
return string(sprint(show_sym, Symbol(s[1:end-3])), "...")
else
return sprint(show_sym, sym)
Expand Down
28 changes: 19 additions & 9 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1709,20 +1709,30 @@ julia> hasmethod(g, Tuple{}, (:a, :b, :c, :d)) # g accepts arbitrary kwargs
true
```
"""
function hasmethod(@nospecialize(f), @nospecialize(t); world::UInt=get_world_counter())
t = signature_type(f, t)
return ccall(:jl_gf_invoke_lookup, Any, (Any, Any, UInt), t, nothing, world) !== nothing
function hasmethod(@nospecialize(f), @nospecialize(t))
return Core._hasmethod(f, t isa Type ? t : to_tuple_type(t))
end

function hasmethod(@nospecialize(f), @nospecialize(t), kwnames::Tuple{Vararg{Symbol}}; world::UInt=get_world_counter())
# TODO: this appears to be doing the wrong queries
hasmethod(f, t, world=world) || return false
isempty(kwnames) && return true
m = which(f, t)
kws = kwarg_decl(m)
function Core.kwcall(kwargs, ::typeof(hasmethod), @nospecialize(f), @nospecialize(t))
world = kwargs.world::UInt # make sure this is the only local, to avoid confusing kwarg_decl()
return ccall(:jl_gf_invoke_lookup, Any, (Any, Any, UInt), signature_type(f, t), nothing, world) !== nothing
end

function hasmethod(f, t, kwnames::Tuple{Vararg{Symbol}}; world::UInt=get_world_counter())
@nospecialize
isempty(kwnames) && return hasmethod(f, t; world)
t = to_tuple_type(t)
ft = Core.Typeof(f)
u = unwrap_unionall(t)::DataType
tt = rewrap_unionall(Tuple{typeof(Core.kwcall), typeof(pairs((;))), ft, u.parameters...}, t)
match = ccall(:jl_gf_invoke_lookup, Any, (Any, Any, UInt), tt, nothing, world)
match === nothing && return false
kws = ccall(:jl_uncompress_argnames, Array{Symbol,1}, (Any,), (match::Method).slot_syms)
isempty(kws) && return true # some kwfuncs simply forward everything directly
for kw in kws
endswith(String(kw), "...") && return true
end
kwnames = Symbol[kwnames[i] for i in 1:length(kwnames)]
return issubset(kwnames, kws)
end

Expand Down
57 changes: 57 additions & 0 deletions test/compiler/inference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4761,3 +4761,60 @@ unknown_sparam_nothrow2(x::Ref{Ref{T}}) where T = @isdefined(T) ? T::Type : noth
@test only(Base.return_types(unknown_sparam_throw, (Any,))) === Union{Nothing,Type}
@test only(Base.return_types(unknown_sparam_nothrow1, (Ref,))) === Type
@test only(Base.return_types(unknown_sparam_nothrow2, (Ref{Ref{T}} where T,))) === Type

function fapplicable end
gapplicable() = Val(applicable(fapplicable))
gapplicable(x) = Val(applicable(fapplicable; x))
@test only(Base.return_types(gapplicable, ())) === Val{false}
@test only(Base.return_types(gapplicable, (Int,))) === Val{false}
fapplicable() = 1
@test only(Base.return_types(gapplicable, ())) === Val{true}
@test only(Base.return_types(gapplicable, (Int,))) === Val{false}
Base.delete_method(which(fapplicable, ()))
@test only(Base.return_types(gapplicable, ())) === Val{false}
@test only(Base.return_types(gapplicable, (Int,))) === Val{false}
fapplicable(; x) = x
@test only(Base.return_types(gapplicable, ())) === Val{true}
@test only(Base.return_types(gapplicable, (Int,))) === Val{true}
@test only(Base.return_types(()) do; applicable(); end) === Union{}
@test only(Base.return_types((Any,)) do x; Val(applicable(x...)); end) == Val
@test only(Base.return_types((Tuple{Vararg{Int}},)) do x; Val(applicable(+, 1, 2, x...)); end) == Val # could be improved to Val{true}
@test only(Base.return_types((Tuple{Vararg{Int}},)) do x; Val(applicable(+, 1, 2, 3, x...)); end) === Val{true}
@test only(Base.return_types((Int,)) do x; Val(applicable(+, 1, x)); end) === Val{true}
@test only(Base.return_types((Union{Int32,Int64},)) do x; Val(applicable(+, 1, x)); end) === Val{true}
@test only(Base.return_types((String,)) do x; Val(applicable(+, 1, x)); end) === Val{false}
fapplicable(::Int, ::Integer) = 2
fapplicable(::Integer, ::Int32) = 3
@test only(Base.return_types((Int32,)) do x; Val(applicable(fapplicable, 1, x)); end) === Val{false}
@test only(Base.return_types((Int64,)) do x; Val(applicable(fapplicable, 1, x)); end) === Val{true}
@test only(Base.return_types((Tuple{Vararg{Int}},)) do x; Val(applicable(tuple, x...)); end) === Val{true}
@test only(Base.return_types((Tuple{Vararg{Int}},)) do x; Val(applicable(sin, 1, x...)); end) == Val
@test only(Base.return_types((Tuple{Vararg{Int}},)) do x; Val(applicable(sin, 1, 2, x...)); end) === Val{false}

function fhasmethod end
ghasmethod() = Val(hasmethod(fhasmethod, Tuple{}))
@test only(Base.return_types(ghasmethod, ())) === Val{false}
fhasmethod() = 1
@test only(Base.return_types(ghasmethod, ())) === Val{true}
Base.delete_method(which(fhasmethod, ()))
@test only(Base.return_types(ghasmethod, ())) === Val{false}
@test only(Base.return_types(()) do; Core._hasmethod(); end) === Any
@test only(Base.return_types(()) do; Core._hasmethod(+, Tuple, 1); end) === Any
@test only(Base.return_types(()) do; Core._hasmethod(+, 1); end) === Bool
@test only(Base.return_types(()) do; Core._hasmethod(+, Tuple{1}); end) === Bool
@test only(Base.return_types((Any,)) do x; Val(hasmethod(x...)); end) == Val
@test only(Base.return_types(()) do; Val(hasmethod(+, Tuple{Int, Int})); end) === Val{true}
@test only(Base.return_types(()) do; Val(hasmethod(+, Tuple{Int, Int, Vararg{Int}})); end) === Val{false}
@test only(Base.return_types(()) do; Val(hasmethod(+, Tuple{Int, Int, Int, Vararg{Int}})); end) === Val{true}
@test only(Base.return_types(()) do; Val(hasmethod(+, Tuple{Int, Int})); end) === Val{true}
@test only(Base.return_types(()) do; Val(hasmethod(+, Tuple{Int, Union{Int32,Int64}})); end) === Val{true}
@test only(Base.return_types(()) do; Val(hasmethod(+, Tuple{Int, Union{Int,String}})); end) === Val{false}
@test only(Base.return_types(()) do; Val(hasmethod(+, Tuple{Int, Any})); end) === Val{false}
@test only(Base.return_types() do; Val(hasmethod(+, Tuple{Int, String})); end) === Val{false}
fhasmethod(::Int, ::Integer) = 2
fhasmethod(::Integer, ::Int32) = 3
@test only(Base.return_types(()) do; Val(hasmethod(fhasmethod, Tuple{Int, Int32})); end) === Val{false}
@test only(Base.return_types(()) do; Val(hasmethod(fhasmethod, Tuple{Int, Int64})); end) === Val{true}
@test only(Base.return_types(()) do; Val(hasmethod(tuple, Tuple{Vararg{Int}})); end) === Val{true}
@test only(Base.return_types(()) do; Val(hasmethod(sin, Tuple{Int, Vararg{Int}})); end) == Val{false}
@test only(Base.return_types(()) do; Val(hasmethod(sin, Tuple{Int, Int, Vararg{Int}})); end) === Val{false}

0 comments on commit 95e0da1

Please sign in to comment.