diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 44d9067679d52..a80090f3dc7e0 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -1560,6 +1560,18 @@ function early_inline_special_case( end end end + if f === compilerbarrier + # check if this `compilerbarrier` has already imposed a barrier on abstract interpretation + # so that it can be eliminated here + length(argtypes) == 3 || return nothing + setting = argtypes[2] + isa(setting, Const) || return nothing + setting = setting.val + isa(setting, Symbol) || return nothing + setting === :const || setting === :conditional || setting === :type || return nothing + # barrierred successfully already, eliminate it + return SomeCase(stmt.args[3]) + end return nothing end diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index b4cf6b3e32eaa..ab1c8401c4738 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -562,6 +562,23 @@ add_tfunc(atomic_pointerswap, 3, 3, (a, v, order) -> (@nospecialize; pointer_elt add_tfunc(atomic_pointermodify, 4, 4, atomic_pointermodify_tfunc, 5) add_tfunc(atomic_pointerreplace, 5, 5, atomic_pointerreplace_tfunc, 5) add_tfunc(donotdelete, 0, INT_INF, (@nospecialize args...)->Nothing, 0) +function compilerbarrier_tfunc(@nospecialize(setting), @nospecialize(val)) + # strongest barrier if a precise information isn't available at compiler time + # XXX we may want to have "compile-time" error instead for such case + isa(setting, Const) || return Any + setting = setting.val + isa(setting, Symbol) || return Any + if setting === :const + return widenconst(val) + elseif setting === :conditional + return widenconditional(val) + elseif setting === :type + return Any + else + return Bottom + end +end +add_tfunc(compilerbarrier, 2, 2, compilerbarrier_tfunc, 5) add_tfunc(Core.finalizer, 2, 4, (@nospecialize args...)->Nothing, 5) # more accurate typeof_tfunc for vararg tuples abstract only in length diff --git a/base/docs/basedocs.jl b/base/docs/basedocs.jl index 99098f87a9196..454d4b394e503 100644 --- a/base/docs/basedocs.jl +++ b/base/docs/basedocs.jl @@ -3061,7 +3061,7 @@ See also [`"`](@ref \") kw"\"\"\"" """ - donotdelete(args...) + Base.donotdelete(args...) This function prevents dead-code elimination (DCE) of itself and any arguments passed to it, but is otherwise the lightest barrier possible. In particular, @@ -3078,9 +3078,10 @@ This is intended for use in benchmarks that want to guarantee that `args` are actually computed. (Otherwise DCE may see that the result of the benchmark is unused and delete the entire benchmark code). -**Note**: `donotdelete` does not affect constant folding. For example, in - `donotdelete(1+1)`, no add instruction needs to be executed at runtime and - the code is semantically equivalent to `donotdelete(2).` +!!! note + `donotdelete` does not affect constant folding. For example, in + `donotdelete(1+1)`, no add instruction needs to be executed at runtime and + the code is semantically equivalent to `donotdelete(2).` # Examples @@ -3097,6 +3098,62 @@ end """ Base.donotdelete +""" + Base.compilerbarrier(setting::Symbol, val) + +This function puts a barrier at a specified compilation phase. +It is supposed to only influence the compilation behavior according to `setting`, +and its runtime semantics is just to return the second argument `val` (except that +this function will perform additional checks on `setting` in a case when `setting` +isn't known precisely at compile-time.) + +Currently either of the following `setting`s is allowed: +- Barriers on abstract interpretation: + * `:type`: the return type of this function call will be inferred as `Any` always + (the strongest barrier on abstract interpretation) + * `:const`: the return type of this function call will be inferred with widening + constant information on `val` + * `:conditional`: the return type of this function call will be inferred with widening + conditional information on `val` (see the example below) +- Any barriers on optimization aren't implemented yet + +!!! note + This function is supposed to be used _with `setting` known precisely at compile-time_. + Note that in a case when the `setting` isn't known precisely at compile-time, the compiler + currently will put the most strongest barrier(s) rather than emitting a compile-time warning. + +# Examples + +```julia +julia> Base.return_types((Int,)) do a + x = compilerbarrier(:type, a) # `x` won't be inferred as `x::Int` + return x + end |> only +Any + +julia> Base.return_types() do + x = compilerbarrier(:const, 42) + if x == 42 # no constant information here, so inference also accounts for the else branch + return x # but `x` is still inferred as `x::Int` at least here + else + return nothing + end + end |> only +Union{Nothing, Int64} + +julia> Base.return_types((Union{Int,Nothing},)) do a + if compilerbarrier(:conditional, isa(a, Int)) + # the conditional information `a::Int` isn't available here (leading to less accurate return type inference) + return a + else + return nothing + end + end |> only +Union{Nothing, Int64} +``` +""" +Base.compilerbarrier + """ Core.finalizer(f, o) diff --git a/base/essentials.jl b/base/essentials.jl index 0fb00c367e391..50cdde9f3adc2 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -1,6 +1,6 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -using Core: CodeInfo, SimpleVector, donotdelete, arrayref +import Core: CodeInfo, SimpleVector, donotdelete, compilerbarrier, arrayref const Callable = Union{Function,Type} @@ -846,8 +846,7 @@ function invoke_in_world(world::UInt, @nospecialize(f), @nospecialize args...; k return Core._call_in_world(world, Core.kwfunc(f), kwargs, f, args...) end -# TODO: possibly make this an intrinsic -inferencebarrier(@nospecialize(x)) = RefValue{Any}(x).x +inferencebarrier(@nospecialize(x)) = compilerbarrier(:type, x) """ isempty(collection) -> Bool diff --git a/src/builtin_proto.h b/src/builtin_proto.h index c54ba37bf34da..d9520bd251b86 100644 --- a/src/builtin_proto.h +++ b/src/builtin_proto.h @@ -55,6 +55,7 @@ DECLARE_BUILTIN(_typebody); DECLARE_BUILTIN(typeof); DECLARE_BUILTIN(_typevar); DECLARE_BUILTIN(donotdelete); +DECLARE_BUILTIN(compilerbarrier); DECLARE_BUILTIN(getglobal); DECLARE_BUILTIN(setglobal); DECLARE_BUILTIN(finalizer); diff --git a/src/builtins.c b/src/builtins.c index bd9e75db57aa6..a41a565b45346 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -1604,6 +1604,19 @@ JL_CALLABLE(jl_f_donotdelete) return jl_nothing; } +JL_CALLABLE(jl_f_compilerbarrier) +{ + JL_NARGS(compilerbarrier, 2, 2); + JL_TYPECHK(compilerbarrier, symbol, args[0]) + jl_sym_t *setting = (jl_sym_t*)args[0]; + if (!(setting == jl_symbol("type") || + setting == jl_symbol("const") || + setting == jl_symbol("conditional"))) + jl_error("The first argument of `compilerbarrier` must be either of `:type`, `:const` or `:conditional`."); + jl_value_t *val = args[1]; + return val; +} + JL_CALLABLE(jl_f_finalizer) { // NOTE the compiler may temporarily insert additional argument for the later inlining pass @@ -1983,6 +1996,7 @@ void jl_init_primitives(void) JL_GC_DISABLED jl_builtin__typebody = add_builtin_func("_typebody!", jl_f__typebody); add_builtin_func("_equiv_typedef", jl_f__equiv_typedef); jl_builtin_donotdelete = add_builtin_func("donotdelete", jl_f_donotdelete); + jl_builtin_compilerbarrier = add_builtin_func("compilerbarrier", jl_f_compilerbarrier); add_builtin_func("finalizer", jl_f_finalizer); // builtin types diff --git a/src/codegen.cpp b/src/codegen.cpp index a939e873d4f65..fea568c5bd0dd 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -1175,6 +1175,7 @@ static const auto &builtin_func_map() { { jl_f_arraysize_addr, new JuliaFunction{XSTR(jl_f_arraysize), get_func_sig, get_func_attrs} }, { jl_f_apply_type_addr, new JuliaFunction{XSTR(jl_f_apply_type), get_func_sig, get_func_attrs} }, { jl_f_donotdelete_addr, new JuliaFunction{XSTR(jl_f_donotdelete), get_donotdelete_sig, get_donotdelete_func_attrs} }, + { jl_f_compilerbarrier_addr, new JuliaFunction{XSTR(jl_f_compilerbarrier), get_func_sig, get_func_attrs} }, { jl_f_finalizer_addr, new JuliaFunction{XSTR(jl_f_finalizer), get_func_sig, get_func_attrs} } }; return builtins; diff --git a/src/staticdata.c b/src/staticdata.c index af3e63c10123c..2e0c69dad3afc 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -80,7 +80,7 @@ extern "C" { // TODO: put WeakRefs on the weak_refs list during deserialization // TODO: handle finalizers -#define NUM_TAGS 155 +#define NUM_TAGS 156 // An array of references that need to be restored from the sysimg // This is a manually constructed dual of the gvars array, which would be produced by codegen for Julia code, for C. @@ -253,6 +253,7 @@ jl_value_t **const*const get_tags(void) { INSERT_TAG(jl_builtin_ifelse); INSERT_TAG(jl_builtin__typebody); INSERT_TAG(jl_builtin_donotdelete); + INSERT_TAG(jl_builtin_compilerbarrier); INSERT_TAG(jl_builtin_getglobal); INSERT_TAG(jl_builtin_setglobal); // n.b. must update NUM_TAGS when you add something here @@ -313,7 +314,7 @@ static const jl_fptr_args_t id_to_fptrs[] = { &jl_f_applicable, &jl_f_invoke, &jl_f_sizeof, &jl_f__expr, &jl_f__typevar, &jl_f_ifelse, &jl_f__structtype, &jl_f__abstracttype, &jl_f__primitivetype, &jl_f__typebody, &jl_f__setsuper, &jl_f__equiv_typedef, &jl_f_get_binding_type, - &jl_f_set_binding_type, &jl_f_opaque_closure_call, &jl_f_donotdelete, + &jl_f_set_binding_type, &jl_f_opaque_closure_call, &jl_f_donotdelete, &jl_f_compilerbarrier, &jl_f_getglobal, &jl_f_setglobal, &jl_f_finalizer, NULL }; diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index 4311a63c66c1d..cc955dbc64dab 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -4123,3 +4123,64 @@ end)[2] == Union{} @time 1 end end)[2] == Union{} + +# compilerbarrier builtin +import Core: compilerbarrier +# runtime semantics +for setting = (:type, :const, :conditional) + @test compilerbarrier(setting, 42) == 42 + @test compilerbarrier(setting, :sym) == :sym +end +@test_throws ErrorException compilerbarrier(:nonexisting, 42) +@test_throws TypeError compilerbarrier("badtype", 42) +@test_throws ArgumentError compilerbarrier(:nonexisting, 42, nothing) +# barrier on abstract interpretation +@test Base.return_types((Int,)) do a + x = compilerbarrier(:type, a) # `x` won't be inferred as `x::Int` + return x +end |> only === Any +@test Base.return_types() do + x = compilerbarrier(:const, 42) + if x == 42 # no constant information here, so inference also accounts for the else branch (leading to less accurate return type inference) + return x # but `x` is still inferred as `x::Int` at least here + else + return nothing + end +end |> only === Union{Int,Nothing} +@test Base.return_types((Union{Int,Nothing},)) do a + if compilerbarrier(:conditional, isa(a, Int)) + # the conditional information `a::Int` isn't available here (leading to less accurate return type inference) + return a + else + return nothing + end +end |> only === Union{Int,Nothing} +@test Base.return_types((Symbol,Int)) do setting, val + compilerbarrier(setting, val) +end |> only === Any # XXX we may want to have "compile-time" error for this instead +for setting = (:type, :const, :conditional) + # a successful barrier on abstract interpretation should be eliminated at the optimization + @test @eval fully_eliminated((Int,)) do a + compilerbarrier($(QuoteNode(setting)), 42) + end +end + +# https://github.com/JuliaLang/julia/issues/46426 +@noinline Base.@assume_effects :nothrow typebarrier() = Base.inferencebarrier(0.0) +@noinline Base.@assume_effects :nothrow constbarrier() = Base.compilerbarrier(:const, 0.0) +let src = code_typed1() do + typebarrier() + end + @test any(isinvoke(:typebarrier), src.code) + @test Base.return_types() do + typebarrier() + end |> only === Any +end +let src = code_typed1() do + constbarrier() + end + @test any(isinvoke(:constbarrier), src.code) + @test Base.return_types() do + constbarrier() + end |> only === Float64 +end