diff --git a/NEWS.md b/NEWS.md index d5406a8661875..76896ab2bad24 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 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/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/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..3606d0fa0a9bc 100644 --- a/base/c.jl +++ b/base/c.jl @@ -733,3 +733,7 @@ name, if desired `"libglib-2.0".g_uri_escape_string(...` macro ccall(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)...) +end diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 9a01f5aa80d59..7c3510263ca42 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -23,11 +23,26 @@ 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") + # 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 @@ -35,6 +50,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 +62,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}[] multiple_matches = napplicable > 1 if f !== nothing && napplicable == 1 && is_method_pure(applicable[1]::MethodMatch) @@ -84,9 +100,11 @@ 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 - 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 @@ -121,9 +139,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 @@ -155,6 +176,14 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), info = ConstCallInfo(info, const_results) end + if seen != napplicable + tristate_merge!(sv, Effects()) + 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) if call_result_unused(sv) && !(rettype === Bottom) @@ -188,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} @@ -197,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) @@ -402,7 +434,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 +503,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 +552,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 +590,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(EFFECTS_TOTAL, terminates=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 +609,83 @@ 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 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 : + isconstType(a) ? (a::DataType).parameters[1] : + (a::DataType).instance) for i in 2:length(argtypes) ] + try + value = Core._call_in_world_total(get_world_counter(interp), 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 + +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, @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 = concrete_eval_const_proven_total_or_error(interp, f, arginfo.argtypes) + add_backedge!(result.edge, sv) + 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 + # 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 # try constant prop' @@ -616,7 +720,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) @@ -624,15 +728,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 @@ -643,7 +739,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 @@ -766,13 +863,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 @@ -1032,6 +1133,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) @@ -1358,7 +1460,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 @@ -1385,9 +1487,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 +1504,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 +1522,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,10 +1594,10 @@ 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 + (;rt, const_result) = const_result end end info = OpaqueClosureCallInfo(match, const_result) @@ -1593,13 +1703,13 @@ 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) - return abstract_eval_global(e.mod, e.name) + return abstract_eval_global(e.mod, e.name, sv) end return Const(e) @@ -1651,18 +1761,25 @@ 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] - if isconcretetype(t) && !ismutabletype(t) + t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) + is_nothrow = true + if isconcretedispatch(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 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 + tristate_merge!(sv, Effects( + ALWAYS_TRUE, # N.B depends on !ismutabletype(t) above + ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_TRUE)) @goto t_computed elseif !isa(at, Const) allconst = false @@ -1673,8 +1790,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 @@ -1685,21 +1804,33 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), t = PartialStruct(t, ats) end end + else + is_nothrow = false end + tristate_merge!(sv, Effects(EFFECTS_TOTAL, + consistent = !ismutabletype(t) ? ALWAYS_TRUE : ALWAYS_FALSE, + nothrow = is_nothrow ? ALWAYS_TRUE : ALWAYS_FALSE)) 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(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{} if length(e.args) >= 5 ea = e.args @@ -1728,13 +1859,29 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), t = Bottom end end + 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, + 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 @@ -1790,14 +1937,28 @@ 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 return ty 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) + tristate_merge!(frame, Effects(EFFECTS_TOTAL, consistent=ALWAYS_FALSE)) + else + tristate_merge!(frame, Effects(EFFECTS_TOTAL, consistent=ALWAYS_FALSE, nothrow=ALWAYS_FALSE)) + end + return ty +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 @@ -1877,6 +2038,17 @@ function widenreturn(@nospecialize(rt), @nospecialize(bestguess), nslots::Int, s return widenconst(rt) end +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 nothing + end + tristate_merge!(frame, Effects(EFFECTS_TOTAL, terminates=TRISTATE_UNKNOWN)) + end + return nothing +end + # make as much progress on `frame` as possible (without handling cycles) function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) @assert !frame.inferred @@ -1914,7 +2086,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_control_backedge!(frame, pc, l) + pc´ = l elseif isa(stmt, GotoIfNot) condx = stmt.cond condt = abstract_eval_value(interp, condx, changes, frame) @@ -1939,6 +2113,7 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) # constant conditions if condval === true elseif condval === false + handle_control_backedge!(frame, pc, l) pc´ = l else # general case @@ -1949,6 +2124,7 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) end newstate_else = stupdate!(states[l], changes_else) if newstate_else !== nothing + handle_control_backedge!(frame, pc, l) # add else branch to active IP list if l < frame.pc´´ frame.pc´´ = l @@ -2024,6 +2200,12 @@ 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(EFFECTS_TOTAL, + effect_free=ALWAYS_FALSE, + nothrow=TRISTATE_UNKNOWN)) + 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..17539f7621c74 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. @@ -113,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 cannot be guaranteed + # 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() + 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, @@ -126,6 +139,8 @@ mutable struct InferenceState Vector{InferenceState}(), # callers_in_cycle #=parent=#nothing, cache === :global, false, false, + Effects(consistent, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, + inbounds_taints_consistency), CachedMethodTable(method_table(interp)), interp) result.result = frame @@ -133,6 +148,17 @@ mutable struct InferenceState return frame end 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: diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 3c00b9faa6d0a..58f20b5ef2a0c 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) ######### @@ -225,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 @@ -297,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+6 || return false - atype = instanceof_tfunc(argextype(args[6], 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+6], src) + dim = argextype(args[i+FOREIGNCALL_ARG_START], src) isa(dim, Const) || return false dimval = dim.val isa(dimval, Int) || return false @@ -312,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) ≥ 7 || return false - atype = instanceof_tfunc(argextype(args[6], src))[1] - dims = argextype(args[7], 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 @@ -621,21 +613,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..a14a23326d58e 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::MethodInstance + 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,14 @@ 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) + 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) + 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 @@ -1104,10 +1122,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 +1241,22 @@ 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) + 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)) + continue + end if result === nothing signature_union = Union{signature_union, match.spec_types} fully_covered &= handle_match!(match, argtypes, flag, state, cases) @@ -1236,7 +1270,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 +1348,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 @@ -1398,6 +1433,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 @@ -1407,7 +1443,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/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..e3f69b2c43e54 100644 --- a/base/compiler/stmtinfo.jl +++ b/base/compiler/stmtinfo.jl @@ -47,6 +47,13 @@ function nmatches(info::UnionSplitInfo) return n end +struct ConstResult + mi::MethodInstance + result + ConstResult(mi::MethodInstance) = new(mi) + ConstResult(mi::MethodInstance, @nospecialize val) = new(mi, val) +end + """ info::ConstCallInfo @@ -56,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}} + results::Vector{Union{Nothing,InferenceResult,ConstResult}} end """ @@ -122,7 +129,7 @@ Optionally keeps `info.result::InferenceResult` that keeps constant information. """ struct InvokeCallInfo match::MethodMatch - result::Union{Nothing,InferenceResult} + result::Union{Nothing,InferenceResult,ConstResult} end """ @@ -134,7 +141,7 @@ Optionally keeps `info.result::InferenceResult` that keeps constant information. """ struct OpaqueClosureCallInfo match::MethodMatch - result::Union{Nothing,InferenceResult} + result::Union{Nothing,InferenceResult,ConstResult} end """ diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index d335995558d8f..2238d43d65b27 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 @@ -1704,6 +1714,85 @@ 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 (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, arraysize +] + +const _CONSISTENT_BUILTINS = Any[ + tuple, # tuple is immutable, thus tuples of egal arguments are egal + ===, + typeof, + nfields, + fieldtype, + apply_type, + isa, + UnionAll, + Core.sizeof, + Core.kwfunc, + Core.ifelse, + (<:), + 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) + + nothrow = false + if (f === Core.getfield || f === Core.isdefined) && length(argtypes) >= 3 + # consistent if the argtype is immutable + if isvarargtype(argtypes[2]) + 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(Effects(), effect_free=ALWAYS_TRUE, terminates=ALWAYS_TRUE) + 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(_CONSISTENT_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(_EFFECT_FREE_BUILTINS, f) || contains_is(_PURE_BUILTINS, f) + + 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 +1935,45 @@ 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() + 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..a15f81abde919 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,16 @@ 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 !ipo_effects.inbounds_taints_consistency && rt === Union{} + return Effects(ipo_effects, consistent=ALWAYS_TRUE) + end + return ipo_effects +end + # inference completed on `me` # update the MethodInstance function finish(me::InferenceState, interp::AbstractInterpreter) @@ -471,6 +485,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 @@ -711,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 @@ -769,6 +788,16 @@ end generating_sysimg() = ccall(:jl_generating_output, Cint, ()) != 0 && JLOptions().incremental == 0 +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 +808,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 +816,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 +849,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..cebb560a2010b 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -22,6 +22,95 @@ 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) + +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 + 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) + +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 + +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), 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 + 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 |= 0x04) + eo.terminates && (e |= 0x08) + eo.terminates_locally && (e |= 0x10) + e +end + +decode_effects_override(e::UInt8) = + EffectsOverride( + (e & 0x01) != 0x00, + (e & 0x02) != 0x00, + (e & 0x04) != 0x00, + (e & 0x08) != 0x00, + (e & 0x10) != 0x00) + """ InferenceResult @@ -34,11 +123,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..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 => 5:typemax(Int), # name, RT, AT, nreq, cconv, 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/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) diff --git a/base/expr.jl b/base/expr.jl index 7719eff3b334b..59e6b075d8db9 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -371,6 +371,187 @@ 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 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 +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 `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. + +!!! 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)``. + + 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 + 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 + This `: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 +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` + +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`. +""" +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) || 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 esc(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/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 diff --git a/base/meta.jl b/base/meta.jl index 2ba9baec9a443..fcf66a7a787b2 100644 --- a/base/meta.jl +++ b/base/meta.jl @@ -393,7 +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) + @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/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/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 diff --git a/base/strings/string.jl b/base/strings/string.jl index c818e2e1844fb..c37e36594119e 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) @@ -95,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/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 ``` 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/builtin_proto.h b/src/builtin_proto.h index c7027f1b67f9e..7b11813e7a58b 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_total); DECLARE_BUILTIN(_call_latest); DECLARE_BUILTIN(replacefield); DECLARE_BUILTIN(const_arrayref); diff --git a/src/builtins.c b/src/builtins.c index e78034446d19a..ca2f56adaf6d8 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -790,6 +790,31 @@ JL_CALLABLE(jl_f__call_in_world) return ret; } +JL_CALLABLE(jl_f__call_in_world_total) +{ + 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; + 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->ptls->in_pure_callback = last_in; + jl_rethrow(); + } + return ret; +} + // tuples --------------------------------------------------------------------- JL_CALLABLE(jl_f_tuple) @@ -1882,6 +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_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 8f153279f23cc..052bae17487a7 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 = 6; + +// 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); @@ -1250,7 +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]; + 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); @@ -1272,8 +1281,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 +1316,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/codegen.cpp b/src/codegen.cpp index c754e7039b72b..590ae7aafd68a 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_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/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.h b/src/julia.h index ff0542307c1cf..20edd53ad39a7 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 __jl_purity_overrides_t { + 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..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, 5); // (fptr, rt, at, cc, narg) + 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,7 +176,15 @@ 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)); + 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++; } @@ -308,6 +316,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 +465,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 +653,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/src/staticdata.c b/src/staticdata.c index fb42d9cdf23f9..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_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/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/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/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/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/ccall.jl b/test/ccall.jl index 8f047ece65be2..c29f8c843bffd 100644 --- a/test/ccall.jl +++ b/test/ccall.jl @@ -1851,3 +1851,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 diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index fe49b2d6000af..79030bd910990 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 @@ -4009,3 +4009,22 @@ 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 + +# 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] diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index f6bd13a0ad154..f2130e1c7eab4 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -928,3 +928,103 @@ 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{}) + +# 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{}) + +# 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}) + +# 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 + +# 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}) 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 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)