From ed737ad2ad6d79ac3f6e7d2cf85078957f1be68c Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Mon, 7 Mar 2022 08:42:27 +0000 Subject: [PATCH] WIP: Semi-concrete IR interpreter Co-authored-by: Ian Atol Co-authored-by: Shuhei Kadowaki --- base/compiler/abstractinterpretation.jl | 298 +++++++------ base/compiler/compiler.jl | 6 +- base/compiler/inferenceresult.jl | 55 +-- base/compiler/inferencestate.jl | 6 + .../ssair/EscapeAnalysis/EscapeAnalysis.jl | 3 +- .../ssair/EscapeAnalysis/interprocedural.jl | 2 +- base/compiler/ssair/driver.jl | 4 +- base/compiler/ssair/inlining.jl | 11 + base/compiler/ssair/ir.jl | 11 +- base/compiler/ssair/irinterp.jl | 402 ++++++++++++++++++ base/compiler/stmtinfo.jl | 13 +- base/compiler/tfuncs.jl | 26 +- base/compiler/typeinfer.jl | 10 +- base/compiler/utilities.jl | 9 + base/essentials.jl | 9 +- test/compiler/EscapeAnalysis/EAUtils.jl | 7 +- test/compiler/inference.jl | 10 + 17 files changed, 700 insertions(+), 182 deletions(-) create mode 100644 base/compiler/ssair/irinterp.jl diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 5699514c53ebfc..c019468d8936dc 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -63,8 +63,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), # 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(; consistent=ALWAYS_TRUE, nonoverlayed)) - return CallMeta(Any, false) + return CallMeta(Any, false, Effects(; consistent=ALWAYS_TRUE, nonoverlayed)) end argtypes = arginfo.argtypes @@ -72,8 +71,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), 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) + return CallMeta(Any, false, Effects()) end (; valid_worlds, applicable, info) = matches @@ -84,19 +82,21 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), conditionals = nothing # keeps refinement information of call argument types when the return type is boolean seen = 0 # number of signatures actually inferred any_const_result = false - const_results = Union{InferenceResult,Nothing,ConstResult}[] + const_results = Union{InferenceResult,Nothing,SemiConcreteResult,ConstResult}[] multiple_matches = napplicable > 1 + fargs = arginfo.fargs + all_effects = EFFECTS_TOTAL if !matches.nonoverlayed # currently we don't have a good way to execute the overlayed method definition, # so we should give up pure/concrete eval when any of the matched methods is overlayed f = nothing - tristate_merge!(sv, Effects(EFFECTS_TOTAL; nonoverlayed=false)) + all_effects = Effects(all_effects; nonoverlayed=false) end + # try pure-evaluation val = pure_eval_call(interp, f, applicable, arginfo, sv) - val !== nothing && return CallMeta(val, MethodResultPure(info)) # TODO: add some sort of edge(s) + val !== nothing && return CallMeta(val, MethodResultPure(info), EFFECTS_TOTAL) # TODO: add some sort of edge(s) - fargs = arginfo.fargs for i in 1:napplicable match = applicable[i]::MethodMatch method = match.method @@ -132,7 +132,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), (; effects, const_result) = const_call_result end end - tristate_merge!(sv, effects) + all_effects = tristate_merge(all_effects, effects) push!(const_results, const_result) any_const_result |= const_result !== nothing this_rt = tmerge(this_rt, rt) @@ -181,7 +181,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), (; effects, const_result) = const_call_result end end - tristate_merge!(sv, effects) + all_effects = tristate_merge(all_effects, effects) push!(const_results, const_result) any_const_result |= const_result !== nothing end @@ -212,11 +212,11 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), if seen != napplicable # there may be unanalyzed effects within unseen dispatch candidate, # but we can still ignore nonoverlayed effect here since we already accounted for it - tristate_merge!(sv, EFFECTS_UNKNOWN) + all_effects = EFFECTS_UNKNOWN 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)) + all_effects = Effects(all_effects, nothrow=TRISTATE_UNKNOWN) end rettype = from_interprocedural!(rettype, sv, arginfo, conditionals) @@ -238,7 +238,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), delete!(sv.pclimitations, caller) end end - return CallMeta(rettype, info) + return CallMeta(rettype, info, all_effects) end struct FailedMethodMatch @@ -382,6 +382,11 @@ function collect_limitations!(@nospecialize(typ), sv::InferenceState) return typ end +function collect_limitations!(@nospecialize(typ), ::IRCode) + @assert !isa(typ, LimitedAccuracy) + return typ +end + function from_interconditional(@nospecialize(typ), sv::InferenceState, (; fargs, argtypes)::ArgInfo, @nospecialize(maybecondinfo)) fargs === nothing && return widenconditional(typ) slot = 0 @@ -708,42 +713,50 @@ function _pure_eval_call(@nospecialize(f), arginfo::ArgInfo) return Const(value) end +# - true: eligible for concrete evaluation +# - false: eligible for semi-concrete evaluation +# - nothing: not eligible for either of it function concrete_eval_eligible(interp::AbstractInterpreter, @nospecialize(f), result::MethodCallResult, arginfo::ArgInfo, sv::InferenceState) # disable concrete-evaluation since this function call is tainted by some overlayed # method and currently there is no direct way to execute overlayed methods - isoverlayed(method_table(interp)) && !is_nonoverlayed(result.edge_effects) && return false - return f !== nothing && - result.edge !== nothing && - is_concrete_eval_eligible(result.edge_effects) && - is_all_const_arg(arginfo) + isoverlayed(method_table(interp)) && !is_nonoverlayed(result.edge_effects) && return nothing + if f !== nothing && result.edge !== nothing && is_concrete_eval_eligible(result.edge_effects) + return is_all_const_arg(arginfo) + else + return nothing + end end -function is_all_const_arg((; argtypes)::ArgInfo) +function is_all_const_arg(argtypes::Vector{Any}) for i = 2:length(argtypes) a = widenconditional(argtypes[i]) isa(a, Const) || isconstType(a) || issingletontype(a) || return false end return true end +is_all_const_arg((; argtypes)::ArgInfo) = is_all_const_arg(argtypes) -function collect_const_args((; argtypes)::ArgInfo) +collect_const_args((; argtypes)::ArgInfo, start_idx::Int = 2) = collect_const_args(argtypes, start_idx) +function collect_const_args(argtypes::Vector{Any}, start_idx::Int = 2) return Any[ let a = widenconditional(argtypes[i]) isa(a, Const) ? a.val : isconstType(a) ? (a::DataType).parameters[1] : (a::DataType).instance - end for i in 2:length(argtypes) ] + end for i = start_idx:length(argtypes) ] end function concrete_eval_call(interp::AbstractInterpreter, @nospecialize(f), result::MethodCallResult, arginfo::ArgInfo, sv::InferenceState) - concrete_eval_eligible(interp, f, result, arginfo, sv) || return nothing + eligibility = concrete_eval_eligible(interp, f, result, arginfo, sv) + eligibility === nothing && return false + eligibility || return true # eligible for semi-concrete evaluation args = collect_const_args(arginfo) world = get_world_counter(interp) value = try Core._call_in_world_total(world, f, args...) catch - # The evaulation threw. By :consistent-cy, we're guaranteed this would have happened at runtime + # The evaluation threw. By :consistent-cy, we're guaranteed this would have happened at runtime return ConstCallResults(Union{}, ConstResult(result.edge, result.edge_effects), result.edge_effects) end if is_inlineable_constant(value) || call_result_unused(sv) @@ -752,9 +765,12 @@ function concrete_eval_call(interp::AbstractInterpreter, # circumstance and may be optimizable. return ConstCallResults(Const(value), ConstResult(result.edge, EFFECTS_TOTAL, value), EFFECTS_TOTAL) end - return nothing + return false end +has_conditional(argtypes::Vector{Any}) = _any(@nospecialize(x)->isa(x, Conditional), argtypes) +has_conditional((; argtypes)::ArgInfo) = has_conditional(argtypes) + function const_prop_enabled(interp::AbstractInterpreter, sv::InferenceState, match::MethodMatch) if !InferenceParams(interp).ipo_constant_propagation add_remark!(interp, sv, "[constprop] Disabled by parameter") @@ -770,10 +786,10 @@ end struct ConstCallResults rt::Any - const_result::Union{InferenceResult, ConstResult} + const_result::Union{InferenceResult, ConstResult, SemiConcreteResult} effects::Effects ConstCallResults(@nospecialize(rt), - const_result::Union{InferenceResult, ConstResult}, + const_result::Union{InferenceResult, ConstResult, SemiConcreteResult}, effects::Effects) = new(rt, const_result, effects) end @@ -784,13 +800,27 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, resul if !const_prop_enabled(interp, sv, match) return nothing end - val = concrete_eval_call(interp, f, result, arginfo, sv) - if val !== nothing + res = concrete_eval_call(interp, f, result, arginfo, sv) + if isa(res, ConstCallResults) add_backedge!(result.edge, sv) - return val + return res end mi = maybe_get_const_prop_profitable(interp, result, f, arginfo, match, sv) mi === nothing && return nothing + # try semi-concrete evaluation + if res::Bool && !has_conditional(arginfo) + mi_cache = WorldView(code_cache(interp), sv.world) + code = get(mi_cache, mi, nothing) + if code !== nothing + ir = codeinst_to_ir(interp, code) + if isa(ir, IRCode) + T = ir_abstract_constant_propagation(interp, mi_cache, sv, mi, ir, arginfo.argtypes) + if !isa(T, Type) || typeintersect(T, Bool) === Union{} + return ConstCallResults(T, SemiConcreteResult(mi, ir, result.edge_effects), result.edge_effects) + end + end + end + end # try constant prop' inf_cache = get_inference_cache(interp) inf_result = cache_lookup(mi, arginfo.argtypes, inf_cache) @@ -1050,6 +1080,7 @@ end # This is only for use with `Conditional`. # In general, usage of this is wrong. +ssa_def_slot(@nospecialize(arg), sv::IRCode) = nothing function ssa_def_slot(@nospecialize(arg), sv::InferenceState) init = sv.currpc while isa(arg, SSAValue) @@ -1075,7 +1106,8 @@ end # refine its type to an array of element types. # Union of Tuples of the same length is converted to Tuple of Unions. # returns an array of types -function precise_container_type(interp::AbstractInterpreter, @nospecialize(itft), @nospecialize(typ), sv::InferenceState) +function precise_container_type(interp::AbstractInterpreter, @nospecialize(itft), @nospecialize(typ), + sv::Union{InferenceState, IRCode}) if isa(typ, PartialStruct) && typ.typ.name === Tuple.name return typ.fields, nothing end @@ -1140,7 +1172,7 @@ function precise_container_type(interp::AbstractInterpreter, @nospecialize(itft) end # simulate iteration protocol on container type up to fixpoint -function abstract_iteration(interp::AbstractInterpreter, @nospecialize(itft), @nospecialize(itertype), sv::InferenceState) +function abstract_iteration(interp::AbstractInterpreter, @nospecialize(itft), @nospecialize(itertype), sv::Union{InferenceState, IRCode}) if isa(itft, Const) iteratef = itft.val else @@ -1154,7 +1186,7 @@ function abstract_iteration(interp::AbstractInterpreter, @nospecialize(itft), @n # WARNING: Changes to the iteration protocol must be reflected here, # this is not just an optimization. # TODO: this doesn't realize that Array, SimpleVector, Tuple, and NamedTuple do not use the iterate protocol - stateordonet === Bottom && return Any[Bottom], AbstractIterationInfo(CallMeta[CallMeta(Bottom, info)]) + stateordonet === Bottom && return Any[Bottom], AbstractIterationInfo(CallMeta[CallMeta(Bottom, info, Effects())]) valtype = statetype = Bottom ret = Any[] calls = CallMeta[call] @@ -1226,20 +1258,19 @@ function abstract_iteration(interp::AbstractInterpreter, @nospecialize(itft), @n end # do apply(af, fargs...), where af is a function value -function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, sv::InferenceState, +function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, sv::Union{InferenceState, IRCode}, max_methods::Int = get_max_methods(sv.mod, interp)) itft = argtype_by_index(argtypes, 2) aft = argtype_by_index(argtypes, 3) - (itft === Bottom || aft === Bottom) && return CallMeta(Bottom, false) + (itft === Bottom || aft === Bottom) && return CallMeta(Bottom, false, Effects()) aargtypes = argtype_tail(argtypes, 4) aftw = widenconst(aft) 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) + return CallMeta(Any, false, Effects()) end end res = Union{} @@ -1294,6 +1325,7 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, sv:: end retinfos = ApplyCallInfo[] retinfo = UnionSplitApplyCallInfo(retinfos) + effects = EFFECTS_TOTAL for i = 1:length(ctypes) ct = ctypes[i] arginfo = infos[i] @@ -1309,6 +1341,7 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, sv:: call = abstract_call(interp, ArgInfo(nothing, ct), sv, max_methods) push!(retinfos, ApplyCallInfo(call.info, arginfo)) res = tmerge(res, call.rt) + effects = tristate_merge(effects, call.effects) if bail_out_apply(interp, res, sv) if i != length(ctypes) # No point carrying forward the info, we're not gonna inline it anyway @@ -1319,7 +1352,7 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, sv:: end # TODO: Add a special info type to capture all the iteration info. # For now, only propagate info if we don't also union-split the iteration - return CallMeta(res, retinfo) + return CallMeta(res, retinfo, effects) end function argtype_by_index(argtypes::Vector{Any}, i::Int) @@ -1341,7 +1374,7 @@ function argtype_tail(argtypes::Vector{Any}, i::Int) end function abstract_call_builtin(interp::AbstractInterpreter, f::Builtin, (; fargs, argtypes)::ArgInfo, - sv::InferenceState, max_methods::Int) + sv::Union{InferenceState, IRCode}, max_methods::Int) @nospecialize f la = length(argtypes) if f === Core.ifelse && fargs isa Vector{Any} && la == 4 @@ -1500,21 +1533,21 @@ end function abstract_invoke(interp::AbstractInterpreter, (; fargs, argtypes)::ArgInfo, sv::InferenceState) ft′ = argtype_by_index(argtypes, 2) ft = widenconst(ft′) - ft === Bottom && return CallMeta(Bottom, false), EFFECTS_THROWS + ft === Bottom && return CallMeta(Bottom, false, EFFECTS_THROWS) (types, isexact, isconcrete, istype) = instanceof_tfunc(argtype_by_index(argtypes, 3)) - types === Bottom && return CallMeta(Bottom, false), EFFECTS_THROWS - isexact || return CallMeta(Any, false), Effects() + types === Bottom && return CallMeta(Bottom, false, EFFECTS_THROWS) + isexact || return CallMeta(Any, false, Effects()) argtype = argtypes_to_type(argtype_tail(argtypes, 4)) nargtype = typeintersect(types, argtype) - nargtype === Bottom && return CallMeta(Bottom, false), EFFECTS_THROWS - nargtype isa DataType || return CallMeta(Any, false), Effects() # other cases are not implemented below - isdispatchelem(ft) || return CallMeta(Any, false), Effects() # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below + nargtype === Bottom && return CallMeta(Bottom, false, EFFECTS_THROWS) + nargtype isa DataType || return CallMeta(Any, false, Effects()) # other cases are not implemented below + isdispatchelem(ft) || return CallMeta(Any, false, Effects()) # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below ft = ft::DataType types = rewrap_unionall(Tuple{ft, unwrap_unionall(types).parameters...}, types)::Type nargtype = Tuple{ft, nargtype.parameters...} argtype = Tuple{ft, argtype.parameters...} match, valid_worlds, overlayed = findsup(types, method_table(interp)) - match === nothing && return CallMeta(Any, false), Effects() + match === nothing && return CallMeta(Any, false, Effects()) update_valid_age!(sv, valid_worlds) method = match.method (ti, env::SimpleVector) = ccall(:jl_type_intersection_with_env, Any, (Any, Any), nargtype, method.sig)::SimpleVector @@ -1541,7 +1574,9 @@ function abstract_invoke(interp::AbstractInterpreter, (; fargs, argtypes)::ArgIn end end effects = Effects(effects; nonoverlayed=!overlayed) - return CallMeta(from_interprocedural!(rt, sv, arginfo, sig), InvokeCallInfo(match, const_result)), effects + rt = from_interprocedural!(rt, sv, arginfo, sig) + info = InvokeCallInfo(match, const_result) + return CallMeta(rt, info, effects) end function invoke_rewrite(xs::Vector{Any}) @@ -1553,8 +1588,8 @@ end # call where the function is known exactly function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), - arginfo::ArgInfo, sv::InferenceState, - max_methods::Int = get_max_methods(f, sv.mod, interp)) + arginfo::ArgInfo, sv::Union{InferenceState, IRCode}, + max_methods::Int = isa(sv, InferenceState) ? get_max_methods(f, sv.mod, interp) : 0) (; fargs, argtypes) = arginfo la = length(argtypes) @@ -1562,37 +1597,30 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), if f === _apply_iterate return abstract_apply(interp, argtypes, sv, max_methods) elseif f === invoke - call, effects = abstract_invoke(interp, arginfo, sv) - tristate_merge!(sv, effects) - return call + return abstract_invoke(interp, arginfo, sv) elseif f === modifyfield! - tristate_merge!(sv, Effects()) # TODO return abstract_modifyfield!(interp, argtypes, sv) end rt = abstract_call_builtin(interp, f, arginfo, sv, max_methods) - tristate_merge!(sv, builtin_effects(f, argtypes, rt)) - return CallMeta(rt, false) + return CallMeta(rt, false, builtin_effects(f, argtypes, rt)) 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) + return CallMeta(Any, false, Effects()) elseif f === Core.kwfunc if la == 2 aty = argtypes[2] if !isvarargtype(aty) ft = widenconst(aty) if isa(ft, DataType) && isdefined(ft.name, :mt) && isdefined(ft.name.mt, :kwsorter) - return CallMeta(Const(ft.name.mt.kwsorter), MethodResultPure()) + return CallMeta(Const(ft.name.mt.kwsorter), MethodResultPure(), EFFECTS_TOTAL) end end end - tristate_merge!(sv, EFFECTS_UNKNOWN) # TODO - return CallMeta(Any, false) + return CallMeta(Any, false, EFFECTS_UNKNOWN) 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_UNKNOWN) # TODO - (la < 2 || la > 4) && return CallMeta(Union{}, false) + (la < 2 || la > 4) && return CallMeta(Union{}, false, EFFECTS_UNKNOWN) n = argtypes[2] ub_var = Const(Any) lb_var = Const(Union{}) @@ -1602,36 +1630,33 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), elseif la == 3 ub_var = argtypes[3] end - return CallMeta(typevar_tfunc(n, lb_var, ub_var), false) + return CallMeta(typevar_tfunc(n, lb_var, ub_var), false, EFFECTS_UNKNOWN #= TODO =#) elseif f === UnionAll - tristate_merge!(sv, EFFECTS_UNKNOWN) # TODO - return CallMeta(abstract_call_unionall(argtypes), false) + return CallMeta(abstract_call_unionall(argtypes), false, EFFECTS_UNKNOWN) elseif f === Tuple && la == 2 - tristate_merge!(sv, EFFECTS_UNKNOWN) # TODO aty = argtypes[2] ty = isvarargtype(aty) ? unwrapva(aty) : widenconst(aty) if !isconcretetype(ty) - return CallMeta(Tuple, false) + return CallMeta(Tuple, false, EFFECTS_UNKNOWN #= TODO =#) end elseif is_return_type(f) - tristate_merge!(sv, EFFECTS_UNKNOWN) # TODO return return_type_tfunc(interp, argtypes, sv) elseif la == 2 && istopfunction(f, :!) # handle Conditional propagation through !Bool aty = argtypes[2] if isa(aty, Conditional) call = abstract_call_gf_by_type(interp, f, ArgInfo(fargs, Any[Const(f), Bool]), Tuple{typeof(f), Bool}, sv, max_methods) # make sure we've inferred `!(::Bool)` - return CallMeta(Conditional(aty.var, aty.elsetype, aty.vtype), call.info) + return CallMeta(Conditional(aty.var, aty.elsetype, aty.vtype), call.info, EFFECTS_TOTAL) end elseif la == 3 && istopfunction(f, :!==) # mark !== as exactly a negated call to === rty = abstract_call_known(interp, (===), arginfo, sv, max_methods).rt if isa(rty, Conditional) - return CallMeta(Conditional(rty.var, rty.elsetype, rty.vtype), false) # swap if-else + return CallMeta(Conditional(rty.var, rty.elsetype, rty.vtype), false, EFFECTS_TOTAL) # swap if-else elseif isa(rty, Const) - return CallMeta(Const(rty.val === false), MethodResultPure()) + return CallMeta(Const(rty.val === false), MethodResultPure(), EFFECTS_TOTAL) end - return CallMeta(rty, false) + return CallMeta(rty, false, EFFECTS_TOTAL) elseif la == 3 && istopfunction(f, :(>:)) # mark issupertype as a exact alias for issubtype # swap T1 and T2 arguments and call <: @@ -1641,26 +1666,26 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), fargs = nothing end argtypes = Any[typeof(<:), argtypes[3], argtypes[2]] - return CallMeta(abstract_call_known(interp, <:, ArgInfo(fargs, argtypes), sv, max_methods).rt, false) + return CallMeta(abstract_call_known(interp, <:, ArgInfo(fargs, argtypes), sv, max_methods).rt, false, EFFECTS_TOTAL) elseif la == 2 && (a2 = argtypes[2]; isa(a2, Const)) && (svecval = a2.val; isa(svecval, SimpleVector)) && istopfunction(f, :length) # mark length(::SimpleVector) as @pure - return CallMeta(Const(length(svecval)), MethodResultPure()) + return CallMeta(Const(length(svecval)), MethodResultPure(), EFFECTS_TOTAL) elseif la == 3 && (a2 = argtypes[2]; isa(a2, Const)) && (svecval = a2.val; isa(svecval, SimpleVector)) && (a3 = argtypes[3]; isa(a3, Const)) && (idx = a3.val; isa(idx, Int)) && istopfunction(f, :getindex) # mark getindex(::SimpleVector, i::Int) as @pure if 1 <= idx <= length(svecval) && isassigned(svecval, idx) - return CallMeta(Const(getindex(svecval, idx)), MethodResultPure()) + return CallMeta(Const(getindex(svecval, idx)), MethodResultPure(), EFFECTS_TOTAL) end elseif la == 2 && istopfunction(f, :typename) - return CallMeta(typename_static(argtypes[2]), MethodResultPure()) + return CallMeta(typename_static(argtypes[2]), MethodResultPure(), EFFECTS_TOTAL) elseif la == 3 && istopfunction(f, :typejoin) if is_all_const_arg(arginfo) val = _pure_eval_call(f, arginfo) - return CallMeta(val === nothing ? Type : val, MethodResultPure()) + return CallMeta(val === nothing ? Type : val, MethodResultPure(), EFFECTS_TOTAL) end end atype = argtypes_to_type(argtypes) @@ -1685,7 +1710,7 @@ function abstract_call_opaque_closure(interp::AbstractInterpreter, closure::Part end end info = OpaqueClosureCallInfo(match, const_result) - return CallMeta(from_interprocedural!(rt, sv, arginfo, match.spec_types), info) + return CallMeta(from_interprocedural!(rt, sv, arginfo, match.spec_types), info, Effects()) end function most_general_argtypes(closure::PartialOpaque) @@ -1700,25 +1725,22 @@ end # call where the function is any lattice element function abstract_call(interp::AbstractInterpreter, arginfo::ArgInfo, - sv::InferenceState, max_methods::Union{Int, Nothing} = nothing) + sv::Union{InferenceState, IRCode}, max_methods::Union{Int, Nothing} = isa(sv, IRCode) ? 0 : nothing) argtypes = arginfo.argtypes ft = argtypes[1] f = singleton_type(ft) if isa(ft, PartialOpaque) newargtypes = copy(argtypes) newargtypes[1] = ft.env - tristate_merge!(sv, Effects()) # TODO return abstract_call_opaque_closure(interp, ft, ArgInfo(arginfo.fargs, newargtypes), sv) elseif (uft = unwrap_unionall(widenconst(ft)); isa(uft, DataType) && uft.name === typename(Core.OpaqueClosure)) - tristate_merge!(sv, Effects()) # TODO - return CallMeta(rewrap_unionall((uft::DataType).parameters[2], widenconst(ft)), false) + return CallMeta(rewrap_unionall((uft::DataType).parameters[2], widenconst(ft)), false, Effects()) elseif f === nothing # non-constant function, but the number of arguments is known # and the ft is not a Builtin or IntrinsicFunction if hasintersect(widenconst(ft), Union{Builtin, Core.OpaqueClosure}) - tristate_merge!(sv, Effects()) add_remark!(interp, sv, "Could not identify method table for call") - return CallMeta(Any, false) + return CallMeta(Any, false, Effects()) end max_methods = max_methods === nothing ? get_max_methods(sv.mod, interp) : max_methods return abstract_call_gf_by_type(interp, nothing, arginfo, argtypes_to_type(argtypes), sv, max_methods) @@ -1775,7 +1797,7 @@ function abstract_eval_cfunction(interp::AbstractInterpreter, e::Expr, vtypes::V nothing end -function abstract_eval_value_expr(interp::AbstractInterpreter, e::Expr, vtypes::VarTable, sv::InferenceState) +function abstract_eval_value_expr(interp::AbstractInterpreter, e::Expr, sv::Union{InferenceState, IRCode}) if e.head === :static_parameter n = e.args[1]::Int t = Any @@ -1790,13 +1812,20 @@ function abstract_eval_value_expr(interp::AbstractInterpreter, e::Expr, vtypes:: end end -function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize(e), vtypes::VarTable, sv::InferenceState) +function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize(e), vtypes::Union{VarTable, Nothing}, sv::Union{InferenceState, IRCode}) if isa(e, QuoteNode) return Const(e.value) elseif isa(e, SSAValue) return abstract_eval_ssavalue(e, sv) - elseif isa(e, SlotNumber) || isa(e, Argument) + elseif isa(e, SlotNumber) return vtypes[slot_id(e)].typ + elseif isa(e, Argument) + if !isa(vtypes, Nothing) + return vtypes[slot_id(e)].typ + else + @assert isa(sv, IRCode) + return sv.argtypes[e.n] + end elseif isa(e, GlobalRef) return abstract_eval_global(e.mod, e.name, sv) end @@ -1804,16 +1833,16 @@ function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize( return Const(e) end -function abstract_eval_value(interp::AbstractInterpreter, @nospecialize(e), vtypes::VarTable, sv::InferenceState) +function abstract_eval_value(interp::AbstractInterpreter, @nospecialize(e), vtypes::Union{VarTable, Nothing}, sv::Union{InferenceState, IRCode}) if isa(e, Expr) - return abstract_eval_value_expr(interp, e, vtypes, sv) + return abstract_eval_value_expr(interp, e, sv) else typ = abstract_eval_special_value(interp, e, vtypes, sv) return collect_limitations!(typ, sv) end end -function collect_argtypes(interp::AbstractInterpreter, ea::Vector{Any}, vtypes::VarTable, sv::InferenceState) +function collect_argtypes(interp::AbstractInterpreter, ea::Vector{Any}, vtypes::Union{VarTable, Nothing}, sv::Union{InferenceState, IRCode}) n = length(ea) argtypes = Vector{Any}(undef, n) @inbounds for i = 1:n @@ -1826,29 +1855,27 @@ function collect_argtypes(interp::AbstractInterpreter, ea::Vector{Any}, vtypes:: return argtypes end -function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), vtypes::VarTable, sv::InferenceState) - if !isa(e, Expr) - if isa(e, PhiNode) - rt = Union{} - for val in e.values - rt = tmerge(rt, abstract_eval_special_value(interp, val, vtypes, sv)) - end - return rt - end - return abstract_eval_special_value(interp, e, vtypes, sv) - end - e = e::Expr +struct RTEffects + rt + effects::Effects + RTEffects(@nospecialize(rt), effects::Effects) = new(rt, effects) +end + +function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtypes::Union{VarTable, Nothing}, sv::Union{InferenceState, IRCode})::RTEffects ehead = e.head if ehead === :call ea = e.args argtypes = collect_argtypes(interp, ea, vtypes, sv) if argtypes === nothing - t = Bottom + rt = Bottom + effects = Effects() else - callinfo = abstract_call(interp, ArgInfo(ea, argtypes), sv) - sv.stmt_info[sv.currpc] = callinfo.info - t = callinfo.rt + (; rt, effects, info) = abstract_call(interp, ArgInfo(ea, argtypes), sv) + if isa(sv, InferenceState) + sv.stmt_info[sv.currpc] = info + end end + return RTEffects(rt, effects) elseif ehead === :new t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) is_nothrow = true @@ -1865,11 +1892,9 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), is_nothrow && (is_nothrow = at ⊑ ft) at = tmeet(at, ft) if at === Bottom - t = Bottom - tristate_merge!(sv, Effects(EFFECTS_TOTAL; + return RTEffects(Bottom, Effects(EFFECTS_TOTAL; # consistent = ALWAYS_TRUE, # N.B depends on !ismutabletype(t) above nothrow = TRISTATE_UNKNOWN)) - @goto t_computed elseif !isa(at, Const) allconst = false end @@ -1896,7 +1921,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), else is_nothrow = false end - tristate_merge!(sv, Effects(EFFECTS_TOTAL; + return RTEffects(t, Effects(EFFECTS_TOTAL; consistent = !ismutabletype(t) ? ALWAYS_TRUE : TRISTATE_UNKNOWN, nothrow = is_nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN)) elseif ehead === :splatnew @@ -1915,11 +1940,10 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), t = PartialStruct(t, at.fields::Vector{Any}) end end - tristate_merge!(sv, Effects(EFFECTS_TOTAL; + return RTEffects(t, Effects(EFFECTS_TOTAL; consistent = ismutabletype(t) ? TRISTATE_UNKNOWN : ALWAYS_TRUE, nothrow = is_nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN)) elseif ehead === :new_opaque_closure - tristate_merge!(sv, Effects()) # TODO t = Union{} if length(e.args) >= 4 ea = e.args @@ -1940,6 +1964,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), end end end + return RTEffects(t, Effects()) elseif ehead === :foreigncall abstract_eval_value(interp, e.args[1], vtypes, sv) t = sp_type_rewrap(e.args[2], sv.linfo, true) @@ -1952,31 +1977,32 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), if isa(cconv, QuoteNode) && isa(cconv.value, Tuple{Symbol, UInt8}) effects = cconv.value[2] effects = decode_effects_override(effects) - tristate_merge!(sv, Effects( + effects = Effects( effects.consistent ? ALWAYS_TRUE : TRISTATE_UNKNOWN, effects.effect_free ? ALWAYS_TRUE : TRISTATE_UNKNOWN, effects.nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN, effects.terminates_globally ? ALWAYS_TRUE : TRISTATE_UNKNOWN, #=nonoverlayed=#true - )) + ) else - tristate_merge!(sv, EFFECTS_UNKNOWN) + effects = EFFECTS_UNKNOWN end + return RTEffects(t, effects) elseif ehead === :cfunction - tristate_merge!(sv, EFFECTS_UNKNOWN) t = e.args[1] isa(t, Type) || (t = Any) abstract_eval_cfunction(interp, e, vtypes, sv) + return RTEffects(t, EFFECTS_UNKNOWN) elseif ehead === :method - tristate_merge!(sv, EFFECTS_UNKNOWN) t = (length(e.args) == 1) ? Any : Nothing + return RTEffects(t, EFFECTS_UNKNOWN) elseif ehead === :copyast - tristate_merge!(sv, EFFECTS_UNKNOWN) t = abstract_eval_value(interp, e.args[1], vtypes, sv) if t isa Const && t.val isa Expr # `copyast` makes copies of Exprs t = Expr end + return RTEffects(t, EFFECTS_UNKNOWN) elseif ehead === :invoke || ehead === :invoke_modify error("type inference data-flow error: tried to double infer a function") elseif ehead === :isdefined @@ -2006,24 +2032,42 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), end end end + return RTEffects(t, EFFECTS_UNKNOWN) else - t = abstract_eval_value_expr(interp, e, vtypes, sv) + t = abstract_eval_value_expr(interp, e, sv) + return RTEffects(t, EFFECTS_UNKNOWN) end - @label t_computed - @assert !isa(t, TypeVar) "unhandled TypeVar" - if isa(t, DataType) && isdefined(t, :instance) - # replace singleton types with their equivalent Const object - t = Const(t.instance) +end + +function abstract_eval_phi(interp::AbstractInterpreter, phi::PhiNode, vtypes::Union{VarTable, Nothing}, sv::Union{InferenceState, IRCode}) + rt = Union{} + for val in phi.values + rt = tmerge(rt, abstract_eval_special_value(interp, val, vtypes, sv)) end + return rt +end + +function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), vtypes::VarTable, sv::InferenceState) + if !isa(e, Expr) + if isa(e, PhiNode) + return abstract_eval_phi(interp, e, vtypes, sv) + end + return abstract_eval_special_value(interp, e, vtypes, sv) + end + (;rt, effects) = abstract_eval_statement_expr(interp, e, vtypes, sv) + tristate_merge!(sv, effects) + e = e::Expr + @assert !isa(rt, TypeVar) "unhandled TypeVar" + rt = maybe_singleton_const(rt) if !isempty(sv.pclimitations) - if t isa Const || t === Union{} + if rt isa Const || rt === Union{} empty!(sv.pclimitations) else - t = LimitedAccuracy(t, sv.pclimitations) + rt = LimitedAccuracy(rt, sv.pclimitations) sv.pclimitations = IdSet{InferenceState}() end end - return t + return rt end function abstract_eval_global(M::Module, s::Symbol) @@ -2035,7 +2079,7 @@ function abstract_eval_global(M::Module, s::Symbol) return ty end -function abstract_eval_global(M::Module, s::Symbol, frame::InferenceState) +function abstract_eval_global(M::Module, s::Symbol, frame::Union{InferenceState, IRCode}) ty = abstract_eval_global(M, s) isa(ty, Const) && return ty if isdefined(M,s) diff --git a/base/compiler/compiler.jl b/base/compiler/compiler.jl index 6991e2d38437ba..e9d40dd0231be6 100644 --- a/base/compiler/compiler.jl +++ b/base/compiler/compiler.jl @@ -131,6 +131,10 @@ include("compiler/methodtable.jl") include("compiler/inferenceresult.jl") include("compiler/inferencestate.jl") +include("compiler/ssair/basicblock.jl") +include("compiler/ssair/domtree.jl") +include("compiler/ssair/ir.jl") + include("compiler/typeutils.jl") include("compiler/typelimits.jl") include("compiler/typelattice.jl") @@ -139,7 +143,7 @@ include("compiler/stmtinfo.jl") include("compiler/abstractinterpretation.jl") include("compiler/typeinfer.jl") -include("compiler/optimize.jl") # TODO: break this up further + extract utilities +include("compiler/optimize.jl") # required for bootstrap # TODO: find why this is needed and remove it. diff --git a/base/compiler/inferenceresult.jl b/base/compiler/inferenceresult.jl index 8e3d3d5917fd0b..9c7481e2b602db 100644 --- a/base/compiler/inferenceresult.jl +++ b/base/compiler/inferenceresult.jl @@ -16,6 +16,36 @@ function is_forwardable_argtype(@nospecialize x) isa(x, PartialOpaque) end +function va_process_argtypes(given_argtypes::Vector{Any}, mi::MethodInstance, + condargs::Union{Vector{Tuple{Int,Int}}, Nothing}=nothing) + isva = mi.def.isva + nargs = Int(mi.def.nargs) + if isva || isvarargtype(given_argtypes[end]) + isva_given_argtypes = Vector{Any}(undef, nargs) + for i = 1:(nargs - isva) + isva_given_argtypes[i] = argtype_by_index(given_argtypes, i) + end + if isva + if length(given_argtypes) < nargs && isvarargtype(given_argtypes[end]) + last = length(given_argtypes) + else + last = nargs + end + isva_given_argtypes[nargs] = tuple_tfunc(given_argtypes[last:end]) + # invalidate `Conditional` imposed on varargs + if condargs !== nothing + for (slotid, i) in condargs + if slotid ≥ last + isva_given_argtypes[i] = widenconditional(isva_given_argtypes[i]) + end + end + end + end + return isva_given_argtypes + end + return given_argtypes +end + # In theory, there could be a `cache` containing a matching `InferenceResult` # for the provided `linfo` and `given_argtypes`. The purpose of this function is # to return a valid value for `cache_lookup(linfo, argtypes, cache).argtypes`, @@ -55,30 +85,7 @@ function matching_cache_argtypes( end given_argtypes[i] = widenconditional(argtype) end - isva = linfo.def.isva - if isva || isvarargtype(given_argtypes[end]) - isva_given_argtypes = Vector{Any}(undef, nargs) - for i = 1:(nargs - isva) - isva_given_argtypes[i] = argtype_by_index(given_argtypes, i) - end - if isva - if length(given_argtypes) < nargs && isvarargtype(given_argtypes[end]) - last = length(given_argtypes) - else - last = nargs - end - isva_given_argtypes[nargs] = tuple_tfunc(given_argtypes[last:end]) - # invalidate `Conditional` imposed on varargs - if condargs !== nothing - for (slotid, i) in condargs - if slotid ≥ last - isva_given_argtypes[i] = widenconditional(isva_given_argtypes[i]) - end - end - end - end - given_argtypes = isva_given_argtypes - end + given_argtypes = va_process_argtypes(given_argtypes, linfo, condargs) @assert length(given_argtypes) == nargs for i in 1:nargs given_argtype = given_argtypes[i] diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index 0790b18bf83bd5..e87e256f3d4998 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -82,6 +82,12 @@ function in(idx::Int, bsbmp::BitSetBoundedMinPrioritySet) return idx in bsbmp.elems end +function append!(bsbmp::BitSetBoundedMinPrioritySet, itr) + for val in itr + push!(bsbmp, val) + end +end + mutable struct InferenceState params::InferenceParams result::InferenceResult # remember where to put the result diff --git a/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl b/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl index 407b447a228a39..64f5e5d960c589 100644 --- a/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl +++ b/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl @@ -31,7 +31,8 @@ import Core.Compiler: # Core.Compiler specific definitions isbitstype, isexpr, is_meta_expr_head, println, widenconst, argextype, singleton_type, fieldcount_noerror, try_compute_field, try_compute_fieldidx, hasintersect, ⊑, intrinsic_nothrow, array_builtin_common_typecheck, arrayset_typecheck, - setfield!_nothrow, alloc_array_ndims, stmt_effect_free, check_effect_free! + setfield!_nothrow, alloc_array_ndims, stmt_effect_free, check_effect_free!, + SemiConcreteResult include(x) = _TOP_MOD.include(@__MODULE__, x) if _TOP_MOD === Core.Compiler diff --git a/base/compiler/ssair/EscapeAnalysis/interprocedural.jl b/base/compiler/ssair/EscapeAnalysis/interprocedural.jl index 9880c13db4ad15..6beb2a9f48db4c 100644 --- a/base/compiler/ssair/EscapeAnalysis/interprocedural.jl +++ b/base/compiler/ssair/EscapeAnalysis/interprocedural.jl @@ -6,7 +6,7 @@ import Core.Compiler: call_sig, argtypes_to_type, is_builtin, is_return_type, istopfunction, validate_sparams, specialize_method, invoke_rewrite -const Linfo = Union{MethodInstance,InferenceResult} +const Linfo = Union{MethodInstance,InferenceResult,SemiConcreteResult} struct CallInfo linfos::Vector{Linfo} nothrow::Bool diff --git a/base/compiler/ssair/driver.jl b/base/compiler/ssair/driver.jl index 7759d8d80b9cc8..03e55af44131c9 100644 --- a/base/compiler/ssair/driver.jl +++ b/base/compiler/ssair/driver.jl @@ -13,12 +13,10 @@ function stmt_effect_free end # imported by EscapeAnalysis function alloc_array_ndims end # imported by EscapeAnalysis function try_compute_field end # imported by EscapeAnalysis -include("compiler/ssair/basicblock.jl") -include("compiler/ssair/domtree.jl") -include("compiler/ssair/ir.jl") include("compiler/ssair/slot2ssa.jl") include("compiler/ssair/inlining.jl") include("compiler/ssair/verify.jl") include("compiler/ssair/legacy.jl") include("compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl") include("compiler/ssair/passes.jl") +include("compiler/ssair/irinterp.jl") diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 73d373f15b9c38..0e4e0f706ba0cf 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -1290,6 +1290,8 @@ function handle_const_call!( push!(cases, InliningCase(result.mi.specTypes, case)) elseif isa(result, InferenceResult) handled_all_cases &= handle_inf_result!(result, argtypes, flag, state, cases, true) + elseif isa(result, SemiConcreteResult) + handled_all_cases &= handle_semi_concrete_result!(result, cases, true) else @assert result === nothing handled_all_cases &= handle_match!(match, argtypes, flag, state, cases, true) @@ -1334,6 +1336,15 @@ function handle_inf_result!( return true end +function handle_semi_concrete_result!(result::SemiConcreteResult, cases::Vector{InliningCase}, allow_abstract::Bool = false) + mi = result.mi + spec_types = mi.specTypes + allow_abstract || isdispatchtuple(spec_types) || return false + validate_sparams(mi.sparam_vals) || return false + push!(cases, InliningCase(spec_types, InliningTodo(mi, result.ir, result.effects))) + return true +end + function const_result_item(result::ConstResult, state::InliningState) if !isdefined(result, :result) || !is_inlineable_constant(result.result) return compileable_specialization(state.et, result.mi, result.effects) diff --git a/base/compiler/ssair/ir.jl b/base/compiler/ssair/ir.jl index 73e70973166c9e..654ff36482dd73 100644 --- a/base/compiler/ssair/ir.jl +++ b/base/compiler/ssair/ir.jl @@ -1030,15 +1030,22 @@ function renumber_ssa2!(@nospecialize(stmt), ssanums::Vector{Any}, used_ssas::Ve end # Used in inlining before we start compacting - Only works at the CFG level -function kill_edge!(bbs::Vector{BasicBlock}, from::Int, to::Int) +function kill_edge!(bbs::Vector{BasicBlock}, from::Int, to::Int, callback=nothing) preds, succs = bbs[to].preds, bbs[from].succs deleteat!(preds, findfirst(x->x === from, preds)::Int) deleteat!(succs, findfirst(x->x === to, succs)::Int) if length(preds) == 0 for succ in copy(bbs[to].succs) - kill_edge!(bbs, to, succ) + kill_edge!(bbs, to, succ, callback) end end + if callback !== nothing + callback(from, to) + end +end + +function kill_edge!(ir::IRCode, from::Int, to::Int, callback=nothing) + kill_edge!(ir.cfg.blocks, from, to, callback) end # N.B.: from and to are non-renamed indices diff --git a/base/compiler/ssair/irinterp.jl b/base/compiler/ssair/irinterp.jl new file mode 100644 index 00000000000000..91ea22102d1215 --- /dev/null +++ b/base/compiler/ssair/irinterp.jl @@ -0,0 +1,402 @@ + +function codeinst_to_ir(interp::AbstractInterpreter, code::CodeInstance) + src = code.inferred + mi = code.def + + if isa(src, Vector{UInt8}) + src = ccall(:jl_uncompress_ir, Any, (Any, Ptr{Cvoid}, Any), mi.def, C_NULL, src::Vector{UInt8})::CodeInfo + end + + isa(src, CodeInfo) || return src + + return inflate_ir(src, mi) +end + +function tristate_merge!(ir::IRCode, e::Effects) + nothing +end + +function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), + arginfo::ArgInfo, @nospecialize(atype), + sv::IRCode, max_methods::Int) + return CallMeta(Any, false, Effects()) +end + +mutable struct TwoPhaseVectorView <: AbstractVector{Int} + const data::Vector{Int} + count::Int + const range::UnitRange{Int} +end +size(tpvv::TwoPhaseVectorView) = (tpvv.count,) +function getindex(tpvv::TwoPhaseVectorView, i::Int) + checkbounds(tpvv, i) + @inbounds tpvv.data[first(tpvv.range) + i - 1] +end +function push!(tpvv::TwoPhaseVectorView, v::Int) + tpvv.count += 1 + tpvv.data[first(tpvv.range) + tpvv.count - 1] = v + return nothing +end + +""" + mutable struct TwoPhaseDefUseMap + +This struct is intended as a memory- and GC-pressure-efficient mechanism +for incrementally computing def-use maps. The idea is that the def-use map +is constructed into two passes over the IR. In the first, we simply count the +the number of uses, computing the number of uses for each def as well as the +total number of uses. In the second pass, we actually fill in the def-use +information. + +The idea is that either of these two phases can be combined with other useful +work that needs to scan the instruction stream anyway, while avoiding the +significant allocation pressure of e.g. allocating an array for every SSA value +or attempting to dynamically move things around as new uses are discovered. + +The def-use map is presented as a vector of vectors. For every def, indexing +into the map will return a vector of uses. +""" +mutable struct TwoPhaseDefUseMap <: AbstractVector{TwoPhaseVectorView} + ssa_uses::Vector{Int} + data::Vector{Int} + complete::Bool +end + +function complete!(tpdum::TwoPhaseDefUseMap) + cumsum = 0 + for i = 1:length(tpdum.ssa_uses) + this_val = cumsum + 1 + cumsum += tpdum.ssa_uses[i] + tpdum.ssa_uses[i] = this_val + end + resize!(tpdum.data, cumsum) + fill!(tpdum.data, 0) + tpdum.complete = true +end + +function TwoPhaseDefUseMap(nssas::Int) + ssa_uses = zeros(Int, nssas) + data = Int[] + complete = false + return TwoPhaseDefUseMap(ssa_uses, data, complete) +end + +function count!(tpdum::TwoPhaseDefUseMap, arg::SSAValue) + @assert !tpdum.complete + tpdum.ssa_uses[arg.id] += 1 +end + +function kill_def_use!(tpdum::TwoPhaseDefUseMap, def::Int, use::Int) + if !tpdum.complete + tpdum.ssa_uses[def] -= 1 + else + @assert false && "TODO" + end +end +kill_def_use!(tpdum::TwoPhaseDefUseMap, def::SSAValue, use::Int) = + kill_def_use!(tpdum, def.id, use) + +function getindex(tpdum::TwoPhaseDefUseMap, idx::Int) + @assert tpdum.complete + range = tpdum.ssa_uses[idx]:(idx == length(tpdum.ssa_uses) ? length(tpdum.data) : (tpdum.ssa_uses[idx + 1] - 1)) + # TODO: Make logarithmic + nelems = 0 + for i in range + tpdum.data[i] == 0 && break + nelems += 1 + end + return TwoPhaseVectorView(tpdum.data, nelems, range) +end + +function concrete_eval_invoke(interp::AbstractInterpreter, ir::IRCode, mi_cache, + sv::InferenceState, inst::Expr) + mi′ = inst.args[1]::MethodInstance + code = get(mi_cache, mi′, nothing) + code === nothing && return nothing + argtypes = collect_argtypes(interp, inst.args[2:end], nothing, ir) + effects = decode_effects(code.ipo_purity_bits) + if is_concrete_eval_eligible(effects) && is_all_const_arg(argtypes) + args = collect_const_args(argtypes, #=start_idx=#1) + world = get_world_counter(interp) + value = try + Core._call_in_world_total(world, args...) + catch + return Union{} + end + if is_inlineable_constant(value) || 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 Const(value) + end + else + ir′ = codeinst_to_ir(interp, code) + if ir′ !== nothing + return ir_abstract_constant_propagation(interp, mi_cache, sv, mi′, ir′, argtypes) + end + end + return nothing +end + +function reprocess_instruction!(interp::AbstractInterpreter, ir::IRCode, mi_cache, + sv::InferenceState, + tpdum::TwoPhaseDefUseMap, idx::Int, bb::Union{Int, Nothing}, + @nospecialize(inst), @nospecialize(typ), + phi_revisit) + function update_phi!(from, to) + if length(ir.cfg.blocks[to].preds) == 0 + return + end + for idx in ir.cfg.blocks[to].stmts + stmt = ir.stmts[idx][:inst] + isa(stmt, Nothing) && continue + isa(stmt, PhiNode) || break + for (i, edge) in enumerate(stmt.edges) + if edge == from + deleteat!(stmt.edges, i) + deleteat!(stmt.values, i) + push!(phi_revisit, idx) + break + end + end + end + end + + if isa(inst, GotoIfNot) + cond = argextype(inst.cond, ir) + if isa(cond, Const) + if isa(inst.cond, SSAValue) + kill_def_use!(tpdum, inst.cond, idx) + end + if bb === nothing + bb = block_for_inst(ir, idx) + end + if (cond.val)::Bool + ir.stmts[idx][:inst] = nothing + kill_edge!(ir, bb, inst.dest, update_phi!) + else + ir.stmts[idx][:inst] = GotoNode(inst.dest) + kill_edge!(ir, bb, bb+1, update_phi!) + end + return true + end + return false + else + if isa(inst, Expr) || isa(inst, PhiNode) + if isa(inst, PhiNode) || inst.head === :call || inst.head === :new + if isa(inst, PhiNode) + rt = abstract_eval_phi(interp, inst, nothing, ir) + else + (;rt, effects) = abstract_eval_statement_expr(interp, inst, nothing, ir) + # All other effects already guaranteed effect free by construction + if effects.nothrow === ALWAYS_TRUE + ir.stmts[idx][:flag] |= IR_FLAG_EFFECT_FREE + end + end + if !(typ ⊑ rt) + ir.stmts[idx][:type] = rt + return true + end + elseif inst.head === :invoke + rr = concrete_eval_invoke(interp, ir, mi_cache, sv, inst) + if rr !== nothing + if !(typ ⊑ rr) + ir.stmts[idx][:type] = rr + return true + end + end + else + ccall(:jl_, Cvoid, (Any,), inst) + error() + end + elseif isa(inst, ReturnNode) + # Handled at the very end + return false + elseif isa(inst, PiNode) + rr = tmeet(argextype(inst.val, ir), inst.typ) + if !(typ ⊑ rr) + ir.stmts[idx][:type] = rr + return true + end + else + ccall(:jl_, Cvoid, (Any,), inst) + error() + end + end + return false +end + +function _ir_abstract_constant_propagation(interp::AbstractInterpreter, mi_cache, frame::InferenceState, mi::MethodInstance, ir, argtypes) + argtypes = va_process_argtypes(argtypes, mi) + argtypes_refined = Bool[!(ir.argtypes[i] ⊑ argtypes[i]) for i = 1:length(argtypes)] + empty!(ir.argtypes) + append!(ir.argtypes, argtypes) + ssa_refined = BitSet() + + ultimate_rt = Union{} + bbs = ir.cfg.blocks + ip = BitSetBoundedMinPrioritySet(length(bbs)) + push!(ip, 1) + all_rets = Int[] + + tpdum = TwoPhaseDefUseMap(length(ir.stmts)) + + """ + process_terminator! + + Process the terminator and add the successor to `ip`. Returns whether a + backedge was seen. + """ + function process_terminator!(ip, bb, idx) + inst = ir.stmts[idx][:inst] + if isa(inst, ReturnNode) + if isdefined(inst, :val) + push!(all_rets, idx) + end + return false + elseif isa(inst, GotoNode) + backedge = inst.label < bb + !backedge && push!(ip, inst.label) + return backedge + elseif isa(inst, GotoIfNot) + backedge = inst.dest < bb + !backedge && push!(ip, inst.dest) + push!(ip, bb + 1) + return backedge + elseif isexpr(inst, :enter) + dest = inst.args[1]::Int + @assert dest > bb + push!(ip, dest) + push!(ip, bb + 1) + return false + else + push!(ip, bb + 1) + return false + end + end + + # Fast path: Scan both use counts and refinement in one single pass of + # of the instructions. In the absence of backedges, this will + # converge. + while !isempty(ip) + bb = popfirst!(ip) + stmts = bbs[bb].stmts + lstmt = last(stmts) + for idx = stmts + inst = ir.stmts[idx][:inst] + typ = ir.stmts[idx][:type] + any_refined = false + for ur in userefs(inst) + val = ur[] + if isa(val, Argument) + any_refined |= argtypes_refined[val.n] + elseif isa(val, SSAValue) + any_refined |= val.id in ssa_refined + count!(tpdum, val) + end + end + if isa(inst, PhiNode) && idx in ssa_refined + any_refined = true + delete!(ssa_refined, idx) + end + if any_refined && reprocess_instruction!(interp, ir, mi_cache, + frame, tpdum, idx, bb, inst, typ, ssa_refined) + push!(ssa_refined, idx) + end + if idx == lstmt && process_terminator!(ip, bb, idx) + @goto residual_scan + end + if ir.stmts[idx][:type] === Bottom + break + end + end + end + @goto compute_rt + +@label residual_scan + stmt_ip = BitSetBoundedMinPrioritySet(length(ir.stmts)) + # Slow Path Phase 1.A: Complete use scanning + while !isempty(ip) + bb = popfirst!(ip) + stmts = bbs[bb].stmts + lstmt = last(stmts) + for idx = stmts + inst = ir.stmts[idx][:inst] + typ = ir.stmts[idx][:type] + for ur in userefs(inst) + val = ur[] + if isa(val, Argument) + if argtypes_refined[val.n] + push!(stmt_ip, idx) + end + elseif isa(val, SSAValue) + count!(tpdum, val) + end + end + idx == lstmt && process_terminator!(ip, bb, idx) + end + end + + # Slow Path Phase 1.B: Assemble def-use map + complete!(tpdum) + push!(ip, 1) + while !isempty(ip) + bb = popfirst!(ip) + stmts = bbs[bb].stmts + lstmt = last(stmts) + for idx = stmts + inst = ir.stmts[idx][:inst] + typ = ir.stmts[idx][:type] + for ur in userefs(inst) + val = ur[] + if isa(val, SSAValue) + push!(tpdum[val.id], idx) + end + end + idx == lstmt && process_terminator!(ip, bb, idx) + end + end + + # Slow Path Phase 2: Use def-use map to converge cycles. + # TODO: It would be possible to return to the fast path after converging + # each cycle, but that's somewhat complicated. + for val in ssa_refined + append!(stmt_ip, tpdum[val]) + end + + while !isempty(stmt_ip) + idx = popfirst!(stmt_ip) + inst = ir.stmts[idx][:inst] + typ = ir.stmts[idx][:type] + if reprocess_instruction!(interp, ir, mi_cache, frame, + tpdum, idx, nothing, inst, typ, ssa_refined) + append!(stmt_ip, tpdum[idx]) + end + end + +@label compute_rt + ultimate_rt = Union{} + for idx in all_rets + bb = block_for_inst(ir.cfg, idx) + if bb != 1 && length(ir.cfg.blocks[bb].preds) == 0 + # Could have discovered this block is dead after the initial scan + continue + end + inst = ir.stmts[idx][:inst] + ultimate_rt = tmerge(ultimate_rt, argextype(inst.val, ir)) + end + return ultimate_rt +end + +function ir_abstract_constant_propagation(interp::AbstractInterpreter, mi_cache, frame::InferenceState, mi::MethodInstance, ir, argtypes) + if __measure_typeinf__[] + inf_frame = Timings.InferenceFrameInfo(mi, frame.world, Any[], Any[], length(ir.argtypes)) + Timings.enter_new_timer(inf_frame) + v = _ir_abstract_constant_propagation(interp, mi_cache, frame, mi, ir, argtypes) + append!(inf_frame.slottypes, ir.argtypes) + Timings.exit_current_timer(inf_frame) + return v + else + return _ir_abstract_constant_propagation(interp, mi_cache, frame, mi, ir, argtypes) + end +end diff --git a/base/compiler/stmtinfo.jl b/base/compiler/stmtinfo.jl index 3eeff0c2c86a8f..bc16a86b359df7 100644 --- a/base/compiler/stmtinfo.jl +++ b/base/compiler/stmtinfo.jl @@ -11,6 +11,7 @@ and any additional information (`call.info`) for a given generic call. struct CallMeta rt::Any info::Any + effects::Effects end """ @@ -55,6 +56,12 @@ struct ConstResult ConstResult(mi::MethodInstance, effects::Effects, @nospecialize val) = new(mi, effects, val) end +struct SemiConcreteResult + mi::MethodInstance + ir::IRCode + effects::Effects +end + """ info::ConstCallInfo @@ -64,7 +71,7 @@ the inference results with constant information `info.results::Vector{Union{Noth """ struct ConstCallInfo call::Union{MethodMatchInfo,UnionSplitInfo} - results::Vector{Union{Nothing,InferenceResult,ConstResult}} + results::Vector{Union{Nothing,InferenceResult,SemiConcreteResult,ConstResult}} end """ @@ -130,7 +137,7 @@ Optionally keeps `info.result::InferenceResult` that keeps constant information. """ struct InvokeCallInfo match::MethodMatch - result::Union{Nothing,InferenceResult,ConstResult} + result::Union{Nothing,InferenceResult,ConstResult,SemiConcreteResult} end """ @@ -142,7 +149,7 @@ Optionally keeps `info.result::InferenceResult` that keeps constant information. """ struct OpaqueClosureCallInfo match::MethodMatch - result::Union{Nothing,InferenceResult,ConstResult} + result::Union{Nothing,InferenceResult,ConstResult,SemiConcreteResult} end """ diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index e6625e2d559259..445488a8beac24 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -1041,10 +1041,10 @@ end function abstract_modifyfield!(interp::AbstractInterpreter, argtypes::Vector{Any}, sv::InferenceState) nargs = length(argtypes) if !isempty(argtypes) && isvarargtype(argtypes[nargs]) - nargs - 1 <= 6 || return CallMeta(Bottom, false) - nargs > 3 || return CallMeta(Any, false) + nargs - 1 <= 6 || return CallMeta(Bottom, false, Effects()) + nargs > 3 || return CallMeta(Any, false, Effects()) else - 5 <= nargs <= 6 || return CallMeta(Bottom, false) + 5 <= nargs <= 6 || return CallMeta(Bottom, false, Effects()) end o = unwrapva(argtypes[2]) f = unwrapva(argtypes[3]) @@ -1067,7 +1067,7 @@ function abstract_modifyfield!(interp::AbstractInterpreter, argtypes::Vector{Any end info = callinfo.info end - return CallMeta(RT, info) + return CallMeta(RT, info, Effects()) end replacefield!_tfunc(o, f, x, v, success_order, failure_order) = (@nospecialize; replacefield!_tfunc(o, f, x, v)) replacefield!_tfunc(o, f, x, v, success_order) = (@nospecialize; replacefield!_tfunc(o, f, x, v)) @@ -1843,7 +1843,7 @@ function builtin_nothrow(@nospecialize(f), argtypes::Array{Any, 1}, @nospecializ end function builtin_tfunction(interp::AbstractInterpreter, @nospecialize(f), argtypes::Array{Any,1}, - sv::Union{InferenceState,Nothing}) + sv::Union{InferenceState,IRCode,Nothing}) if f === tuple return tuple_tfunc(argtypes) end @@ -2028,7 +2028,7 @@ function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, s if isa(af_argtype, DataType) && af_argtype <: Tuple argtypes_vec = Any[aft, af_argtype.parameters...] if contains_is(argtypes_vec, Union{}) - return CallMeta(Const(Union{}), false) + return CallMeta(Const(Union{}), false, EFFECTS_UNKNOWN) end # Run the abstract_call without restricting abstract call # sites. Otherwise, our behavior model of abstract_call @@ -2041,32 +2041,32 @@ function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, s rt = widenconditional(call.rt) if isa(rt, Const) # output was computed to be constant - return CallMeta(Const(typeof(rt.val)), info) + return CallMeta(Const(typeof(rt.val)), info, EFFECTS_UNKNOWN) end rt = widenconst(rt) if rt === Bottom || (isconcretetype(rt) && !iskindtype(rt)) # output cannot be improved so it is known for certain - return CallMeta(Const(rt), info) + return CallMeta(Const(rt), info, EFFECTS_UNKNOWN) elseif !isempty(sv.pclimitations) # conservatively express uncertainty of this result # in two ways: both as being a subtype of this, and # because of LimitedAccuracy causes - return CallMeta(Type{<:rt}, info) + return CallMeta(Type{<:rt}, info, EFFECTS_UNKNOWN) elseif (isa(tt, Const) || isconstType(tt)) && (isa(aft, Const) || isconstType(aft)) # input arguments were known for certain # XXX: this doesn't imply we know anything about rt - return CallMeta(Const(rt), info) + return CallMeta(Const(rt), info, EFFECTS_UNKNOWN) elseif isType(rt) - return CallMeta(Type{rt}, info) + return CallMeta(Type{rt}, info, Effects()) else - return CallMeta(Type{<:rt}, info) + return CallMeta(Type{<:rt}, info, Effects()) end end end end end - return CallMeta(Type, false) + return CallMeta(Type, false, Effects()) end # N.B.: typename maps type equivalence classes to a single value diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 4efdd629208b65..0c735dadca0404 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -45,6 +45,8 @@ function _typeinf_identifier(frame::Core.Compiler.InferenceState) return mi_info end +_typeinf_identifier(frame::InferenceFrameInfo) = frame + """ Core.Compiler.Timing(mi_info, start_time, ...) @@ -330,13 +332,15 @@ already_inferred_quick_test(interp::NativeInterpreter, mi::MethodInstance) = already_inferred_quick_test(interp::AbstractInterpreter, mi::MethodInstance) = false -function maybe_compress_codeinfo(interp::AbstractInterpreter, linfo::MethodInstance, ci::CodeInfo) +function maybe_compress_codeinfo(interp::AbstractInterpreter, linfo::MethodInstance, ci::CodeInfo, ipo_effects::Effects) def = linfo.def toplevel = !isa(def, Method) if toplevel return ci end if may_discard_trees(interp) + # TODO: We may want to check is_concrete_eval_eligible(ipo_effects) here, but at the moment, + # inlineable is also required for semi-concrete constprop. cache_the_tree = ci.inferred && (ci.inlineable || isa_compileable_sig(linfo.specTypes, def)) else cache_the_tree = true @@ -366,7 +370,7 @@ function transform_result_for_cache(interp::AbstractInterpreter, linfo::MethodIn if inferred_result isa CodeInfo inferred_result.min_world = first(valid_worlds) inferred_result.max_world = last(valid_worlds) - inferred_result = maybe_compress_codeinfo(interp, linfo, inferred_result) + inferred_result = maybe_compress_codeinfo(interp, linfo, inferred_result, ipo_effects) end # The global cache can only handle objects that codegen understands if !isa(inferred_result, Union{CodeInfo, Vector{UInt8}, ConstAPI}) @@ -836,7 +840,7 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize if code isa CodeInstance # return existing rettype if the code is already inferred if code.inferred === nothing && is_stmt_inline(get_curr_ssaflag(caller)) # we already inferred this edge previously and decided to discarded the inferred code - # but the inlinear will request to use it, we re-infer it here and keep it around in the local cache + # but the inliner 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) diff --git a/base/compiler/utilities.jl b/base/compiler/utilities.jl index 3c243f5e2e34e2..197d44963628fc 100644 --- a/base/compiler/utilities.jl +++ b/base/compiler/utilities.jl @@ -240,6 +240,15 @@ function singleton_type(@nospecialize(ft)) return nothing end +function maybe_singleton_const(@nospecialize(t)) + if isa(t, DataType) && isdefined(t, :instance) + return Const(t.instance) + elseif isconstType(t) + return Const(t.parameters[1]) + end + return t +end + ################### # SSAValues/Slots # ################### diff --git a/base/essentials.jl b/base/essentials.jl index b837b556ed9107..ffc5af7bccec13 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -688,6 +688,12 @@ struct Colon <: Function end const (:) = Colon() +# TODO: Change lowering to do this automatically +@eval struct Val{x} + (T::Type{Val{x}} where x)() = $(Expr(:new, :T)) +end + + """ Val(c) @@ -708,8 +714,7 @@ julia> f(Val(true)) "Good" ``` """ -struct Val{x} -end +Val Val(x) = Val{x}() diff --git a/test/compiler/EscapeAnalysis/EAUtils.jl b/test/compiler/EscapeAnalysis/EAUtils.jl index 3ae9b41a0ddac4..d0170a825c658e 100644 --- a/test/compiler/EscapeAnalysis/EAUtils.jl +++ b/test/compiler/EscapeAnalysis/EAUtils.jl @@ -72,7 +72,8 @@ import Core: import .CC: InferenceResult, OptimizationState, IRCode, copy as cccopy, @timeit, convert_to_ircode, slot2reg, compact!, ssa_inlining_pass!, sroa_pass!, - adce_pass!, type_lift_pass!, JLOptions, verify_ir, verify_linetable + adce_pass!, type_lift_pass!, JLOptions, verify_ir, verify_linetable, + SemiConcreteResult import .EA: analyze_escapes, ArgEscapeCache, EscapeInfo, EscapeState, is_ipo_profitable # when working outside of Core.Compiler, @@ -188,9 +189,11 @@ function cache_escapes!(interp::EscapeAnalyzer, end function get_escape_cache(interp::EscapeAnalyzer) - return function (linfo::Union{InferenceResult,MethodInstance}) + return function (linfo::Union{InferenceResult,MethodInstance,SemiConcreteResult}) if isa(linfo, InferenceResult) ecache = get(interp.cache, linfo, nothing) + elseif isa(linfo, SemiConcreteResult) + ecache = get(interp.cache, linfo, nothing) else ecache = get(GLOBAL_ESCAPE_CACHE, linfo, nothing) end diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index 266cb1628f8c65..02648019d5dfc1 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -4100,3 +4100,13 @@ invoke44763(x) = Base.@invoke increase_x44763!(x) invoke44763(42) end |> only === Int @test x44763 == 0 + +# Test that semi-concrete interpretation doesn't break on functions with while loops in them. +@Base.assume_effects :consistent :effect_free :terminates_globally function pure_annotated_loop(x::Int, y::Int) + for i = 1:2 + x += y + end + return y +end +call_pure_annotated_loop(x) = Val{pure_annotated_loop(x, 1)}() +@test Core.Compiler.return_type(call_pure_annotated_loop, Tuple{Int}) == Val{1}