From 8e30135afd2a9f493020263f80cf63da1de32ca3 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Fri, 27 May 2022 15:22:32 -0400 Subject: [PATCH] effect overrides: Add notaskstate & refactor slightly (#45448) * effect overrides: Add notaskstate & refactor slightly This adds an effect override for `:notaskstate` and shifts the `:total`, meta-effect around slightly to prepare for any potential future expansion. In particular `:total` now means the maximum possible set of effects and should likely include any future additions. The assume_effects macro learned to do negation, so we can now write things like `:total :!nothrow`, which will be helpful writing tests that care about one effect in particular, but are otherwise total. The previous `:total_may_throw`, was renamed `:foldable` and includes the effects required to allow compile-time constant folding. At this point I don't anticipate the introduction of additional effects that would affect constant-foldability, but if such an effect were introduced, it would be added to `:foldable`. Note however, that `:foldable` does not include `:notaskstate` (though as noted in the docstring, because of the strong requirements of `:consistent` and `:effect_free`, it is implied that anything annotated `:notaskstate` may be DCEd and thus `:notaskstate` is implied). Nevertheless, `:notaskstate` is not included in the `:foldable` override and future effect additions may further separate `:foldable` from `:total`. * minor tweaks Co-authored-by: Shuhei Kadowaki --- base/compiler/abstractinterpretation.jl | 2 +- base/compiler/typeinfer.jl | 3 + base/compiler/types.jl | 6 +- base/deprecated.jl | 4 +- base/essentials.jl | 10 +- base/expr.jl | 134 +++++++++++++++++------- base/promotion.jl | 4 +- base/reflection.jl | 20 ++-- base/tuple.jl | 2 +- src/julia.h | 1 + src/method.c | 3 +- test/compiler/inference.jl | 2 +- test/compiler/inline.jl | 2 +- 13 files changed, 133 insertions(+), 60 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 080a0e218cb1d..734b3d1859cb2 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -1999,7 +1999,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), effects.nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN, effects.terminates_globally ? ALWAYS_TRUE : TRISTATE_UNKNOWN, #=nonoverlayed=#true, - #=notaskstate=#TRISTATE_UNKNOWN + effects.notaskstate ? ALWAYS_TRUE : TRISTATE_UNKNOWN )) else tristate_merge!(sv, EFFECTS_UNKNOWN) diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index fea45f6daad3a..fb4a732692833 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -453,6 +453,9 @@ function adjust_effects(sv::InferenceState) if is_effect_overridden(override, :terminates_globally) ipo_effects = Effects(ipo_effects; terminates=ALWAYS_TRUE) end + if is_effect_overridden(override, :notaskstate) + ipo_effects = Effects(ipo_effects; notaskstate=ALWAYS_TRUE) + end end return ipo_effects diff --git a/base/compiler/types.jl b/base/compiler/types.jl index 6a30d987d0f1f..9ceccd843e1d0 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -124,6 +124,7 @@ is_terminates(effects::Effects) = effects.terminates === ALWAYS_TRUE is_notaskstate(effects::Effects) = effects.notaskstate === ALWAYS_TRUE is_nonoverlayed(effects::Effects) = effects.nonoverlayed +# implies :notaskstate, but not explicitly checked here is_concrete_eval_eligible(effects::Effects) = is_consistent(effects) && is_effect_free(effects) && @@ -179,6 +180,7 @@ struct EffectsOverride nothrow::Bool terminates_globally::Bool terminates_locally::Bool + notaskstate::Bool end function encode_effects_override(eo::EffectsOverride) @@ -188,6 +190,7 @@ function encode_effects_override(eo::EffectsOverride) eo.nothrow && (e |= 0x04) eo.terminates_globally && (e |= 0x08) eo.terminates_locally && (e |= 0x10) + eo.notaskstate && (e |= 0x20) return e end @@ -197,7 +200,8 @@ function decode_effects_override(e::UInt8) (e & 0x02) != 0x00, (e & 0x04) != 0x00, (e & 0x08) != 0x00, - (e & 0x10) != 0x00) + (e & 0x10) != 0x00, + (e & 0x20) != 0x00) end """ diff --git a/base/deprecated.jl b/base/deprecated.jl index 8367c6e5bcf4b..65107d0968fee 100644 --- a/base/deprecated.jl +++ b/base/deprecated.jl @@ -265,11 +265,11 @@ getindex(match::Core.MethodMatch, field::Int) = tuple_type_head(T::Type) = fieldtype(T, 1) tuple_type_cons(::Type, ::Type{Union{}}) = Union{} function tuple_type_cons(::Type{S}, ::Type{T}) where T<:Tuple where S - @_total_may_throw_meta + @_foldable_meta Tuple{S, T.parameters...} end function parameter_upper_bound(t::UnionAll, idx) - @_total_may_throw_meta + @_foldable_meta return rewrap_unionall((unwrap_unionall(t)::DataType).parameters[idx], t) end diff --git a/base/essentials.jl b/base/essentials.jl index 3ceef212c4bd2..74c4624856896 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -209,16 +209,18 @@ macro _total_meta() #=:effect_free=#true, #=:nothrow=#true, #=:terminates_globally=#true, - #=:terminates_locally=#false)) + #=:terminates_locally=#false, + #=:notaskstate=#true)) end -# can be used in place of `@assume_effects :total_may_throw` (supposed to be used for bootstrapping) -macro _total_may_throw_meta() +# can be used in place of `@assume_effects :foldable` (supposed to be used for bootstrapping) +macro _foldable_meta() return _is_internal(__module__) && Expr(:meta, Expr(:purity, #=:consistent=#true, #=:effect_free=#true, #=:nothrow=#false, #=:terminates_globally=#true, - #=:terminates_locally=#false)) + #=:terminates_locally=#false, + #=:notaskstate=#false)) end # another version of inlining that propagates an inbounds context diff --git a/base/expr.jl b/base/expr.jl index e0cd8a9b0a32c..920cbd73b802e 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -406,7 +406,7 @@ julia> code_typed() do 1 ─ return 479001600 ) => Int64 -julia> Base.@assume_effects :total_may_throw @ccall jl_type_intersection(Vector{Int}::Any, Vector{<:Integer}::Any)::Any +julia> Base.@assume_effects :total !:nothrow @ccall jl_type_intersection(Vector{Int}::Any, Vector{<:Integer}::Any)::Any Vector{Int64} (alias for Array{Int64, 1}) ``` @@ -427,6 +427,8 @@ The following `setting`s are supported. - `:nothrow` - `:terminates_globally` - `:terminates_locally` +- `:notaskstate` +- `:foldable` - `:total` --- @@ -535,59 +537,109 @@ non-termination if the method calls some other method that does not terminate. `:terminates_globally` implies `:terminates_locally`. --- -# `:total` +# `:notaskstate` + +The `:notaskstate` setting asserts that the method does not use or modify the +local task state (task local storage, RNG state, etc.) and may thus be safely +moved between tasks without observable results. + +!!! note + The implementation of exception handling makes use of state stored in the + task object. However, this state is currently not considered to be within + the scope of `:notaskstate` and is tracked separately using the `:nothrow` + effect. + +!!! note + The `:notaskstate` assertion concerns the state of the *currently running task*. + If a reference to a `Task` object is obtained by some other means that + does not consider which task is *currently* running, the `:notaskstate` + effect need not be tainted. This is true, even if said task object happens + to be `===` to the currently running task. + +!!! note + Access to task state usually also results in the tainting of other effects, + such as `:effect_free` (if task state is modified) or `:consistent` (if + task state is used in the computation of the result). In particular, + code that is not `:notaskstate`, but is `:effect_free` and `:consistent` + may still be dead-code-eliminated and thus promoted to `:total`. + +--- +# `:foldable` + +This setting is a convenient shortcut for the set of effects that the compiler +requires to be guaranteed to constant fold a call at compile time. It is +currently equivalent to the following `setting`s: -This `setting` combines the following other assertions: - `:consistent` - `:effect_free` -- `:nothrow` - `:terminates_globally` -and is a convenient shortcut. + +!!! note + This list in particular does not include `:nothrow`. The compiler will still + attempt constant propagation and note any thrown error at compile time. Note + however, that by the `:consistent`-cy requirements, any such annotated call + must consistently throw given the same argument values. --- -# `:total_may_throw` +# `:total` -This `setting` combines the following other assertions: +This `setting` is the maximum possible set of effects. It currently implies +the following other `setting`s: - `:consistent` - `:effect_free` +- `:nothrow` - `:terminates_globally` -and is a convenient shortcut. +- `:notaskstate` -!!! note - This setting is particularly useful since it allows the compiler to evaluate a call of - the applied method when all the call arguments are fully known to be constant, no matter - if the call results in an error or not. - - `@assume_effects :total_may_throw` is similar to [`@pure`](@ref) with the primary - distinction that the `:consistent`-cy requirement applies world-age wise rather - than globally as described above. However, in particular, a method annotated - `@pure` should always be `:total` or `:total_may_throw`. - Another advantage is that effects introduced by `@assume_effects` are propagated to - callers interprocedurally while a purity defined by `@pure` is not. +!!! warning + `:total` is a very strong assertion and will likely gain additional semantics + in future versions of julia (e.g. if additional effects are added and included + in the definition of `:total`). As a result, it should be used with care. + Whenever possible, prefer to use the minimum possible set of specific effect + assertions required for a particular application. In cases where a large + number of effect overrides apply to a set of functions, a custom macro is + recommended over the use of `:total`. + +--- + +## Negated effects + +Effect names may be prefixed by `!` to indicate that the effect should be removed +from an earlier meta effect. For example, `:total !:nothrow` indicates that while +the call is generally total, it may however throw. + +## Comparison to `@pure` + +`@assume_effects :foldable` is similar to [`@pure`](@ref) with the primary +distinction that the `:consistent`-cy requirement applies world-age wise rather +than globally as described above. However, in particular, a method annotated +`@pure` should always be at least `:foldable`. +Another advantage is that effects introduced by `@assume_effects` are propagated to +callers interprocedurally while a purity defined by `@pure` is not. """ macro assume_effects(args...) - (consistent, effect_free, nothrow, terminates_globally, terminates_locally) = - (false, false, false, false, false, false) - for setting in args[1:end-1] - if isa(setting, QuoteNode) - setting = setting.value - end + (consistent, effect_free, nothrow, terminates_globally, terminates_locally, notaskstate) = + (false, false, false, false, false, false, false) + for org_setting in args[1:end-1] + (setting, val) = compute_assumed_setting(org_setting) if setting === :consistent - consistent = true + consistent = val elseif setting === :effect_free - effect_free = true + effect_free = val elseif setting === :nothrow - nothrow = true + nothrow = val elseif setting === :terminates_globally - terminates_globally = true + terminates_globally = val elseif setting === :terminates_locally - terminates_locally = true + terminates_locally = val + elseif setting === :notaskstate + notaskstate = val + elseif setting === :foldable + consistent = effect_free = terminates_globally = val elseif setting === :total - consistent = effect_free = nothrow = terminates_globally = true - elseif setting === :total_may_throw - consistent = effect_free = terminates_globally = true + consistent = effect_free = nothrow = terminates_globally = notaskstate = val else - throw(ArgumentError("@assume_effects $setting not supported")) + throw(ArgumentError("@assume_effects $org_setting not supported")) end end ex = args[end] @@ -595,11 +647,21 @@ macro assume_effects(args...) if ex.head === :macrocall && ex.args[1] == Symbol("@ccall") ex.args[1] = GlobalRef(Base, Symbol("@ccall_effects")) insert!(ex.args, 3, Core.Compiler.encode_effects_override(Core.Compiler.EffectsOverride( - consistent, effect_free, nothrow, terminates_globally, terminates_locally + consistent, effect_free, nothrow, terminates_globally, terminates_locally, notaskstate ))) return esc(ex) end - return esc(pushmeta!(ex, :purity, consistent, effect_free, nothrow, terminates_globally, terminates_locally)) + return esc(pushmeta!(ex, :purity, consistent, effect_free, nothrow, terminates_globally, terminates_locally, notaskstate)) +end + +function compute_assumed_setting(@nospecialize(setting), val::Bool=true) + if isexpr(setting, :call) && setting.args[1] === :(!) + return compute_assumed_setting(setting.args[2], !val) + elseif isa(setting, QuoteNode) + return compute_assumed_setting(setting.value, val) + else + return (setting, val) + end end """ diff --git a/base/promotion.jl b/base/promotion.jl index 8e05a86b8b763..39d01fcbbfb42 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -128,7 +128,7 @@ end # WARNING: this is wrong for some objects for which subtyping is broken # (Core.Compiler.isnotbrokensubtype), use only simple types for `b` function typesplit(@nospecialize(a), @nospecialize(b)) - @_total_may_throw_meta + @_foldable_meta if a <: b return Bottom end @@ -180,7 +180,7 @@ function promote_typejoin_union(::Type{T}) where T end function typejoin_union_tuple(T::DataType) - @_total_may_throw_meta + @_foldable_meta u = Base.unwrap_unionall(T) p = (u::DataType).parameters lr = length(p)::Int diff --git a/base/reflection.jl b/base/reflection.jl index 046193c16951a..c0bb28190cd62 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -357,7 +357,7 @@ Memory allocation minimum alignment for instances of this type. Can be called on any `isconcretetype`. """ function datatype_alignment(dt::DataType) - @_total_may_throw_meta + @_foldable_meta dt.layout == C_NULL && throw(UndefRefError()) alignment = unsafe_load(convert(Ptr{DataTypeLayout}, dt.layout)).alignment return Int(alignment) @@ -374,7 +374,7 @@ LLT_ALIGN(x, sz) = (x + sz - 1) & -sz # amount of total space taken by T when stored in a container function aligned_sizeof(@nospecialize T::Type) - @_total_may_throw_meta + @_foldable_meta if isbitsunion(T) _, sz, al = uniontype_layout(T) return LLT_ALIGN(sz, al) @@ -397,7 +397,7 @@ with no intervening padding bytes. Can be called on any `isconcretetype`. """ function datatype_haspadding(dt::DataType) - @_total_may_throw_meta + @_foldable_meta dt.layout == C_NULL && throw(UndefRefError()) flags = unsafe_load(convert(Ptr{DataTypeLayout}, dt.layout)).flags return flags & 1 == 1 @@ -410,7 +410,7 @@ Return the number of fields known to this datatype's layout. Can be called on any `isconcretetype`. """ function datatype_nfields(dt::DataType) - @_total_may_throw_meta + @_foldable_meta dt.layout == C_NULL && throw(UndefRefError()) return unsafe_load(convert(Ptr{DataTypeLayout}, dt.layout)).nfields end @@ -422,7 +422,7 @@ Return whether instances of this type can contain references to gc-managed memor Can be called on any `isconcretetype`. """ function datatype_pointerfree(dt::DataType) - @_total_may_throw_meta + @_foldable_meta dt.layout == C_NULL && throw(UndefRefError()) npointers = unsafe_load(convert(Ptr{DataTypeLayout}, dt.layout)).npointers return npointers == 0 @@ -438,7 +438,7 @@ Can be called on any `isconcretetype`. See also [`fieldoffset`](@ref). """ function datatype_fielddesc_type(dt::DataType) - @_total_may_throw_meta + @_foldable_meta dt.layout == C_NULL && throw(UndefRefError()) flags = unsafe_load(convert(Ptr{DataTypeLayout}, dt.layout)).flags return (flags >> 1) & 3 @@ -706,7 +706,7 @@ julia> structinfo(Base.Filesystem.StatStruct) (0x0000000000000060, :ctime, Float64) ``` """ -fieldoffset(x::DataType, idx::Integer) = (@_total_may_throw_meta; ccall(:jl_get_field_offset, Csize_t, (Any, Cint), x, idx)) +fieldoffset(x::DataType, idx::Integer) = (@_foldable_meta; ccall(:jl_get_field_offset, Csize_t, (Any, Cint), x, idx)) """ fieldtype(T, name::Symbol | index::Int) @@ -752,7 +752,7 @@ julia> Base.fieldindex(Foo, :z, false) ``` """ function fieldindex(T::DataType, name::Symbol, err::Bool=true) - @_total_may_throw_meta + @_foldable_meta return Int(ccall(:jl_field_index, Cint, (Any, Any, Cint), T, name, err)+1) end @@ -776,7 +776,7 @@ Get the number of fields that an instance of the given type would have. An error is thrown if the type is too abstract to determine this. """ function fieldcount(@nospecialize t) - @_total_may_throw_meta + @_foldable_meta if t isa UnionAll || t isa Union t = argument_datatype(t) if t === nothing @@ -828,7 +828,7 @@ julia> fieldtypes(Foo) (Int64, String) ``` """ -fieldtypes(T::Type) = (@_total_may_throw_meta; ntupleany(i -> fieldtype(T, i), fieldcount(T))) +fieldtypes(T::Type) = (@_foldable_meta; ntupleany(i -> fieldtype(T, i), fieldcount(T))) # return all instances, for types that can be enumerated diff --git a/base/tuple.jl b/base/tuple.jl index a600e23b8f213..4b26d08859cec 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -345,7 +345,7 @@ fill_to_length(t::Tuple{}, val, ::Val{2}) = (val, val) if nameof(@__MODULE__) === :Base function tuple_type_tail(T::Type) - @_total_may_throw_meta # TODO: this method is wrong (and not :total_may_throw) + @_foldable_meta # TODO: this method is wrong (and not :foldable) if isa(T, UnionAll) return UnionAll(T.var, tuple_type_tail(T.body)) elseif isa(T, Union) diff --git a/src/julia.h b/src/julia.h index 4a5126b62ff8d..8ccfb36eb0cef 100644 --- a/src/julia.h +++ b/src/julia.h @@ -242,6 +242,7 @@ typedef union __jl_purity_overrides_t { // is guaranteed to terminate, but does not make // assertions about any called functions. uint8_t ipo_terminates_locally : 1; + uint8_t ipo_notaskstate : 1; } overrides; uint8_t bits; } _jl_purity_overrides_t; diff --git a/src/method.c b/src/method.c index 18cf3846bd33f..33abedcfdb62e 100644 --- a/src/method.c +++ b/src/method.c @@ -322,12 +322,13 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) else if (ma == (jl_value_t*)jl_no_constprop_sym) li->constprop = 2; else if (jl_is_expr(ma) && ((jl_expr_t*)ma)->head == jl_purity_sym) { - if (jl_expr_nargs(ma) == 5) { + if (jl_expr_nargs(ma) == 6) { li->purity.overrides.ipo_consistent = jl_unbox_bool(jl_exprarg(ma, 0)); li->purity.overrides.ipo_effect_free = jl_unbox_bool(jl_exprarg(ma, 1)); li->purity.overrides.ipo_nothrow = jl_unbox_bool(jl_exprarg(ma, 2)); li->purity.overrides.ipo_terminates = jl_unbox_bool(jl_exprarg(ma, 3)); li->purity.overrides.ipo_terminates_locally = jl_unbox_bool(jl_exprarg(ma, 4)); + li->purity.overrides.ipo_notaskstate = jl_unbox_bool(jl_exprarg(ma, 5)); } } else diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index 40bc0361a41fa..7c3af077adc7b 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -4115,7 +4115,7 @@ const CONST_DICT = let d = Dict() end d end -Base.@assume_effects :total_may_throw getcharid(c) = CONST_DICT[c] +Base.@assume_effects :foldable getcharid(c) = CONST_DICT[c] @noinline callf(f, args...) = f(args...) function entry_to_be_invalidated(c) return callf(getcharid, c) diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 4f2e8f8783f58..ba53198c120b6 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -1190,7 +1190,7 @@ recur_termination22(x) = x * recur_termination21(x-1) end const ___CONST_DICT___ = Dict{Any,Any}(Symbol(c) => i for (i, c) in enumerate('a':'z')) -Base.@assume_effects :total_may_throw concrete_eval( +Base.@assume_effects :foldable concrete_eval( f, args...; kwargs...) = f(args...; kwargs...) @test fully_eliminated() do concrete_eval(getindex, ___CONST_DICT___, :a)