From 2b87116694ba9e6485a74234f4660695735462e5 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 16 Jan 2022 18:56:53 -0500 Subject: [PATCH 01/20] Implement new effect system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * TLDR Before: ``` julia> let b = Expr(:block, (:(y += sin($x)) for x in randn(1000))...) @eval function f_sin_perf() y = 0.0 $b y end end f_sin_perf (generic function with 1 method) julia> @time @code_typed f_sin_perf() 15.707267 seconds (25.95 M allocations: 1.491 GiB, 3.30% gc time) [lots of junk] ``` After: ``` julia> @time @code_typed f_sin_perf() 0.016818 seconds (187.35 k allocations: 7.901 MiB, 99.73% compilation time) CodeInfo( 1 ─ return 27.639138714768546 ) => Float64 ``` so roughly a 1000x improvement in compile time performance for const-prop heavy functions. There are also run time improvements for functions that have patterns like: ``` function some_function_to_big_to_be_inlined_but_pure(x) .... end function foo(x) some_function_to_big_to_be_inlined_but_pure(x) return x end ``` The inliner will now be able to see that some_function_to_big_to_be_inlined_but_pure is effect free, even without inlining it and just delete it, improving runtime performance (if some_function_to_big_to_be_inlined_but_pure is small enough to be inlined, there is a small compile time throughput win, by being able to delete it without inlining, but that's a smaller gain than the compile time gain above). * Motivation / Overview There are two motivations for this work. The first is the above mentioned improvement in compiler performance for const-prop heavy functions. This comes up a fair bit in various Modeling & Simulation codes we have where Julia code is often auto-generated from some combination of parameterized model codes and data. This ends up creating enormous functions with significant need for constant propagation (~50k statements with ~20k constant calls are not uncommon). Our current compiler was designed for people occasionally throwing a `sqrt(2)` or something in a function, not 20k of them, so performance is quite bad. The second motivation is to have finer grained control over our purity modeling. We have `@Base.pure`, but that has somewhat nebulous semantics and is quite a big hammer that is not appropriate in most situations. These may seem like orthogonal concerns at first, but they are not. The compile time issues fundamentally stem from us running constant propagation in inference's abstract interpreter. However, for simple, pure functions, that is entirely unnecessary, because we have a super-fast, JIT compiler version of that function just laying around in general. The issue is that we currently, we generally do not know when it is legal to run the JIT-compiled version of the function and when we need to abstractly interpret it. However, if the compiler were able to figure out an appropriate notion of purity, it could start doing that (which is what it does now for `@Base.pure` functions). This PR adds that kind of notion of purity, converges it along with type information during inference and then makes use of it to speed up evaluation of constant propagation (where it is legal to do so), as well as improving the inliner. * The new purity notions The new purity model consists of four different kinds flags per code instance. For builtins and intrinsics the existing effect free and nothrow models are re-used. There is also a new macro `@Base.assume_effects` available, which can set the purity base case for methods or `:foreigncall`s. Here is the docstring for that macro, which also explains the semantics of the new purity flags: ``` @assume_effects setting... ex @assume_effects(setting..., ex) `@assume_effects` overrides the compiler's effect modeling for the given method. `ex` must be a method definition. WARNING: Improper use of this macro causes undefined behavior (including crashes, incorrect answers, or other hard to track bugs). Use with care an only if absolutely required. In general, each `setting` value makes an assertion about the behavior of the function, without requiring the compiler to prove that this behavior is indeed true. These assertions are made for all world ages. It is thus advisable to limit the use of generic functions that may later be extended to invalidate the assumption (which would cause undefined behavior). The following `settings` are supported. ** `:idempotent` The `:idempotent` setting asserts that for egal inputs: - The manner of termination (return value, exception, non-termination) will always be the same. - If the method returns, the results will always be egal. Note: This in particular implies that the return value of the method must be immutable. Multiple allocations of mutable objects (even with identical contents) are not egal. Note: The idempotency assertion is made world-arge wise. More formally, write fₐ for the evaluation of `f` in world-age `a`, then we require: ∀ a, x, y: x === y → fₐ(x) === fₐ(y) However, for two world ages `a, b` s.t. `a != b`, we may have `fₐ(x) !== fₐ(y)`` Note: A further implication is that idempontent functions may not make their return value dependent on the state of the heap or any other global state that is not constant for a given world age. Note: The idempontency includes all legal rewrites performed by the optimizizer. For example, floating-point fastmath operations are not considered idempotent, because the optimizer may rewrite them causing the output to not be idempotent, even for the same world age (e.g. because one ran in the interpreter, while the other was optimized). ** `:effect_free` The `:effect_free` setting asserts that the method is free of externally semantically visible side effects. The following is an incomplete list of externally semantically visible side effects: - Changing the value of a global variable. - Mutating the heap (e.g. an array or mutable value), except as noted below - Changing the method table (e.g. through calls to eval) - File/Network/etc. I/O - Task switching However, the following are explicitly not semantically visible, even if they may be observable: - Memory allocations (both mutable and immutable) - Elapsed time - Garbage collection - Heap mutations of objects whose lifetime does not exceed the method (i.e. were allocated in the method and do not escape). - The returned value (which is externally visible, but not a side effect) The rule of thumb here is that an externally visible side effect is anything that would affect the execution of the remainder of the program if the function were not executed. Note: The effect free assertion is made both for the method itself and any code that is executed by the method. Keep in mind that the assertion must be valid for all world ages and limit use of this assertion accordingly. ** `:nothrow` The `:nothrow` settings asserts that this method does not terminate abnormally (i.e. will either always return a value or never return). Note: It is permissible for :nothrow annotated methods to make use of exception handling internally as long as the exception is not rethrown out of the method itself. Note: MethodErrors and similar exceptions count as abnormal termination. ** `:terminates_globally` The `:terminates_globally` settings asserts that this method will eventually terminate (either normally or abnormally), i.e. does not infinite loop. Note: The compiler will consider this a strong indication that the method will terminate relatively *quickly* and may (if otherwise legal), call this method at compile time. I.e. it is a bad idea to annotate this setting on a method that *technically*, but not *practically*, terminates. Note: The `terminates_globally` assertion, covers any other methods called by the annotated method. ** `:terminates_locally` The `:terminates_locally` setting is like `:terminates_globally`, except that it only applies to syntactic control flow *within* the annotated method. It is this a much weaker (and thus safer) assertion that allows for the possibility of non-termination if the method calls some other method that does not terminate. Note: `terminates_globally` implies `terminates_locally`. * `:total` The `setting` combines the following other assertions: - `:idempotent` - `:effect_free` - `:nothrow` - `:terminates_globally` and is a convenient shortcut. Note: `@assume_effects :total` is similar to `@Base.pure` with the primary distinction that the idempotency requirement applies world-age wise rather than globally as described above. However, in particular, a method annotated `@Base.pure` is always total. ``` * Changes to data structures - Each CodeInstance gains two sets of four flags corresponding to the notions above (except terminates_locally, which is just a type inference flag). One set of flags tracks IPO-valid information (as determined by inference), the other set of flags tracks optimizer-valid information (as determined after optimization). Otherwise they have identical semantics. - Method and CodeInfo each gain 5 bit flags corresponding 1:1 to the purity notions defined above. No separate distinction is made between IPO valid and optimizer valid flags here. We might in the future want such a distinction, but I'm hoping to get away without it for now, since the IPO-vs-optimizer distinction is a bit subtle and I don't really want to expose that to the user. - `:foreigncall` gains an extra argument (after `cconv`) to describe the effects of the call. * Algorithm Relatively straightforward. - Every call or builtin accumulates its effect information into the current frame. - Finding an effect (throw/global side effect/non-idempotenct, etc.) taints the entire frame. Idempotency is technically a dataflow property, but that is not modeled here and any non-idempotent intrinsic will taint the idempotency flag, even if it does not contribute to the return value. I don't think that's a huge problem in practice, because currently we only use idempotency if effect-free is also set and in effect-free functions you'd generally expect every statement to contribute to the return value. - Any backedge taints the termination effect, as does any recursion - Unknown statements (generic calls, things I haven't gotten around to) taint all effects --- base/boot.jl | 7 +- base/c.jl | 1 + base/compiler/abstractinterpretation.jl | 183 +++++++++++++++++++--- base/compiler/inferencestate.jl | 5 + base/compiler/optimize.jl | 37 +---- base/compiler/ssair/inlining.jl | 85 +++++++--- base/compiler/ssair/passes.jl | 6 +- base/compiler/ssair/show.jl | 15 ++ base/compiler/stmtinfo.jl | 12 +- base/compiler/tfuncs.jl | 106 +++++++++++++ base/compiler/typeinfer.jl | 70 +++++++-- base/compiler/types.jl | 65 +++++++- base/compiler/validation.jl | 2 +- base/expr.jl | 154 ++++++++++++++++++ base/floatfuncs.jl | 4 +- base/meta.jl | 2 + base/show.jl | 4 +- base/strings/string.jl | 4 +- doc/src/devdocs/llvm.md | 2 +- src/ast.c | 2 + src/ccall.cpp | 13 +- src/dump.c | 6 + src/gf.c | 18 ++- src/ircode.c | 6 +- src/jltypes.c | 28 ++-- src/julia-syntax.scm | 7 +- src/julia.h | 40 +++++ src/julia_internal.h | 1 + src/method.c | 16 +- stdlib/InteractiveUtils/test/runtests.jl | 2 +- stdlib/Serialization/src/Serialization.jl | 11 +- test/ccall.jl | 3 +- test/compiler/inference.jl | 6 +- test/compiler/inline.jl | 32 ++++ test/compiler/irpasses.jl | 2 +- test/testenv.jl | 2 +- 36 files changed, 814 insertions(+), 145 deletions(-) diff --git a/base/boot.jl b/base/boot.jl index abdd7987ce901..ecc037407685e 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -418,9 +418,10 @@ eval(Core, :(LineInfoNode(mod::Module, @nospecialize(method), file::Symbol, line $(Expr(:new, :LineInfoNode, :mod, :method, :file, :line, :inlined_at)))) eval(Core, :(CodeInstance(mi::MethodInstance, @nospecialize(rettype), @nospecialize(inferred_const), @nospecialize(inferred), const_flags::Int32, - min_world::UInt, max_world::UInt, relocatability::UInt8) = - ccall(:jl_new_codeinst, Ref{CodeInstance}, (Any, Any, Any, Any, Int32, UInt, UInt, UInt8), - mi, rettype, inferred_const, inferred, const_flags, min_world, max_world, relocatability))) + min_world::UInt, max_world::UInt, ipo_effects::UInt8, effects::UInt8, + relocatability::UInt8) = + ccall(:jl_new_codeinst, Ref{CodeInstance}, (Any, Any, Any, Any, Int32, UInt, UInt, UInt8, UInt8, UInt8), + mi, rettype, inferred_const, inferred, const_flags, min_world, max_world, ipo_effects, effects, relocatability))) eval(Core, :(Const(@nospecialize(v)) = $(Expr(:new, :Const, :v)))) eval(Core, :(PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields)))) eval(Core, :(PartialOpaque(@nospecialize(typ), @nospecialize(env), isva::Bool, parent::MethodInstance, source::Method) = $(Expr(:new, :PartialOpaque, :typ, :env, :isva, :parent, :source)))) diff --git a/base/c.jl b/base/c.jl index fb0d4e7dc0583..fdcc405b3146d 100644 --- a/base/c.jl +++ b/base/c.jl @@ -676,6 +676,7 @@ function ccall_macro_lower(convention, func, rettype, types, args, nreq) esc(etypes), nreq, QuoteNode(convention), + nothing, realargs..., gcroots...) push!(lowering, exp) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 9a01f5aa80d59..a27bffaf4902f 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -23,11 +23,24 @@ end const empty_bitset = BitSet() +function should_infer_for_effects(sv::InferenceState) + sv.ipo_effects.terminates === ALWAYS_TRUE && + sv.ipo_effects.effect_free === ALWAYS_TRUE +end + function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), arginfo::ArgInfo, @nospecialize(atype), sv::InferenceState, max_methods::Int = get_max_methods(sv.mod, interp)) - if sv.params.unoptimize_throw_blocks && is_stmt_throw_block(get_curr_ssaflag(sv)) + if !should_infer_for_effects(sv) && + sv.params.unoptimize_throw_blocks && + is_stmt_throw_block(get_curr_ssaflag(sv)) + # Disable inference of calls in throw blocks, since we're unlikely to + # need their types. There is one exception however: If up until now, the + # function has not seen any side effects, we would like to make sure there + # aren't any in the throw block either to enable other optimizations. add_remark!(interp, sv, "Skipped call in throw block") + tristate_merge!(sv, Effects(ALWAYS_TRUE, TRISTATE_UNKNOWN, + TRISTATE_UNKNOWN, TRISTATE_UNKNOWN)) return CallMeta(Any, false) end @@ -35,6 +48,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), matches = find_matching_methods(argtypes, atype, method_table(interp, sv), InferenceParams(interp).MAX_UNION_SPLITTING, max_methods) if isa(matches, FailedMethodMatch) add_remark!(interp, sv, matches.reason) + tristate_merge!(sv, Effects()) return CallMeta(Any, false) end @@ -46,7 +60,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), conditionals = nothing # keeps refinement information of call argument types when the return type is boolean seen = 0 # number of signatures actually inferred any_const_result = false - const_results = Union{InferenceResult,Nothing}[] + const_results = Union{InferenceResult,Nothing,ConstResult,Bool}[] multiple_matches = napplicable > 1 if f !== nothing && napplicable == 1 && is_method_pure(applicable[1]::MethodMatch) @@ -58,6 +72,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), end fargs = arginfo.fargs + merged_sig = Union{} for i in 1:napplicable match = applicable[i]::MethodMatch method = match.method @@ -78,6 +93,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), for sig_n in splitsigs result = abstract_call_method(interp, method, sig_n, svec(), multiple_matches, sv) rt, edge = result.rt, result.edge + tristate_merge!(sv, result.edge_effects) if edge !== nothing push!(edges, edge) end @@ -113,6 +129,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), result = abstract_call_method(interp, method, sig, match.sparams, multiple_matches, sv) this_rt, edge = result.rt, result.edge + tristate_merge!(sv, result.edge_effects) if edge !== nothing push!(edges, edge) end @@ -133,6 +150,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), this_rt = widenwrappedconditional(this_rt) @assert !(this_conditional isa Conditional) "invalid lattice element returned from inter-procedural context" seen += 1 + merged_sig = Union{merged_sig, sig} rettype = tmerge(rettype, this_rt) if this_conditional !== Bottom && is_lattice_bool(rettype) && fargs !== nothing if conditionals === nothing @@ -155,6 +173,13 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), info = ConstCallInfo(info, const_results) end + if seen != napplicable + tristate_merge!(sv, Effects()) + elseif !(atype <: merged_sig) + # Account for the fact that we may encounter a non-covered signature. + tristate_merge!(sv, Effects(ALWAYS_TRUE, ALWAYS_TRUE, TRISTATE_UNKNOWN, ALWAYS_TRUE)) + end + rettype = from_interprocedural!(rettype, sv, arginfo, conditionals) if call_result_unused(sv) && !(rettype === Bottom) @@ -402,7 +427,7 @@ const RECURSION_MSG = "Bounded recursion detected. Call was widened to force con function abstract_call_method(interp::AbstractInterpreter, method::Method, @nospecialize(sig), sparams::SimpleVector, hardlimit::Bool, sv::InferenceState) if method.name === :depwarn && isdefined(Main, :Base) && method.module === Main.Base add_remark!(interp, sv, "Refusing to infer into `depwarn`") - return MethodCallResult(Any, false, false, nothing) + return MethodCallResult(Any, false, false, nothing, Effects()) end topmost = nothing # Limit argument type tuple growth of functions: @@ -471,7 +496,7 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp # we have a self-cycle in the call-graph, but not in the inference graph (typically): # break this edge now (before we record it) by returning early # (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases) - return MethodCallResult(Any, true, true, nothing) + return MethodCallResult(Any, true, true, nothing, Effects()) end topmost = nothing edgecycle = true @@ -520,7 +545,7 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp # since it's very unlikely that we'll try to inline this, # or want make an invoke edge to its calling convention return type. # (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases) - return MethodCallResult(Any, true, true, nothing) + return MethodCallResult(Any, true, true, nothing, Effects()) end add_remark!(interp, sv, RECURSION_MSG) topmost = topmost::InferenceState @@ -558,11 +583,17 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp sparams = recomputed[2]::SimpleVector end - rt, edge = typeinf_edge(interp, method, sig, sparams, sv) + rt, edge, edge_effects = typeinf_edge(interp, method, sig, sparams, sv) if edge === nothing edgecycle = edgelimited = true end - return MethodCallResult(rt, edgecycle, edgelimited, edge) + if edgecycle + # Some sort of recursion was detected. Even if we did not limit types, + # we cannot guarantee that the call will terminate. + edge_effects = tristate_merge(edge_effects, + Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, TRISTATE_UNKNOWN)) + end + return MethodCallResult(rt, edgecycle, edgelimited, edge, edge_effects) end # keeps result and context information of abstract method call, will be used by succeeding constant-propagation @@ -571,17 +602,63 @@ struct MethodCallResult edgecycle::Bool edgelimited::Bool edge::Union{Nothing,MethodInstance} + edge_effects::Effects function MethodCallResult(@nospecialize(rt), edgecycle::Bool, edgelimited::Bool, - edge::Union{Nothing,MethodInstance}) - return new(rt, edgecycle, edgelimited, edge) + edge::Union{Nothing,MethodInstance}, + edge_effects::Effects) + return new(rt, edgecycle, edgelimited, edge, edge_effects) + end +end + +function is_all_const_arg((; fargs, argtypes)::ArgInfo) + for a in argtypes + if !isa(a, Const) && !isconstType(a) && !issingletontype(a) + return false + end + end + return true +end + +function pure_eval_call_const_args(@nospecialize(f), argtypes::Vector{Any}) + args = Any[ (a = widenconditional(argtypes[i]); + isa(a, Const) ? a.val : + isconstType(a) ? (a::DataType).parameters[1] : + (a::DataType).instance) for i in 2:length(argtypes) ] + try + value = Core._apply_pure(f, args) + return Const(value) + catch e + return nothing + end +end + +function const_prop_enabled(interp::AbstractInterpreter, sv::InferenceState, match::MethodMatch) + if !InferenceParams(interp).ipo_constant_propagation + add_remark!(interp, sv, "[constprop] Disabled by parameter") + return false + end + method = match.method + if method.constprop == 0x02 + add_remark!(interp, sv, "[constprop] Disabled by method parameter") + return false end + return true end function abstract_call_method_with_const_args(interp::AbstractInterpreter, result::MethodCallResult, @nospecialize(f), arginfo::ArgInfo, match::MethodMatch, sv::InferenceState, va_override::Bool) + if !const_prop_enabled(interp, sv, match) + return nothing + end + if f !== nothing && result.edge !== nothing && is_total_or_error(result.edge_effects) && is_all_const_arg(arginfo) + rt = pure_eval_call_const_args(f, arginfo.argtypes) # TODO: Needs to be called in the correct world age + add_backedge!(result.edge, sv) + rt === nothing && return Union{}, false # The evaulation threw. By idempontency, we're guaranteed this would have happened at runtime + return rt, ConstResult(result.edge, rt.val) + end mi = maybe_get_const_prop_profitable(interp, result, f, arginfo, match, sv) mi === nothing && return nothing # try constant prop' @@ -624,15 +701,7 @@ end function maybe_get_const_prop_profitable(interp::AbstractInterpreter, result::MethodCallResult, @nospecialize(f), arginfo::ArgInfo, match::MethodMatch, sv::InferenceState) - if !InferenceParams(interp).ipo_constant_propagation - add_remark!(interp, sv, "[constprop] Disabled by parameter") - return nothing - end method = match.method - if method.constprop == 0x02 - add_remark!(interp, sv, "[constprop] Disabled by method parameter") - return nothing - end force = force_const_prop(interp, f, method) force || const_prop_entry_heuristic(interp, result, sv) || return nothing nargs::Int = method.nargs @@ -1385,9 +1454,12 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), elseif f === modifyfield! return abstract_modifyfield!(interp, argtypes, sv) end - return CallMeta(abstract_call_builtin(interp, f, arginfo, sv, max_methods), false) + rt = abstract_call_builtin(interp, f, arginfo, sv, max_methods) + tristate_merge!(sv, builtin_effects(f, argtypes, rt)) + return CallMeta(rt, false) elseif isa(f, Core.OpaqueClosure) # calling an OpaqueClosure about which we have no information returns no information + tristate_merge!(sv, Effects()) return CallMeta(Any, false) elseif f === Core.kwfunc if la == 2 @@ -1399,10 +1471,12 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), end end end + tristate_merge!(sv, Effects()) # TODO return CallMeta(Any, false) elseif f === TypeVar # Manually look through the definition of TypeVar to # make sure to be able to get `PartialTypeVar`s out. + tristate_merge!(sv, Effects()) # TODO (la < 2 || la > 4) && return CallMeta(Union{}, false) n = argtypes[2] ub_var = Const(Any) @@ -1415,14 +1489,17 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), end return CallMeta(typevar_tfunc(n, lb_var, ub_var), false) elseif f === UnionAll + tristate_merge!(sv, Effects()) # TODO return CallMeta(abstract_call_unionall(argtypes), false) elseif f === Tuple && la == 2 + tristate_merge!(sv, Effects()) # TODO aty = argtypes[2] ty = isvarargtype(aty) ? unwrapva(aty) : widenconst(aty) if !isconcretetype(ty) return CallMeta(Tuple, false) end elseif is_return_type(f) + tristate_merge!(sv, Effects()) # TODO return return_type_tfunc(interp, argtypes, sv) elseif la == 2 && istopfunction(f, :!) # handle Conditional propagation through !Bool @@ -1484,7 +1561,7 @@ function abstract_call_opaque_closure(interp::AbstractInterpreter, closure::Part match = MethodMatch(sig, Core.svec(), closure.source, sig <: rewrap_unionall(sigT, tt)) const_result = nothing if !result.edgecycle - const_result = abstract_call_method_with_const_args(interp, result, closure, + const_result = abstract_call_method_with_const_args(interp, result, nothing, arginfo, match, sv, closure.isva) if const_result !== nothing rt, const_result = const_result @@ -1599,7 +1676,7 @@ function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize( elseif isa(e, SlotNumber) || isa(e, Argument) return vtypes[slot_id(e)].typ elseif isa(e, GlobalRef) - return abstract_eval_global(e.mod, e.name) + return abstract_eval_global(e.mod, e.name, sv) end return Const(e) @@ -1651,18 +1728,23 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), t = callinfo.rt end elseif ehead === :new - t = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv))[1] + t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) + is_nothrow = false if isconcretetype(t) && !ismutabletype(t) nargs = length(e.args) - 1 ats = Vector{Any}(undef, nargs) local anyrefine = false local allconst = true + is_nothrow = isexact && isconcretedispatch(t) for i = 2:length(e.args) at = widenconditional(abstract_eval_value(interp, e.args[i], vtypes, sv)) ft = fieldtype(t, i-1) at = tmeet(at, ft) if at === Bottom t = Bottom + tristate_merge!(sv, Effects( + !ismutabletype(t) ? ALWAYS_TRUE : ALWAYS_FALSE, + ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_TRUE)) @goto t_computed elseif !isa(at, Const) allconst = false @@ -1684,22 +1766,35 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), elseif anyrefine t = PartialStruct(t, ats) end + else + is_nothrow = false end end + tristate_merge!(sv, Effects( + !ismutabletype(t) ? ALWAYS_TRUE : ALWAYS_FALSE, + ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_TRUE)) elseif ehead === :splatnew - t = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv))[1] + t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) + is_nothrow = false # TODO: More precision if length(e.args) == 2 && isconcretetype(t) && !ismutabletype(t) at = abstract_eval_value(interp, e.args[2], vtypes, sv) n = fieldcount(t) if isa(at, Const) && isa(at.val, Tuple) && n == length(at.val::Tuple) && let t = t, at = at; _all(i->getfield(at.val::Tuple, i) isa fieldtype(t, i), 1:n); end + is_nothrow = isexact && isconcretedispatch(t) t = Const(ccall(:jl_new_structt, Any, (Any, Any), t, at.val)) elseif isa(at, PartialStruct) && at ⊑ Tuple && n == length(at.fields::Vector{Any}) && let t = t, at = at; _all(i->(at.fields::Vector{Any})[i] ⊑ fieldtype(t, i), 1:n); end + is_nothrow = isexact && isconcretedispatch(t) t = PartialStruct(t, at.fields::Vector{Any}) end end + tristate_merge!(sv, Effects( + ismutabletype(t) ? ALWAYS_FALSE : ALWAYS_TRUE, + ALWAYS_TRUE, is_nothrow ? ALWAYS_TRUE : ALWAYS_FALSE, + ALWAYS_TRUE)) elseif ehead === :new_opaque_closure + tristate_merge!(sv, Effects()) # TODO t = Union{} if length(e.args) >= 5 ea = e.args @@ -1728,13 +1823,28 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), t = Bottom end end + effects = e.args[6] + if isa(effects, UInt8) + effects = decode_effects_override(effects) + tristate_merge!(sv, Effects( + effects.consistent ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + effects.effect_free ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + effects.nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + effects.terminates ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + )) + else + tristate_merge!(sv, Effects()) + end elseif ehead === :cfunction + tristate_merge!(sv, Effects()) t = e.args[1] isa(t, Type) || (t = Any) abstract_eval_cfunction(interp, e, vtypes, sv) elseif ehead === :method + tristate_merge!(sv, Effects()) t = (length(e.args) == 1) ? Any : Nothing elseif ehead === :copyast + tristate_merge!(sv, Effects()) t = abstract_eval_value(interp, e.args[1], vtypes, sv) if t isa Const && t.val isa Expr # `copyast` makes copies of Exprs @@ -1798,6 +1908,19 @@ function abstract_eval_global(M::Module, s::Symbol) return ty end +function abstract_eval_global(M::Module, s::Symbol, frame::InferenceState) + if isdefined(M,s) + if isconst(M,s) + return Const(getfield(M,s)) + else + tristate_merge!(frame, Effects(ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE)) + end + else + tristate_merge!(frame, Effects(ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_TRUE)) + end + return Any +end + function abstract_eval_ssavalue(s::SSAValue, src::CodeInfo) typ = (src.ssavaluetypes::Vector{Any})[s.id] if typ === NOT_FOUND @@ -1877,6 +2000,12 @@ function widenreturn(@nospecialize(rt), @nospecialize(bestguess), nslots::Int, s return widenconst(rt) end +function handle_backedge!(frame::InferenceState, from, to) + if from > to + tristate_merge!(frame, Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, TRISTATE_UNKNOWN)) + end +end + # make as much progress on `frame` as possible (without handling cycles) function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) @assert !frame.inferred @@ -1914,7 +2043,9 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) sn = slot_id(stmt.slot) changes[sn] = VarState(Bottom, true) elseif isa(stmt, GotoNode) - pc´ = (stmt::GotoNode).label + l = (stmt::GotoNode).label + handle_backedge!(frame, pc, l) + pc´ = l elseif isa(stmt, GotoIfNot) condx = stmt.cond condt = abstract_eval_value(interp, condx, changes, frame) @@ -1939,6 +2070,7 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) # constant conditions if condval === true elseif condval === false + handle_backedge!(frame, pc, l) pc´ = l else # general case @@ -1949,6 +2081,7 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) end newstate_else = stupdate!(states[l], changes_else) if newstate_else !== nothing + handle_backedge!(frame, pc, l) # add else branch to active IP list if l < frame.pc´´ frame.pc´´ = l @@ -2024,6 +2157,10 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) lhs = stmt.args[1] if isa(lhs, SlotNumber) changes = StateUpdate(lhs, VarState(t, false), changes, false) + elseif isa(lhs, GlobalRef) + tristate_merge!(frame, Effects(ALWAYS_TRUE, ALWAYS_FALSE, TRISTATE_UNKNOWN, ALWAYS_TRUE)) + elseif !isa(lhs, SSAValue) + tristate_merge!(frame, Effects()) end elseif hd === :method stmt = stmt::Expr diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index ff9ffa5456458..475b259cc04cf 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -59,6 +59,9 @@ mutable struct InferenceState inferred::Bool dont_work_on_me::Bool + # Inferred purity flags + ipo_effects::Effects + # The place to look up methods while working on this function. # In particular, we cache method lookup results for the same function to # fast path repeated queries. @@ -126,6 +129,7 @@ mutable struct InferenceState Vector{InferenceState}(), # callers_in_cycle #=parent=#nothing, cache === :global, false, false, + Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE), CachedMethodTable(method_table(interp)), interp) result.result = frame @@ -133,6 +137,7 @@ mutable struct InferenceState return frame end end +Effects(state::InferenceState) = state.ipo_effects function compute_trycatch(code::Vector{Any}, ip::BitSet) # The goal initially is to record the frame like this for the state at exit: diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 3c00b9faa6d0a..80492d8633165 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -149,16 +149,6 @@ const IR_FLAG_THROW_BLOCK = 0x01 << 3 # thus be both pure and effect free. const IR_FLAG_EFFECT_FREE = 0x01 << 4 -# known to be always effect-free (in particular nothrow) -const _PURE_BUILTINS = Any[tuple, svec, ===, typeof, nfields] - -# known to be effect-free if the are nothrow -const _PURE_OR_ERROR_BUILTINS = [ - fieldtype, apply_type, isa, UnionAll, - getfield, arrayref, const_arrayref, arraysize, isdefined, Core.sizeof, - Core.kwfunc, Core.ifelse, Core._typevar, (<:), -] - const TOP_TUPLE = GlobalRef(Core, :tuple) ######### @@ -298,11 +288,11 @@ function alloc_array_ndims(name::Symbol) end function alloc_array_no_throw(args::Vector{Any}, ndims::Int, src::Union{IRCode,IncrementalCompact}) - length(args) ≥ ndims+6 || return false - atype = instanceof_tfunc(argextype(args[6], src))[1] + length(args) ≥ ndims+7 || return false + atype = instanceof_tfunc(argextype(args[7], src))[1] dims = Csize_t[] for i in 1:ndims - dim = argextype(args[i+6], src) + dim = argextype(args[i+7], src) isa(dim, Const) || return false dimval = dim.val isa(dimval, Int) || return false @@ -312,9 +302,9 @@ function alloc_array_no_throw(args::Vector{Any}, ndims::Int, src::Union{IRCode,I end function new_array_no_throw(args::Vector{Any}, src::Union{IRCode,IncrementalCompact}) - length(args) ≥ 7 || return false - atype = instanceof_tfunc(argextype(args[6], src))[1] - dims = argextype(args[7], src) + length(args) ≥ 8 || return false + atype = instanceof_tfunc(argextype(args[7], src))[1] + dims = argextype(args[8], src) isa(dims, Const) || return dims === Tuple{} dimsval = dims.val isa(dimsval, Tuple{Vararg{Int}}) || return false @@ -621,21 +611,6 @@ function slot2reg(ir::IRCode, ci::CodeInfo, sv::OptimizationState) return ir end -# whether `f` is pure for inference -function is_pure_intrinsic_infer(f::IntrinsicFunction) - return !(f === Intrinsics.pointerref || # this one is volatile - f === Intrinsics.pointerset || # this one is never effect-free - f === Intrinsics.llvmcall || # this one is never effect-free - f === Intrinsics.arraylen || # this one is volatile - f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps) - f === Intrinsics.have_fma || # this one depends on the runtime environment - f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime -end - -# whether `f` is effect free if nothrow -intrinsic_effect_free_if_nothrow(f) = f === Intrinsics.pointerref || - f === Intrinsics.have_fma || is_pure_intrinsic_infer(f) - ## Computing the cost of a function body # saturating sum (inputs are nonnegative), prevents overflow with typemax(Int) below diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index a3faba0e76f5d..55c5d204bfa1e 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -15,6 +15,8 @@ struct ResolvedInliningSpec # If the function being inlined is a single basic block we can use a # simpler inlining algorithm. This flag determines whether that's allowed linear_inline_eligible::Bool + # Effects of the call statement + effects::Effects end """ @@ -49,11 +51,16 @@ struct SomeCase SomeCase(val) = new(val) end +struct InvokeCase + invoke::Union{MethodInstance, Nothing} + effects::Effects +end + struct InliningCase sig # ::Type item # Union{InliningTodo, MethodInstance, ConstantCase} function InliningCase(@nospecialize(sig), @nospecialize(item)) - @assert isa(item, Union{InliningTodo, MethodInstance, ConstantCase}) "invalid inlining item" + @assert isa(item, Union{InliningTodo, InvokeCase, ConstantCase}) "invalid inlining item" return new(sig, item) end end @@ -508,9 +515,11 @@ function ir_inline_unionsplit!(compact::IncrementalCompact, idx::Int, end if isa(case, InliningTodo) val = ir_inline_item!(compact, idx, argexprs′, linetable, case, boundscheck, todo_bbs) - elseif isa(case, MethodInstance) + elseif isa(case, InvokeCase) + effect_free = is_removable_if_unused(case.effects) val = insert_node_here!(compact, - NewInstruction(Expr(:invoke, case, argexprs′...), typ, line)) + NewInstruction(Expr(:invoke, case.invoke, argexprs′...), typ, nothing, + line, effect_free ? IR_FLAG_EFFECT_FREE : IR_FLAG_NULL, effect_free)) else case = case::ConstantCase val = case.val @@ -719,16 +728,22 @@ function rewrite_apply_exprargs!( return new_argtypes end -function compileable_specialization(et::Union{EdgeTracker, Nothing}, match::MethodMatch) +function compileable_specialization(et::Union{EdgeTracker, Nothing}, match::MethodMatch, effects::Effects) mi = specialize_method(match; compilesig=true) mi !== nothing && et !== nothing && push!(et, mi::MethodInstance) - return mi + mi === nothing && return nothing + return InvokeCase(mi, effects) end -function compileable_specialization(et::Union{EdgeTracker, Nothing}, (; linfo)::InferenceResult) +function compileable_specialization(et::Union{EdgeTracker, Nothing}, linfo::MethodInstance, effects::Effects) mi = specialize_method(linfo.def::Method, linfo.specTypes, linfo.sparam_vals; compilesig=true) mi !== nothing && et !== nothing && push!(et, mi::MethodInstance) - return mi + mi === nothing && return nothing + return InvokeCase(mi, effects) +end + +function compileable_specialization(et::Union{EdgeTracker, Nothing}, (; linfo)::InferenceResult, effects::Effects) + return compileable_specialization(et, linfo, effects) end function resolve_todo(todo::InliningTodo, state::InliningState, flag::UInt8) @@ -746,6 +761,7 @@ function resolve_todo(todo::InliningTodo, state::InliningState, flag::UInt8) else src = inferred_src end + effects = match.ipo_effects else code = get(state.mi_cache, mi, nothing) if code isa CodeInstance @@ -756,7 +772,9 @@ function resolve_todo(todo::InliningTodo, state::InliningState, flag::UInt8) else src = code.inferred end + effects = decode_effects(code.ipo_purity_bits) else + effects = Effects() src = code end end @@ -764,13 +782,13 @@ function resolve_todo(todo::InliningTodo, state::InliningState, flag::UInt8) # the duplicated check might have been done already within `analyze_method!`, but still # we need it here too since we may come here directly using a constant-prop' result if !state.params.inlining || is_stmt_noinline(flag) - return compileable_specialization(et, match) + return compileable_specialization(et, match, effects) end src = inlining_policy(state.interp, src, flag, mi, argtypes) if src === nothing - return compileable_specialization(et, match) + return compileable_specialization(et, match, effects) end if isa(src, IRCode) @@ -778,7 +796,7 @@ function resolve_todo(todo::InliningTodo, state::InliningState, flag::UInt8) end et !== nothing && push!(et, mi) - return InliningTodo(mi, src) + return InliningTodo(mi, src, effects) end function resolve_todo((; fully_covered, atype, cases, #=bbs=#)::UnionSplit, state::InliningState, flag::UInt8) @@ -820,13 +838,9 @@ function analyze_method!(match::MethodMatch, argtypes::Vector{Any}, et = state.et - if !state.params.inlining || is_stmt_noinline(flag) - return compileable_specialization(et, match) - end - # See if there exists a specialization for this method signature mi = specialize_method(match; preexisting=true) # Union{Nothing, MethodInstance} - isa(mi, MethodInstance) || return compileable_specialization(et, match) + isa(mi, MethodInstance) || return compileable_specialization(et, match, Effects()) todo = InliningTodo(mi, match, argtypes) # If we don't have caches here, delay resolving this MethodInstance @@ -835,17 +849,17 @@ function analyze_method!(match::MethodMatch, argtypes::Vector{Any}, return resolve_todo(todo, state, flag) end -function InliningTodo(mi::MethodInstance, ir::IRCode) - return InliningTodo(mi, ResolvedInliningSpec(ir, linear_inline_eligible(ir))) +function InliningTodo(mi::MethodInstance, ir::IRCode, effects::Effects) + return InliningTodo(mi, ResolvedInliningSpec(ir, linear_inline_eligible(ir), effects)) end -function InliningTodo(mi::MethodInstance, src::Union{CodeInfo, Array{UInt8, 1}}) +function InliningTodo(mi::MethodInstance, src::Union{CodeInfo, Array{UInt8, 1}}, effects::Effects) if !isa(src, CodeInfo) src = ccall(:jl_uncompress_ir, Any, (Any, Ptr{Cvoid}, Any), mi.def, C_NULL, src::Vector{UInt8})::CodeInfo end - @timeit "inline IR inflation" begin - return InliningTodo(mi, inflate_ir(src, mi)::IRCode) + @timeit "inline IR inflation" begin; + return InliningTodo(mi, inflate_ir(src, mi)::IRCode, effects) end end @@ -854,10 +868,13 @@ function handle_single_case!( @nospecialize(case), todo::Vector{Pair{Int, Any}}, params::OptimizationParams, isinvoke::Bool = false) if isa(case, ConstantCase) ir[SSAValue(idx)][:inst] = case.val - elseif isa(case, MethodInstance) + elseif isa(case, InvokeCase) + if is_total(case.effects) + inline_const_if_inlineable!(ir[SSAValue(idx)]) && return nothing + end isinvoke && rewrite_invoke_exprargs!(stmt) stmt.head = :invoke - pushfirst!(stmt.args, case) + pushfirst!(stmt.args, case.invoke) elseif case === nothing # Do, well, nothing else @@ -1104,10 +1121,10 @@ function process_simple!(ir::IRCode, idx::Int, state::InliningState, todo::Vecto length(info.results) == 1 || return nothing match = info.results[1]::MethodMatch match.fully_covers || return nothing - case = compileable_specialization(state.et, match) + case = compileable_specialization(state.et, match, Effects()) case === nothing && return nothing stmt.head = :invoke_modify - pushfirst!(stmt.args, case) + pushfirst!(stmt.args, case.invoke) ir.stmts[idx][:inst] = stmt end return nothing @@ -1223,6 +1240,21 @@ function handle_const_call!( for match in meth j += 1 result = results[j] + if result === false + # Inference determined that this call is guaranteed to throw. + # Do not inline. + fully_covered = false + continue + end + if isa(result, ConstResult) + case = ConstantCase(quoted(result.result)) + if !is_inlineable_constant(result.result) + case = compileable_specialization(state.et, result.mi, Effects()) + end + signature_union = Union{signature_union, result.mi.specTypes} + push!(cases, InliningCase(result.mi.specTypes, case)) + continue + end if result === nothing signature_union = Union{signature_union, match.spec_types} fully_covered &= handle_match!(match, argtypes, flag, state, cases) @@ -1236,7 +1268,7 @@ function handle_const_call!( # if the signature is fully covered and there is only one applicable method, # we can try to inline it even if the signature is not a dispatch tuple atype = argtypes_to_type(argtypes) - if length(cases) == 0 && length(results) == 1 + if length(cases) == 0 && length(results) == 1 && isa(results[1], InferenceResult) (; mi) = item = InliningTodo(results[1]::InferenceResult, argtypes) state.mi_cache !== nothing && (item = resolve_todo(item, state, flag)) validate_sparams(mi.sparam_vals) || return nothing @@ -1314,6 +1346,7 @@ function assemble_inline_todo!(ir::IRCode, state::InliningState) # todo = (inline_idx, (isva, isinvoke, na), method, spvals, inline_linetable, inline_ir, lie) todo = Pair{Int, Any}[] et = state.et + for idx in 1:length(ir.stmts) simpleres = process_simple!(ir, idx, state, todo) simpleres === nothing && continue @@ -1454,7 +1487,7 @@ function late_inline_special_case!( return SomeCase(typevar_call) elseif f === UnionAll && length(argtypes) == 3 && (argtypes[2] ⊑ TypeVar) unionall_call = Expr(:foreigncall, QuoteNode(:jl_type_unionall), Any, svec(Any, Any), - 0, QuoteNode(:ccall), stmt.args[2], stmt.args[3]) + 0, QuoteNode(:ccall), nothing, stmt.args[2], stmt.args[3]) return SomeCase(unionall_call) elseif is_return_type(f) if isconstType(type) diff --git a/base/compiler/ssair/passes.jl b/base/compiler/ssair/passes.jl index 67610f0c1df60..9c30a44c27cec 100644 --- a/base/compiler/ssair/passes.jl +++ b/base/compiler/ssair/passes.jl @@ -701,7 +701,7 @@ function sroa_pass!(ir::IRCode) nccallargs = length(stmt.args[3]::SimpleVector) preserved = Int[] new_preserves = Any[] - for pidx in (6+nccallargs):length(stmt.args) + for pidx in (7+nccallargs):length(stmt.args) preserved_arg = stmt.args[pidx] isa(preserved_arg, SSAValue) || continue let intermediaries = SPCSet() @@ -993,10 +993,10 @@ end function form_new_preserves(origex::Expr, intermediates::Vector{Int}, new_preserves::Vector{Any}) newex = Expr(:foreigncall) nccallargs = length(origex.args[3]::SimpleVector) - for i in 1:(6+nccallargs-1) + for i in 1:(7+nccallargs-1) push!(newex.args, origex.args[i]) end - for i in (6+nccallargs):length(origex.args) + for i in (7+nccallargs):length(origex.args) x = origex.args[i] # don't need to preserve intermediaries if isa(x, SSAValue) && x.id in intermediates diff --git a/base/compiler/ssair/show.jl b/base/compiler/ssair/show.jl index 9598d3e8cfa26..1e98dda039040 100644 --- a/base/compiler/ssair/show.jl +++ b/base/compiler/ssair/show.jl @@ -790,4 +790,19 @@ function show_ir(io::IO, code::Union{IRCode, CodeInfo}, config::IRShowConfig=def nothing end +tristate_letter(t::TriState) = t === ALWAYS_TRUE ? '+' : t === ALWAYS_FALSE ? '!' : '?' +tristate_color(t::TriState) = t === ALWAYS_TRUE ? :green : t === ALWAYS_FALSE ? :red : :orange + +function Base.show(io::IO, e::Core.Compiler.Effects) + print(io, "(") + printstyled(io, string(tristate_letter(e.consistent), 'c'); color=tristate_color(e.consistent)) + print(io, ',') + printstyled(io, string(tristate_letter(e.effect_free), 'e'); color=tristate_color(e.effect_free)) + print(io, ',') + printstyled(io, string(tristate_letter(e.nothrow), 'n'); color=tristate_color(e.nothrow)) + print(io, ',') + printstyled(io, string(tristate_letter(e.terminates), 't'); color=tristate_color(e.terminates)) + print(io, ')') +end + @specialize diff --git a/base/compiler/stmtinfo.jl b/base/compiler/stmtinfo.jl index ca8c7d0d27d56..2bb4298173fde 100644 --- a/base/compiler/stmtinfo.jl +++ b/base/compiler/stmtinfo.jl @@ -47,6 +47,12 @@ function nmatches(info::UnionSplitInfo) return n end +struct ConstResult + mi::MethodInstance + result + ConstResult(mi::MethodInstance, @nospecialize val) = new(mi, val) +end + """ info::ConstCallInfo @@ -56,7 +62,7 @@ the inference results with constant information `info.results::Vector{Union{Noth """ struct ConstCallInfo call::Union{MethodMatchInfo,UnionSplitInfo} - results::Vector{Union{Nothing,InferenceResult}} + results::Vector{Union{Nothing,InferenceResult,ConstResult,Bool}} end """ @@ -122,7 +128,7 @@ Optionally keeps `info.result::InferenceResult` that keeps constant information. """ struct InvokeCallInfo match::MethodMatch - result::Union{Nothing,InferenceResult} + result::Union{Nothing,InferenceResult,ConstResult,Bool} end """ @@ -134,7 +140,7 @@ Optionally keeps `info.result::InferenceResult` that keeps constant information. """ struct OpaqueClosureCallInfo match::MethodMatch - result::Union{Nothing,InferenceResult} + result::Union{Nothing,InferenceResult,ConstResult,Bool} end """ diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index d335995558d8f..4342bcbec6e57 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -1704,6 +1704,72 @@ function _builtin_nothrow(@nospecialize(f), argtypes::Array{Any,1}, @nospecializ return false end +# known to be always effect-free (in particular nothrow) +const _PURE_BUILTINS = Any[tuple, svec, ===, typeof, nfields] + +# known to be effect-free if the are nothrow +const _PURE_OR_ERROR_BUILTINS = [ + fieldtype, apply_type, isa, UnionAll, + getfield, arrayref, const_arrayref, isdefined, Core.sizeof, + Core.kwfunc, Core.ifelse, Core._typevar, (<:), + typeassert, throw +] + +const _IDEMPOTENT_BUILTINS = Any[ + tuple, # tuple is immutable, thus tuples of egal arguments are egal + ===, + typeof, + nfields, + fieldtype, + apply_type, + isa, + UnionAll, + isdefined, + Core.sizeof, + Core.kwfunc, + Core.ifelse, + Core._typevar, + (<:), + typeassert, + throw +] + +const _SPECIAL_BUILTINS = Any[ + Core._apply_iterate +] + +function builtin_effects(f::Builtin, argtypes::Vector{Any}, rt) + if isa(f, IntrinsicFunction) + return intrinsic_effects(f, argtypes) + end + + @assert !contains_is(_SPECIAL_BUILTINS, f) + + if (f === Core.getfield || f === Core.isdefined) && length(argtypes) >= 2 + # consistent if the argtype is immutable + if isvarargtype(argtypes[2]) + return Effects(TRISTATE_UNKNOWN, ALWAYS_TRUE, TRISTATE_UNKNOWN, ALWAYS_TRUE) + end + s = widenconst(argtypes[2]) + if isType(s) || !isa(s, DataType) || isabstracttype(s) + return Effects(TRISTATE_UNKNOWN, ALWAYS_TRUE, TRISTATE_UNKNOWN, ALWAYS_TRUE) + end + s = s::DataType + ipo_consistent = !ismutabletype(s) + else + ipo_consistent = contains_is(_IDEMPOTENT_BUILTINS, f) + end + effect_free = contains_is(_PURE_OR_ERROR_BUILTINS, f) || contains_is(_PURE_BUILTINS, f) + nothrow = isvarargtype(argtypes[end]) ? false : + builtin_nothrow(f, argtypes[2:end], rt) + + return Effects( + ipo_consistent ? ALWAYS_TRUE : ALWAYS_FALSE, + effect_free ? ALWAYS_TRUE : ALWAYS_FALSE, + nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + ALWAYS_TRUE) +end + function builtin_nothrow(@nospecialize(f), argtypes::Array{Any, 1}, @nospecialize(rt)) rt === Bottom && return false contains_is(_PURE_BUILTINS, f) && return true @@ -1846,6 +1912,46 @@ function intrinsic_nothrow(f::IntrinsicFunction, argtypes::Array{Any, 1}) return true end +# whether `f` is pure for inference +function is_pure_intrinsic_infer(f::IntrinsicFunction) + return !(f === Intrinsics.pointerref || # this one is volatile + f === Intrinsics.pointerset || # this one is never effect-free + f === Intrinsics.llvmcall || # this one is never effect-free + f === Intrinsics.arraylen || # this one is volatile + f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps) + f === Intrinsics.have_fma || # this one depends on the runtime environment + f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime +end + +# whether `f` is effect free if nothrow +intrinsic_effect_free_if_nothrow(f) = f === Intrinsics.pointerref || + f === Intrinsics.have_fma || is_pure_intrinsic_infer(f) + +function intrinsic_effects(f::IntrinsicFunction, argtypes::Vector{Any}) + if f === Intrinsics.llvmcall + # llvmcall can do arbitrary things + return Effects(TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, + TRISTATE_UNKNOWN, TRISTATE_UNKNOWN) + end + + ipo_consistent = !(f === Intrinsics.pointerref || # this one is volatile + f === Intrinsics.arraylen || # this one is volatile + f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps) + f === Intrinsics.have_fma || # this one depends on the runtime environment + f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime + + effect_free = !(f === Intrinsics.pointerset) + + nothrow = isvarargtype(argtypes[end]) ? false : + intrinsic_nothrow(f, argtypes[2:end]) + + return Effects( + ipo_consistent ? ALWAYS_TRUE : ALWAYS_FALSE, + effect_free ? ALWAYS_TRUE : ALWAYS_FALSE, + nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + ALWAYS_TRUE) +end + # TODO: this function is a very buggy and poor model of the return_type function # since abstract_call_gf_by_type is a very inaccurate model of _method and of typeinf_type, # while this assumes that it is an absolutely precise and accurate and exact model of both diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 749462b25fa0b..9f678dff64b49 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -278,7 +278,8 @@ function _typeinf(interp::AbstractInterpreter, frame::InferenceState) end function CodeInstance(result::InferenceResult, @nospecialize(inferred_result), - valid_worlds::WorldRange, relocatability::UInt8) + valid_worlds::WorldRange, effects::Effects, ipo_effects::Effects, + relocatability::UInt8) local const_flags::Int32 result_type = result.result @assert !(result_type isa LimitedAccuracy) @@ -310,7 +311,8 @@ function CodeInstance(result::InferenceResult, @nospecialize(inferred_result), end return CodeInstance(result.linfo, widenconst(result_type), rettype_const, inferred_result, - const_flags, first(valid_worlds), last(valid_worlds), relocatability) + const_flags, first(valid_worlds), last(valid_worlds), + encode_effects(effects), encode_effects(ipo_effects), relocatability) end # For the NativeInterpreter, we don't need to do an actual cache query to know @@ -385,7 +387,9 @@ function cache_result!(interp::AbstractInterpreter, result::InferenceResult) if !already_inferred inferred_result = transform_result_for_cache(interp, linfo, valid_worlds, result.src) relocatability = isa(inferred_result, Vector{UInt8}) ? inferred_result[end] : UInt8(0) - code_cache(interp)[linfo] = CodeInstance(result, inferred_result, valid_worlds, relocatability) + code_cache(interp)[linfo] = CodeInstance(result, inferred_result, valid_worlds, + # TODO: Actually do something with non-IPO effects + result.ipo_effects, result.ipo_effects, relocatability) end unlock_mi_inference(interp, linfo) nothing @@ -413,6 +417,17 @@ function cycle_fix_limited(@nospecialize(typ), sv::InferenceState) return typ end +function rt_adjust_effects(@nospecialize(rt), ipo_effects::Effects) + # Always throwing an error counts or never returning both count as consistent, + # but we don't currently model idempontency using dataflow, so we don't notice. + # Fix that up here to improve precision. + if rt === Union{} + return Effects(ALWAYS_TRUE, ipo_effects.effect_free, + ipo_effects.nothrow, ipo_effects.terminates) + end + return ipo_effects +end + # inference completed on `me` # update the MethodInstance function finish(me::InferenceState, interp::AbstractInterpreter) @@ -471,6 +486,7 @@ function finish(me::InferenceState, interp::AbstractInterpreter) end me.result.valid_worlds = me.valid_worlds me.result.result = me.bestguess + me.result.ipo_effects = rt_adjust_effects(me.bestguess, me.ipo_effects) validate_code_in_debug_mode(me.linfo, me.src, "inferred") nothing end @@ -769,6 +785,33 @@ end generating_sysimg() = ccall(:jl_generating_output, Cint, ()) != 0 && JLOptions().incremental == 0 +function tristate_merge(old, new) + (old === ALWAYS_FALSE || new === ALWAYS_FALSE) && return ALWAYS_FALSE + old === TRISTATE_UNKNOWN && return old + return new +end + +function tristate_merge(old::Effects, new::Effects) + Effects(tristate_merge( + old.consistent, new.consistent), + tristate_merge( + old.effect_free, new.effect_free), + tristate_merge( + old.nothrow, new.nothrow), + tristate_merge( + old.terminates, new.terminates)) +end + +function tristate_merge!(caller::InferenceState, callee::Effects) + caller.ipo_effects = tristate_merge(caller.ipo_effects, callee) +end + +function tristate_merge!(caller::InferenceState, callee::InferenceState) + tristate_merge!(caller, Effects(callee)) +end + +ipo_effects(code::CodeInstance) = decode_effects(code.ipo_purity_bits) + # compute (and cache) an inferred AST and return the current best estimate of the result type function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize(atype), sparams::SimpleVector, caller::InferenceState) mi = specialize_method(method, atype, sparams)::MethodInstance @@ -779,6 +822,7 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize # but the inlinear will request to use it, we re-infer it here and keep it around in the local cache cache = :local else + effects = ipo_effects(code) update_valid_age!(caller, WorldRange(min_world(code), max_world(code))) rettype = code.rettype if isdefined(code, :rettype_const) @@ -786,23 +830,23 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize # the second subtyping conditions are necessary to distinguish usual cases # from rare cases when `Const` wrapped those extended lattice type objects if isa(rettype_const, Vector{Any}) && !(Vector{Any} <: rettype) - return PartialStruct(rettype, rettype_const), mi + return PartialStruct(rettype, rettype_const), mi, effects elseif isa(rettype_const, PartialOpaque) && rettype <: Core.OpaqueClosure - return rettype_const, mi + return rettype_const, mi, effects elseif isa(rettype_const, InterConditional) && !(InterConditional <: rettype) - return rettype_const, mi + return rettype_const, mi, effects else - return Const(rettype_const), mi + return Const(rettype_const), mi, effects end else - return rettype, mi + return rettype, mi, effects end end else cache = :global # cache edge targets by default end if ccall(:jl_get_module_infer, Cint, (Any,), method.module) == 0 && !generating_sysimg() - return Any, nothing + return Any, nothing, Effects() end if !caller.cached && caller.parent === nothing # this caller exists to return to the user @@ -819,22 +863,22 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize if frame === nothing # can't get the source for this, so we know nothing unlock_mi_inference(interp, mi) - return Any, nothing + return Any, nothing, Effects() end if caller.cached || caller.parent !== nothing # don't involve uncached functions in cycle resolution frame.parent = caller end typeinf(interp, frame) update_valid_age!(frame, caller) - return frame.bestguess, frame.inferred ? mi : nothing + return frame.bestguess, frame.inferred ? mi : nothing, rt_adjust_effects(frame.bestguess, Effects(frame)) elseif frame === true # unresolvable cycle - return Any, nothing + return Any, nothing, Effects() end # return the current knowledge about this cycle frame = frame::InferenceState update_valid_age!(frame, caller) - return frame.bestguess, nothing + return frame.bestguess, nothing, rt_adjust_effects(frame.bestguess, Effects(frame)) end #### entry points for inferring a MethodInstance given a type signature #### diff --git a/base/compiler/types.jl b/base/compiler/types.jl index e5894ab3d3f89..dc9006762d57c 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -22,6 +22,67 @@ struct ArgInfo argtypes::Vector{Any} end +struct TriState; state::UInt8; end +const ALWAYS_FALSE = TriState(0x00) +const ALWAYS_TRUE = TriState(0x01) +const TRISTATE_UNKNOWN = TriState(0x02) + +struct Effects + consistent::TriState + effect_free::TriState + nothrow::TriState + terminates::TriState +end +Effects() = Effects(TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, TRISTATE_UNKNOWN) + +is_total_or_error(effects::Effects) = + effects.consistent === ALWAYS_TRUE && effects.effect_free === ALWAYS_TRUE && + effects.terminates === ALWAYS_TRUE + +is_total(effects::Effects) = + is_total_or_error(effects) && effects.nothrow === ALWAYS_TRUE + +is_removable_if_unused(effects::Effects) = + effects.effect_free === ALWAYS_TRUE && + effects.terminates === ALWAYS_TRUE && + effects.nothrow === ALWAYS_TRUE + +const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE) + +encode_effects(e::Effects) = e.consistent.state | (e.effect_free.state << 2) | (e.nothrow.state << 4) | (e.terminates.state << 6) +decode_effects(e::UInt8) = + Effects(TriState(e & 0x3), + TriState((e >> 2) & 0x3), + TriState((e >> 4) & 0x3), + TriState((e >> 6) & 0x3)) + +struct EffectsOverride + consistent::Bool + effect_free::Bool + nothrow::Bool + terminates::Bool + terminates_locally::Bool +end + +function encode_effects_override(eo::EffectsOverride) + e = 0x00 + eo.consistent && (e |= 0x01) + eo.effect_free && (e |= 0x02) + eo.nothrow && (e |= 0x02) + eo.terminates && (e |= 0x04) + eo.terminates_locally && (e |= 0x08) + e +end + +decode_effects_override(e::UInt8) = + EffectsOverride( + e & 0x01 != 0x00, + (e >> 1) & 0x01 != 0x00, + (e >> 2) & 0x01 != 0x00, + (e >> 3) & 0x01 != 0x00, + (e >> 4) & 0x01 != 0x00) + + """ InferenceResult @@ -34,11 +95,13 @@ mutable struct InferenceResult result # ::Type, or InferenceState if WIP src #::Union{CodeInfo, OptimizationState, Nothing} # if inferred copy is available valid_worlds::WorldRange # if inference and optimization is finished + ipo_effects::Effects # if inference is finished + effects::Effects # if optimization is finished function InferenceResult(linfo::MethodInstance, arginfo#=::Union{Nothing,Tuple{ArgInfo,InferenceState}}=# = nothing, va_override::Bool = false) argtypes, overridden_by_const = matching_cache_argtypes(linfo, arginfo, va_override) - return new(linfo, argtypes, overridden_by_const, Any, nothing, WorldRange()) + return new(linfo, argtypes, overridden_by_const, Any, nothing, WorldRange(), Effects(), Effects()) end end diff --git a/base/compiler/validation.jl b/base/compiler/validation.jl index bcde5d894159c..fb4cf1f820f1c 100644 --- a/base/compiler/validation.jl +++ b/base/compiler/validation.jl @@ -23,7 +23,7 @@ const VALID_EXPR_HEADS = IdDict{Symbol,UnitRange{Int}}( :copyast => 1:1, :meta => 0:typemax(Int), :global => 1:1, - :foreigncall => 5:typemax(Int), # name, RT, AT, nreq, cconv, args..., roots... + :foreigncall => 6:typemax(Int), # name, RT, AT, nreq, cconv, effects, args..., roots... :cfunction => 5:5, :isdefined => 1:1, :code_coverage_effect => 0:0, diff --git a/base/expr.jl b/base/expr.jl index 7719eff3b334b..f3a694960aa3e 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -371,6 +371,160 @@ macro constprop(setting, ex) throw(ArgumentError("@constprop $setting not supported")) end +""" + @assume_effects setting... ex + @assume_effects(setting..., ex) + +`@assume_effects` overrides the compiler's effect modeling for the given method. +`ex` must be a method definition. + +WARNING: Improper use of this macro causes undefined behavior (including crashes, +incorrect answers, or other hard to track bugs). Use with care and only if absolutely +required. + +In general, each `setting` value makes an assertion about the behavior of the +function, without requiring the compiler to prove that this behavior is indeed +true. These assertions are made for all world ages. It is thus advisable to limit +the use of generic functions that may later be extended to invalidate the +assumption (which would cause undefined behavior). + +The following `settings` are supported. +# `:consistent` + +The `:consistent` setting asserts that for egal inputs: + - The manner of termination (return value, exception, non-termination) will always be the same. + - If the method returns, the results will always be egal. + +Note: This in particular implies that the return value of the method must be + immutable. Multiple allocations of mutable objects (even with identical + contents) are not egal. + +Note: The :consistent-cy assertion is made world-arge wise. More formally, write + fᵢ for the evaluation of `f` in world-age `i`, then we require: + + ∀ i, x, y: x === y → fᵢ(x) === fᵢ(y) + + However, for two world ages `i, j` s.t. `i != j`, we may have `fᵢ(x) !== fⱼ(y)`. + +Note: A further implication is that :consistent functions may not make their + return value dependent on the state of the heap or any other global state + that is not constant for a given world age. + +Note: The :consistent-cy includes all legal rewrites performed by the optimizizer. + For example, floating-point fastmath operations are not considered :consistent, + because the optimizer may rewrite them causing the output to not be :consistent, + even for the same world age (e.g. because one ran in the interpreter, while + the other was optimized). + +Note: If :consistent functions terminate by throwing an exception, that exception + itself is not required to meet the egality requirement specified above. + +# `:effect_free` + +The `:effect_free` setting asserts that the method is free of externally semantically +visible side effects. The following is an incomplete list of externally semantically +visible side effects: + + - Changing the value of a global variable. + - Mutating the heap (e.g. an array or mutable value), except as noted below + - Changing the method table (e.g. through calls to eval) + - File/Network/etc. I/O + - Task switching + +However, the following are explicitly not semantically visible, even if they +may be observable: + + - Memory allocations (both mutable and immutable) + - Elapsed time + - Garbage collection + - Heap mutations of objects whose lifetime does not exceed the method (i.e. + were allocated in the method and do not escape). + - The returned value (which is externally visible, but not a side effect) + +The rule of thumb here is that an externally visible side effect is anything +that would affect the execution of the remainder of the program if the function +were not executed. + +Note: The effect free assertion is made both for the method itself and any code + that is executed by the method. Keep in mind that the assertion must be + valid for all world ages and limit use of this assertion accordingly. + +# `:nothrow` + +The `:nothrow` settings asserts that this method does not terminate abnormally +(i.e. will either always return a value or never return). + +Note: It is permissible for :nothrow annotated methods to make use of exception + handling internally as long as the exception is not rethrown out of the + method itself. + +Note: MethodErrors and similar exceptions count as abnormal termination. + +# `:terminates_globally` + +The `:terminates_globally` settings asserts that this method will eventually terminate +(either normally or abnormally), i.e. does not loop indefinitely. + +Note: The compiler will consider this a strong indication that the method will + terminate relatively *quickly* and may (if otherwise legal), call this + method at compile time. I.e. it is a bad idea to annotate this setting + on a method that *technically*, but not *practically*, terminates. + +Note: The `terminates_globally` assertion covers any other methods called by + the annotated method. + +# `:terminates_locally` + +The `:terminates_locally` setting is like `:terminates_globally`, except that it only +applies to syntactic control flow *within* the annotated method. It is thus +a much weaker (and thus safer) assertion that allows for the possibility of +non-termination if the method calls some other method that does not terminate. + +Note: `terminates_globally` implies `terminates_locally`. + +# `:total` + +The `setting` combines the following other assertions: + - `:consistent` + - `:effect_free` + - `:nothrow` + - `:terminates_globally` +and is a convenient shortcut. + +Note: `@assume_effects :total` is similar to `@Base.pure` 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 + `@Base.pure` is always total. +""" +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 + if setting === :consistent + consistent = true + elseif setting === :effect_free + effect_free = true + elseif setting === :nothrow + nothrow = true + elseif setting === :terminates_globally + terminates_globally = true + elseif setting === :terminates_locally + terminates_locally = true + elseif setting === :total + consistent = effect_free = nothrow = terminates_globally = true + else + throw(ArgumentError("@assume_effects $setting not supported")) + end + end + ex = args[end] + isa(ex, Expr) || ArgumentError("Bad expression `$ex` in @constprop [settings] ex") + return pushmeta!(ex, :purity, consistent, effect_free, nothrow, terminates_globally, terminates_locally) +end + + """ @propagate_inbounds diff --git a/base/floatfuncs.jl b/base/floatfuncs.jl index 3963927f7e717..d1164005d3e44 100644 --- a/base/floatfuncs.jl +++ b/base/floatfuncs.jl @@ -419,8 +419,8 @@ fma_llvm(x::Float64, y::Float64, z::Float64) = fma_float(x, y, z) # Disable LLVM's fma if it is incorrect, e.g. because LLVM falls back # onto a broken system libm; if so, use a software emulated fma -fma(x::Float32, y::Float32, z::Float32) = Core.Intrinsics.have_fma(Float32) ? fma_llvm(x,y,z) : fma_emulated(x,y,z) -fma(x::Float64, y::Float64, z::Float64) = Core.Intrinsics.have_fma(Float64) ? fma_llvm(x,y,z) : fma_emulated(x,y,z) +@assume_effects :consistent fma(x::Float32, y::Float32, z::Float32) = Core.Intrinsics.have_fma(Float32) ? fma_llvm(x,y,z) : fma_emulated(x,y,z) +@assume_effects :consistent fma(x::Float64, y::Float64, z::Float64) = Core.Intrinsics.have_fma(Float64) ? fma_llvm(x,y,z) : fma_emulated(x,y,z) function fma(a::Float16, b::Float16, c::Float16) Float16(muladd(Float32(a), Float32(b), Float32(c))) #don't use fma if the hardware doesn't have it. diff --git a/base/meta.jl b/base/meta.jl index 2ba9baec9a443..6c8f28beb87ff 100644 --- a/base/meta.jl +++ b/base/meta.jl @@ -394,6 +394,8 @@ function _partially_inline!(@nospecialize(x), slot_replacements::Vector{Any}, @assert isa(x.args[4], Int) elseif i == 5 @assert isa((x.args[5]::QuoteNode).value, Symbol) + elseif i == 6 + @assert isa(x.args[6], Union{Nothing, UInt8}) else x.args[i] = _partially_inline!(x.args[i], slot_replacements, type_signature, static_param_values, diff --git a/base/show.jl b/base/show.jl index 14a7d6f68cf98..d4bc9a26423ed 100644 --- a/base/show.jl +++ b/base/show.jl @@ -2486,7 +2486,9 @@ module IRShow const Compiler = Core.Compiler using Core.IR import ..Base - import .Compiler: IRCode, ReturnNode, GotoIfNot, CFG, scan_ssa_use!, Argument, isexpr, compute_basic_blocks, block_for_inst + import .Compiler: IRCode, ReturnNode, GotoIfNot, CFG, scan_ssa_use!, Argument, + isexpr, compute_basic_blocks, block_for_inst, + TriState, Effects, ALWAYS_TRUE, ALWAYS_FALSE Base.getindex(r::Compiler.StmtRange, ind::Integer) = Compiler.getindex(r, ind) Base.size(r::Compiler.StmtRange) = Compiler.size(r) Base.first(r::Compiler.StmtRange) = Compiler.first(r) diff --git a/base/strings/string.jl b/base/strings/string.jl index c818e2e1844fb..b7d93ed3ffcf5 100644 --- a/base/strings/string.jl +++ b/base/strings/string.jl @@ -71,7 +71,9 @@ function unsafe_string(p::Union{Ptr{UInt8},Ptr{Int8}}) ccall(:jl_cstr_to_string, Ref{String}, (Ptr{UInt8},), p) end -_string_n(n::Integer) = ccall(:jl_alloc_string, Ref{String}, (Csize_t,), n) +# This is @assume_effects :effect_free :nothrow :terminates_globally @ccall jl_alloc_string(n::Csize_t)::Ref{String}, +# but the macro is not available at this time in bootstrap, so we write it manually. +@eval _string_n(n::Integer) = $(Expr(:foreigncall, QuoteNode(:jl_alloc_string), Ref{String}, Expr(:call, Expr(:core, :svec), :Csize_t), 1, QuoteNode(:ccall), 0xe, :(convert(Csize_t, n)))) """ String(s::AbstractString) diff --git a/doc/src/devdocs/llvm.md b/doc/src/devdocs/llvm.md index 1e983949ea0b6..4cf30446dabae 100644 --- a/doc/src/devdocs/llvm.md +++ b/doc/src/devdocs/llvm.md @@ -271,7 +271,7 @@ need to make sure that the array does stay alive while we're doing the [`ccall`](@ref). To understand how this is done, first recall the lowering of the above code: ```julia -return $(Expr(:foreigncall, :(:foo), Cvoid, svec(Ptr{Float64}), 0, :(:ccall), Expr(:foreigncall, :(:jl_array_ptr), Ptr{Float64}, svec(Any), 0, :(:ccall), :(A)), :(A))) +return $(Expr(:foreigncall, :(:foo), Cvoid, svec(Ptr{Float64}), 0, :(:ccall), nothing, Expr(:foreigncall, :(:jl_array_ptr), Ptr{Float64}, svec(Any), 0, :(:ccall), nothing, :(A)), :(A))) ``` The last `:(A)`, is an extra argument list inserted during lowering that informs the code generator which Julia level values need to be kept alive for the diff --git a/src/ast.c b/src/ast.c index ef8fff7e39e7a..5dfd2107d6e3e 100644 --- a/src/ast.c +++ b/src/ast.c @@ -82,6 +82,7 @@ JL_DLLEXPORT jl_sym_t *jl_propagate_inbounds_sym; JL_DLLEXPORT jl_sym_t *jl_specialize_sym; JL_DLLEXPORT jl_sym_t *jl_aggressive_constprop_sym; JL_DLLEXPORT jl_sym_t *jl_no_constprop_sym; +JL_DLLEXPORT jl_sym_t *jl_purity_sym; JL_DLLEXPORT jl_sym_t *jl_nospecialize_sym; JL_DLLEXPORT jl_sym_t *jl_macrocall_sym; JL_DLLEXPORT jl_sym_t *jl_colon_sym; @@ -330,6 +331,7 @@ void jl_init_common_symbols(void) jl_propagate_inbounds_sym = jl_symbol("propagate_inbounds"); jl_aggressive_constprop_sym = jl_symbol("aggressive_constprop"); jl_no_constprop_sym = jl_symbol("no_constprop"); + jl_purity_sym = jl_symbol("purity"); jl_isdefined_sym = jl_symbol("isdefined"); jl_nospecialize_sym = jl_symbol("nospecialize"); jl_specialize_sym = jl_symbol("specialize"); diff --git a/src/ccall.cpp b/src/ccall.cpp index 8f153279f23cc..792b379d6bbf2 100644 --- a/src/ccall.cpp +++ b/src/ccall.cpp @@ -1240,7 +1240,9 @@ static const std::string verify_ccall_sig(jl_value_t *&rt, jl_value_t *at, return ""; } -// Expr(:foreigncall, pointer, rettype, (argtypes...), nreq, cconv, args..., roots...) +const int fc_args_start = 7; + +// Expr(:foreigncall, pointer, rettype, (argtypes...), nreq, cconv, effects, args..., roots...) static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs) { JL_NARGSV(ccall, 5); @@ -1252,6 +1254,9 @@ static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs) assert(jl_is_quotenode(args[5])); jl_sym_t *cc_sym = *(jl_sym_t**)args[5]; assert(jl_is_symbol(cc_sym)); + jl_value_t *effects = args[6]; + assert(jl_is_nothing(effects) || jl_is_uint8(effects)); + (void)effects; // currently only used by inference native_sym_arg_t symarg = {}; JL_GC_PUSH3(&rt, &at, &symarg.gcroot); @@ -1272,8 +1277,8 @@ static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs) } auto ccallarg = [=] (size_t i) { - assert(i < nccallargs && i + 6 <= nargs); - return args[6 + i]; + assert(i < nccallargs && i + fc_args_start <= nargs); + return args[fc_args_start + i]; }; auto _is_libjulia_func = [&] (uintptr_t ptr, StringRef name) { @@ -1307,7 +1312,7 @@ static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs) // emit roots SmallVector gc_uses; - for (size_t i = nccallargs + 6; i <= nargs; i++) { + for (size_t i = nccallargs + fc_args_start; i <= nargs; i++) { // Julia (expression) value of current parameter gcroot jl_value_t *argi_root = args[i]; if (jl_is_long(argi_root)) diff --git a/src/dump.c b/src/dump.c index 4a448bcb23376..168034d89236d 100644 --- a/src/dump.c +++ b/src/dump.c @@ -517,6 +517,8 @@ static void jl_serialize_code_instance(jl_serializer_state *s, jl_code_instance_ write_uint8(s->s, TAG_CODE_INSTANCE); write_uint8(s->s, flags); + write_uint8(s->s, codeinst->ipo_purity_bits); + write_uint8(s->s, codeinst->purity_bits); jl_serialize_value(s, (jl_value_t*)codeinst->def); if (write_ret_type) { jl_serialize_value(s, codeinst->inferred); @@ -704,6 +706,7 @@ static void jl_serialize_value_(jl_serializer_state *s, jl_value_t *v, int as_li write_int8(s->s, m->pure); write_int8(s->s, m->is_for_opaque_closure); write_int8(s->s, m->constprop); + write_uint8(s->s, m->purity.bits); jl_serialize_value(s, (jl_value_t*)m->slot_syms); jl_serialize_value(s, (jl_value_t*)m->roots); jl_serialize_value(s, (jl_value_t*)m->root_blocks); @@ -1572,6 +1575,7 @@ static jl_value_t *jl_deserialize_value_method(jl_serializer_state *s, jl_value_ m->pure = read_int8(s->s); m->is_for_opaque_closure = read_int8(s->s); m->constprop = read_int8(s->s); + m->purity.bits = read_uint8(s->s); m->slot_syms = jl_deserialize_value(s, (jl_value_t**)&m->slot_syms); jl_gc_wb(m, m->slot_syms); m->roots = (jl_array_t*)jl_deserialize_value(s, (jl_value_t**)&m->roots); @@ -1652,6 +1656,8 @@ static jl_value_t *jl_deserialize_value_code_instance(jl_serializer_state *s, jl int flags = read_uint8(s->s); int validate = (flags >> 0) & 3; int constret = (flags >> 2) & 1; + codeinst->ipo_purity_bits = read_uint8(s->s); + codeinst->purity_bits = read_uint8(s->s); codeinst->def = (jl_method_instance_t*)jl_deserialize_value(s, (jl_value_t**)&codeinst->def); jl_gc_wb(codeinst, codeinst->def); codeinst->inferred = jl_deserialize_value(s, &codeinst->inferred); diff --git a/src/gf.c b/src/gf.c index d4a5df81a8d1a..7c42a9b802df3 100644 --- a/src/gf.c +++ b/src/gf.c @@ -206,7 +206,8 @@ JL_DLLEXPORT jl_value_t *jl_methtable_lookup(jl_methtable_t *mt, jl_value_t *typ JL_DLLEXPORT jl_code_instance_t* jl_new_codeinst( jl_method_instance_t *mi, jl_value_t *rettype, jl_value_t *inferred_const, jl_value_t *inferred, - int32_t const_flags, size_t min_world, size_t max_world, uint8_t relocatability); + int32_t const_flags, size_t min_world, size_t max_world, + uint8_t ipo_effects, uint8_t effects, uint8_t relocatability); JL_DLLEXPORT void jl_mi_cache_insert(jl_method_instance_t *mi JL_ROOTING_ARGUMENT, jl_code_instance_t *ci JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED); @@ -243,7 +244,7 @@ jl_datatype_t *jl_mk_builtin_func(jl_datatype_t *dt, const char *name, jl_fptr_a jl_code_instance_t *codeinst = jl_new_codeinst(mi, (jl_value_t*)jl_any_type, jl_nothing, jl_nothing, - 0, 1, ~(size_t)0, 0); + 0, 1, ~(size_t)0, 0, 0, 0); jl_mi_cache_insert(mi, codeinst); codeinst->specptr.fptr1 = fptr; codeinst->invoke = jl_fptr_args; @@ -366,7 +367,7 @@ JL_DLLEXPORT jl_code_instance_t *jl_get_method_inferred( } codeinst = jl_new_codeinst( mi, rettype, NULL, NULL, - 0, min_world, max_world, 0); + 0, min_world, max_world, 0, 0, 0); jl_mi_cache_insert(mi, codeinst); return codeinst; } @@ -374,7 +375,8 @@ JL_DLLEXPORT jl_code_instance_t *jl_get_method_inferred( JL_DLLEXPORT jl_code_instance_t *jl_new_codeinst( jl_method_instance_t *mi, jl_value_t *rettype, jl_value_t *inferred_const, jl_value_t *inferred, - int32_t const_flags, size_t min_world, size_t max_world, uint8_t relocatability + int32_t const_flags, size_t min_world, size_t max_world, + uint8_t ipo_effects, uint8_t effects, uint8_t relocatability /*, jl_array_t *edges, int absolute_max*/) { jl_task_t *ct = jl_current_task; @@ -400,6 +402,8 @@ JL_DLLEXPORT jl_code_instance_t *jl_new_codeinst( codeinst->precompile = 0; codeinst->next = NULL; codeinst->relocatability = relocatability; + codeinst->ipo_purity_bits = ipo_effects; + codeinst->purity_bits = effects; return codeinst; } @@ -2009,7 +2013,7 @@ jl_code_instance_t *jl_compile_method_internal(jl_method_instance_t *mi, size_t if (unspec && jl_atomic_load_relaxed(&unspec->invoke)) { jl_code_instance_t *codeinst = jl_new_codeinst(mi, (jl_value_t*)jl_any_type, NULL, NULL, - 0, 1, ~(size_t)0, 0); + 0, 1, ~(size_t)0, 0, 0, 0); codeinst->isspecsig = 0; codeinst->specptr = unspec->specptr; codeinst->rettype_const = unspec->rettype_const; @@ -2027,7 +2031,7 @@ jl_code_instance_t *jl_compile_method_internal(jl_method_instance_t *mi, size_t if (!jl_code_requires_compiler(src)) { jl_code_instance_t *codeinst = jl_new_codeinst(mi, (jl_value_t*)jl_any_type, NULL, NULL, - 0, 1, ~(size_t)0, 0); + 0, 1, ~(size_t)0, 0, 0, 0); codeinst->invoke = jl_fptr_interpret_call; jl_mi_cache_insert(mi, codeinst); record_precompile_statement(mi); @@ -2062,7 +2066,7 @@ jl_code_instance_t *jl_compile_method_internal(jl_method_instance_t *mi, size_t return ucache; } codeinst = jl_new_codeinst(mi, (jl_value_t*)jl_any_type, NULL, NULL, - 0, 1, ~(size_t)0, 0); + 0, 1, ~(size_t)0, 0, 0, 0); codeinst->isspecsig = 0; codeinst->specptr = ucache->specptr; codeinst->rettype_const = ucache->rettype_const; diff --git a/src/ircode.c b/src/ircode.c index 5be83ed4caac3..73e99f2281491 100644 --- a/src/ircode.c +++ b/src/ircode.c @@ -731,6 +731,7 @@ JL_DLLEXPORT jl_array_t *jl_compress_ir(jl_method_t *m, jl_code_info_t *code) jl_code_info_flags_t flags = code_info_flags(code->pure, code->propagate_inbounds, code->inlineable, code->inferred, code->constprop); write_uint8(s.s, flags.packed); + write_uint8(s.s, code->purity.bits); size_t nslots = jl_array_len(code->slotflags); assert(nslots >= m->nargs && nslots < INT32_MAX); // required by generated functions @@ -820,6 +821,7 @@ JL_DLLEXPORT jl_code_info_t *jl_uncompress_ir(jl_method_t *m, jl_code_instance_t code->inlineable = flags.bits.inlineable; code->propagate_inbounds = flags.bits.propagate_inbounds; code->pure = flags.bits.pure; + code->purity.bits = read_uint8(s.s); size_t nslots = read_int32(&src); code->slotflags = jl_alloc_array_1d(jl_array_uint8_type, nslots); @@ -935,7 +937,7 @@ JL_DLLEXPORT ssize_t jl_ir_nslots(jl_array_t *data) } else { assert(jl_typeis(data, jl_array_uint8_type)); - int nslots = jl_load_unaligned_i32((char*)data->data + 1); + int nslots = jl_load_unaligned_i32((char*)data->data + 2); return nslots; } } @@ -946,7 +948,7 @@ JL_DLLEXPORT uint8_t jl_ir_slotflag(jl_array_t *data, size_t i) if (jl_is_code_info(data)) return ((uint8_t*)((jl_code_info_t*)data)->slotflags->data)[i]; assert(jl_typeis(data, jl_array_uint8_type)); - return ((uint8_t*)data->data)[1 + sizeof(int32_t) + i]; + return ((uint8_t*)data->data)[2 + sizeof(int32_t) + i]; } JL_DLLEXPORT jl_array_t *jl_uncompress_argnames(jl_value_t *syms) diff --git a/src/jltypes.c b/src/jltypes.c index 8e25ff9fa9a5f..c37c474c4ce4a 100644 --- a/src/jltypes.c +++ b/src/jltypes.c @@ -2347,7 +2347,7 @@ void jl_init_types(void) JL_GC_DISABLED jl_code_info_type = jl_new_datatype(jl_symbol("CodeInfo"), core, jl_any_type, jl_emptysvec, - jl_perm_symsvec(19, + jl_perm_symsvec(20, "code", "codelocs", "ssavaluetypes", @@ -2366,8 +2366,9 @@ void jl_init_types(void) JL_GC_DISABLED "inlineable", "propagate_inbounds", "pure", - "constprop"), - jl_svec(19, + "constprop", + "purity"), + jl_svec(20, jl_array_any_type, jl_array_int32_type, jl_any_type, @@ -2386,14 +2387,15 @@ void jl_init_types(void) JL_GC_DISABLED jl_bool_type, jl_bool_type, jl_bool_type, + jl_uint8_type, jl_uint8_type), jl_emptysvec, - 0, 1, 19); + 0, 1, 20); jl_method_type = jl_new_datatype(jl_symbol("Method"), core, jl_any_type, jl_emptysvec, - jl_perm_symsvec(28, + jl_perm_symsvec(29, "name", "module", "file", @@ -2421,8 +2423,9 @@ void jl_init_types(void) JL_GC_DISABLED "isva", "pure", "is_for_opaque_closure", - "constprop"), - jl_svec(28, + "constprop", + "purity"), + jl_svec(29, jl_symbol_type, jl_module_type, jl_symbol_type, @@ -2450,6 +2453,7 @@ void jl_init_types(void) JL_GC_DISABLED jl_bool_type, jl_bool_type, jl_bool_type, + jl_uint8_type, jl_uint8_type), jl_emptysvec, 0, 1, 10); @@ -2485,7 +2489,7 @@ void jl_init_types(void) JL_GC_DISABLED jl_code_instance_type = jl_new_datatype(jl_symbol("CodeInstance"), core, jl_any_type, jl_emptysvec, - jl_perm_symsvec(12, + jl_perm_symsvec(14, "def", "next", "min_world", @@ -2495,9 +2499,10 @@ void jl_init_types(void) JL_GC_DISABLED "inferred", //"edges", //"absolute_max", + "ipo_purity_bits", "purity_bits", "isspecsig", "precompile", "invoke", "specptr", // function object decls "relocatability"), - jl_svec(12, + jl_svec(14, jl_method_instance_type, jl_any_type, jl_ulong_type, @@ -2507,6 +2512,7 @@ void jl_init_types(void) JL_GC_DISABLED jl_any_type, //jl_any_type, //jl_bool_type, + jl_uint8_type, jl_uint8_type, jl_bool_type, jl_bool_type, jl_any_type, jl_any_type, // fptrs @@ -2659,8 +2665,8 @@ void jl_init_types(void) JL_GC_DISABLED jl_svecset(jl_methtable_type->types, 11, jl_uint8_type); jl_svecset(jl_method_type->types, 12, jl_method_instance_type); jl_svecset(jl_method_instance_type->types, 6, jl_code_instance_type); - jl_svecset(jl_code_instance_type->types, 9, jl_voidpointer_type); - jl_svecset(jl_code_instance_type->types, 10, jl_voidpointer_type); + jl_svecset(jl_code_instance_type->types, 11, jl_voidpointer_type); + jl_svecset(jl_code_instance_type->types, 12, jl_voidpointer_type); jl_compute_field_offsets(jl_datatype_type); jl_compute_field_offsets(jl_typename_type); diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 07753233e204e..2d862bd7572ff 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -1062,6 +1062,7 @@ (foreigncall ,name ,RT (call (core svec) ,@(reverse! T)) ,(if isseq (- (length atypes) 1) 0) ; 0 or number of arguments before ... in definition ',cconv + (null) ,.(reverse! C) ,@GC)) ; GC root ordering is arbitrary (let* ((a (car A)) @@ -4325,13 +4326,13 @@ f(x) = yt(x) (not (tuple-call? fptr)))) (let* ((args (cond ((eq? (car e) 'foreigncall) - ;; NOTE: 2nd to 5th arguments of ccall must be left in place + ;; NOTE: 2nd to 6th arguments of ccall must be left in place ;; the 1st should be compiled if an atom. (append (if (atom-or-not-tuple-call? (cadr e)) (compile-args (list (cadr e)) break-labels) (list (cadr e))) - (list-head (cddr e) 4) - (compile-args (list-tail e 6) break-labels))) + (list-head (cddr e) 5) + (compile-args (list-tail e 7) break-labels))) ;; NOTE: arguments of cfunction must be left in place ;; except for argument 2 (fptr) ((eq? (car e) 'cfunction) diff --git a/src/julia.h b/src/julia.h index ff0542307c1cf..e16e852562897 100644 --- a/src/julia.h +++ b/src/julia.h @@ -232,6 +232,21 @@ typedef struct _jl_line_info_node_t { intptr_t inlined_at; } jl_line_info_node_t; +typedef union { + struct { + uint8_t ipo_consistent : 1; + uint8_t ipo_effect_free : 1; + uint8_t ipo_nothrow : 1; + uint8_t ipo_terminates : 1; + // Weaker form of `terminates` that asserts + // that any control flow syntactically in the method + // is guaranteed to terminate, but does not make + // assertions about any called functions. + uint8_t ipo_terminates_locally : 1; + } overrides; + uint8_t bits; +} _jl_purity_overrides_t; + // This type describes a single function body typedef struct _jl_code_info_t { // ssavalue-indexed arrays of properties: @@ -265,6 +280,7 @@ typedef struct _jl_code_info_t { uint8_t pure; // uint8 settings uint8_t constprop; // 0 = use heuristic; 1 = aggressive; 2 = none + _jl_purity_overrides_t purity; } jl_code_info_t; // This type describes a single method definition, and stores data @@ -319,6 +335,10 @@ typedef struct _jl_method_t { // uint8 settings uint8_t constprop; // 0x00 = use heuristic; 0x01 = aggressive; 0x02 = none + // Override the conclusions of inter-procedural effect analysis, + // forcing the conclusion to always true. + _jl_purity_overrides_t purity; + // hidden fields: // lock for modifications to the method jl_mutex_t writelock; @@ -371,6 +391,26 @@ typedef struct _jl_code_instance_t { //TODO: jl_array_t *edges; // stored information about edges from this object //TODO: uint8_t absolute_max; // whether true max world is unknown + // purity results + union { + uint8_t ipo_purity_bits; + struct { + uint8_t ipo_consistent:2; + uint8_t ipo_effect_free:2; + uint8_t ipo_nothrow:2; + uint8_t ipo_terminates:2; + } ipo_purity_flags; + }; + union { + uint8_t purity_bits; + struct { + uint8_t consistent:2; + uint8_t effect_free:2; + uint8_t nothrow:2; + uint8_t terminates:2; + } purity_flags; + }; + // compilation state cache uint8_t isspecsig; // if specptr is a specialized function signature for specTypes->rettype _Atomic(uint8_t) precompile; // if set, this will be added to the output system image diff --git a/src/julia_internal.h b/src/julia_internal.h index 5b7b81e7a66c5..c7454e2d5a904 100644 --- a/src/julia_internal.h +++ b/src/julia_internal.h @@ -1433,6 +1433,7 @@ extern JL_DLLEXPORT jl_sym_t *jl_propagate_inbounds_sym; extern JL_DLLEXPORT jl_sym_t *jl_specialize_sym; extern JL_DLLEXPORT jl_sym_t *jl_aggressive_constprop_sym; extern JL_DLLEXPORT jl_sym_t *jl_no_constprop_sym; +extern JL_DLLEXPORT jl_sym_t *jl_purity_sym; extern JL_DLLEXPORT jl_sym_t *jl_nospecialize_sym; extern JL_DLLEXPORT jl_sym_t *jl_macrocall_sym; extern JL_DLLEXPORT jl_sym_t *jl_colon_sym; diff --git a/src/method.c b/src/method.c index 0b615e7e46dd5..42dbf1478c568 100644 --- a/src/method.c +++ b/src/method.c @@ -146,7 +146,7 @@ static jl_value_t *resolve_globals(jl_value_t *expr, jl_module_t *module, jl_sve return expr; } if (e->head == jl_foreigncall_sym) { - JL_NARGSV(ccall method definition, 5); // (fptr, rt, at, cc, narg) + JL_NARGSV(ccall method definition, 6); // (fptr, rt, at, nreq, cc, effects) jl_value_t *rt = jl_exprarg(e, 1); jl_value_t *at = jl_exprarg(e, 2); if (!jl_is_type(rt)) { @@ -177,6 +177,9 @@ static jl_value_t *resolve_globals(jl_value_t *expr, jl_module_t *module, jl_sve JL_TYPECHK(ccall method definition, long, jl_exprarg(e, 3)); JL_TYPECHK(ccall method definition, quotenode, jl_exprarg(e, 4)); JL_TYPECHK(ccall method definition, symbol, *(jl_value_t**)jl_exprarg(e, 4)); + if (!jl_is_nothing(jl_exprarg(e, 5))) { + JL_TYPECHK(ccall method definition, uint8, jl_exprarg(e, 5)); + } jl_exprargset(e, 0, resolve_globals(jl_exprarg(e, 0), module, sparam_vals, binding_effects, 1)); i++; } @@ -308,6 +311,15 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) li->constprop = 1; 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) { + 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)); + } + } else jl_array_ptr_set(meta, ins++, ma); } @@ -448,6 +460,7 @@ JL_DLLEXPORT jl_code_info_t *jl_new_code_info_uninit(void) src->pure = 0; src->edges = jl_nothing; src->constprop = 0; + src->purity.bits = 0; return src; } @@ -635,6 +648,7 @@ static void jl_method_set_source(jl_method_t *m, jl_code_info_t *src) m->called = called; m->pure = src->pure; m->constprop = src->constprop; + m->purity.bits = src->purity.bits; jl_add_function_name_to_lineinfo(src, (jl_value_t*)m->name); jl_array_t *copy = NULL; diff --git a/stdlib/InteractiveUtils/test/runtests.jl b/stdlib/InteractiveUtils/test/runtests.jl index 8372fb16d3a13..05e3a744644e1 100644 --- a/stdlib/InteractiveUtils/test/runtests.jl +++ b/stdlib/InteractiveUtils/test/runtests.jl @@ -314,7 +314,7 @@ end # manually generate a broken function, which will break codegen # and make sure Julia doesn't crash -@eval @noinline f_broken_code() = 0 +@eval @noinline @Base.constprop :none f_broken_code() = 0 let m = which(f_broken_code, ()) let src = Base.uncompressed_ast(m) src.code = Any[ diff --git a/stdlib/Serialization/src/Serialization.jl b/stdlib/Serialization/src/Serialization.jl index 7df53f216716f..f36999d63d311 100644 --- a/stdlib/Serialization/src/Serialization.jl +++ b/stdlib/Serialization/src/Serialization.jl @@ -79,7 +79,7 @@ const TAGS = Any[ @assert length(TAGS) == 255 -const ser_version = 16 # do not make changes without bumping the version #! +const ser_version = 17 # do not make changes without bumping the version #! format_version(::AbstractSerializer) = ser_version format_version(s::Serializer) = s.version @@ -419,6 +419,7 @@ function serialize(s::AbstractSerializer, meth::Method) serialize(s, meth.isva) serialize(s, meth.is_for_opaque_closure) serialize(s, meth.constprop) + serialize(s, meth.purity) if isdefined(meth, :source) serialize(s, Base._uncompressed_ast(meth, meth.source)) else @@ -1026,12 +1027,16 @@ function deserialize(s::AbstractSerializer, ::Type{Method}) isva = deserialize(s)::Bool is_for_opaque_closure = false constprop = 0x00 + purity = 0x00 template_or_is_opaque = deserialize(s) if isa(template_or_is_opaque, Bool) is_for_opaque_closure = template_or_is_opaque if format_version(s) >= 14 constprop = deserialize(s)::UInt8 end + if format_version(s) >= 17 + purity = deserialize(s)::UInt8 + end template = deserialize(s) else template = template_or_is_opaque @@ -1051,6 +1056,7 @@ function deserialize(s::AbstractSerializer, ::Type{Method}) meth.isva = isva meth.is_for_opaque_closure = is_for_opaque_closure meth.constprop = constprop + meth.purity = purity if template !== nothing # TODO: compress template meth.source = template::CodeInfo @@ -1182,6 +1188,9 @@ function deserialize(s::AbstractSerializer, ::Type{CodeInfo}) if format_version(s) >= 14 ci.constprop = deserialize(s)::UInt8 end + if format_version(s) >= 17 + ci.purity = deserialize(s)::UInt8 + end return ci end diff --git a/test/ccall.jl b/test/ccall.jl index 8f047ece65be2..55942d8310d82 100644 --- a/test/ccall.jl +++ b/test/ccall.jl @@ -1695,6 +1695,7 @@ end :($(Expr(:escape, :(($(Expr(:core, :svec)))(Cstring, Cint, Cint))))), 0, :(:ccall), + nothing, :arg1, :arg2, :arg3, :arg1root, :arg2root, :arg3root)) end) @@ -1711,7 +1712,7 @@ end end local arg1root = $(GlobalRef(Base, :cconvert))($(Expr(:escape, :Cstring)), $(Expr(:escape, "bar"))) local arg1 = $(GlobalRef(Base, :unsafe_convert))($(Expr(:escape, :Cstring)), arg1root) - $(Expr(:foreigncall, :func, :($(Expr(:escape, :Cvoid))), :($(Expr(:escape, :(($(Expr(:core, :svec)))(Cstring))))), 0, :(:ccall), :arg1, :arg1root)) + $(Expr(:foreigncall, :func, :($(Expr(:escape, :Cvoid))), :($(Expr(:escape, :(($(Expr(:core, :svec)))(Cstring))))), 0, :(:ccall), nothing, :arg1, :arg1root)) end) end diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index fe49b2d6000af..24373b04d54bf 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -3419,10 +3419,10 @@ end # Recursive function @eval module _Recursive f(n::Integer) = n == 0 ? 0 : f(n-1) + 1 end timing = time_inference() do - @eval _Recursive.f(5) + @eval _Recursive.f(Base.inferencebarrier(5)) end - @test depth(timing) == 3 # root -> f -> + - @test length(flatten_times(timing)) == 3 # root, f, + + @test 2 <= depth(timing) <= 3 # root -> f (-> +) + @test 2 <= length(flatten_times(timing)) <= 3 # root, f, + # Functions inferred with multiple constants @eval module C diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index f6bd13a0ad154..96ea63e2cafa8 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -928,3 +928,35 @@ end @test count(iscall((src, Core.get_binding_type)), src.code) == 1 @test m.g() === Any end + +# have_fma elimination inside ^ +f_pow() = ^(2.0, -1.0) +@test fully_eliminated(f_pow, Tuple{}) + +# unused total, noinline function +@noinline function f_total_noinline(x) + return x + 1.0 +end +@noinline function f_voltatile_escape(ptr) + unsafe_store!(ptr, 0) +end +function f_call_total_noinline_unused(x) + f_total_noinline(x) + return x +end +function f_call_volatile_escape(ptr) + f_voltatile_escape(ptr) + return ptr +end + +@test fully_eliminated(f_call_total_noinline_unused, Tuple{Float64}) +@test !fully_eliminated(f_call_volatile_escape, Tuple{Ptr{Int}}) + +let b = Expr(:block, (:(y += sin($x)) for x in randn(1000))...) + @eval function f_sin_perf() + y = 0.0 + $b + y + end +end +@test fully_eliminated(f_sin_perf, Tuple{}) diff --git a/test/compiler/irpasses.jl b/test/compiler/irpasses.jl index 9eb77490cbdb4..2d23c3f1e97d1 100644 --- a/test/compiler/irpasses.jl +++ b/test/compiler/irpasses.jl @@ -764,7 +764,7 @@ let @assert some_ccall !== nothing stmt = src.code[some_ccall] nccallargs = length(stmt.args[3]::Core.SimpleVector) - preserves = stmt.args[6+nccallargs:end] + preserves = stmt.args[7+nccallargs:end] @test length(refs) == 2 @test length(preserves) == 2 @test all(alloc -> alloc in preserves, refs) diff --git a/test/testenv.jl b/test/testenv.jl index 2aeee0f6dfc80..41706dd24e75e 100644 --- a/test/testenv.jl +++ b/test/testenv.jl @@ -43,7 +43,7 @@ if !@isdefined(testenv_defined) const curmod = @__MODULE__ const curmod_name = fullname(curmod) const curmod_str = curmod === Main ? "Main" : join(curmod_name, ".") - const curmod_prefix = "$(["$m." for m in curmod_name]...)" + const curmod_prefix = curmod === Main ? "" : "$(["$m." for m in curmod_name]...)" # platforms that support cfunction with closures # (requires LLVM back-end support for trampoline intrinsics) From bfa17ba4b513838f66862753f2bcada591213332 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Tue, 18 Jan 2022 01:20:30 -0500 Subject: [PATCH 02/20] Make INV_2PI a tuple Without this, the compiler cannot assume that the range reduction is idempotent to make use of the new fast constprop code path. In the future this could potentially be an ImmutableArray, but since this is relatively small, a tuple is probably fine. --- base/special/rem_pio2.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/special/rem_pio2.jl b/base/special/rem_pio2.jl index 7242eb8f17b69..4ec9945885e7e 100644 --- a/base/special/rem_pio2.jl +++ b/base/special/rem_pio2.jl @@ -23,7 +23,7 @@ # @printf "0x%016x,\n" k # I -= k # end -const INV_2PI = UInt64[ +const INV_2PI = ( 0x28be_60db_9391_054a, 0x7f09_d5f4_7d4d_3770, 0x36d8_a566_4f10_e410, @@ -42,7 +42,7 @@ const INV_2PI = UInt64[ 0x5d49_eeb1_faf9_7c5e, 0xcf41_ce7d_e294_a4ba, 0x9afe_d7ec_47e3_5742, - 0x1580_cc11_bf1e_daea] + 0x1580_cc11_bf1e_daea) @inline function cody_waite_2c_pio2(x::Float64, fn, n) pio2_1 = 1.57079632673412561417e+00 From aaeb0d2e7b4df6cf87c25935579e1fcb71710962 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sat, 22 Jan 2022 00:29:59 -0500 Subject: [PATCH 03/20] Evalute :total function in the proper world --- base/compiler/abstractinterpretation.jl | 9 +++++---- src/builtin_proto.h | 1 + src/builtins.c | 27 +++++++++++++++++++++++++ src/codegen.cpp | 1 + src/staticdata.c | 2 +- test/compiler/inference.jl | 9 +++++++++ 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index a27bffaf4902f..14d261fe8572b 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -621,13 +621,14 @@ function is_all_const_arg((; fargs, argtypes)::ArgInfo) return true end -function pure_eval_call_const_args(@nospecialize(f), argtypes::Vector{Any}) +function pure_eval_call_const_args(interp::AbstractInterpreter, + @nospecialize(f), argtypes::Vector{Any}) args = Any[ (a = widenconditional(argtypes[i]); isa(a, Const) ? a.val : isconstType(a) ? (a::DataType).parameters[1] : (a::DataType).instance) for i in 2:length(argtypes) ] try - value = Core._apply_pure(f, args) + value = Core._call_in_world_nonpure(get_world_counter(interp), f, args...) return Const(value) catch e return nothing @@ -654,9 +655,9 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, resul return nothing end if f !== nothing && result.edge !== nothing && is_total_or_error(result.edge_effects) && is_all_const_arg(arginfo) - rt = pure_eval_call_const_args(f, arginfo.argtypes) # TODO: Needs to be called in the correct world age + rt = pure_eval_call_const_args(interp, f, arginfo.argtypes) add_backedge!(result.edge, sv) - rt === nothing && return Union{}, false # The evaulation threw. By idempontency, we're guaranteed this would have happened at runtime + rt === nothing && return Union{}, false # The evaulation threw. By :consistent-cy, we're guaranteed this would have happened at runtime return rt, ConstResult(result.edge, rt.val) end mi = maybe_get_const_prop_profitable(interp, result, f, arginfo, match, sv) diff --git a/src/builtin_proto.h b/src/builtin_proto.h index c7027f1b67f9e..3712ea99d38fc 100644 --- a/src/builtin_proto.h +++ b/src/builtin_proto.h @@ -29,6 +29,7 @@ DECLARE_BUILTIN(arrayref); DECLARE_BUILTIN(arrayset); DECLARE_BUILTIN(arraysize); DECLARE_BUILTIN(_call_in_world); +DECLARE_BUILTIN(_call_in_world_nonpure); DECLARE_BUILTIN(_call_latest); DECLARE_BUILTIN(replacefield); DECLARE_BUILTIN(const_arrayref); diff --git a/src/builtins.c b/src/builtins.c index e78034446d19a..f4b200e1e2c96 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -790,6 +790,32 @@ JL_CALLABLE(jl_f__call_in_world) return ret; } +JL_CALLABLE(jl_f__call_in_world_nonpure) +{ + JL_NARGSV(_apply_in_world_nonpure, 2); + JL_TYPECHK(_apply_in_world, ulong, args[0]); + jl_task_t *ct = jl_current_task; + int last_in = ct->ptls->in_pure_callback; + jl_value_t *ret = NULL; + size_t last_age = ct->world_age; + JL_TRY { + ct->ptls->in_pure_callback = 1; + size_t world = jl_unbox_ulong(args[0]); + ct->world_age = jl_atomic_load_acquire(&jl_world_counter); + if (ct->world_age > world) + ct->world_age = world; + ret = jl_apply(&args[1], nargs - 1); + ct->world_age = last_age; + ct->ptls->in_pure_callback = last_in; + } + JL_CATCH { + ct->world_age = last_age; + ct->ptls->in_pure_callback = last_in; + jl_rethrow(); + } + return ret; +} + // tuples --------------------------------------------------------------------- JL_CALLABLE(jl_f_tuple) @@ -1882,6 +1908,7 @@ void jl_init_primitives(void) JL_GC_DISABLED add_builtin_func("_apply_pure", jl_f__apply_pure); add_builtin_func("_call_latest", jl_f__call_latest); add_builtin_func("_call_in_world", jl_f__call_in_world); + add_builtin_func("_call_in_world_nonpure", jl_f__call_in_world_nonpure); 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 c754e7039b72b..195e4643d676b 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -8155,6 +8155,7 @@ extern "C" void jl_init_llvm(void) { jl_f__apply_pure_addr, new JuliaFunction{XSTR(jl_f__apply_pure), get_func_sig, get_func_attrs} }, { jl_f__call_latest_addr, new JuliaFunction{XSTR(jl_f__call_latest), get_func_sig, get_func_attrs} }, { jl_f__call_in_world_addr, new JuliaFunction{XSTR(jl_f__call_in_world), get_func_sig, get_func_attrs} }, + { jl_f__call_in_world_nonpure_addr, new JuliaFunction{XSTR(jl_f__call_in_world_nonpure), get_func_sig, get_func_attrs} }, { jl_f_throw_addr, new JuliaFunction{XSTR(jl_f_throw), get_func_sig, get_func_attrs} }, { jl_f_tuple_addr, jltuple_func }, { jl_f_svec_addr, new JuliaFunction{XSTR(jl_f_svec), get_func_sig, get_func_attrs} }, diff --git a/src/staticdata.c b/src/staticdata.c index fb42d9cdf23f9..202479a84bddb 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -245,7 +245,7 @@ static htable_t field_replace; 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_iterate, &jl_f__apply_pure, - &jl_f__call_latest, &jl_f__call_in_world, &jl_f_isdefined, + &jl_f__call_latest, &jl_f__call_in_world, &jl_f__call_in_world_nonpure, &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_swapfield, &jl_f_modifyfield, &jl_f_replacefield, &jl_f_fieldtype, &jl_f_nfields, diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index 24373b04d54bf..eef7d0eb91547 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -4009,3 +4009,12 @@ end end |> only === Union{} end end + +# Test that purity modeling doesn't accidentally introduce new world age issues +f_redefine_me(x) = x+1 +f_call_redefine() = f_redefine_me(0) +f_mk_opaque() = @Base.Experimental.opaque ()->Base.inferencebarrier(f_call_redefine)() +const op_capture_world = f_mk_opaque() +f_redefine_me(x) = x+2 +@test op_capture_world() == 1 +@test f_mk_opaque()() == 2 From 405a4f8172e8370fe8e9c3be07c6a096b52a1ccc Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sat, 22 Jan 2022 01:58:09 -0500 Subject: [PATCH 04/20] Finish effects implementation for ccall --- base/c.jl | 10 +++++++--- base/compiler/types.jl | 7 +++---- base/expr.jl | 9 ++++++++- test/ccall.jl | 12 +++++++++--- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/base/c.jl b/base/c.jl index fdcc405b3146d..5b7f6bb0efa7d 100644 --- a/base/c.jl +++ b/base/c.jl @@ -639,7 +639,7 @@ function ccall_macro_parse(expr::Expr) end -function ccall_macro_lower(convention, func, rettype, types, args, nreq) +function ccall_macro_lower(convention, effects, func, rettype, types, args, nreq) lowering = [] realargs = [] gcroots = [] @@ -676,7 +676,7 @@ function ccall_macro_lower(convention, func, rettype, types, args, nreq) esc(etypes), nreq, QuoteNode(convention), - nothing, + effects, realargs..., gcroots...) push!(lowering, exp) @@ -732,5 +732,9 @@ The string literal could also be used directly before the function name, if desired `"libglib-2.0".g_uri_escape_string(...` """ macro ccall(expr) - return ccall_macro_lower(:ccall, ccall_macro_parse(expr)...) + return ccall_macro_lower(:ccall, nothing, ccall_macro_parse(expr)...) +end + +macro ccall_effects(effects, expr) + return ccall_macro_lower(:ccall, effects, ccall_macro_parse(expr)...) end diff --git a/base/compiler/types.jl b/base/compiler/types.jl index dc9006762d57c..ab1c02c083498 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -68,9 +68,9 @@ function encode_effects_override(eo::EffectsOverride) e = 0x00 eo.consistent && (e |= 0x01) eo.effect_free && (e |= 0x02) - eo.nothrow && (e |= 0x02) - eo.terminates && (e |= 0x04) - eo.terminates_locally && (e |= 0x08) + eo.nothrow && (e |= 0x04) + eo.terminates && (e |= 0x08) + eo.terminates_locally && (e |= 0x10) e end @@ -82,7 +82,6 @@ decode_effects_override(e::UInt8) = (e >> 3) & 0x01 != 0x00, (e >> 4) & 0x01 != 0x00) - """ InferenceResult diff --git a/base/expr.jl b/base/expr.jl index f3a694960aa3e..dfd36f3e877e3 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -520,7 +520,14 @@ macro assume_effects(args...) end end ex = args[end] - isa(ex, Expr) || ArgumentError("Bad expression `$ex` in @constprop [settings] ex") + isa(ex, Expr) || throw(ArgumentError("Bad expression `$ex` in @constprop [settings] ex")) + 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 + ))) + return esc(ex) + end return pushmeta!(ex, :purity, consistent, effect_free, nothrow, terminates_globally, terminates_locally) end diff --git a/test/ccall.jl b/test/ccall.jl index 55942d8310d82..f2ac071557de4 100644 --- a/test/ccall.jl +++ b/test/ccall.jl @@ -36,7 +36,7 @@ end Core.svec(Ptr{Ptr{Cchar}}, Cstring, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, Cfloat, Cfloat, Cfloat, Cfloat, Cfloat, Cfloat, Cfloat, Cfloat, Cfloat), - 2, :(:cdecl), + 2, :(:cdecl), nothing, :(Base.unsafe_convert(Ptr{Ptr{Cchar}}, strp)), :(Base.unsafe_convert(Cstring, fmt)), 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, Cfloat(1.1), Cfloat(2.2), Cfloat(3.3), Cfloat(4.4), Cfloat(5.5), Cfloat(6.6), Cfloat(7.7), Cfloat(8.8), Cfloat(9.9), @@ -1676,7 +1676,7 @@ using Base: ccall_macro_parse, ccall_macro_lower end @testset "ensure the base-case of @ccall works, including library name and pointer interpolation" begin - call = ccall_macro_lower(:ccall, ccall_macro_parse( :( libstring.func( + call = ccall_macro_lower(:ccall, nothing, ccall_macro_parse( :( libstring.func( str::Cstring, num1::Cint, num2::Cint @@ -1700,7 +1700,7 @@ end end) # pointer interpolation - call = ccall_macro_lower(:ccall, ccall_macro_parse(:( $(Expr(:$, :fptr))("bar"::Cstring)::Cvoid ))...) + call = ccall_macro_lower(:ccall, nothing, ccall_macro_parse(:( $(Expr(:$, :fptr))("bar"::Cstring)::Cvoid ))...) @test Base.remove_linenums!(call) == Base.remove_linenums!( quote func = $(Expr(:escape, :fptr)) @@ -1852,3 +1852,9 @@ end @test cglobal33413_literal() != C_NULL @test cglobal33413_literal_notype() != C_NULL end + +@testset "ccall_effects" begin + ctest_total(x) = @Base.assume_effects :total @ccall libccalltest.ctest(x::Complex{Int})::Complex{Int} + ctest_total_const() = Val{ctest_total(1 + 2im)}() + Core.Compiler.return_type(ctest_total_const, Tuple{}) == Val{2 + 0im} +end From 7bbf41165e465319f01db3c918200b7d2f91b4fe Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 23 Jan 2022 20:45:47 -0500 Subject: [PATCH 05/20] Add missing `esc` --- base/expr.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/expr.jl b/base/expr.jl index dfd36f3e877e3..c9e56156b7aff 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -528,7 +528,7 @@ macro assume_effects(args...) ))) return esc(ex) end - return pushmeta!(ex, :purity, consistent, effect_free, nothrow, terminates_globally, terminates_locally) + return esc(pushmeta!(ex, :purity, consistent, effect_free, nothrow, terminates_globally, terminates_locally)) end From c5f380449134c36edeb56e11ab43f96754d44448 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 23 Jan 2022 20:38:01 -0500 Subject: [PATCH 06/20] Actually make use of terminates_locally override --- base/compiler/abstractinterpretation.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 14d261fe8572b..e2663da4f3301 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -2001,8 +2001,11 @@ function widenreturn(@nospecialize(rt), @nospecialize(bestguess), nslots::Int, s return widenconst(rt) end -function handle_backedge!(frame::InferenceState, from, to) +function handle_control_backedge!(frame::InferenceState, from, to) if from > to + if isa(frame.linfo.def, Method) && decode_effects_override(frame.linfo.def.purity).terminates_locally + return + end tristate_merge!(frame, Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, TRISTATE_UNKNOWN)) end end @@ -2045,7 +2048,7 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) changes[sn] = VarState(Bottom, true) elseif isa(stmt, GotoNode) l = (stmt::GotoNode).label - handle_backedge!(frame, pc, l) + handle_control_backedge!(frame, pc, l) pc´ = l elseif isa(stmt, GotoIfNot) condx = stmt.cond @@ -2071,7 +2074,7 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) # constant conditions if condval === true elseif condval === false - handle_backedge!(frame, pc, l) + handle_control_backedge!(frame, pc, l) pc´ = l else # general case @@ -2082,7 +2085,7 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) end newstate_else = stupdate!(states[l], changes_else) if newstate_else !== nothing - handle_backedge!(frame, pc, l) + handle_control_backedge!(frame, pc, l) # add else branch to active IP list if l < frame.pc´´ frame.pc´´ = l From d821b5ad062ff295eb743648079d88454a672303 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 23 Jan 2022 20:41:46 -0500 Subject: [PATCH 07/20] Mark ^(x::Float64, n::Integer) as locally terminating --- base/math.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/math.jl b/base/math.jl index 319d96246c55b..af86c11c01b26 100644 --- a/base/math.jl +++ b/base/math.jl @@ -18,7 +18,7 @@ export sin, cos, sincos, tan, sinh, cosh, tanh, asin, acos, atan, import .Base: log, exp, sin, cos, tan, sinh, cosh, tanh, asin, acos, atan, asinh, acosh, atanh, sqrt, log2, log10, max, min, minmax, ^, exp2, muladd, rem, - exp10, expm1, log1p, @constprop + exp10, expm1, log1p, @constprop, @assume_effects using .Base: sign_mask, exponent_mask, exponent_one, exponent_half, uinttype, significand_mask, @@ -1033,7 +1033,7 @@ end return pow_body(x, n) end -@noinline function pow_body(x::Float64, n::Integer) +@assume_effects :terminates_locally @noinline function pow_body(x::Float64, n::Integer) y = 1.0 xnlo = ynlo = 0.0 if n < 0 From 2f4c0920a92d6936951f00c1b3628a70fd6ccf9c Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Tue, 1 Feb 2022 19:16:23 +0000 Subject: [PATCH 08/20] Shove effects into calling convention field --- base/c.jl | 7 +++---- base/compiler/abstractinterpretation.jl | 5 +++-- base/compiler/optimize.jl | 14 ++++++++------ base/compiler/ssair/inlining.jl | 2 +- base/compiler/ssair/passes.jl | 6 +++--- base/compiler/validation.jl | 2 +- base/meta.jl | 4 +--- base/strings/string.jl | 2 +- doc/src/devdocs/llvm.md | 2 +- src/ccall.cpp | 17 ++++++++++------- src/julia-syntax.scm | 7 +++---- src/method.c | 13 +++++++++---- test/ccall.jl | 9 ++++----- test/compiler/irpasses.jl | 2 +- 14 files changed, 49 insertions(+), 43 deletions(-) diff --git a/base/c.jl b/base/c.jl index 5b7f6bb0efa7d..3606d0fa0a9bc 100644 --- a/base/c.jl +++ b/base/c.jl @@ -639,7 +639,7 @@ function ccall_macro_parse(expr::Expr) end -function ccall_macro_lower(convention, effects, func, rettype, types, args, nreq) +function ccall_macro_lower(convention, func, rettype, types, args, nreq) lowering = [] realargs = [] gcroots = [] @@ -676,7 +676,6 @@ function ccall_macro_lower(convention, effects, func, rettype, types, args, nreq esc(etypes), nreq, QuoteNode(convention), - effects, realargs..., gcroots...) push!(lowering, exp) @@ -732,9 +731,9 @@ The string literal could also be used directly before the function name, if desired `"libglib-2.0".g_uri_escape_string(...` """ macro ccall(expr) - return ccall_macro_lower(:ccall, nothing, ccall_macro_parse(expr)...) + return ccall_macro_lower(:ccall, ccall_macro_parse(expr)...) end macro ccall_effects(effects, expr) - return ccall_macro_lower(:ccall, effects, ccall_macro_parse(expr)...) + return ccall_macro_lower((:ccall, effects), ccall_macro_parse(expr)...) end diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index e2663da4f3301..ff7db43cde28f 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -1824,8 +1824,9 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), t = Bottom end end - effects = e.args[6] - if isa(effects, UInt8) + cconv = e.args[5] + if isa(cconv, QuoteNode) && isa(cconv.value, Tuple{Symbol, UInt8}) + effects = cconv.value[2] effects = decode_effects_override(effects) tristate_merge!(sv, Effects( effects.consistent ? ALWAYS_TRUE : TRISTATE_UNKNOWN, diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 80492d8633165..e8d6f66e49d53 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -287,12 +287,14 @@ function alloc_array_ndims(name::Symbol) return nothing end +const FOREIGNCALL_ARG_START = 6 + function alloc_array_no_throw(args::Vector{Any}, ndims::Int, src::Union{IRCode,IncrementalCompact}) - length(args) ≥ ndims+7 || return false - atype = instanceof_tfunc(argextype(args[7], src))[1] + length(args) ≥ ndims+FOREIGNCALL_ARG_START || return false + atype = instanceof_tfunc(argextype(args[FOREIGNCALL_ARG_START], src))[1] dims = Csize_t[] for i in 1:ndims - dim = argextype(args[i+7], src) + dim = argextype(args[i+FOREIGNCALL_ARG_START], src) isa(dim, Const) || return false dimval = dim.val isa(dimval, Int) || return false @@ -302,9 +304,9 @@ function alloc_array_no_throw(args::Vector{Any}, ndims::Int, src::Union{IRCode,I end function new_array_no_throw(args::Vector{Any}, src::Union{IRCode,IncrementalCompact}) - length(args) ≥ 8 || return false - atype = instanceof_tfunc(argextype(args[7], src))[1] - dims = argextype(args[8], src) + length(args) ≥ FOREIGNCALL_ARG_START+1 || return false + atype = instanceof_tfunc(argextype(args[FOREIGNCALL_ARG_START], src))[1] + dims = argextype(args[FOREIGNCALL_ARG_START+1], src) isa(dims, Const) || return dims === Tuple{} dimsval = dims.val isa(dimsval, Tuple{Vararg{Int}}) || return false diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 55c5d204bfa1e..464f111d6c603 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -1487,7 +1487,7 @@ function late_inline_special_case!( return SomeCase(typevar_call) elseif f === UnionAll && length(argtypes) == 3 && (argtypes[2] ⊑ TypeVar) unionall_call = Expr(:foreigncall, QuoteNode(:jl_type_unionall), Any, svec(Any, Any), - 0, QuoteNode(:ccall), nothing, stmt.args[2], stmt.args[3]) + 0, QuoteNode(:ccall), stmt.args[2], stmt.args[3]) return SomeCase(unionall_call) elseif is_return_type(f) if isconstType(type) diff --git a/base/compiler/ssair/passes.jl b/base/compiler/ssair/passes.jl index 9c30a44c27cec..67610f0c1df60 100644 --- a/base/compiler/ssair/passes.jl +++ b/base/compiler/ssair/passes.jl @@ -701,7 +701,7 @@ function sroa_pass!(ir::IRCode) nccallargs = length(stmt.args[3]::SimpleVector) preserved = Int[] new_preserves = Any[] - for pidx in (7+nccallargs):length(stmt.args) + for pidx in (6+nccallargs):length(stmt.args) preserved_arg = stmt.args[pidx] isa(preserved_arg, SSAValue) || continue let intermediaries = SPCSet() @@ -993,10 +993,10 @@ end function form_new_preserves(origex::Expr, intermediates::Vector{Int}, new_preserves::Vector{Any}) newex = Expr(:foreigncall) nccallargs = length(origex.args[3]::SimpleVector) - for i in 1:(7+nccallargs-1) + for i in 1:(6+nccallargs-1) push!(newex.args, origex.args[i]) end - for i in (7+nccallargs):length(origex.args) + for i in (6+nccallargs):length(origex.args) x = origex.args[i] # don't need to preserve intermediaries if isa(x, SSAValue) && x.id in intermediates diff --git a/base/compiler/validation.jl b/base/compiler/validation.jl index fb4cf1f820f1c..77ee422b6ffcd 100644 --- a/base/compiler/validation.jl +++ b/base/compiler/validation.jl @@ -23,7 +23,7 @@ const VALID_EXPR_HEADS = IdDict{Symbol,UnitRange{Int}}( :copyast => 1:1, :meta => 0:typemax(Int), :global => 1:1, - :foreigncall => 6:typemax(Int), # name, RT, AT, nreq, cconv, effects, args..., roots... + :foreigncall => 5:typemax(Int), # name, RT, AT, nreq, (cconv, effects), args..., roots... :cfunction => 5:5, :isdefined => 1:1, :code_coverage_effect => 0:0, diff --git a/base/meta.jl b/base/meta.jl index 6c8f28beb87ff..fcf66a7a787b2 100644 --- a/base/meta.jl +++ b/base/meta.jl @@ -393,9 +393,7 @@ function _partially_inline!(@nospecialize(x), slot_replacements::Vector{Any}, elseif i == 4 @assert isa(x.args[4], Int) elseif i == 5 - @assert isa((x.args[5]::QuoteNode).value, Symbol) - elseif i == 6 - @assert isa(x.args[6], Union{Nothing, UInt8}) + @assert isa((x.args[5]::QuoteNode).value, Union{Symbol, Tuple{Symbol, UInt8}}) else x.args[i] = _partially_inline!(x.args[i], slot_replacements, type_signature, static_param_values, diff --git a/base/strings/string.jl b/base/strings/string.jl index b7d93ed3ffcf5..b0ea1976899cd 100644 --- a/base/strings/string.jl +++ b/base/strings/string.jl @@ -73,7 +73,7 @@ end # This is @assume_effects :effect_free :nothrow :terminates_globally @ccall jl_alloc_string(n::Csize_t)::Ref{String}, # but the macro is not available at this time in bootstrap, so we write it manually. -@eval _string_n(n::Integer) = $(Expr(:foreigncall, QuoteNode(:jl_alloc_string), Ref{String}, Expr(:call, Expr(:core, :svec), :Csize_t), 1, QuoteNode(:ccall), 0xe, :(convert(Csize_t, n)))) +@eval _string_n(n::Integer) = $(Expr(:foreigncall, QuoteNode(:jl_alloc_string), Ref{String}, Expr(:call, Expr(:core, :svec), :Csize_t), 1, QuoteNode((:ccall,0xe)), :(convert(Csize_t, n)))) """ String(s::AbstractString) diff --git a/doc/src/devdocs/llvm.md b/doc/src/devdocs/llvm.md index 4cf30446dabae..1e983949ea0b6 100644 --- a/doc/src/devdocs/llvm.md +++ b/doc/src/devdocs/llvm.md @@ -271,7 +271,7 @@ need to make sure that the array does stay alive while we're doing the [`ccall`](@ref). To understand how this is done, first recall the lowering of the above code: ```julia -return $(Expr(:foreigncall, :(:foo), Cvoid, svec(Ptr{Float64}), 0, :(:ccall), nothing, Expr(:foreigncall, :(:jl_array_ptr), Ptr{Float64}, svec(Any), 0, :(:ccall), nothing, :(A)), :(A))) +return $(Expr(:foreigncall, :(:foo), Cvoid, svec(Ptr{Float64}), 0, :(:ccall), Expr(:foreigncall, :(:jl_array_ptr), Ptr{Float64}, svec(Any), 0, :(:ccall), :(A)), :(A))) ``` The last `:(A)`, is an extra argument list inserted during lowering that informs the code generator which Julia level values need to be kept alive for the diff --git a/src/ccall.cpp b/src/ccall.cpp index 792b379d6bbf2..507f6ee86ec3e 100644 --- a/src/ccall.cpp +++ b/src/ccall.cpp @@ -1240,9 +1240,9 @@ static const std::string verify_ccall_sig(jl_value_t *&rt, jl_value_t *at, return ""; } -const int fc_args_start = 7; +const int fc_args_start = 6; -// Expr(:foreigncall, pointer, rettype, (argtypes...), nreq, cconv, effects, args..., roots...) +// Expr(:foreigncall, pointer, rettype, (argtypes...), nreq, [cconv | (cconv, effects)], args..., roots...) static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs) { JL_NARGSV(ccall, 5); @@ -1252,11 +1252,14 @@ static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs) size_t nccallargs = jl_svec_len(at); size_t nreqargs = jl_unbox_long(args[4]); // if vararg assert(jl_is_quotenode(args[5])); - jl_sym_t *cc_sym = *(jl_sym_t**)args[5]; - assert(jl_is_symbol(cc_sym)); - jl_value_t *effects = args[6]; - assert(jl_is_nothing(effects) || jl_is_uint8(effects)); - (void)effects; // currently only used by inference + jl_value_t *jlcc = jl_quotenode_value(args[5]); + jl_sym_t *cc_sym = NULL; + if (jl_is_symbol(jlcc)) { + cc_sym = (jl_sym_t*)jlcc; + } else if (jl_is_tuple(jlcc)) { + cc_sym = (jl_sym_t*)jl_get_nth_field_noalloc(jlcc, 0); + assert(jl_is_symbol(cc_sym)); + } native_sym_arg_t symarg = {}; JL_GC_PUSH3(&rt, &at, &symarg.gcroot); diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 2d862bd7572ff..07753233e204e 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -1062,7 +1062,6 @@ (foreigncall ,name ,RT (call (core svec) ,@(reverse! T)) ,(if isseq (- (length atypes) 1) 0) ; 0 or number of arguments before ... in definition ',cconv - (null) ,.(reverse! C) ,@GC)) ; GC root ordering is arbitrary (let* ((a (car A)) @@ -4326,13 +4325,13 @@ f(x) = yt(x) (not (tuple-call? fptr)))) (let* ((args (cond ((eq? (car e) 'foreigncall) - ;; NOTE: 2nd to 6th arguments of ccall must be left in place + ;; NOTE: 2nd to 5th arguments of ccall must be left in place ;; the 1st should be compiled if an atom. (append (if (atom-or-not-tuple-call? (cadr e)) (compile-args (list (cadr e)) break-labels) (list (cadr e))) - (list-head (cddr e) 5) - (compile-args (list-tail e 7) break-labels))) + (list-head (cddr e) 4) + (compile-args (list-tail e 6) break-labels))) ;; NOTE: arguments of cfunction must be left in place ;; except for argument 2 (fptr) ((eq? (car e) 'cfunction) diff --git a/src/method.c b/src/method.c index 42dbf1478c568..d68757114b2b4 100644 --- a/src/method.c +++ b/src/method.c @@ -146,7 +146,7 @@ static jl_value_t *resolve_globals(jl_value_t *expr, jl_module_t *module, jl_sve return expr; } if (e->head == jl_foreigncall_sym) { - JL_NARGSV(ccall method definition, 6); // (fptr, rt, at, nreq, cc, effects) + JL_NARGSV(ccall method definition, 5); // (fptr, rt, at, nreq, (cc, effects)) jl_value_t *rt = jl_exprarg(e, 1); jl_value_t *at = jl_exprarg(e, 2); if (!jl_is_type(rt)) { @@ -176,9 +176,14 @@ static jl_value_t *resolve_globals(jl_value_t *expr, jl_module_t *module, jl_sve check_c_types("ccall method definition", rt, at); JL_TYPECHK(ccall method definition, long, jl_exprarg(e, 3)); JL_TYPECHK(ccall method definition, quotenode, jl_exprarg(e, 4)); - JL_TYPECHK(ccall method definition, symbol, *(jl_value_t**)jl_exprarg(e, 4)); - if (!jl_is_nothing(jl_exprarg(e, 5))) { - JL_TYPECHK(ccall method definition, uint8, jl_exprarg(e, 5)); + jl_value_t *cc = jl_quotenode_value(jl_exprarg(e, 4)); + if (!jl_is_symbol(cc)) { + JL_TYPECHK(ccall method definition, tuple, cc); + if (jl_nfields(cc) != 2) { + jl_error("In ccall calling convention, expected two argument tuple or symbol."); + } + JL_TYPECHK(ccall method definition, symbol, jl_get_nth_field(cc, 0)); + JL_TYPECHK(ccall method definition, uint8, jl_get_nth_field(cc, 1)); } jl_exprargset(e, 0, resolve_globals(jl_exprarg(e, 0), module, sparam_vals, binding_effects, 1)); i++; diff --git a/test/ccall.jl b/test/ccall.jl index f2ac071557de4..c29f8c843bffd 100644 --- a/test/ccall.jl +++ b/test/ccall.jl @@ -36,7 +36,7 @@ end Core.svec(Ptr{Ptr{Cchar}}, Cstring, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, Cfloat, Cfloat, Cfloat, Cfloat, Cfloat, Cfloat, Cfloat, Cfloat, Cfloat), - 2, :(:cdecl), nothing, + 2, :(:cdecl), :(Base.unsafe_convert(Ptr{Ptr{Cchar}}, strp)), :(Base.unsafe_convert(Cstring, fmt)), 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, Cfloat(1.1), Cfloat(2.2), Cfloat(3.3), Cfloat(4.4), Cfloat(5.5), Cfloat(6.6), Cfloat(7.7), Cfloat(8.8), Cfloat(9.9), @@ -1676,7 +1676,7 @@ using Base: ccall_macro_parse, ccall_macro_lower end @testset "ensure the base-case of @ccall works, including library name and pointer interpolation" begin - call = ccall_macro_lower(:ccall, nothing, ccall_macro_parse( :( libstring.func( + call = ccall_macro_lower(:ccall, ccall_macro_parse( :( libstring.func( str::Cstring, num1::Cint, num2::Cint @@ -1695,12 +1695,11 @@ end :($(Expr(:escape, :(($(Expr(:core, :svec)))(Cstring, Cint, Cint))))), 0, :(:ccall), - nothing, :arg1, :arg2, :arg3, :arg1root, :arg2root, :arg3root)) end) # pointer interpolation - call = ccall_macro_lower(:ccall, nothing, ccall_macro_parse(:( $(Expr(:$, :fptr))("bar"::Cstring)::Cvoid ))...) + call = ccall_macro_lower(:ccall, ccall_macro_parse(:( $(Expr(:$, :fptr))("bar"::Cstring)::Cvoid ))...) @test Base.remove_linenums!(call) == Base.remove_linenums!( quote func = $(Expr(:escape, :fptr)) @@ -1712,7 +1711,7 @@ end end local arg1root = $(GlobalRef(Base, :cconvert))($(Expr(:escape, :Cstring)), $(Expr(:escape, "bar"))) local arg1 = $(GlobalRef(Base, :unsafe_convert))($(Expr(:escape, :Cstring)), arg1root) - $(Expr(:foreigncall, :func, :($(Expr(:escape, :Cvoid))), :($(Expr(:escape, :(($(Expr(:core, :svec)))(Cstring))))), 0, :(:ccall), nothing, :arg1, :arg1root)) + $(Expr(:foreigncall, :func, :($(Expr(:escape, :Cvoid))), :($(Expr(:escape, :(($(Expr(:core, :svec)))(Cstring))))), 0, :(:ccall), :arg1, :arg1root)) end) end diff --git a/test/compiler/irpasses.jl b/test/compiler/irpasses.jl index 2d23c3f1e97d1..9eb77490cbdb4 100644 --- a/test/compiler/irpasses.jl +++ b/test/compiler/irpasses.jl @@ -764,7 +764,7 @@ let @assert some_ccall !== nothing stmt = src.code[some_ccall] nccallargs = length(stmt.args[3]::Core.SimpleVector) - preserves = stmt.args[7+nccallargs:end] + preserves = stmt.args[6+nccallargs:end] @test length(refs) == 2 @test length(preserves) == 2 @test all(alloc -> alloc in preserves, refs) From b9d6a21b3929b7cb4504b1cfa3250d024397eb4b Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Wed, 2 Feb 2022 07:30:53 +0000 Subject: [PATCH 09/20] Make inbounds taint consistency Inbounds and `--check-bounds=no` basically make the assertion: If this is dynamically reached during exceution then the index will be inbounds. However, effects on a function are a stronger statement. In particular, for *any* input values (not just the dynamically reached ones), the effects need to hold. This is in particular true, because inference can run functions that are dynamically dead, e.g. ``` if unknown_bool_return() # false at runtime, but inference doesn't know x = sin(1.0) end ``` Inference will (and we want it to) run the `sin(1.0)` even though it is not dynamically reached. For the moment, make any use of `--check-bounds=no` or `@inbounds` taint the consistency effect, which is semantically meaningful and prevents inference from running the function. In the future, we may want more precise tracking of inbounds that would let us recover some precision here. --- base/compiler/inferencestate.jl | 23 +++++++++++++++++- base/compiler/tfuncs.jl | 41 ++++++++++++++++++++++++++------- base/compiler/typeinfer.jl | 6 +++-- base/compiler/types.jl | 7 +++++- test/boundscheck_exec.jl | 12 ++++++++++ test/compiler/inference.jl | 10 ++++++++ 6 files changed, 87 insertions(+), 12 deletions(-) diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index 475b259cc04cf..7a35324926574 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -116,6 +116,16 @@ mutable struct InferenceState valid_worlds = WorldRange(src.min_world, src.max_world == typemax(UInt) ? get_world_counter() : src.max_world) + # TODO: Currently, any :inbounds declaration taints consistency, + # because we cannot be guaranteed whether or not boundschecks + # will be eliminated and if they are, we cnanot be guaranteed + # that no undefined behavior will occurr (the effects assumptions + # are stronger than the inbounds assumptions, since the latter + # requires dynamic reachability, while the former is global). + inbounds = inbounds_option() + inbounds_taints_consistency = !(inbounds == :on || (inbounds == :default && !any_inbounds(code))) + consistent = inbounds_taints_consistency ? TRISTATE_UNKNOWN : ALWAYS_TRUE + @assert cache === :no || cache === :local || cache === :global frame = new( params, result, linfo, @@ -129,7 +139,8 @@ mutable struct InferenceState Vector{InferenceState}(), # callers_in_cycle #=parent=#nothing, cache === :global, false, false, - Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE), + Effects(consistent, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, + inbounds_taints_consistency), CachedMethodTable(method_table(interp)), interp) result.result = frame @@ -139,6 +150,16 @@ mutable struct InferenceState end Effects(state::InferenceState) = state.ipo_effects +function any_inbounds(code::Vector{Any}) + for i=1:length(code) + stmt = code[i] + if isa(stmt, Expr) && stmt.head === :inbounds + return true + end + end + return false +end + function compute_trycatch(code::Vector{Any}, ip::BitSet) # The goal initially is to record the frame like this for the state at exit: # 1: (enter 3) # == 0 diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index 4342bcbec6e57..299e64456c8e6 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -701,7 +701,7 @@ function try_compute_fieldidx(typ::DataType, @nospecialize(field)) return field end -function getfield_nothrow(argtypes::Vector{Any}) +function getfield_boundscheck(argtypes::Vector{Any}) # ::Union{Bool, Nothing, Type{Bool}} if length(argtypes) == 2 boundscheck = Bool elseif length(argtypes) == 3 @@ -712,11 +712,21 @@ function getfield_nothrow(argtypes::Vector{Any}) elseif length(argtypes) == 4 boundscheck = argtypes[4] else - return false + return nothing + end + widenconst(boundscheck) !== Bool && return nothing + boundscheck = widenconditional(boundscheck) + if isa(boundscheck, Const) + return boundscheck.val + else + return Bool end - widenconst(boundscheck) !== Bool && return false - bounds_check_disabled = isa(boundscheck, Const) && boundscheck.val === false - return getfield_nothrow(argtypes[1], argtypes[2], !bounds_check_disabled) +end + +function getfield_nothrow(argtypes::Vector{Any}) + boundscheck = getfield_boundscheck(argtypes) + boundscheck === nothing && return false + return getfield_nothrow(argtypes[1], argtypes[2], !(boundscheck === false)) end function getfield_nothrow(@nospecialize(s00), @nospecialize(name), boundscheck::Bool) # If we don't have boundscheck and don't know the field, don't even bother @@ -1745,7 +1755,8 @@ function builtin_effects(f::Builtin, argtypes::Vector{Any}, rt) @assert !contains_is(_SPECIAL_BUILTINS, f) - if (f === Core.getfield || f === Core.isdefined) && length(argtypes) >= 2 + nothrow = false + if (f === Core.getfield || f === Core.isdefined) && length(argtypes) >= 3 # consistent if the argtype is immutable if isvarargtype(argtypes[2]) return Effects(TRISTATE_UNKNOWN, ALWAYS_TRUE, TRISTATE_UNKNOWN, ALWAYS_TRUE) @@ -1756,12 +1767,26 @@ function builtin_effects(f::Builtin, argtypes::Vector{Any}, rt) end s = s::DataType ipo_consistent = !ismutabletype(s) + nothrow = false + if f === Core.getfield && !isvarargtype(argtypes[end]) && + getfield_boundscheck(argtypes[2:end]) !== true + # If we cannot independently prove inboundsness, taint consistency. + # The inbounds-ness assertion requires dynamic reachability, while + # :consistent needs to be true for all input values. + # N.B. We do not taint for `--check-bounds=no` here -that happens in + # InferenceState. + nothrow = getfield_nothrow(argtypes[2], argtypes[3], true) + ipo_consistent &= nothrow + end else ipo_consistent = contains_is(_IDEMPOTENT_BUILTINS, f) end + # If we computed nothrow above for getfield, no need to repeat the procedure here + if !nothrow + nothrow = isvarargtype(argtypes[end]) ? false : + builtin_nothrow(f, argtypes[2:end], rt) + end effect_free = contains_is(_PURE_OR_ERROR_BUILTINS, f) || contains_is(_PURE_BUILTINS, f) - nothrow = isvarargtype(argtypes[end]) ? false : - builtin_nothrow(f, argtypes[2:end], rt) return Effects( ipo_consistent ? ALWAYS_TRUE : ALWAYS_FALSE, diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 9f678dff64b49..bf748950f134d 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -421,7 +421,7 @@ function rt_adjust_effects(@nospecialize(rt), ipo_effects::Effects) # Always throwing an error counts or never returning both count as consistent, # but we don't currently model idempontency using dataflow, so we don't notice. # Fix that up here to improve precision. - if rt === Union{} + if !ipo_effects.inbounds_taints_consistency && rt === Union{} return Effects(ALWAYS_TRUE, ipo_effects.effect_free, ipo_effects.nothrow, ipo_effects.terminates) end @@ -799,7 +799,9 @@ function tristate_merge(old::Effects, new::Effects) tristate_merge( old.nothrow, new.nothrow), tristate_merge( - old.terminates, new.terminates)) + old.terminates, new.terminates), + old.inbounds_taints_consistency || + new.inbounds_taints_consistency) end function tristate_merge!(caller::InferenceState, callee::Effects) diff --git a/base/compiler/types.jl b/base/compiler/types.jl index ab1c02c083498..af10903348993 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -32,7 +32,12 @@ struct Effects effect_free::TriState nothrow::TriState terminates::TriState + # This effect is currently only tracked in inference and modified + # :consistent before caching. We may want to track it in the future. + inbounds_taints_consistency::Bool end +Effects(consistent::TriState, effect_free::TriState, nothrow::TriState, terminates::TriState) = + Effects(consistent, effect_free, nothrow, terminates, false) Effects() = Effects(TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, TRISTATE_UNKNOWN) is_total_or_error(effects::Effects) = @@ -54,7 +59,7 @@ decode_effects(e::UInt8) = Effects(TriState(e & 0x3), TriState((e >> 2) & 0x3), TriState((e >> 4) & 0x3), - TriState((e >> 6) & 0x3)) + TriState((e >> 6) & 0x3), false) struct EffectsOverride consistent::Bool diff --git a/test/boundscheck_exec.jl b/test/boundscheck_exec.jl index 735cd88c13758..4040a2739730f 100644 --- a/test/boundscheck_exec.jl +++ b/test/boundscheck_exec.jl @@ -272,4 +272,16 @@ end end end + +# Test that --check-bounds=off doesn't permit const prop of indices into +# function that are not dynamically reachable (the same test for @inbounds +# is in the compiler tests). +function f_boundscheck_elim(n) + # Inbounds here assumes that this is only ever called with n==0, but of + # course the compiler has no way of knowing that, so it must not attempt + # to run the @inbounds `getfield(sin, 1)`` that ntuple generates. + ntuple(x->getfield(sin, x), n) +end +@test Tuple{} <: code_typed(f_boundscheck_elim, Tuple{Int})[1][2] + end diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index eef7d0eb91547..79030bd910990 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -4018,3 +4018,13 @@ const op_capture_world = f_mk_opaque() f_redefine_me(x) = x+2 @test op_capture_world() == 1 @test f_mk_opaque()() == 2 + +# Test that purity doesn't try to accidentally run unreachable code due to +# boundscheck elimination +function f_boundscheck_elim(n) + # Inbounds here assumes that this is only ever called with n==0, but of + # course the compiler has no way of knowing that, so it must not attempt + # to run the @inbounds `getfield(sin, 1)`` that ntuple generates. + ntuple(x->(@inbounds getfield(sin, x)), n) +end +@test Tuple{} <: code_typed(f_boundscheck_elim, Tuple{Int})[1][2] From c3fceeda6184ebd602f9a978e77a78b506d8d884 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sat, 22 Jan 2022 18:06:44 -0500 Subject: [PATCH 10/20] Allow constprop to refine effects --- base/compiler/abstractinterpretation.jl | 45 ++++++++++++++++++------- base/compiler/ssair/inlining.jl | 13 +++++-- base/compiler/stmtinfo.jl | 1 + test/compiler/inline.jl | 13 +++++++ 4 files changed, 56 insertions(+), 16 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index ff7db43cde28f..bd3a21dcbdb9f 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -93,16 +93,17 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), for sig_n in splitsigs result = abstract_call_method(interp, method, sig_n, svec(), multiple_matches, sv) rt, edge = result.rt, result.edge - tristate_merge!(sv, result.edge_effects) if edge !== nothing push!(edges, edge) end this_argtypes = isa(matches, MethodMatches) ? argtypes : matches.applicable_argtypes[i] this_arginfo = ArgInfo(fargs, this_argtypes) const_result = abstract_call_method_with_const_args(interp, result, f, this_arginfo, match, sv, false) + effects = result.edge_effects if const_result !== nothing - rt, const_result = const_result + (;rt, effects, const_result) = const_result end + tristate_merge!(sv, effects) push!(const_results, const_result) if const_result !== nothing any_const_result = true @@ -129,7 +130,6 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), result = abstract_call_method(interp, method, sig, match.sparams, multiple_matches, sv) this_rt, edge = result.rt, result.edge - tristate_merge!(sv, result.edge_effects) if edge !== nothing push!(edges, edge) end @@ -138,9 +138,12 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), this_argtypes = isa(matches, MethodMatches) ? argtypes : matches.applicable_argtypes[i] this_arginfo = ArgInfo(fargs, this_argtypes) const_result = abstract_call_method_with_const_args(interp, result, f, this_arginfo, match, sv, false) + effects = result.edge_effects if const_result !== nothing - this_rt, const_result = const_result + this_rt = const_result.rt + (; effects, const_result) = const_result end + tristate_merge!(sv, effects) push!(const_results, const_result) if const_result !== nothing any_const_result = true @@ -648,6 +651,12 @@ function const_prop_enabled(interp::AbstractInterpreter, sv::InferenceState, mat return true end +struct ConstCallResults + rt::Any + const_result::Union{InferenceResult, ConstResult} + effects::Effects +end + function abstract_call_method_with_const_args(interp::AbstractInterpreter, result::MethodCallResult, @nospecialize(f), arginfo::ArgInfo, match::MethodMatch, sv::InferenceState, va_override::Bool) @@ -657,8 +666,13 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, resul if f !== nothing && result.edge !== nothing && is_total_or_error(result.edge_effects) && is_all_const_arg(arginfo) rt = pure_eval_call_const_args(interp, f, arginfo.argtypes) add_backedge!(result.edge, sv) - rt === nothing && return Union{}, false # The evaulation threw. By :consistent-cy, we're guaranteed this would have happened at runtime - return rt, ConstResult(result.edge, rt.val) + rt === nothing && return ConstCallResults(Union{}, ConstResult(result.edge), result.edge_effects) # The evaulation threw. By :consistent-cy, we're guaranteed this would have happened at runtime + if is_inlineable_constant(rt.val) || call_result_unused(sv) + # If the constant is not inlineable, still do the const-prop, since the + # code that led to the creation of the Const may be inlineable in the same + # circumstance and may be optimizable. + return ConstCallResults(rt, ConstResult(result.edge, rt.val), EFFECTS_TOTAL) + end end mi = maybe_get_const_prop_profitable(interp, result, f, arginfo, match, sv) mi === nothing && return nothing @@ -694,7 +708,7 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, resul # if constant inference hits a cycle, just bail out isa(result, InferenceState) && return nothing add_backedge!(mi, sv) - return Pair{Any,InferenceResult}(result, inf_result) + return ConstCallResults(result, inf_result, inf_result.ipo_effects) end # if there's a possibility we could get a better result (hopefully without doing too much work) @@ -713,7 +727,8 @@ function maybe_get_const_prop_profitable(interp::AbstractInterpreter, result::Me return nothing end all_overridden = is_all_overridden(arginfo, sv) - if !force && !const_prop_function_heuristic(interp, f, arginfo, nargs, all_overridden, sv) + if !force && !const_prop_function_heuristic(interp, f, arginfo, nargs, all_overridden, + sv.ipo_effects.nothrow === ALWAYS_TRUE, sv) add_remark!(interp, sv, "[constprop] Disabled by function heuristic") return nothing end @@ -836,13 +851,17 @@ end function const_prop_function_heuristic( _::AbstractInterpreter, @nospecialize(f), (; argtypes)::ArgInfo, - nargs::Int, all_overridden::Bool, _::InferenceState) + nargs::Int, all_overridden::Bool, still_nothrow::Bool, _::InferenceState) if nargs > 1 if istopfunction(f, :getindex) || istopfunction(f, :setindex!) arrty = argtypes[2] # don't propagate constant index into indexing of non-constant array if arrty isa Type && arrty <: AbstractArray && !issingletontype(arrty) - return false + # For static arrays, allow the constprop if we could possibly + # deduce nothrow as a result. + if !still_nothrow || ismutabletype(arrty) + return false + end elseif arrty ⊑ Array return false end @@ -1428,7 +1447,7 @@ function abstract_invoke(interp::AbstractInterpreter, (; fargs, argtypes)::ArgIn # end const_result = abstract_call_method_with_const_args(interp, result, singleton_type(ft′), arginfo, match, sv, false) if const_result !== nothing - rt, const_result = const_result + (;rt, const_result) = const_result end return CallMeta(from_interprocedural!(rt, sv, arginfo, sig), InvokeCallInfo(match, const_result)) end @@ -1565,7 +1584,7 @@ function abstract_call_opaque_closure(interp::AbstractInterpreter, closure::Part const_result = abstract_call_method_with_const_args(interp, result, nothing, arginfo, match, sv, closure.isva) if const_result !== nothing - rt, const_result = const_result + (;rt, const_result) = const_result end end info = OpaqueClosureCallInfo(match, const_result) @@ -1773,7 +1792,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), end tristate_merge!(sv, Effects( !ismutabletype(t) ? ALWAYS_TRUE : ALWAYS_FALSE, - ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_TRUE)) + ALWAYS_TRUE, is_nothrow ? ALWAYS_TRUE : ALWAYS_FALSE, ALWAYS_TRUE)) elseif ehead === :splatnew t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) is_nothrow = false # TODO: More precision diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 464f111d6c603..a4aa72343362d 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -1247,9 +1247,10 @@ function handle_const_call!( continue end if isa(result, ConstResult) - case = ConstantCase(quoted(result.result)) - if !is_inlineable_constant(result.result) - case = compileable_specialization(state.et, result.mi, Effects()) + if !isdefined(result, :result) || !is_inlineable_constant(result.result) + case = compileable_specialization(state.et, result.mi, EFFECTS_TOTAL) + else + case = ConstantCase(quoted(result.result)) end signature_union = Union{signature_union, result.mi.specTypes} push!(cases, InliningCase(result.mi.specTypes, case)) @@ -1431,6 +1432,7 @@ function early_inline_special_case( params::OptimizationParams) params.inlining || return nothing (; f, ft, argtypes) = sig + if isa(type, Const) # || isconstType(type) val = type.val is_inlineable_constant(val) || return nothing @@ -1462,11 +1464,14 @@ function late_inline_special_case!( params::OptimizationParams) params.inlining || return nothing (; f, ft, argtypes) = sig + isinlining = params.inlining if length(argtypes) == 3 && istopfunction(f, :!==) # special-case inliner for !== that precedes _methods_by_ftype union splitting # and that works, even though inference generally avoids inferring the `!==` Method if isa(type, Const) return SomeCase(quoted(type.val)) + elseif !isinlining + return nothing end cmp_call = Expr(:call, GlobalRef(Core, :(===)), stmt.args[2], stmt.args[3]) cmp_call_ssa = insert_node!(ir, idx, effect_free(NewInstruction(cmp_call, Bool))) @@ -1477,6 +1482,8 @@ function late_inline_special_case!( # that works, even though inference generally avoids inferring the `>:` Method if isa(type, Const) && _builtin_nothrow(<:, Any[argtypes[3], argtypes[2]], type) return SomeCase(quoted(type.val)) + elseif !isinlining + return nothing end subtype_call = Expr(:call, GlobalRef(Core, :(<:)), stmt.args[3], stmt.args[2]) return SomeCase(subtype_call) diff --git a/base/compiler/stmtinfo.jl b/base/compiler/stmtinfo.jl index 2bb4298173fde..25f5d54a24669 100644 --- a/base/compiler/stmtinfo.jl +++ b/base/compiler/stmtinfo.jl @@ -50,6 +50,7 @@ end struct ConstResult mi::MethodInstance result + ConstResult(mi::MethodInstance) = new(mi) ConstResult(mi::MethodInstance, @nospecialize val) = new(mi, val) end diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 96ea63e2cafa8..0ae816614b484 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -960,3 +960,16 @@ let b = Expr(:block, (:(y += sin($x)) for x in randn(1000))...) end end @test fully_eliminated(f_sin_perf, Tuple{}) + +# Test that we inline the constructor of something that is not const-inlineable +const THE_REF = Ref{Int}(0) +struct FooTheRef + x::Ref + FooTheRef() = new(THE_REF) +end +f_make_the_ref() = FooTheRef() +f_make_the_ref_but_dont_return_it() = (FooTheRef(); nothing) +let src = code_typed1(f_make_the_ref, ()) + @test count(x->isexpr(x, :new), src.code) == 1 +end +@test fully_eliminated(f_make_the_ref_but_dont_return_it, Tuple{}) From 7f14975e8e604addf519cdfe486bd75eaa261580 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Thu, 3 Feb 2022 03:45:13 +0000 Subject: [PATCH 11/20] Properly taint unknown call in apply --- base/compiler/abstractinterpretation.jl | 1 + test/compiler/inline.jl | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index bd3a21dcbdb9f..aada6b2d4dc17 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -1121,6 +1121,7 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, sv:: if !isa(aft, Const) && !isa(aft, PartialOpaque) && (!isType(aftw) || has_free_typevars(aftw)) if !isconcretetype(aftw) || (aftw <: Builtin) add_remark!(interp, sv, "Core._apply_iterate called on a function of a non-concrete type") + tristate_merge!(sv, Effects()) # bail now, since it seems unlikely that abstract_call will be able to do any better after splitting # this also ensures we don't call abstract_call_gf_by_type below on an IntrinsicFunction or Builtin return CallMeta(Any, false) diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 0ae816614b484..b9f4cd5c96236 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -973,3 +973,11 @@ let src = code_typed1(f_make_the_ref, ()) @test count(x->isexpr(x, :new), src.code) == 1 end @test fully_eliminated(f_make_the_ref_but_dont_return_it, Tuple{}) + +# Test that the Core._apply_iterate bail path taints effects +function f_apply_bail(f) + f(()...) + return nothing +end +f_call_apply_bail(f) = f_apply_bail(f) +@test !fully_eliminated(f_call_apply_bail, Tuple{Function}) From 4b506e59450770c71394cd57966ac1b6bc040565 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Thu, 3 Feb 2022 04:44:31 +0000 Subject: [PATCH 12/20] Add NEWS and doc anchor --- NEWS.md | 1 + doc/src/base/base.md | 1 + 2 files changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index d5406a8661875..12315bff68ac6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -57,6 +57,7 @@ Compiler/Runtime improvements * Abstract callsite can now be inlined or statically resolved as far as the callsite has a single matching method ([#43113]). * Builtin function are now a bit more like generic functions, and can be enumerated with `methods` ([#43865]). +* Inference now tracks various effects such as sideeffectful-ness and nothrow-ness on a per-specialization basis. Code heavilty dependent on constant propagation should see significant compile-time performance improvements and certain cases (e.g. calls to unlinineable functions that are nevertheless effect free) should see runtime performance improvements. Effects may be overwritten manually with the `@Base.assume_effects` macro. (#43852). Command-line option changes --------------------------- diff --git a/doc/src/base/base.md b/doc/src/base/base.md index 80280997b30d1..93d0547098706 100644 --- a/doc/src/base/base.md +++ b/doc/src/base/base.md @@ -285,6 +285,7 @@ Base.@simd Base.@polly Base.@generated Base.@pure +Base.@assume_effects Base.@deprecate ``` From c2ba6538e9e1fb7d5f260a817cc78f88ceb6ecdf Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Thu, 3 Feb 2022 22:50:24 +0000 Subject: [PATCH 13/20] Correct effect modeling for arraysize --- base/compiler/optimize.jl | 2 +- base/compiler/ssair/inlining.jl | 2 +- base/compiler/tfuncs.jl | 8 ++++---- base/strings/string.jl | 2 +- test/compiler/inline.jl | 3 +++ 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index e8d6f66e49d53..58f20b5ef2a0c 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -215,7 +215,7 @@ function stmt_effect_free(@nospecialize(stmt), @nospecialize(rt), src::Union{IRC M, s = argextype(args[2], src), argextype(args[3], src) return get_binding_type_effect_free(M, s) end - contains_is(_PURE_OR_ERROR_BUILTINS, f) || return false + contains_is(_EFFECT_FREE_BUILTINS, f) || return false rt === Bottom && return false return _builtin_nothrow(f, Any[argextype(args[i], src) for i = 2:length(args)], rt) elseif head === :new diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index a4aa72343362d..a8dd6867568c3 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -1442,7 +1442,7 @@ function early_inline_special_case( end elseif ispuretopfunction(f) || contains_is(_PURE_BUILTINS, f) return SomeCase(quoted(val)) - elseif contains_is(_PURE_OR_ERROR_BUILTINS, f) + elseif contains_is(_EFFECT_FREE_BUILTINS, f) if _builtin_nothrow(f, argtypes[2:end], type) return SomeCase(quoted(val)) end diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index 299e64456c8e6..c7cc9e872c633 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -1717,12 +1717,12 @@ end # known to be always effect-free (in particular nothrow) const _PURE_BUILTINS = Any[tuple, svec, ===, typeof, nfields] -# known to be effect-free if the are nothrow -const _PURE_OR_ERROR_BUILTINS = [ +# known to be effect-free (but not necessarily nothrow) +const _EFFECT_FREE_BUILTINS = [ fieldtype, apply_type, isa, UnionAll, getfield, arrayref, const_arrayref, isdefined, Core.sizeof, Core.kwfunc, Core.ifelse, Core._typevar, (<:), - typeassert, throw + typeassert, throw, arraysize ] const _IDEMPOTENT_BUILTINS = Any[ @@ -1786,7 +1786,7 @@ function builtin_effects(f::Builtin, argtypes::Vector{Any}, rt) nothrow = isvarargtype(argtypes[end]) ? false : builtin_nothrow(f, argtypes[2:end], rt) end - effect_free = contains_is(_PURE_OR_ERROR_BUILTINS, f) || contains_is(_PURE_BUILTINS, f) + effect_free = contains_is(_EFFECT_FREE_BUILTINS, f) || contains_is(_PURE_BUILTINS, f) return Effects( ipo_consistent ? ALWAYS_TRUE : ALWAYS_FALSE, diff --git a/base/strings/string.jl b/base/strings/string.jl index b0ea1976899cd..c37e36594119e 100644 --- a/base/strings/string.jl +++ b/base/strings/string.jl @@ -97,7 +97,7 @@ String(s::CodeUnits{UInt8,String}) = s.s pointer(s::String) = unsafe_convert(Ptr{UInt8}, s) pointer(s::String, i::Integer) = pointer(s) + Int(i)::Int - 1 -@pure ncodeunits(s::String) = Core.sizeof(s) +ncodeunits(s::String) = Core.sizeof(s) codeunit(s::String) = UInt8 @inline function codeunit(s::String, i::Integer) diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index b9f4cd5c96236..2e8e3181be1ef 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -981,3 +981,6 @@ function f_apply_bail(f) end f_call_apply_bail(f) = f_apply_bail(f) @test !fully_eliminated(f_call_apply_bail, Tuple{Function}) + +# Test that arraysize has proper effect modeling +@test fully_eliminated(M->(size(M, 2); nothing), Tuple{Matrix{Float64}}) From 012d762427370689ece0dc44bb45bc4fdd383b1d Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Thu, 3 Feb 2022 23:22:47 +0000 Subject: [PATCH 14/20] Address Shuhei's review --- base/compiler/abstractinterpretation.jl | 20 +++++++++++++------- base/compiler/inferencestate.jl | 4 ++-- base/compiler/ssair/inlining.jl | 2 +- base/compiler/stmtinfo.jl | 6 +++--- base/compiler/typeinfer.jl | 19 ------------------- base/compiler/types.jl | 19 +++++++++++++++++++ base/expr.jl | 6 +++--- 7 files changed, 41 insertions(+), 35 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index aada6b2d4dc17..ca75e817bf105 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -60,7 +60,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), conditionals = nothing # keeps refinement information of call argument types when the return type is boolean seen = 0 # number of signatures actually inferred any_const_result = false - const_results = Union{InferenceResult,Nothing,ConstResult,Bool}[] + const_results = Union{InferenceResult,Nothing,ConstResult}[] multiple_matches = napplicable > 1 if f !== nothing && napplicable == 1 && is_method_pure(applicable[1]::MethodMatch) @@ -179,7 +179,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), if seen != napplicable tristate_merge!(sv, Effects()) elseif !(atype <: merged_sig) - # Account for the fact that we may encounter a non-covered signature. + # Account for the fact that we may encounter a MethodError with a non-covered signature. tristate_merge!(sv, Effects(ALWAYS_TRUE, ALWAYS_TRUE, TRISTATE_UNKNOWN, ALWAYS_TRUE)) end @@ -624,7 +624,8 @@ function is_all_const_arg((; fargs, argtypes)::ArgInfo) return true end -function pure_eval_call_const_args(interp::AbstractInterpreter, +function concrete_eval_const_proven_total_or_error( + interp::AbstractInterpreter, @nospecialize(f), argtypes::Vector{Any}) args = Any[ (a = widenconditional(argtypes[i]); isa(a, Const) ? a.val : @@ -655,6 +656,10 @@ struct ConstCallResults rt::Any const_result::Union{InferenceResult, ConstResult} effects::Effects + ConstCallResults(@nospecialize(rt), + const_result::Union{InferenceResult, ConstResult}, + effects::Effects) = + new(rt, const_result, effects) end function abstract_call_method_with_const_args(interp::AbstractInterpreter, result::MethodCallResult, @@ -664,7 +669,7 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, resul return nothing end if f !== nothing && result.edge !== nothing && is_total_or_error(result.edge_effects) && is_all_const_arg(arginfo) - rt = pure_eval_call_const_args(interp, f, arginfo.argtypes) + rt = concrete_eval_const_proven_total_or_error(interp, f, arginfo.argtypes) add_backedge!(result.edge, sv) rt === nothing && return ConstCallResults(Union{}, ConstResult(result.edge), result.edge_effects) # The evaulation threw. By :consistent-cy, we're guaranteed this would have happened at runtime if is_inlineable_constant(rt.val) || call_result_unused(sv) @@ -1764,7 +1769,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), if at === Bottom t = Bottom tristate_merge!(sv, Effects( - !ismutabletype(t) ? ALWAYS_TRUE : ALWAYS_FALSE, + ALWAYS_TRUE, # N.B depends on !ismutabletype(t) above ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_TRUE)) @goto t_computed elseif !isa(at, Const) @@ -2022,9 +2027,10 @@ function widenreturn(@nospecialize(rt), @nospecialize(bestguess), nslots::Int, s return widenconst(rt) end -function handle_control_backedge!(frame::InferenceState, from, to) +function handle_control_backedge!(frame::InferenceState, from::Int, to::Int) if from > to - if isa(frame.linfo.def, Method) && decode_effects_override(frame.linfo.def.purity).terminates_locally + def = frame.linfo.def + if isa(def, Method) && decode_effects_override(def.purity).terminates_locally return end tristate_merge!(frame, Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, TRISTATE_UNKNOWN)) diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index 7a35324926574..d079bcac7582a 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -118,12 +118,12 @@ mutable struct InferenceState # TODO: Currently, any :inbounds declaration taints consistency, # because we cannot be guaranteed whether or not boundschecks - # will be eliminated and if they are, we cnanot be guaranteed + # will be eliminated and if they are, we cannot be guaranteed # that no undefined behavior will occurr (the effects assumptions # are stronger than the inbounds assumptions, since the latter # requires dynamic reachability, while the former is global). inbounds = inbounds_option() - inbounds_taints_consistency = !(inbounds == :on || (inbounds == :default && !any_inbounds(code))) + inbounds_taints_consistency = !(inbounds === :on || (inbounds === :default && !any_inbounds(code))) consistent = inbounds_taints_consistency ? TRISTATE_UNKNOWN : ALWAYS_TRUE @assert cache === :no || cache === :local || cache === :global diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index a8dd6867568c3..2b1e94904136e 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -52,7 +52,7 @@ struct SomeCase end struct InvokeCase - invoke::Union{MethodInstance, Nothing} + invoke::MethodInstance effects::Effects end diff --git a/base/compiler/stmtinfo.jl b/base/compiler/stmtinfo.jl index 25f5d54a24669..e3f69b2c43e54 100644 --- a/base/compiler/stmtinfo.jl +++ b/base/compiler/stmtinfo.jl @@ -63,7 +63,7 @@ the inference results with constant information `info.results::Vector{Union{Noth """ struct ConstCallInfo call::Union{MethodMatchInfo,UnionSplitInfo} - results::Vector{Union{Nothing,InferenceResult,ConstResult,Bool}} + results::Vector{Union{Nothing,InferenceResult,ConstResult}} end """ @@ -129,7 +129,7 @@ Optionally keeps `info.result::InferenceResult` that keeps constant information. """ struct InvokeCallInfo match::MethodMatch - result::Union{Nothing,InferenceResult,ConstResult,Bool} + result::Union{Nothing,InferenceResult,ConstResult} end """ @@ -141,7 +141,7 @@ Optionally keeps `info.result::InferenceResult` that keeps constant information. """ struct OpaqueClosureCallInfo match::MethodMatch - result::Union{Nothing,InferenceResult,ConstResult,Bool} + result::Union{Nothing,InferenceResult,ConstResult} end """ diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index bf748950f134d..eee9f665d6115 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -785,25 +785,6 @@ end generating_sysimg() = ccall(:jl_generating_output, Cint, ()) != 0 && JLOptions().incremental == 0 -function tristate_merge(old, new) - (old === ALWAYS_FALSE || new === ALWAYS_FALSE) && return ALWAYS_FALSE - old === TRISTATE_UNKNOWN && return old - return new -end - -function tristate_merge(old::Effects, new::Effects) - Effects(tristate_merge( - old.consistent, new.consistent), - tristate_merge( - old.effect_free, new.effect_free), - tristate_merge( - old.nothrow, new.nothrow), - tristate_merge( - old.terminates, new.terminates), - old.inbounds_taints_consistency || - new.inbounds_taints_consistency) -end - function tristate_merge!(caller::InferenceState, callee::Effects) caller.ipo_effects = tristate_merge(caller.ipo_effects, callee) end diff --git a/base/compiler/types.jl b/base/compiler/types.jl index af10903348993..c6086b11543e6 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -27,6 +27,12 @@ const ALWAYS_FALSE = TriState(0x00) const ALWAYS_TRUE = TriState(0x01) const TRISTATE_UNKNOWN = TriState(0x02) +function tristate_merge(old::TriState, new::TriState) + (old === ALWAYS_FALSE || new === ALWAYS_FALSE) && return ALWAYS_FALSE + old === TRISTATE_UNKNOWN && return old + return new +end + struct Effects consistent::TriState effect_free::TriState @@ -61,6 +67,19 @@ decode_effects(e::UInt8) = TriState((e >> 4) & 0x3), TriState((e >> 6) & 0x3), false) +function tristate_merge(old::Effects, new::Effects) + Effects(tristate_merge( + old.consistent, new.consistent), + tristate_merge( + old.effect_free, new.effect_free), + tristate_merge( + old.nothrow, new.nothrow), + tristate_merge( + old.terminates, new.terminates), + old.inbounds_taints_consistency || + new.inbounds_taints_consistency) +end + struct EffectsOverride consistent::Bool effect_free::Bool diff --git a/base/expr.jl b/base/expr.jl index c9e56156b7aff..6b7aed117ed2c 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -376,7 +376,7 @@ end @assume_effects(setting..., ex) `@assume_effects` overrides the compiler's effect modeling for the given method. -`ex` must be a method definition. +`ex` must be a method definition or `@ccall` expression. WARNING: Improper use of this macro causes undefined behavior (including crashes, incorrect answers, or other hard to track bugs). Use with care and only if absolutely @@ -399,7 +399,7 @@ Note: This in particular implies that the return value of the method must be immutable. Multiple allocations of mutable objects (even with identical contents) are not egal. -Note: The :consistent-cy assertion is made world-arge wise. More formally, write +Note: The :consistent-cy assertion is made world-age wise. More formally, write fᵢ for the evaluation of `f` in world-age `i`, then we require: ∀ i, x, y: x === y → fᵢ(x) === fᵢ(y) @@ -410,7 +410,7 @@ Note: A further implication is that :consistent functions may not make their return value dependent on the state of the heap or any other global state that is not constant for a given world age. -Note: The :consistent-cy includes all legal rewrites performed by the optimizizer. +Note: The :consistent-cy includes all legal rewrites performed by the optimizer. For example, floating-point fastmath operations are not considered :consistent, because the optimizer may rewrite them causing the output to not be :consistent, even for the same world age (e.g. because one ran in the interpreter, while From c25f4de1018d50b5d31edd5eb4aa795fece862cd Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Fri, 4 Feb 2022 00:12:47 +0000 Subject: [PATCH 15/20] Fix regression on inference time benchmark The issue wasn't actually the changes here, they just added additional error paths which bridged inference into the Base printing code, which as usual takes a fairly long time to infer. Add some judicious barriers and nospecialize statements to bring inference time back down. --- base/abstractarray.jl | 14 ++++++++++---- base/array.jl | 2 +- base/errorshow.jl | 4 ++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/base/abstractarray.jl b/base/abstractarray.jl index 98ac697ab4d12..1b21201ffa3e8 100644 --- a/base/abstractarray.jl +++ b/base/abstractarray.jl @@ -1238,10 +1238,16 @@ function unsafe_getindex(A::AbstractArray, I...) r end +struct CanonicalIndexError + func::String + type::Any + CanonicalIndexError(func::String, @nospecialize(type)) = new(func, type) +end + error_if_canonical_getindex(::IndexLinear, A::AbstractArray, ::Int) = - error("getindex not defined for ", typeof(A)) + throw(CanonicalIndexError("getindex", typeof(A))) error_if_canonical_getindex(::IndexCartesian, A::AbstractArray{T,N}, ::Vararg{Int,N}) where {T,N} = - error("getindex not defined for ", typeof(A)) + throw(CanonicalIndexError("getindex", typeof(A))) error_if_canonical_getindex(::IndexStyle, ::AbstractArray, ::Any...) = nothing ## Internal definitions @@ -1333,9 +1339,9 @@ function unsafe_setindex!(A::AbstractArray, v, I...) end error_if_canonical_setindex(::IndexLinear, A::AbstractArray, ::Int) = - error("setindex! not defined for ", typeof(A)) + throw(CanonicalIndexError("setindex!", typeof(A))) error_if_canonical_setindex(::IndexCartesian, A::AbstractArray{T,N}, ::Vararg{Int,N}) where {T,N} = - error("setindex! not defined for ", typeof(A)) + throw(CanonicalIndexError("setindex!", typeof(A))) error_if_canonical_setindex(::IndexStyle, ::AbstractArray, ::Any...) = nothing ## Internal definitions diff --git a/base/array.jl b/base/array.jl index 807f99342e25f..5b9b5b25dcf15 100644 --- a/base/array.jl +++ b/base/array.jl @@ -213,7 +213,7 @@ function bitsunionsize(u::Union) end length(a::Array) = arraylen(a) -elsize(::Type{<:Array{T}}) where {T} = aligned_sizeof(T) +elsize(@nospecialize _::Type{A}) where {T,A<:Array{T}} = aligned_sizeof(T) sizeof(a::Array) = Core.sizeof(a) function isassigned(a::Array, i::Int...) diff --git a/base/errorshow.jl b/base/errorshow.jl index 8b8ec532355a8..eaa294a8f7a46 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -170,6 +170,10 @@ function showerror(io::IO, ex::InexactError) Experimental.show_error_hints(io, ex) end +function showerror(io::IO, ex::CanonicalIndexError) + print(io, "CanonicalIndexError: ", ex.func, " not defined for ", ex.type) +end + typesof(@nospecialize args...) = Tuple{Any[ Core.Typeof(args[i]) for i in 1:length(args) ]...} function print_with_compare(io::IO, @nospecialize(a::DataType), @nospecialize(b::DataType), color::Symbol) From bfeaf3e814e6d8442151e3a2fb5b563101670501 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Fri, 4 Feb 2022 16:19:32 +0900 Subject: [PATCH 16/20] refine docstrings of `@assume_effects` This commit tries to render the docstring of `@assume_effects` within Documenter.jl-generated HTML: - render bullet points - codify the names of settings - use math syntax - use note admonitions --- base/expr.jl | 138 +++++++++++++++++++++++++++++---------------------- 1 file changed, 79 insertions(+), 59 deletions(-) diff --git a/base/expr.jl b/base/expr.jl index 6b7aed117ed2c..59e6b075d8db9 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -388,91 +388,109 @@ true. These assertions are made for all world ages. It is thus advisable to limi the use of generic functions that may later be extended to invalidate the assumption (which would cause undefined behavior). -The following `settings` are supported. +The following `setting`s are supported. +- `:consistent` +- `:effect_free` +- `:nothrow` +- `:terminates_globally` +- `:terminates_locally` +- `:total` + +--- # `:consistent` The `:consistent` setting asserts that for egal inputs: - - The manner of termination (return value, exception, non-termination) will always be the same. - - If the method returns, the results will always be egal. - -Note: This in particular implies that the return value of the method must be - immutable. Multiple allocations of mutable objects (even with identical - contents) are not egal. +- The manner of termination (return value, exception, non-termination) will always be the same. +- If the method returns, the results will always be egal. -Note: The :consistent-cy assertion is made world-age wise. More formally, write - fᵢ for the evaluation of `f` in world-age `i`, then we require: - - ∀ i, x, y: x === y → fᵢ(x) === fᵢ(y) +!!! note + This in particular implies that the return value of the method must be + immutable. Multiple allocations of mutable objects (even with identical + contents) are not egal. - However, for two world ages `i, j` s.t. `i != j`, we may have `fᵢ(x) !== fⱼ(y)`. +!!! note + The `:consistent`-cy assertion is made world-age wise. More formally, write + ``fᵢ`` for the evaluation of ``f`` in world-age ``i``, then we require: + ```math + ∀ i, x, y: x ≡ y → fᵢ(x) ≡ fᵢ(y) + ``` + However, for two world ages ``i``, ``j`` s.t. ``i ≠ j``, we may have ``fᵢ(x) ≢ fⱼ(y)``. -Note: A further implication is that :consistent functions may not make their - return value dependent on the state of the heap or any other global state - that is not constant for a given world age. + A further implication is that `:consistent` functions may not make their + return value dependent on the state of the heap or any other global state + that is not constant for a given world age. -Note: The :consistent-cy includes all legal rewrites performed by the optimizer. - For example, floating-point fastmath operations are not considered :consistent, - because the optimizer may rewrite them causing the output to not be :consistent, - even for the same world age (e.g. because one ran in the interpreter, while - the other was optimized). +!!! note + The `:consistent`-cy includes all legal rewrites performed by the optimizer. + For example, floating-point fastmath operations are not considered `:consistent`, + because the optimizer may rewrite them causing the output to not be `:consistent`, + even for the same world age (e.g. because one ran in the interpreter, while + the other was optimized). -Note: If :consistent functions terminate by throwing an exception, that exception - itself is not required to meet the egality requirement specified above. +!!! note + If `:consistent` functions terminate by throwing an exception, that exception + itself is not required to meet the egality requirement specified above. +--- # `:effect_free` The `:effect_free` setting asserts that the method is free of externally semantically visible side effects. The following is an incomplete list of externally semantically visible side effects: - - - Changing the value of a global variable. - - Mutating the heap (e.g. an array or mutable value), except as noted below - - Changing the method table (e.g. through calls to eval) - - File/Network/etc. I/O - - Task switching +- Changing the value of a global variable. +- Mutating the heap (e.g. an array or mutable value), except as noted below +- Changing the method table (e.g. through calls to eval) +- File/Network/etc. I/O +- Task switching However, the following are explicitly not semantically visible, even if they may be observable: - - - Memory allocations (both mutable and immutable) - - Elapsed time - - Garbage collection - - Heap mutations of objects whose lifetime does not exceed the method (i.e. - were allocated in the method and do not escape). - - The returned value (which is externally visible, but not a side effect) +- Memory allocations (both mutable and immutable) +- Elapsed time +- Garbage collection +- Heap mutations of objects whose lifetime does not exceed the method (i.e. + were allocated in the method and do not escape). +- The returned value (which is externally visible, but not a side effect) The rule of thumb here is that an externally visible side effect is anything that would affect the execution of the remainder of the program if the function were not executed. -Note: The effect free assertion is made both for the method itself and any code - that is executed by the method. Keep in mind that the assertion must be - valid for all world ages and limit use of this assertion accordingly. +!!! note + The `:effect_free` assertion is made both for the method itself and any code + that is executed by the method. Keep in mind that the assertion must be + valid for all world ages and limit use of this assertion accordingly. +--- # `:nothrow` The `:nothrow` settings asserts that this method does not terminate abnormally (i.e. will either always return a value or never return). -Note: It is permissible for :nothrow annotated methods to make use of exception - handling internally as long as the exception is not rethrown out of the - method itself. +!!! note + It is permissible for `:nothrow` annotated methods to make use of exception + handling internally as long as the exception is not rethrown out of the + method itself. -Note: MethodErrors and similar exceptions count as abnormal termination. +!!! note + `MethodErrors` and similar exceptions count as abnormal termination. +--- # `:terminates_globally` The `:terminates_globally` settings asserts that this method will eventually terminate (either normally or abnormally), i.e. does not loop indefinitely. -Note: The compiler will consider this a strong indication that the method will - terminate relatively *quickly* and may (if otherwise legal), call this - method at compile time. I.e. it is a bad idea to annotate this setting - on a method that *technically*, but not *practically*, terminates. +!!! note + This `:terminates_globally` assertion covers any other methods called by the annotated method. -Note: The `terminates_globally` assertion covers any other methods called by - the annotated method. +!!! note + The compiler will consider this a strong indication that the method will + terminate relatively *quickly* and may (if otherwise legal), call this + method at compile time. I.e. it is a bad idea to annotate this setting + on a method that *technically*, but not *practically*, terminates. +--- # `:terminates_locally` The `:terminates_locally` setting is like `:terminates_globally`, except that it only @@ -480,21 +498,24 @@ applies to syntactic control flow *within* the annotated method. It is thus a much weaker (and thus safer) assertion that allows for the possibility of non-termination if the method calls some other method that does not terminate. -Note: `terminates_globally` implies `terminates_locally`. +!!! note + `:terminates_globally` implies `:terminates_locally`. +--- # `:total` -The `setting` combines the following other assertions: - - `:consistent` - - `:effect_free` - - `:nothrow` - - `:terminates_globally` +This `setting` combines the following other assertions: +- `:consistent` +- `:effect_free` +- `:nothrow` +- `:terminates_globally` and is a convenient shortcut. -Note: `@assume_effects :total` is similar to `@Base.pure` 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 - `@Base.pure` is always total. +!!! note + `@assume_effects :total` is similar to `@Base.pure` 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 + `@Base.pure` is always `:total`. """ macro assume_effects(args...) (consistent, effect_free, nothrow, terminates_globally, terminates_locally) = @@ -531,7 +552,6 @@ macro assume_effects(args...) return esc(pushmeta!(ex, :purity, consistent, effect_free, nothrow, terminates_globally, terminates_locally)) end - """ @propagate_inbounds From f0b081b2028928a1fba6b9507fc8e7b758fa4b84 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Fri, 4 Feb 2022 16:34:04 +0900 Subject: [PATCH 17/20] improve effect analysis on allocation Improves `:nothrow` assertion for mutable allocations. Also adds missing `IR_FLAG_EFFECT_FREE` flagging for non-inlined callees in `handle_single_case!` so that we can do more dead code elimination. --- base/compiler/abstractinterpretation.jl | 23 ++++++++++++--------- base/compiler/optimize.jl | 2 +- base/compiler/ssair/inlining.jl | 7 ++++--- test/compiler/inline.jl | 27 +++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index ca75e817bf105..413447db48b8a 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -1696,9 +1696,9 @@ end function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize(e), vtypes::VarTable, sv::InferenceState) if isa(e, QuoteNode) - return Const((e::QuoteNode).value) + return Const(e.value) elseif isa(e, SSAValue) - return abstract_eval_ssavalue(e::SSAValue, sv.src) + return abstract_eval_ssavalue(e, sv) elseif isa(e, SlotNumber) || isa(e, Argument) return vtypes[slot_id(e)].typ elseif isa(e, GlobalRef) @@ -1755,16 +1755,18 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), end elseif ehead === :new t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) - is_nothrow = false - if isconcretetype(t) && !ismutabletype(t) + is_nothrow = true + if isconcretetype(t) + fcount = fieldcount(t) nargs = length(e.args) - 1 + is_nothrow && (is_nothrow = fcount ≥ nargs) ats = Vector{Any}(undef, nargs) local anyrefine = false local allconst = true - is_nothrow = isexact && isconcretedispatch(t) for i = 2:length(e.args) at = widenconditional(abstract_eval_value(interp, e.args[i], vtypes, sv)) ft = fieldtype(t, i-1) + is_nothrow && (is_nothrow = at ⊑ ft) at = tmeet(at, ft) if at === Bottom t = Bottom @@ -1781,8 +1783,10 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), end ats[i-1] = at end - # For now, don't allow partially initialized Const/PartialStruct - if fieldcount(t) == nargs + # For now, don't allow: + # - Const/PartialStruct of mutables + # - partially initialized Const/PartialStruct + if !ismutabletype(t) && fcount == nargs if allconst argvals = Vector{Any}(undef, nargs) for j in 1:nargs @@ -1792,9 +1796,9 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), elseif anyrefine t = PartialStruct(t, ats) end - else - is_nothrow = false end + else + is_nothrow = false end tristate_merge!(sv, Effects( !ismutabletype(t) ? ALWAYS_TRUE : ALWAYS_FALSE, @@ -1948,6 +1952,7 @@ function abstract_eval_global(M::Module, s::Symbol, frame::InferenceState) return Any end +abstract_eval_ssavalue(s::SSAValue, sv::InferenceState) = abstract_eval_ssavalue(s, sv.src) function abstract_eval_ssavalue(s::SSAValue, src::CodeInfo) typ = (src.ssavaluetypes::Vector{Any})[s.id] if typ === NOT_FOUND diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 58f20b5ef2a0c..156c0457dfc6d 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -223,7 +223,7 @@ function stmt_effect_free(@nospecialize(stmt), @nospecialize(rt), src::Union{IRC # `Expr(:new)` of unknown type could raise arbitrary TypeError. typ, isexact = instanceof_tfunc(typ) isexact || return false - isconcretedispatch(typ) || return false + isconcretetype(typ) || return false typ = typ::DataType fieldcount(typ) >= length(args) - 1 || return false for fld_idx in 1:(length(args) - 1) diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 2b1e94904136e..5bc31171cd0ce 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -869,12 +869,13 @@ function handle_single_case!( if isa(case, ConstantCase) ir[SSAValue(idx)][:inst] = case.val elseif isa(case, InvokeCase) - if is_total(case.effects) - inline_const_if_inlineable!(ir[SSAValue(idx)]) && return nothing - end + is_total(case.effects) && inline_const_if_inlineable!(ir[SSAValue(idx)]) && return nothing isinvoke && rewrite_invoke_exprargs!(stmt) stmt.head = :invoke pushfirst!(stmt.args, case.invoke) + if is_removable_if_unused(case.effects) + ir[SSAValue(idx)][:flag] |= IR_FLAG_EFFECT_FREE + end elseif case === nothing # Do, well, nothing else diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 2e8e3181be1ef..5737d99055710 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -984,3 +984,30 @@ f_call_apply_bail(f) = f_apply_bail(f) # Test that arraysize has proper effect modeling @test fully_eliminated(M->(size(M, 2); nothing), Tuple{Matrix{Float64}}) + +# DCE of non-inlined callees +@noinline noninlined_dce_simple(a) = identity(a) +@test fully_eliminated((String,)) do s + noninlined_dce_simple(s) + nothing +end +@noinline noninlined_dce_new(a::String) = Some(a) +@test fully_eliminated((String,)) do s + noninlined_dce_new(s) + nothing +end +mutable struct SafeRef{T} + x::T +end +Base.getindex(s::SafeRef) = getfield(s, 1) +Base.setindex!(s::SafeRef, x) = setfield!(s, 1, x) +@noinline noninlined_dce_new(a::Symbol) = SafeRef(a) +@test fully_eliminated((Symbol,)) do s + noninlined_dce_new(s) + nothing +end +# should be resolved once we merge https://github.com/JuliaLang/julia/pull/43923 +@test_broken fully_eliminated((Union{Symbol,String},)) do s + noninlined_dce_new(s) + nothing +end From 511ddfa0306e3931a5f1094c2e29fd2c57e15239 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Wed, 9 Feb 2022 12:03:09 +0900 Subject: [PATCH 18/20] address some reviews --- base/compiler/abstractinterpretation.jl | 2 +- base/compiler/optimize.jl | 2 +- base/compiler/ssair/inlining.jl | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 413447db48b8a..0233b8b22f363 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -1756,7 +1756,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), elseif ehead === :new t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) is_nothrow = true - if isconcretetype(t) + if isconcretedispatch(t) fcount = fieldcount(t) nargs = length(e.args) - 1 is_nothrow && (is_nothrow = fcount ≥ nargs) diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 156c0457dfc6d..58f20b5ef2a0c 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -223,7 +223,7 @@ function stmt_effect_free(@nospecialize(stmt), @nospecialize(rt), src::Union{IRC # `Expr(:new)` of unknown type could raise arbitrary TypeError. typ, isexact = instanceof_tfunc(typ) isexact || return false - isconcretetype(typ) || return false + isconcretedispatch(typ) || return false typ = typ::DataType fieldcount(typ) >= length(args) - 1 || return false for fld_idx in 1:(length(args) - 1) diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 5bc31171cd0ce..a14a23326d58e 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -1465,14 +1465,11 @@ function late_inline_special_case!( params::OptimizationParams) params.inlining || return nothing (; f, ft, argtypes) = sig - isinlining = params.inlining if length(argtypes) == 3 && istopfunction(f, :!==) # special-case inliner for !== that precedes _methods_by_ftype union splitting # and that works, even though inference generally avoids inferring the `!==` Method if isa(type, Const) return SomeCase(quoted(type.val)) - elseif !isinlining - return nothing end cmp_call = Expr(:call, GlobalRef(Core, :(===)), stmt.args[2], stmt.args[3]) cmp_call_ssa = insert_node!(ir, idx, effect_free(NewInstruction(cmp_call, Bool))) @@ -1483,8 +1480,6 @@ function late_inline_special_case!( # that works, even though inference generally avoids inferring the `>:` Method if isa(type, Const) && _builtin_nothrow(<:, Any[argtypes[3], argtypes[2]], type) return SomeCase(quoted(type.val)) - elseif !isinlining - return nothing end subtype_call = Expr(:call, GlobalRef(Core, :(<:)), stmt.args[3], stmt.args[2]) return SomeCase(subtype_call) From ab90932fad057a9503d57b2c8efaa256c9e72ce4 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Wed, 9 Feb 2022 13:30:33 +0000 Subject: [PATCH 19/20] Address Jameson's review feedback --- base/compiler/abstractinterpretation.jl | 53 +++++++++++++++---------- base/compiler/inferencestate.jl | 2 +- base/compiler/tfuncs.jl | 13 +++--- base/compiler/typeinfer.jl | 7 +++- base/compiler/types.jl | 15 ++++--- src/builtin_proto.h | 2 +- src/builtins.c | 7 ++-- src/ccall.cpp | 5 ++- src/codegen.cpp | 2 +- src/julia.h | 2 +- src/staticdata.c | 2 +- test/compiler/inline.jl | 17 ++++++++ 12 files changed, 79 insertions(+), 48 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 0233b8b22f363..31f84780a0cdf 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -39,8 +39,10 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), # function has not seen any side effects, we would like to make sure there # aren't any in the throw block either to enable other optimizations. add_remark!(interp, sv, "Skipped call in throw block") - tristate_merge!(sv, Effects(ALWAYS_TRUE, TRISTATE_UNKNOWN, - TRISTATE_UNKNOWN, TRISTATE_UNKNOWN)) + # At this point we are guaranteed to end up throwing on this path, + # which is all that's required for :consistent-cy. Of course, we don't + # know anything else about this statement. + tristate_merge!(sv, Effects(Effects(), consistent=ALWAYS_TRUE)) return CallMeta(Any, false) end @@ -72,7 +74,6 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), end fargs = arginfo.fargs - merged_sig = Union{} for i in 1:napplicable match = applicable[i]::MethodMatch method = match.method @@ -153,7 +154,6 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), this_rt = widenwrappedconditional(this_rt) @assert !(this_conditional isa Conditional) "invalid lattice element returned from inter-procedural context" seen += 1 - merged_sig = Union{merged_sig, sig} rettype = tmerge(rettype, this_rt) if this_conditional !== Bottom && is_lattice_bool(rettype) && fargs !== nothing if conditionals === nothing @@ -178,9 +178,10 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), if seen != napplicable tristate_merge!(sv, Effects()) - elseif !(atype <: merged_sig) - # Account for the fact that we may encounter a MethodError with a non-covered signature. - tristate_merge!(sv, Effects(ALWAYS_TRUE, ALWAYS_TRUE, TRISTATE_UNKNOWN, ALWAYS_TRUE)) + elseif isa(matches, MethodMatches) ? (!matches.fullmatch || any_ambig(matches)) : + (!_all(b->b, matches.fullmatches) || any_ambig(matches)) + # Account for the fact that we may encounter a MethodError with a non-covered or ambiguous signature. + tristate_merge!(sv, Effects(EFFECTS_TOTAL, nothrow=TRISTATE_UNKNOWN)) end rettype = from_interprocedural!(rettype, sv, arginfo, conditionals) @@ -216,6 +217,8 @@ struct MethodMatches mt::Core.MethodTable fullmatch::Bool end +any_ambig(info::MethodMatchInfo) = info.results.ambig +any_ambig(m::MethodMatches) = any_ambig(m.info) struct UnionSplitMethodMatches applicable::Vector{Any} @@ -225,6 +228,7 @@ struct UnionSplitMethodMatches mts::Vector{Core.MethodTable} fullmatches::Vector{Bool} end +any_ambig(m::UnionSplitMethodMatches) = _any(any_ambig, m.info.matches) function find_matching_methods(argtypes::Vector{Any}, @nospecialize(atype), method_table::MethodTableView, union_split::Int, max_methods::Int) @@ -594,7 +598,7 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp # Some sort of recursion was detected. Even if we did not limit types, # we cannot guarantee that the call will terminate. edge_effects = tristate_merge(edge_effects, - Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, TRISTATE_UNKNOWN)) + Effects(EFFECTS_TOTAL, terminates=TRISTATE_UNKNOWN)) end return MethodCallResult(rt, edgecycle, edgelimited, edge, edge_effects) end @@ -632,7 +636,7 @@ function concrete_eval_const_proven_total_or_error( isconstType(a) ? (a::DataType).parameters[1] : (a::DataType).instance) for i in 2:length(argtypes) ] try - value = Core._call_in_world_nonpure(get_world_counter(interp), f, args...) + value = Core._call_in_world_total(get_world_counter(interp), f, args...) return Const(value) catch e return nothing @@ -671,7 +675,10 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, resul if f !== nothing && result.edge !== nothing && is_total_or_error(result.edge_effects) && is_all_const_arg(arginfo) rt = concrete_eval_const_proven_total_or_error(interp, f, arginfo.argtypes) add_backedge!(result.edge, sv) - rt === nothing && return ConstCallResults(Union{}, ConstResult(result.edge), result.edge_effects) # The evaulation threw. By :consistent-cy, we're guaranteed this would have happened at runtime + if rt === nothing + # The evaulation threw. By :consistent-cy, we're guaranteed this would have happened at runtime + return ConstCallResults(Union{}, ConstResult(result.edge), result.edge_effects) + end if is_inlineable_constant(rt.val) || call_result_unused(sv) # If the constant is not inlineable, still do the const-prop, since the # code that led to the creation of the Const may be inlineable in the same @@ -1800,9 +1807,9 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), else is_nothrow = false end - tristate_merge!(sv, Effects( - !ismutabletype(t) ? ALWAYS_TRUE : ALWAYS_FALSE, - ALWAYS_TRUE, is_nothrow ? ALWAYS_TRUE : ALWAYS_FALSE, ALWAYS_TRUE)) + tristate_merge!(sv, Effects(EFFECTS_TOTAL, + consistent = !ismutabletype(t) ? ALWAYS_TRUE : ALWAYS_FALSE, + nothrow = is_nothrow ? ALWAYS_TRUE : ALWAYS_FALSE)) elseif ehead === :splatnew t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) is_nothrow = false # TODO: More precision @@ -1819,10 +1826,9 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), t = PartialStruct(t, at.fields::Vector{Any}) end end - tristate_merge!(sv, Effects( - ismutabletype(t) ? ALWAYS_FALSE : ALWAYS_TRUE, - ALWAYS_TRUE, is_nothrow ? ALWAYS_TRUE : ALWAYS_FALSE, - ALWAYS_TRUE)) + tristate_merge!(sv, Effects(EFFECTS_TOTAL, + consistent = ismutabletype(t) ? ALWAYS_FALSE : ALWAYS_TRUE, + nothrow = is_nothrow ? ALWAYS_TRUE : ALWAYS_FALSE)) elseif ehead === :new_opaque_closure tristate_merge!(sv, Effects()) # TODO t = Union{} @@ -1944,10 +1950,10 @@ function abstract_eval_global(M::Module, s::Symbol, frame::InferenceState) if isconst(M,s) return Const(getfield(M,s)) else - tristate_merge!(frame, Effects(ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE)) + tristate_merge!(frame, Effects(EFFECTS_TOTAL, consistent=ALWAYS_FALSE)) end else - tristate_merge!(frame, Effects(ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_TRUE)) + tristate_merge!(frame, Effects(EFFECTS_TOTAL, consistent=ALWAYS_FALSE, nothrow=ALWAYS_FALSE)) end return Any end @@ -2036,10 +2042,11 @@ function handle_control_backedge!(frame::InferenceState, from::Int, to::Int) if from > to def = frame.linfo.def if isa(def, Method) && decode_effects_override(def.purity).terminates_locally - return + return nothing end - tristate_merge!(frame, Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, TRISTATE_UNKNOWN)) + tristate_merge!(frame, Effects(EFFECTS_TOTAL, terminates=TRISTATE_UNKNOWN)) end + return nothing end # make as much progress on `frame` as possible (without handling cycles) @@ -2194,7 +2201,9 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) if isa(lhs, SlotNumber) changes = StateUpdate(lhs, VarState(t, false), changes, false) elseif isa(lhs, GlobalRef) - tristate_merge!(frame, Effects(ALWAYS_TRUE, ALWAYS_FALSE, TRISTATE_UNKNOWN, ALWAYS_TRUE)) + tristate_merge!(frame, Effects(EFFECTS_TOTAL, + effect_free=ALWAYS_FALSE, + nothrow=TRISTATE_UNKNOWN)) elseif !isa(lhs, SSAValue) tristate_merge!(frame, Effects()) end diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index d079bcac7582a..17539f7621c74 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -119,7 +119,7 @@ mutable struct InferenceState # TODO: Currently, any :inbounds declaration taints consistency, # because we cannot be guaranteed whether or not boundschecks # will be eliminated and if they are, we cannot be guaranteed - # that no undefined behavior will occurr (the effects assumptions + # that no undefined behavior will occur (the effects assumptions # are stronger than the inbounds assumptions, since the latter # requires dynamic reachability, while the former is global). inbounds = inbounds_option() diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index c7cc9e872c633..2238d43d65b27 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -1725,7 +1725,7 @@ const _EFFECT_FREE_BUILTINS = [ typeassert, throw, arraysize ] -const _IDEMPOTENT_BUILTINS = Any[ +const _CONSISTENT_BUILTINS = Any[ tuple, # tuple is immutable, thus tuples of egal arguments are egal ===, typeof, @@ -1734,11 +1734,9 @@ const _IDEMPOTENT_BUILTINS = Any[ apply_type, isa, UnionAll, - isdefined, Core.sizeof, Core.kwfunc, Core.ifelse, - Core._typevar, (<:), typeassert, throw @@ -1759,11 +1757,11 @@ function builtin_effects(f::Builtin, argtypes::Vector{Any}, rt) if (f === Core.getfield || f === Core.isdefined) && length(argtypes) >= 3 # consistent if the argtype is immutable if isvarargtype(argtypes[2]) - return Effects(TRISTATE_UNKNOWN, ALWAYS_TRUE, TRISTATE_UNKNOWN, ALWAYS_TRUE) + return Effects(Effects(), effect_free=ALWAYS_TRUE, terminates=ALWAYS_TRUE) end s = widenconst(argtypes[2]) if isType(s) || !isa(s, DataType) || isabstracttype(s) - return Effects(TRISTATE_UNKNOWN, ALWAYS_TRUE, TRISTATE_UNKNOWN, ALWAYS_TRUE) + return Effects(Effects(), effect_free=ALWAYS_TRUE, terminates=ALWAYS_TRUE) end s = s::DataType ipo_consistent = !ismutabletype(s) @@ -1779,7 +1777,7 @@ function builtin_effects(f::Builtin, argtypes::Vector{Any}, rt) ipo_consistent &= nothrow end else - ipo_consistent = contains_is(_IDEMPOTENT_BUILTINS, f) + ipo_consistent = contains_is(_CONSISTENT_BUILTINS, f) end # If we computed nothrow above for getfield, no need to repeat the procedure here if !nothrow @@ -1955,8 +1953,7 @@ intrinsic_effect_free_if_nothrow(f) = f === Intrinsics.pointerref || function intrinsic_effects(f::IntrinsicFunction, argtypes::Vector{Any}) if f === Intrinsics.llvmcall # llvmcall can do arbitrary things - return Effects(TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, - TRISTATE_UNKNOWN, TRISTATE_UNKNOWN) + return Effects() end ipo_consistent = !(f === Intrinsics.pointerref || # this one is volatile diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index eee9f665d6115..a15f81abde919 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -422,8 +422,7 @@ function rt_adjust_effects(@nospecialize(rt), ipo_effects::Effects) # but we don't currently model idempontency using dataflow, so we don't notice. # Fix that up here to improve precision. if !ipo_effects.inbounds_taints_consistency && rt === Union{} - return Effects(ALWAYS_TRUE, ipo_effects.effect_free, - ipo_effects.nothrow, ipo_effects.terminates) + return Effects(ipo_effects, consistent=ALWAYS_TRUE) end return ipo_effects end @@ -727,9 +726,13 @@ function merge_call_chain!(parent::InferenceState, ancestor::InferenceState, chi # then add all backedges of parent <- parent.parent # and merge all of the callers into ancestor.callers_in_cycle # and ensure that walking the parent list will get the same result (DAG) from everywhere + # Also taint the termination effect, because we can no longer guarantee the absence + # of recursion. + tristate_merge!(parent, Effects(EFFECTS_TOTAL, terminates=TRISTATE_UNKNOWN)) while true add_cycle_backedge!(child, parent, parent.currpc) union_caller_cycle!(ancestor, child) + tristate_merge!(child, Effects(EFFECTS_TOTAL, terminates=TRISTATE_UNKNOWN)) child = parent child === ancestor && break parent = child.parent::InferenceState diff --git a/base/compiler/types.jl b/base/compiler/types.jl index c6086b11543e6..cebb560a2010b 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -46,6 +46,11 @@ Effects(consistent::TriState, effect_free::TriState, nothrow::TriState, terminat Effects(consistent, effect_free, nothrow, terminates, false) Effects() = Effects(TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, TRISTATE_UNKNOWN) +Effects(e::Effects; consistent::TriState=e.consistent, + effect_free::TriState = e.effect_free, nothrow::TriState=e.nothrow, terminates::TriState=e.terminates, + inbounds_taints_consistency::Bool = e.inbounds_taints_consistency) = + Effects(consistent, effect_free, nothrow, terminates, inbounds_taints_consistency) + is_total_or_error(effects::Effects) = effects.consistent === ALWAYS_TRUE && effects.effect_free === ALWAYS_TRUE && effects.terminates === ALWAYS_TRUE @@ -100,11 +105,11 @@ end decode_effects_override(e::UInt8) = EffectsOverride( - e & 0x01 != 0x00, - (e >> 1) & 0x01 != 0x00, - (e >> 2) & 0x01 != 0x00, - (e >> 3) & 0x01 != 0x00, - (e >> 4) & 0x01 != 0x00) + (e & 0x01) != 0x00, + (e & 0x02) != 0x00, + (e & 0x04) != 0x00, + (e & 0x08) != 0x00, + (e & 0x10) != 0x00) """ InferenceResult diff --git a/src/builtin_proto.h b/src/builtin_proto.h index 3712ea99d38fc..7b11813e7a58b 100644 --- a/src/builtin_proto.h +++ b/src/builtin_proto.h @@ -29,7 +29,7 @@ DECLARE_BUILTIN(arrayref); DECLARE_BUILTIN(arrayset); DECLARE_BUILTIN(arraysize); DECLARE_BUILTIN(_call_in_world); -DECLARE_BUILTIN(_call_in_world_nonpure); +DECLARE_BUILTIN(_call_in_world_total); DECLARE_BUILTIN(_call_latest); DECLARE_BUILTIN(replacefield); DECLARE_BUILTIN(const_arrayref); diff --git a/src/builtins.c b/src/builtins.c index f4b200e1e2c96..ca2f56adaf6d8 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -790,9 +790,9 @@ JL_CALLABLE(jl_f__call_in_world) return ret; } -JL_CALLABLE(jl_f__call_in_world_nonpure) +JL_CALLABLE(jl_f__call_in_world_total) { - JL_NARGSV(_apply_in_world_nonpure, 2); + JL_NARGSV(_call_in_world_total, 2); JL_TYPECHK(_apply_in_world, ulong, args[0]); jl_task_t *ct = jl_current_task; int last_in = ct->ptls->in_pure_callback; @@ -809,7 +809,6 @@ JL_CALLABLE(jl_f__call_in_world_nonpure) ct->ptls->in_pure_callback = last_in; } JL_CATCH { - ct->world_age = last_age; ct->ptls->in_pure_callback = last_in; jl_rethrow(); } @@ -1908,7 +1907,7 @@ void jl_init_primitives(void) JL_GC_DISABLED add_builtin_func("_apply_pure", jl_f__apply_pure); add_builtin_func("_call_latest", jl_f__call_latest); add_builtin_func("_call_in_world", jl_f__call_in_world); - add_builtin_func("_call_in_world_nonpure", jl_f__call_in_world_nonpure); + add_builtin_func("_call_in_world_total", jl_f__call_in_world_total); 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/ccall.cpp b/src/ccall.cpp index 507f6ee86ec3e..052bae17487a7 100644 --- a/src/ccall.cpp +++ b/src/ccall.cpp @@ -1256,10 +1256,11 @@ static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs) jl_sym_t *cc_sym = NULL; if (jl_is_symbol(jlcc)) { cc_sym = (jl_sym_t*)jlcc; - } else if (jl_is_tuple(jlcc)) { + } + else if (jl_is_tuple(jlcc)) { cc_sym = (jl_sym_t*)jl_get_nth_field_noalloc(jlcc, 0); - assert(jl_is_symbol(cc_sym)); } + assert(jl_is_symbol(cc_sym)); native_sym_arg_t symarg = {}; JL_GC_PUSH3(&rt, &at, &symarg.gcroot); diff --git a/src/codegen.cpp b/src/codegen.cpp index 195e4643d676b..590ae7aafd68a 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -8155,7 +8155,7 @@ extern "C" void jl_init_llvm(void) { jl_f__apply_pure_addr, new JuliaFunction{XSTR(jl_f__apply_pure), get_func_sig, get_func_attrs} }, { jl_f__call_latest_addr, new JuliaFunction{XSTR(jl_f__call_latest), get_func_sig, get_func_attrs} }, { jl_f__call_in_world_addr, new JuliaFunction{XSTR(jl_f__call_in_world), get_func_sig, get_func_attrs} }, - { jl_f__call_in_world_nonpure_addr, new JuliaFunction{XSTR(jl_f__call_in_world_nonpure), get_func_sig, get_func_attrs} }, + { jl_f__call_in_world_total_addr, new JuliaFunction{XSTR(jl_f__call_in_world_total), get_func_sig, get_func_attrs} }, { jl_f_throw_addr, new JuliaFunction{XSTR(jl_f_throw), get_func_sig, get_func_attrs} }, { jl_f_tuple_addr, jltuple_func }, { jl_f_svec_addr, new JuliaFunction{XSTR(jl_f_svec), get_func_sig, get_func_attrs} }, diff --git a/src/julia.h b/src/julia.h index e16e852562897..20edd53ad39a7 100644 --- a/src/julia.h +++ b/src/julia.h @@ -232,7 +232,7 @@ typedef struct _jl_line_info_node_t { intptr_t inlined_at; } jl_line_info_node_t; -typedef union { +typedef union __jl_purity_overrides_t { struct { uint8_t ipo_consistent : 1; uint8_t ipo_effect_free : 1; diff --git a/src/staticdata.c b/src/staticdata.c index 202479a84bddb..2605aede78d2b 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -245,7 +245,7 @@ static htable_t field_replace; 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_iterate, &jl_f__apply_pure, - &jl_f__call_latest, &jl_f__call_in_world, &jl_f__call_in_world_nonpure, &jl_f_isdefined, + &jl_f__call_latest, &jl_f__call_in_world, &jl_f__call_in_world_total, &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_swapfield, &jl_f_modifyfield, &jl_f_replacefield, &jl_f_fieldtype, &jl_f_nfields, diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 5737d99055710..f2130e1c7eab4 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -1011,3 +1011,20 @@ end noninlined_dce_new(s) nothing end + +# Test that ambigous calls don't accidentally get nothrow effect +ambig_effect_test(a::Int, b) = 1 +ambig_effect_test(a, b::Int) = 1 +ambig_effect_test(a, b) = 1 +global ambig_unknown_type_global=1 +@noinline function conditionally_call_ambig(b::Bool, a) + if b + ambig_effect_test(a, ambig_unknown_type_global) + end + return 0 +end +function call_call_ambig(b::Bool) + conditionally_call_ambig(b, 1) + return 1 +end +@test !fully_eliminated(call_call_ambig, Tuple{Bool}) From b30e99ca2ca70406e09d36ea1971f9cb6f57c8ad Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Wed, 9 Feb 2022 14:28:58 +0000 Subject: [PATCH 20/20] Fix tests - address rebase issues --- NEWS.md | 2 +- base/compiler/abstractinterpretation.jl | 16 ++++++++-------- test/abstractarray.jl | 18 +++++++++--------- test/broadcast.jl | 4 ++-- test/read.jl | 2 +- test/strings/basic.jl | 4 ++-- test/strings/util.jl | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NEWS.md b/NEWS.md index 12315bff68ac6..76896ab2bad24 100644 --- a/NEWS.md +++ b/NEWS.md @@ -57,7 +57,7 @@ Compiler/Runtime improvements * Abstract callsite can now be inlined or statically resolved as far as the callsite has a single matching method ([#43113]). * Builtin function are now a bit more like generic functions, and can be enumerated with `methods` ([#43865]). -* Inference now tracks various effects such as sideeffectful-ness and nothrow-ness on a per-specialization basis. Code heavilty dependent on constant propagation should see significant compile-time performance improvements and certain cases (e.g. calls to unlinineable functions that are nevertheless effect free) should see runtime performance improvements. Effects may be overwritten manually with the `@Base.assume_effects` macro. (#43852). +* Inference now tracks various effects such as sideeffectful-ness and nothrow-ness on a per-specialization basis. Code heavily dependent on constant propagation should see significant compile-time performance improvements and certain cases (e.g. calls to uninlinable functions that are nevertheless effect free) should see runtime performance improvements. Effects may be overwritten manually with the `@Base.assume_effects` macro. (#43852). Command-line option changes --------------------------- diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 31f84780a0cdf..7c3510263ca42 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -1937,8 +1937,10 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), end function abstract_eval_global(M::Module, s::Symbol) - if isdefined(M,s) && isconst(M,s) - return Const(getfield(M,s)) + if isdefined(M,s) + if isconst(M,s) + return Const(getfield(M,s)) + end end ty = ccall(:jl_binding_type, Any, (Any, Any), M, s) ty === nothing && return Any @@ -1946,16 +1948,14 @@ function abstract_eval_global(M::Module, s::Symbol) end function abstract_eval_global(M::Module, s::Symbol, frame::InferenceState) + ty = abstract_eval_global(M, s) + isa(ty, Const) && return ty if isdefined(M,s) - if isconst(M,s) - return Const(getfield(M,s)) - else - tristate_merge!(frame, Effects(EFFECTS_TOTAL, consistent=ALWAYS_FALSE)) - end + tristate_merge!(frame, Effects(EFFECTS_TOTAL, consistent=ALWAYS_FALSE)) else tristate_merge!(frame, Effects(EFFECTS_TOTAL, consistent=ALWAYS_FALSE, nothrow=ALWAYS_FALSE)) end - return Any + return ty end abstract_eval_ssavalue(s::SSAValue, sv::InferenceState) = abstract_eval_ssavalue(s, sv.src) diff --git a/test/abstractarray.jl b/test/abstractarray.jl index 95bd33424c3d0..a33cf53698d1c 100644 --- a/test/abstractarray.jl +++ b/test/abstractarray.jl @@ -529,7 +529,7 @@ mutable struct TestThrowNoGetindex{T} <: AbstractVector{T} end @testset "ErrorException if getindex is not defined" begin Base.length(::TestThrowNoGetindex) = 2 Base.size(::TestThrowNoGetindex) = (2,) - @test_throws ErrorException isassigned(TestThrowNoGetindex{Float64}(), 1) + @test_throws Base.CanonicalIndexError isassigned(TestThrowNoGetindex{Float64}(), 1) end function test_in_bounds(::Type{TestAbstractArray}) @@ -565,10 +565,10 @@ end function test_getindex_internals(::Type{TestAbstractArray}) U = UnimplementedFastArray{Int, 2}() V = UnimplementedSlowArray{Int, 2}() - @test_throws ErrorException getindex(U, 1) - @test_throws ErrorException Base.unsafe_getindex(U, 1) - @test_throws ErrorException getindex(V, 1, 1) - @test_throws ErrorException Base.unsafe_getindex(V, 1, 1) + @test_throws Base.CanonicalIndexError getindex(U, 1) + @test_throws Base.CanonicalIndexError Base.unsafe_getindex(U, 1) + @test_throws Base.CanonicalIndexError getindex(V, 1, 1) + @test_throws Base.CanonicalIndexError Base.unsafe_getindex(V, 1, 1) end function test_setindex!_internals(::Type{T}, shape, ::Type{TestAbstractArray}) where T @@ -583,10 +583,10 @@ end function test_setindex!_internals(::Type{TestAbstractArray}) U = UnimplementedFastArray{Int, 2}() V = UnimplementedSlowArray{Int, 2}() - @test_throws ErrorException setindex!(U, 0, 1) - @test_throws ErrorException Base.unsafe_setindex!(U, 0, 1) - @test_throws ErrorException setindex!(V, 0, 1, 1) - @test_throws ErrorException Base.unsafe_setindex!(V, 0, 1, 1) + @test_throws Base.CanonicalIndexError setindex!(U, 0, 1) + @test_throws Base.CanonicalIndexError Base.unsafe_setindex!(U, 0, 1) + @test_throws Base.CanonicalIndexError setindex!(V, 0, 1, 1) + @test_throws Base.CanonicalIndexError Base.unsafe_setindex!(V, 0, 1, 1) end function test_get(::Type{TestAbstractArray}) diff --git a/test/broadcast.jl b/test/broadcast.jl index 6d85ac8624ca8..5cddd0cb174f8 100644 --- a/test/broadcast.jl +++ b/test/broadcast.jl @@ -695,12 +695,12 @@ end A = [[1,2,3],4:5,6] A[1] .= 0 @test A[1] == [0,0,0] - @test_throws ErrorException A[2] .= 0 + @test_throws Base.CanonicalIndexError A[2] .= 0 @test_throws MethodError A[3] .= 0 A = [[1,2,3],4:5] A[1] .= 0 @test A[1] == [0,0,0] - @test_throws ErrorException A[2] .= 0 + @test_throws Base.CanonicalIndexError A[2] .= 0 end # Issue #22180 diff --git a/test/read.jl b/test/read.jl index d26f2463dcbd1..91b5043ae2a55 100644 --- a/test/read.jl +++ b/test/read.jl @@ -604,7 +604,7 @@ end read!(io, @view y[4:7]) @test y[4:7] == v seekstart(io) - @test_throws ErrorException read!(io, @view z[4:6]) + @test_throws Base.CanonicalIndexError read!(io, @view z[4:6]) end # Bulk read from pipe diff --git a/test/strings/basic.jl b/test/strings/basic.jl index 1da897667a2ea..c1df87420d7da 100644 --- a/test/strings/basic.jl +++ b/test/strings/basic.jl @@ -1039,7 +1039,7 @@ let s = "∀x∃y", u = codeunits(s) @test u[1] == 0xe2 @test u[2] == 0x88 @test u[8] == 0x79 - @test_throws ErrorException (u[1] = 0x00) + @test_throws Base.CanonicalIndexError (u[1] = 0x00) @test collect(u) == b"∀x∃y" @test Base.elsize(u) == Base.elsize(typeof(u)) == 1 end @@ -1100,4 +1100,4 @@ end let d = Dict(lazy"$(1+2) is 3" => 3) @test d["3 is 3"] == 3 end -end \ No newline at end of file +end diff --git a/test/strings/util.jl b/test/strings/util.jl index b313a0fa1af4a..8957513e37f25 100644 --- a/test/strings/util.jl +++ b/test/strings/util.jl @@ -629,7 +629,7 @@ let testb() = b"0123" b = testb() @test eltype(b) === UInt8 @test b isa AbstractVector - @test_throws ErrorException b[4] = '4' + @test_throws Base.CanonicalIndexError b[4] = '4' @test testb() == UInt8['0','1','2','3'] end