From f9496fb371fbdb8433d2559d35285075d1d8cb9a Mon Sep 17 00:00:00 2001 From: Chris Foster Date: Fri, 3 Jul 2020 19:13:59 +1000 Subject: [PATCH] Add Base.invoke_in_world and _apply_in_world builtin (#35844) New builtin `Core._apply_in_world` to allow frozen-world APIs to be implemented in pure Julia code. Also add an internal, experimental API `Base.invoke_in_world()` in analogy to `Base.invokelatest()` to make this easier to use (especially for keyword args) and to give us a place to put some documentation. --- base/essentials.jl | 34 ++++++++++++++++++++++++++++++++++ src/builtin_proto.h | 1 + src/builtins.c | 19 +++++++++++++++++++ src/codegen.cpp | 1 + src/staticdata.c | 3 ++- test/worlds.jl | 14 ++++++++++++++ 6 files changed, 71 insertions(+), 1 deletion(-) diff --git a/base/essentials.jl b/base/essentials.jl index 3177ca569b4c1..69e6bafd8b03e 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -716,6 +716,40 @@ function invokelatest(@nospecialize(f), @nospecialize args...; kwargs...) Core._apply_latest(inner) end +""" + invoke_in_world(world, f, args...; kwargs...) + +Call `f(args...; kwargs...)` in a fixed world age, `world`. + +This is useful for infrastructure running in the user's Julia session which is +not part of the user's program. For example, things related to the REPL, editor +support libraries, etc. In these cases it can be useful to prevent unwanted +method invalidation and recompilation latency, and to prevent the user from +breaking supporting infrastructure by mistake. + +The current world age can be queried using [`Base.get_world_counter()`](@ref) +and stored for later use within the lifetime of the current Julia session, or +when serializing and reloading the system image. + +Technically, `invoke_in_world` will prevent any function called by `f` from +being extended by the user during their Julia session. That is, generic +function method tables seen by `f` (and any functions it calls) will be frozen +as they existed at the given `world` age. In a sense, this is like the opposite +of [`invokelatest`](@ref). + +!!! note + It is not valid to store world ages obtained in precompilation for later use. + This is because precompilation generates a "parallel universe" where the + world age refers to system state unrelated to the main Julia session. +""" +function invoke_in_world(world::UInt, @nospecialize(f), @nospecialize args...; kwargs...) + if isempty(kwargs) + return Core._apply_in_world(world, f, args) + end + inner() = f(args...; kwargs...) + Core._apply_in_world(world, inner) +end + # TODO: possibly make this an intrinsic inferencebarrier(@nospecialize(x)) = Ref{Any}(x)[] diff --git a/src/builtin_proto.h b/src/builtin_proto.h index a13cebdace3b1..8021c404bd5e7 100644 --- a/src/builtin_proto.h +++ b/src/builtin_proto.h @@ -24,6 +24,7 @@ DECLARE_BUILTIN(typeof); DECLARE_BUILTIN(sizeof); DECLARE_BUILTIN(issubtype); DECLARE_BUILTIN(isa); DECLARE_BUILTIN(_apply); DECLARE_BUILTIN(_apply_pure); DECLARE_BUILTIN(_apply_latest); DECLARE_BUILTIN(_apply_iterate); +DECLARE_BUILTIN(_apply_in_world); DECLARE_BUILTIN(isdefined); DECLARE_BUILTIN(nfields); DECLARE_BUILTIN(tuple); DECLARE_BUILTIN(svec); DECLARE_BUILTIN(getfield); DECLARE_BUILTIN(setfield); diff --git a/src/builtins.c b/src/builtins.c index 5457adae0f64e..dfedd2e850d9b 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -707,6 +707,24 @@ JL_CALLABLE(jl_f__apply_latest) return ret; } +// Like `_apply`, but runs in the specified world. +// If world > jl_world_counter, run in the latest world. +JL_CALLABLE(jl_f__apply_in_world) +{ + JL_NARGSV(_apply_in_world, 2); + jl_ptls_t ptls = jl_get_ptls_states(); + size_t last_age = ptls->world_age; + JL_TYPECHK(_apply_in_world, ulong, args[0]); + size_t world = jl_unbox_ulong(args[0]); + world = world <= jl_world_counter ? world : jl_world_counter; + if (!ptls->in_pure_callback) { + ptls->world_age = world; + } + jl_value_t *ret = do_apply(NULL, args+1, nargs-1, NULL); + ptls->world_age = last_age; + return ret; +} + // tuples --------------------------------------------------------------------- JL_CALLABLE(jl_f_tuple) @@ -1527,6 +1545,7 @@ void jl_init_primitives(void) JL_GC_DISABLED jl_builtin_svec = add_builtin_func("svec", jl_f_svec); add_builtin_func("_apply_pure", jl_f__apply_pure); add_builtin_func("_apply_latest", jl_f__apply_latest); + add_builtin_func("_apply_in_world", jl_f__apply_in_world); add_builtin_func("_typevar", jl_f__typevar); add_builtin_func("_structtype", jl_f__structtype); add_builtin_func("_abstracttype", jl_f__abstracttype); diff --git a/src/codegen.cpp b/src/codegen.cpp index 3463dcc6164f7..05cf45bd610f1 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -786,6 +786,7 @@ static const std::map builtin_func_map = { { &jl_f__apply_iterate, new JuliaFunction{"jl_f__apply_iterate", get_func_sig, get_func_attrs} }, { &jl_f__apply_pure, new JuliaFunction{"jl_f__apply_pure", get_func_sig, get_func_attrs} }, { &jl_f__apply_latest, new JuliaFunction{"jl_f__apply_latest", get_func_sig, get_func_attrs} }, + { &jl_f__apply_in_world, new JuliaFunction{"jl_f__apply_in_world", get_func_sig, get_func_attrs} }, { &jl_f_throw, new JuliaFunction{"jl_f_throw", get_func_sig, get_func_attrs} }, { &jl_f_tuple, jltuple_func }, { &jl_f_svec, new JuliaFunction{"jl_f_svec", get_func_sig, get_func_attrs} }, diff --git a/src/staticdata.c b/src/staticdata.c index d2e66d8cbd648..e8bed2c19ccfb 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -117,7 +117,8 @@ void *native_functions; // This is a manually constructed dual of the fvars array, which would be produced by codegen for Julia code, for C. static const jl_fptr_args_t id_to_fptrs[] = { &jl_f_throw, &jl_f_is, &jl_f_typeof, &jl_f_issubtype, &jl_f_isa, - &jl_f_typeassert, &jl_f__apply, &jl_f__apply_iterate, &jl_f__apply_pure, &jl_f__apply_latest, &jl_f_isdefined, + &jl_f_typeassert, &jl_f__apply, &jl_f__apply_iterate, &jl_f__apply_pure, + &jl_f__apply_latest, &jl_f__apply_in_world, &jl_f_isdefined, &jl_f_tuple, &jl_f_svec, &jl_f_intrinsic_call, &jl_f_invoke_kwsorter, &jl_f_getfield, &jl_f_setfield, &jl_f_fieldtype, &jl_f_nfields, &jl_f_arrayref, &jl_f_const_arrayref, &jl_f_arrayset, &jl_f_arraysize, &jl_f_apply_type, diff --git a/test/worlds.jl b/test/worlds.jl index 878dd24adadb4..429e611e26851 100644 --- a/test/worlds.jl +++ b/test/worlds.jl @@ -324,3 +324,17 @@ w = worlds(mi) abstract type Colorant35855 end Base.convert(::Type{C}, c) where C<:Colorant35855 = false @test_broken worlds(mi) == w + +# invoke_in_world +f_inworld(x) = "world one; x=$x" +g_inworld(x; y) = "world one; x=$x, y=$y" +wc_aiw1 = get_world_counter() +# redefine f_inworld, g_inworld, and check that we can invoke both versions +f_inworld(x) = "world two; x=$x" +g_inworld(x; y) = "world two; x=$x, y=$y" +wc_aiw2 = get_world_counter() +@test Base.invoke_in_world(wc_aiw1, f_inworld, 2) == "world one; x=2" +@test Base.invoke_in_world(wc_aiw2, f_inworld, 2) == "world two; x=2" +@test Base.invoke_in_world(wc_aiw1, g_inworld, 2, y=3) == "world one; x=2, y=3" +@test Base.invoke_in_world(wc_aiw2, g_inworld, 2, y=3) == "world two; x=2, y=3" +