From ee5db5a054954afb52674ecd448153d00ab5db3d Mon Sep 17 00:00:00 2001 From: Jameson Nash Date: Fri, 22 Nov 2024 21:11:53 +0000 Subject: [PATCH 1/2] support passing a specific Method to invoke The main purpose of this is integrating with external compilers using overlay method tables, where the Method might need to come from a source other than regular dispatch. Note that this is generally much less well optimized at runtime than generic dispatch, so this shouldn't be treated as expecting to give a performance boost in any dynamic cases. Trivial examples: julia> let m = which(+, (Int, Int)) @eval f(i, j) = invoke(+, $m, i, j) end julia> f(2,2) 4 julia> let m = which(Core.kwcall, (@NamedTuple{base::Int}, typeof(string), Int)) @eval f(i, base) = invoke(Core.kwcall, $m, (;base), string, i) end julia> f(20,16) "14" --- Compiler/src/abstractinterpretation.jl | 57 +++++++++++++++----------- Compiler/src/abstractlattice.jl | 2 +- Compiler/src/utilities.jl | 4 +- NEWS.md | 1 + base/docs/basedocs.jl | 15 ++++++- src/builtins.c | 30 +++++++++----- test/core.jl | 7 ++++ 7 files changed, 77 insertions(+), 39 deletions(-) diff --git a/Compiler/src/abstractinterpretation.jl b/Compiler/src/abstractinterpretation.jl index a3abbf814165a..5946adf80ad52 100644 --- a/Compiler/src/abstractinterpretation.jl +++ b/Compiler/src/abstractinterpretation.jl @@ -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 @@ -2218,26 +2217,38 @@ 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 @@ -2245,7 +2256,7 @@ function abstract_invoke(interp::AbstractInterpreter, arginfo::ArgInfo, si::Stmt 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 diff --git a/Compiler/src/abstractlattice.jl b/Compiler/src/abstractlattice.jl index 645c865d085b3..c1f3050739170 100644 --- a/Compiler/src/abstractlattice.jl +++ b/Compiler/src/abstractlattice.jl @@ -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) diff --git a/Compiler/src/utilities.jl b/Compiler/src/utilities.jl index 11d926f0c9d4e..29f3dfa4afd4a 100644 --- a/Compiler/src/utilities.jl +++ b/Compiler/src/utilities.jl @@ -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 diff --git a/NEWS.md b/NEWS.md index 535d14208f0b8..3a821f5031f5c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 ([#TBD]). Standard library changes ------------------------ diff --git a/base/docs/basedocs.jl b/base/docs/basedocs.jl index c872244964160..5119ceaf2164a 100644 --- a/base/docs/basedocs.jl +++ b/base/docs/basedocs.jl @@ -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; diff --git a/src/builtins.c b/src/builtins.c index b129cca0ee71d..c6b0bf130550b 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -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; @@ -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 ------------------------------------------ diff --git a/test/core.jl b/test/core.jl index 836532d661638..39d02d5d567c9 100644 --- a/test/core.jl +++ b/test/core.jl @@ -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" From 160d33a9aba10e953f35a8804466fcbc7337326c Mon Sep 17 00:00:00 2001 From: Jameson Nash Date: Wed, 27 Nov 2024 16:57:12 -0500 Subject: [PATCH 2/2] Update NEWS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mosè Giordano <765740+giordano@users.noreply.github.com> --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 3a821f5031f5c..61bad831e261c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -119,7 +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 ([#TBD]). +* `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 ------------------------