Skip to content

support passing a specific Method to invoke #56692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 34 additions & 23 deletions Compiler/src/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -856,8 +856,7 @@ end

struct InvokeCall
types # ::Type
lookupsig # ::Type
InvokeCall(@nospecialize(types), @nospecialize(lookupsig)) = new(types, lookupsig)
InvokeCall(@nospecialize(types)) = new(types)
end

struct ConstCallResult
Expand Down Expand Up @@ -2218,34 +2217,46 @@ function abstract_invoke(interp::AbstractInterpreter, arginfo::ArgInfo, si::Stmt
ft′ = argtype_by_index(argtypes, 2)
ft = widenconst(ft′)
ft === Bottom && return Future(CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()))
(types, isexact, isconcrete, istype) = instanceof_tfunc(argtype_by_index(argtypes, 3), false)
isexact || return Future(CallMeta(Any, Any, Effects(), NoCallInfo()))
unwrapped = unwrap_unionall(types)
types === Bottom && return Future(CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()))
if !(unwrapped isa DataType && unwrapped.name === Tuple.name)
return Future(CallMeta(Bottom, TypeError, EFFECTS_THROWS, NoCallInfo()))
end
argtype = argtypes_to_type(argtype_tail(argtypes, 4))
nargtype = typeintersect(types, argtype)
nargtype === Bottom && return Future(CallMeta(Bottom, TypeError, EFFECTS_THROWS, NoCallInfo()))
nargtype isa DataType || return Future(CallMeta(Any, Any, Effects(), NoCallInfo())) # other cases are not implemented below
isdispatchelem(ft) || return Future(CallMeta(Any, Any, Effects(), NoCallInfo())) # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below
ft = ft::DataType
lookupsig = rewrap_unionall(Tuple{ft, unwrapped.parameters...}, types)::Type
nargtype = Tuple{ft, nargtype.parameters...}
argtype = Tuple{ft, argtype.parameters...}
matched, valid_worlds = findsup(lookupsig, method_table(interp))
matched === nothing && return Future(CallMeta(Any, Any, Effects(), NoCallInfo()))
update_valid_age!(sv, valid_worlds)
method = matched.method
types = argtype_by_index(argtypes, 3)
if types isa Const && types.val isa Method
method = types.val::Method
types = method # argument value
lookupsig = method.sig # edge kind
argtype = argtypes_to_type(pushfirst!(argtype_tail(argtypes, 4), ft))
nargtype = typeintersect(lookupsig, argtype)
nargtype === Bottom && return Future(CallMeta(Bottom, TypeError, EFFECTS_THROWS, NoCallInfo()))
nargtype isa DataType || return Future(CallMeta(Any, Any, Effects(), NoCallInfo())) # other cases are not implemented below
else
widenconst(types) >: Method && return Future(CallMeta(Any, Any, Effects(), NoCallInfo()))
(types, isexact, isconcrete, istype) = instanceof_tfunc(argtype_by_index(argtypes, 3), false)
isexact || return Future(CallMeta(Any, Any, Effects(), NoCallInfo()))
unwrapped = unwrap_unionall(types)
types === Bottom && return Future(CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()))
if !(unwrapped isa DataType && unwrapped.name === Tuple.name)
return Future(CallMeta(Bottom, TypeError, EFFECTS_THROWS, NoCallInfo()))
end
argtype = argtypes_to_type(argtype_tail(argtypes, 4))
nargtype = typeintersect(types, argtype)
nargtype === Bottom && return Future(CallMeta(Bottom, TypeError, EFFECTS_THROWS, NoCallInfo()))
nargtype isa DataType || return Future(CallMeta(Any, Any, Effects(), NoCallInfo())) # other cases are not implemented below
isdispatchelem(ft) || return Future(CallMeta(Any, Any, Effects(), NoCallInfo())) # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below
ft = ft::DataType
lookupsig = rewrap_unionall(Tuple{ft, unwrapped.parameters...}, types)::Type
nargtype = Tuple{ft, nargtype.parameters...}
argtype = Tuple{ft, argtype.parameters...}
matched, valid_worlds = findsup(lookupsig, method_table(interp))
matched === nothing && return Future(CallMeta(Any, Any, Effects(), NoCallInfo()))
update_valid_age!(sv, valid_worlds)
method = matched.method
end
tienv = ccall(:jl_type_intersection_with_env, Any, (Any, Any), nargtype, method.sig)::SimpleVector
ti = tienv[1]
env = tienv[2]::SimpleVector
mresult = abstract_call_method(interp, method, ti, env, false, si, sv)::Future
match = MethodMatch(ti, env, method, argtype <: method.sig)
ft′_box = Core.Box(ft′)
lookupsig_box = Core.Box(lookupsig)
invokecall = InvokeCall(types, lookupsig)
invokecall = InvokeCall(types)
return Future{CallMeta}(mresult, interp, sv) do result, interp, sv
(; rt, exct, effects, edge, volatile_inf_result) = result
local ft′ = ft′_box.contents
Expand Down
2 changes: 1 addition & 1 deletion Compiler/src/abstractlattice.jl
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ end
if isa(t, Const)
# don't consider mutable values useful constants
val = t.val
return isa(val, Symbol) || isa(val, Type) || !ismutable(val)
return isa(val, Symbol) || isa(val, Type) || isa(val, Method) || !ismutable(val)
end
isa(t, PartialTypeVar) && return false # this isn't forwardable
return is_const_prop_profitable_arg(widenlattice(𝕃), t)
Expand Down
4 changes: 2 additions & 2 deletions Compiler/src/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ function count_const_size(@nospecialize(x), count_self::Bool = true)
# No definite size
(isa(x, GenericMemory) || isa(x, String) || isa(x, SimpleVector)) &&
return MAX_INLINE_CONST_SIZE + 1
if isa(x, Module)
# We allow modules, because we already assume they are externally
if isa(x, Module) || isa(x, Method)
# We allow modules and methods, because we already assume they are externally
# rooted, so we count their contents as 0 size.
return sizeof(Ptr{Cvoid})
end
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ New library features
* `Base.require_one_based_indexing` and `Base.has_offset_axes` are now public ([#56196])
* New `ltruncate`, `rtruncate` and `ctruncate` functions for truncating strings to text width, accounting for char widths ([#55351])
* `isless` (and thus `cmp`, sorting, etc.) is now supported for zero-dimensional `AbstractArray`s ([#55772])
* `invoke` now supports passing a Method instead of a type signature making this interface somewhat more flexible for certain uncommon use cases ([#56692]).

Standard library changes
------------------------
Expand Down
15 changes: 13 additions & 2 deletions base/docs/basedocs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2030,21 +2030,32 @@ applicable

"""
invoke(f, argtypes::Type, args...; kwargs...)
invoke(f, argtypes::Method, args...; kwargs...)

Invoke a method for the given generic function `f` matching the specified types `argtypes` on the
specified arguments `args` and passing the keyword arguments `kwargs`. The arguments `args` must
conform with the specified types in `argtypes`, i.e. conversion is not automatically performed.
This method allows invoking a method other than the most specific matching method, which is useful
when the behavior of a more general definition is explicitly needed (often as part of the
implementation of a more specific method of the same function).
implementation of a more specific method of the same function). However, because this means
the runtime must do more work, `invoke` is generally also slower--sometimes significantly
so--than doing normal dispatch with a regular call.

Be careful when using `invoke` for functions that you don't write. What definition is used
Be careful when using `invoke` for functions that you don't write. What definition is used
for given `argtypes` is an implementation detail unless the function is explicitly states
that calling with certain `argtypes` is a part of public API. For example, the change
between `f1` and `f2` in the example below is usually considered compatible because the
change is invisible by the caller with a normal (non-`invoke`) call. However, the change is
visible if you use `invoke`.

# Passing a `Method` instead of a signature
The `argtypes` argument may be a `Method`, in which case the ordinary method table lookup is
bypassed entirely and the given method is invoked directly. Needing this feature is uncommon.
Note in particular that the specified `Method` may be entirely unreachable from ordinary dispatch
(or ordinary invoke), e.g. because it was replaced or fully covered by more specific methods.
If the method is part of the ordinary method table, this call behaves similar
to `invoke(f, method.sig, args...)`.

# Examples
```jldoctest
julia> f(x::Real) = x^2;
Expand Down
30 changes: 19 additions & 11 deletions src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -931,22 +931,27 @@ JL_CALLABLE(jl_f__call_in_world_total)

// tuples ---------------------------------------------------------------------

JL_CALLABLE(jl_f_tuple)
static jl_value_t *arg_tuple(jl_value_t *a1, jl_value_t **args, size_t nargs)
{
size_t i;
if (nargs == 0)
return (jl_value_t*)jl_emptytuple;
jl_datatype_t *tt = jl_inst_arg_tuple_type(args[0], &args[1], nargs, 0);
jl_datatype_t *tt = jl_inst_arg_tuple_type(a1, args, nargs, 0);
JL_GC_PROMISE_ROOTED(tt); // it is a concrete type
if (tt->instance != NULL)
return tt->instance;
jl_task_t *ct = jl_current_task;
jl_value_t *jv = jl_gc_alloc(ct->ptls, jl_datatype_size(tt), tt);
for (i = 0; i < nargs; i++)
set_nth_field(tt, jv, i, args[i], 0);
set_nth_field(tt, jv, i, i == 0 ? a1 : args[i - 1], 0);
return jv;
}

JL_CALLABLE(jl_f_tuple)
{
if (nargs == 0)
return (jl_value_t*)jl_emptytuple;
return arg_tuple(args[0], &args[1], nargs);
}

JL_CALLABLE(jl_f_svec)
{
size_t i;
Expand Down Expand Up @@ -1577,14 +1582,17 @@ JL_CALLABLE(jl_f_invoke)
{
JL_NARGSV(invoke, 2);
jl_value_t *argtypes = args[1];
JL_GC_PUSH1(&argtypes);
if (!jl_is_tuple_type(jl_unwrap_unionall(args[1])))
jl_type_error("invoke", (jl_value_t*)jl_anytuple_type_type, args[1]);
if (jl_is_method(argtypes)) {
jl_method_t *m = (jl_method_t*)argtypes;
if (!jl_tuple1_isa(args[0], &args[2], nargs - 1, (jl_datatype_t*)m->sig))
jl_type_error("invoke: argument type error", argtypes, arg_tuple(args[0], &args[2], nargs - 1));
return jl_gf_invoke_by_method(m, args[0], &args[2], nargs - 1);
}
if (!jl_is_tuple_type(jl_unwrap_unionall(argtypes)))
jl_type_error("invoke", (jl_value_t*)jl_anytuple_type_type, argtypes);
if (!jl_tuple_isa(&args[2], nargs - 2, (jl_datatype_t*)argtypes))
jl_type_error("invoke: argument type error", argtypes, jl_f_tuple(NULL, &args[2], nargs - 2));
jl_value_t *res = jl_gf_invoke(argtypes, args[0], &args[2], nargs - 1);
JL_GC_POP();
return res;
return jl_gf_invoke(argtypes, args[0], &args[2], nargs - 1);
}

// Expr constructor for internal use ------------------------------------------
Expand Down
7 changes: 7 additions & 0 deletions test/core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8352,3 +8352,10 @@ macro define_call(sym)
end
@test eval(Expr(:toplevel, :(@define_call(f_macro_defined1)))) == 1
@test @define_call(f_macro_defined2) == 1

let m = which(+, (Int, Int))
@eval f56692(i) = invoke(+, $m, i, 4)
global g56692() = f56692(5) == 9 ? "true" : false
end
@test @inferred(f56692(3)) == 7
@test @inferred(g56692()) == "true"