From 03bfcbf5145f39388a9485e6c9519aa0c7353225 Mon Sep 17 00:00:00 2001 From: Elliot Saba Date: Tue, 26 Nov 2019 12:35:15 -0800 Subject: [PATCH] Add `AbstractInterpreter` to parameterize compilation pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows selective overriding of the compilation pipeline through multiple dispatch, enabling projects like `XLA.jl` to maintain separate inference caches, inference algorithms or heuristic algorithms while inferring and lowering code. In particular, it defines a new type, `AbstractInterpreter`, that represents an abstract interpretation pipeline. This `AbstractInterpreter` has a single defined concrete subtype, `NativeInterpreter`, that represents the native Julia compilation pipeline. The `NativeInterpreter` contains within it all the compiler parameters previously contained within `Params`, split into two pieces: `InferenceParams` and `OptimizationParams`, used within type inference and optimization, respectively. The interpreter object is then threaded throughout most of the type inference pipeline, and allows for straightforward prototyping and replacement of the compiler internals. As a simple example of the kind of workflow this enables, I include here a simple testing script showing how to use this to easily get a list of the number of times a function is inferred during type inference by overriding just two functions within the compiler. First, I will define here some simple methods to make working with inference a bit easier: ```julia using Core.Compiler import Core.Compiler: InferenceParams, OptimizationParams, get_world_counter, get_inference_cache """ @infer_function interp foo(1, 2) [show_steps=true] [show_ir=false] Infer a function call using the given interpreter object, return the inference object. Set keyword arguments to modify verbosity: * Set `show_steps` to `true` to see the `InferenceResult` step by step. * Set `show_ir` to `true` to see the final type-inferred Julia IR. """ macro infer_function(interp, func_call, kwarg_exs...) if !isa(func_call, Expr) || func_call.head != :call error("@infer_function requires a function call") end local func = func_call.args[1] local args = func_call.args[2:end] kwargs = [] for ex in kwarg_exs if ex isa Expr && ex.head === :(=) && ex.args[1] isa Symbol push!(kwargs, first(ex.args) => last(ex.args)) else error("Invalid @infer_function kwarg $(ex)") end end return quote infer_function($(esc(interp)), $(esc(func)), typeof.(($(args)...,)); $(esc(kwargs))...) end end function infer_function(interp, f, tt; show_steps::Bool=false, show_ir::Bool=false) # Find all methods that are applicable to these types fms = methods(f, tt) if length(fms) != 1 error("Unable to find single applicable method for $f with types $tt") end # Take the first applicable method method = first(fms) # Build argument tuple method_args = Tuple{typeof(f), tt...} # Grab the appropriate method instance for these types mi = Core.Compiler.specialize_method(method, method_args, Core.svec()) # Construct InferenceResult to hold the result, result = Core.Compiler.InferenceResult(mi) if show_steps @info("Initial result, before inference: ", result) end # Create an InferenceState to begin inference, give it a world that is always newest world = Core.Compiler.get_world_counter() frame = Core.Compiler.InferenceState(result, #=cached=# true, interp) # Run type inference on this frame. Because the interpreter is embedded # within this InferenceResult, we don't need to pass the interpreter in. Core.Compiler.typeinf_local(interp, frame) if show_steps @info("Ending result, post-inference: ", result) end if show_ir @info("Inferred source: ", result.result.src) end # Give the result back return result end ``` Next, we define a simple function and pass it through: ```julia function foo(x, y) return x + y * x end native_interpreter = Core.Compiler.NativeInterpreter() inferred = @infer_function native_interpreter foo(1.0, 2.0) show_steps=true show_ir=true ``` This gives a nice output such as the following: ```julia-repl ┌ Info: Initial result, before inference: └ result = foo(::Float64, ::Float64) => Any ┌ Info: Ending result, post-inference: └ result = foo(::Float64, ::Float64) => Float64 ┌ Info: Inferred source: │ result.result.src = │ CodeInfo( │ @ REPL[1]:3 within `foo' │ 1 ─ %1 = (y * x)::Float64 │ │ %2 = (x + %1)::Float64 │ └── return %2 └ ) ``` We can then define a custom `AbstractInterpreter` subtype that will override two specific pieces of the compilation process; managing the runtime inference cache. While it will transparently pass all information through to a bundled `NativeInterpreter`, it has the ability to force cache misses in order to re-infer things so that we can easily see how many methods (and which) would be inferred to compile a certain method: ```julia struct CountingInterpreter <: Compiler.AbstractInterpreter visited_methods::Set{Core.Compiler.MethodInstance} methods_inferred::Ref{UInt64} # Keep around a native interpreter so that we can sub off to "super" functions native_interpreter::Core.Compiler.NativeInterpreter end CountingInterpreter() = CountingInterpreter( Set{Core.Compiler.MethodInstance}(), Ref(UInt64(0)), Core.Compiler.NativeInterpreter(), ) InferenceParams(ci::CountingInterpreter) = InferenceParams(ci.native_interpreter) OptimizationParams(ci::CountingInterpreter) = OptimizationParams(ci.native_interpreter) get_world_counter(ci::CountingInterpreter) = get_world_counter(ci.native_interpreter) get_inference_cache(ci::CountingInterpreter) = get_inference_cache(ci.native_interpreter) function Core.Compiler.inf_for_methodinstance(interp::CountingInterpreter, mi::Core.Compiler.MethodInstance, min_world::UInt, max_world::UInt=min_world) # Hit our own cache; if it exists, pass on to the main runtime if mi in interp.visited_methods return Core.Compiler.inf_for_methodinstance(interp.native_interpreter, mi, min_world, max_world) end # Otherwise, we return `nothing`, forcing a cache miss return nothing end function Core.Compiler.cache_result(interp::CountingInterpreter, result::Core.Compiler.InferenceResult, min_valid::UInt, max_valid::UInt) push!(interp.visited_methods, result.linfo) interp.methods_inferred[] += 1 return Core.Compiler.cache_result(interp.native_interpreter, result, min_valid, max_valid) end function reset!(interp::CountingInterpreter) empty!(interp.visited_methods) interp.methods_inferred[] = 0 return nothing end ``` Running it on our testing function: ```julia counting_interpreter = CountingInterpreter() inferred = @infer_function counting_interpreter foo(1.0, 2.0) @info("Cumulative number of methods inferred: $(counting_interpreter.methods_inferred[])") inferred = @infer_function counting_interpreter foo(1, 2) show_ir=true @info("Cumulative number of methods inferred: $(counting_interpreter.methods_inferred[])") inferred = @infer_function counting_interpreter foo(1.0, 2.0) @info("Cumulative number of methods inferred: $(counting_interpreter.methods_inferred[])") reset!(counting_interpreter) @info("Cumulative number of methods inferred: $(counting_interpreter.methods_inferred[])") inferred = @infer_function counting_interpreter foo(1.0, 2.0) @info("Cumulative number of methods inferred: $(counting_interpreter.methods_inferred[])") ``` Also gives us a nice result: ``` [ Info: Cumulative number of methods inferred: 2 ┌ Info: Inferred source: │ result.result.src = │ CodeInfo( │ @ /Users/sabae/src/julia-compilerhack/AbstractInterpreterTest.jl:81 within `foo' │ 1 ─ %1 = (y * x)::Int64 │ │ %2 = (x + %1)::Int64 │ └── return %2 └ ) [ Info: Cumulative number of methods inferred: 4 [ Info: Cumulative number of methods inferred: 4 [ Info: Cumulative number of methods inferred: 0 [ Info: Cumulative number of methods inferred: 2 ``` --- base/compiler/abstractinterpretation.jl | 143 +++++++++---------- base/compiler/bootstrap.jl | 6 +- base/compiler/compiler.jl | 2 +- base/compiler/inferenceresult.jl | 12 -- base/compiler/inferencestate.jl | 18 +-- base/compiler/optimize.jl | 47 ++++--- base/compiler/params.jl | 72 ---------- base/compiler/ssair/inlining.jl | 32 ++--- base/compiler/tfuncs.jl | 16 +-- base/compiler/typeinfer.jl | 82 +++++------ base/compiler/types.jl | 177 ++++++++++++++++++++++++ base/compiler/utilities.jl | 2 +- base/reflection.jl | 9 +- base/show.jl | 14 ++ doc/src/devdocs/inference.md | 7 +- stdlib/REPL/src/REPLCompletions.jl | 4 +- test/compiler/inference.jl | 6 +- test/reflection.jl | 5 +- 18 files changed, 387 insertions(+), 267 deletions(-) delete mode 100644 base/compiler/params.jl create mode 100644 base/compiler/types.jl diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index f1c17a6d750c3..c244eb2b3198d 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -4,6 +4,8 @@ # constants # ############# +const DEFAULT_INTERPRETER = NativeInterpreter(UInt(0)) + const CoreNumType = Union{Int32, Int64, Float32, Float64} const _REF_NAME = Ref.body.name @@ -16,8 +18,8 @@ const _REF_NAME = Ref.body.name call_result_unused(frame::InferenceState, pc::LineNum=frame.currpc) = isexpr(frame.src.code[frame.currpc], :call) && isempty(frame.ssavalue_uses[pc]) -function abstract_call_gf_by_type(@nospecialize(f), argtypes::Vector{Any}, @nospecialize(atype), sv::InferenceState, - max_methods = sv.params.MAX_METHODS) +function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), argtypes::Vector{Any}, @nospecialize(atype), sv::InferenceState, + max_methods = InferenceParams(interp).MAX_METHODS) atype_params = unwrap_unionall(atype).parameters ft = unwrap_unionall(atype_params[1]) # TODO: ccall jl_method_table_for here isa(ft, DataType) || return Any # the function being called is unknown. can't properly handle this backedge right now @@ -37,17 +39,17 @@ function abstract_call_gf_by_type(@nospecialize(f), argtypes::Vector{Any}, @nosp end min_valid = UInt[typemin(UInt)] max_valid = UInt[typemax(UInt)] - splitunions = 1 < countunionsplit(atype_params) <= sv.params.MAX_UNION_SPLITTING + splitunions = 1 < countunionsplit(atype_params) <= InferenceParams(interp).MAX_UNION_SPLITTING if splitunions splitsigs = switchtupleunion(atype) applicable = Any[] for sig_n in splitsigs - xapplicable = _methods_by_ftype(sig_n, max_methods, sv.params.world, min_valid, max_valid) + xapplicable = _methods_by_ftype(sig_n, max_methods, get_world_counter(interp), min_valid, max_valid) xapplicable === false && return Any append!(applicable, xapplicable) end else - applicable = _methods_by_ftype(atype, max_methods, sv.params.world, min_valid, max_valid) + applicable = _methods_by_ftype(atype, max_methods, get_world_counter(interp), min_valid, max_valid) if applicable === false # this means too many methods matched # (assume this will always be true, so we don't compute / update valid age in this case) @@ -84,12 +86,12 @@ function abstract_call_gf_by_type(@nospecialize(f), argtypes::Vector{Any}, @nosp sigtuple = unwrap_unionall(sig)::DataType splitunions = false this_rt = Bottom - # TODO: splitunions = 1 < countunionsplit(sigtuple.parameters) * napplicable <= sv.params.MAX_UNION_SPLITTING + # TODO: splitunions = 1 < countunionsplit(sigtuple.parameters) * napplicable <= InferenceParams(interp).MAX_UNION_SPLITTING # currently this triggers a bug in inference recursion detection if splitunions splitsigs = switchtupleunion(sig) for sig_n in splitsigs - rt, edgecycle1, edge = abstract_call_method(method, sig_n, svec(), multiple_matches, sv) + rt, edgecycle1, edge = abstract_call_method(interp, method, sig_n, svec(), multiple_matches, sv) if edge !== nothing push!(edges, edge) end @@ -98,7 +100,7 @@ function abstract_call_gf_by_type(@nospecialize(f), argtypes::Vector{Any}, @nosp this_rt === Any && break end else - this_rt, edgecycle1, edge = abstract_call_method(method, sig, match[2]::SimpleVector, multiple_matches, sv) + this_rt, edgecycle1, edge = abstract_call_method(interp, method, sig, match[2]::SimpleVector, multiple_matches, sv) edgecycle |= edgecycle1::Bool if edge !== nothing push!(edges, edge) @@ -117,11 +119,11 @@ function abstract_call_gf_by_type(@nospecialize(f), argtypes::Vector{Any}, @nosp end # try constant propagation if only 1 method is inferred to non-Bottom # this is in preparation for inlining, or improving the return result - if nonbot > 0 && seen == napplicable && !edgecycle && isa(rettype, Type) && sv.params.ipo_constant_propagation + if nonbot > 0 && seen == napplicable && !edgecycle && isa(rettype, Type) && InferenceParams(interp).ipo_constant_propagation # if there's a possibility we could constant-propagate a better result # (hopefully without doing too much work), try to do that now # TODO: it feels like this could be better integrated into abstract_call_method / typeinf_edge - const_rettype = abstract_call_method_with_const_args(rettype, f, argtypes, applicable[nonbot]::SimpleVector, sv) + const_rettype = abstract_call_method_with_const_args(interp, rettype, f, argtypes, applicable[nonbot]::SimpleVector, sv) if const_rettype ⊑ rettype # use the better result, if it's a refinement of rettype rettype = const_rettype @@ -174,7 +176,7 @@ function const_prop_profitable(@nospecialize(arg)) return false end -function abstract_call_method_with_const_args(@nospecialize(rettype), @nospecialize(f), argtypes::Vector{Any}, match::SimpleVector, sv::InferenceState) +function abstract_call_method_with_const_args(interp::AbstractInterpreter, @nospecialize(rettype), @nospecialize(f), argtypes::Vector{Any}, match::SimpleVector, sv::InferenceState) method = match[3]::Method nargs::Int = method.nargs method.isva && (nargs -= 1) @@ -215,7 +217,7 @@ function abstract_call_method_with_const_args(@nospecialize(rettype), @nospecial istopfunction(f, :<<) || istopfunction(f, :>>)) return Any end - force_inference = allconst || sv.params.aggressive_constant_propagation + force_inference = allconst || InferenceParams(interp).aggressive_constant_propagation if istopfunction(f, :getproperty) || istopfunction(f, :setproperty!) force_inference = true end @@ -226,7 +228,7 @@ function abstract_call_method_with_const_args(@nospecialize(rettype), @nospecial mi = mi::MethodInstance # decide if it's likely to be worthwhile if !force_inference - code = inf_for_methodinstance(mi, sv.params.world) + code = inf_for_methodinstance(interp, mi, get_world_counter(interp)) declared_inline = isdefined(method, :source) && ccall(:jl_ast_flag_inlineable, Bool, (Any,), method.source) cache_inlineable = declared_inline if isdefined(code, :inferred) && !cache_inlineable @@ -241,14 +243,15 @@ function abstract_call_method_with_const_args(@nospecialize(rettype), @nospecial return Any end end - inf_result = cache_lookup(mi, argtypes, sv.params.cache) + inf_cache = get_inference_cache(interp) + inf_result = cache_lookup(mi, argtypes, inf_cache) if inf_result === nothing inf_result = InferenceResult(mi, argtypes) - frame = InferenceState(inf_result, #=cache=#false, sv.params) + frame = InferenceState(inf_result, #=cache=#false, interp) frame.limited = true frame.parent = sv - push!(sv.params.cache, inf_result) - typeinf(frame) || return Any + push!(inf_cache, inf_result) + typeinf(interp, frame) || return Any end result = inf_result.result isa(result, InferenceState) && return Any # TODO: unexpected, is this recursive constant inference? @@ -256,7 +259,7 @@ function abstract_call_method_with_const_args(@nospecialize(rettype), @nospecial return result end -function abstract_call_method(method::Method, @nospecialize(sig), sparams::SimpleVector, hardlimit::Bool, sv::InferenceState) +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 return Any, false, nothing end @@ -356,7 +359,7 @@ function abstract_call_method(method::Method, @nospecialize(sig), sparams::Simpl comparison = method.sig end # see if the type is actually too big (relative to the caller), and limit it if required - newsig = limit_type_size(sig, comparison, hardlimit ? comparison : sv.linfo.specTypes, sv.params.TUPLE_COMPLEXITY_LIMIT_DEPTH, spec_len) + newsig = limit_type_size(sig, comparison, hardlimit ? comparison : sv.linfo.specTypes, InferenceParams(interp).TUPLE_COMPLEXITY_LIMIT_DEPTH, spec_len) if newsig !== sig # continue inference, but note that we've limited parameter complexity @@ -393,7 +396,7 @@ function abstract_call_method(method::Method, @nospecialize(sig), sparams::Simpl # while !(newsig in seen) # push!(seen, newsig) # lsig = length((unwrap_unionall(sig)::DataType).parameters) - # newsig = limit_type_size(newsig, sig, sv.linfo.specTypes, sv.params.TUPLE_COMPLEXITY_LIMIT_DEPTH, lsig) + # newsig = limit_type_size(newsig, sig, sv.linfo.specTypes, InferenceParams(interp).TUPLE_COMPLEXITY_LIMIT_DEPTH, lsig) # recomputed = ccall(:jl_type_intersection_with_env, Any, (Any, Any), newsig, method.sig)::SimpleVector # newsig = recomputed[2] # end @@ -401,7 +404,7 @@ function abstract_call_method(method::Method, @nospecialize(sig), sparams::Simpl sparams = recomputed[2]::SimpleVector end - rt, edge = typeinf_edge(method, sig, sparams, sv) + rt, edge = typeinf_edge(interp, method, sig, sparams, sv) if edge === nothing edgecycle = true end @@ -435,7 +438,7 @@ 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(@nospecialize(itft), @nospecialize(typ), vtypes::VarTable, sv::InferenceState) +function precise_container_type(interp::AbstractInterpreter, @nospecialize(itft), @nospecialize(typ), vtypes::VarTable, sv::InferenceState) if isa(typ, PartialStruct) && typ.typ.name === Tuple.name return typ.fields end @@ -497,12 +500,12 @@ function precise_container_type(@nospecialize(itft), @nospecialize(typ), vtypes: elseif tti0 <: Array return Any[Vararg{eltype(tti0)}] else - return abstract_iteration(itft, typ, vtypes, sv) + return abstract_iteration(interp, itft, typ, vtypes, sv) end end # simulate iteration protocol on container type up to fixpoint -function abstract_iteration(@nospecialize(itft), @nospecialize(itertype), vtypes::VarTable, sv::InferenceState) +function abstract_iteration(interp, @nospecialize(itft), @nospecialize(itertype), vtypes::VarTable, sv::InferenceState) if !isdefined(Main, :Base) || !isdefined(Main.Base, :iterate) || !isconst(Main.Base, :iterate) return Any[Vararg{Any}] end @@ -514,7 +517,7 @@ function abstract_iteration(@nospecialize(itft), @nospecialize(itertype), vtypes else return Any[Vararg{Any}] end - stateordonet = abstract_call_known(iteratef, nothing, Any[itft, itertype], vtypes, sv) + stateordonet = abstract_call_known(interp, iteratef, nothing, Any[itft, itertype], vtypes, sv) # Return Bottom if this is not an iterator. # WARNING: Changes to the iteration protocol must be reflected here, # this is not just an optimization. @@ -522,7 +525,7 @@ function abstract_iteration(@nospecialize(itft), @nospecialize(itertype), vtypes valtype = statetype = Bottom ret = Any[] stateordonet = widenconst(stateordonet) - while !(Nothing <: stateordonet) && length(ret) < sv.params.MAX_TUPLE_SPLAT + while !(Nothing <: stateordonet) && length(ret) < InferenceParams(interp).MAX_TUPLE_SPLAT if !isa(stateordonet, DataType) || !(stateordonet <: Tuple) || isvatuple(stateordonet) || length(stateordonet.parameters) != 2 break end @@ -533,7 +536,7 @@ function abstract_iteration(@nospecialize(itft), @nospecialize(itertype), vtypes valtype = stateordonet.parameters[1] statetype = stateordonet.parameters[2] push!(ret, valtype) - stateordonet = abstract_call_known(iteratef, nothing, Any[Const(iteratef), itertype, statetype], vtypes, sv) + stateordonet = abstract_call_known(interp, iteratef, nothing, Any[Const(iteratef), itertype, statetype], vtypes, sv) stateordonet = widenconst(stateordonet) end if stateordonet === Nothing @@ -550,7 +553,7 @@ function abstract_iteration(@nospecialize(itft), @nospecialize(itertype), vtypes end valtype = tmerge(valtype, nounion.parameters[1]) statetype = tmerge(statetype, nounion.parameters[2]) - stateordonet = abstract_call_known(iteratef, nothing, Any[Const(iteratef), itertype, statetype], vtypes, sv) + stateordonet = abstract_call_known(interp, iteratef, nothing, Any[Const(iteratef), itertype, statetype], vtypes, sv) stateordonet = widenconst(stateordonet) end push!(ret, Vararg{valtype}) @@ -558,8 +561,8 @@ function abstract_iteration(@nospecialize(itft), @nospecialize(itertype), vtypes end # do apply(af, fargs...), where af is a function value -function abstract_apply(@nospecialize(itft), @nospecialize(aft), aargtypes::Vector{Any}, vtypes::VarTable, sv::InferenceState, - max_methods = sv.params.MAX_METHODS) +function abstract_apply(interp::AbstractInterpreter, @nospecialize(itft), @nospecialize(aft), aargtypes::Vector{Any}, vtypes::VarTable, sv::InferenceState, + max_methods = InferenceParams(interp).MAX_METHODS) aftw = widenconst(aft) if !isa(aft, Const) && (!isType(aftw) || has_free_typevars(aftw)) if !isconcretetype(aftw) || (aftw <: Builtin) @@ -571,12 +574,12 @@ function abstract_apply(@nospecialize(itft), @nospecialize(aft), aargtypes::Vect end res = Union{} nargs = length(aargtypes) - splitunions = 1 < countunionsplit(aargtypes) <= sv.params.MAX_APPLY_UNION_ENUM + splitunions = 1 < countunionsplit(aargtypes) <= InferenceParams(interp).MAX_APPLY_UNION_ENUM ctypes = Any[Any[aft]] for i = 1:nargs ctypes´ = [] for ti in (splitunions ? uniontypes(aargtypes[i]) : Any[aargtypes[i]]) - cti = precise_container_type(itft, ti, vtypes, sv) + cti = precise_container_type(interp, itft, ti, vtypes, sv) if _any(t -> t === Bottom, cti) continue end @@ -601,7 +604,7 @@ function abstract_apply(@nospecialize(itft), @nospecialize(aft), aargtypes::Vect break end end - rt = abstract_call(nothing, ct, vtypes, sv, max_methods) + rt = abstract_call(interp, nothing, ct, vtypes, sv, max_methods) res = tmerge(res, rt) if res === Any break @@ -658,19 +661,19 @@ function argtype_tail(argtypes::Vector{Any}, i::Int) end # call where the function is known exactly -function abstract_call_known(@nospecialize(f), fargs::Union{Nothing,Vector{Any}}, argtypes::Vector{Any}, vtypes::VarTable, sv::InferenceState, max_methods = sv.params.MAX_METHODS) +function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), fargs::Union{Nothing,Vector{Any}}, argtypes::Vector{Any}, vtypes::VarTable, sv::InferenceState, max_methods = InferenceParams(interp).MAX_METHODS) la = length(argtypes) if isa(f, Builtin) if f === _apply ft = argtype_by_index(argtypes, 2) ft === Bottom && return Bottom - return abstract_apply(nothing, ft, argtype_tail(argtypes, 3), vtypes, sv, max_methods) + return abstract_apply(interp, nothing, ft, argtype_tail(argtypes, 3), vtypes, sv, max_methods) elseif f === _apply_iterate itft = argtype_by_index(argtypes, 2) ft = argtype_by_index(argtypes, 3) (itft === Bottom || ft === Bottom) && return Bottom - return abstract_apply(itft, ft, argtype_tail(argtypes, 4), vtypes, sv, max_methods) + return abstract_apply(interp, itft, ft, argtype_tail(argtypes, 4), vtypes, sv, max_methods) elseif f === ifelse && fargs isa Vector{Any} && la == 4 && argtypes[2] isa Conditional # try to simulate this as a real conditional (`cnd ? x : y`), so that the penalty for using `ifelse` instead isn't too high cnd = argtypes[2]::Conditional @@ -686,9 +689,9 @@ function abstract_call_known(@nospecialize(f), fargs::Union{Nothing,Vector{Any}} end return tmerge(tx, ty) end - rt = builtin_tfunction(f, argtypes[2:end], sv) + rt = builtin_tfunction(interp, f, argtypes[2:end], sv) if f === getfield && isa(fargs, Vector{Any}) && la == 3 && isa(argtypes[3], Const) && isa(argtypes[3].val, Int) && argtypes[2] ⊑ Tuple - cti = precise_container_type(nothing, argtypes[2], vtypes, sv) + cti = precise_container_type(interp, nothing, argtypes[2], vtypes, sv) idx = argtypes[3].val if 1 <= idx <= length(cti) rt = unwrapva(cti[idx]) @@ -817,7 +820,7 @@ function abstract_call_known(@nospecialize(f), fargs::Union{Nothing,Vector{Any}} elseif f === Tuple && la == 2 && !isconcretetype(widenconst(argtypes[2])) return Tuple elseif is_return_type(f) - rt_rt = return_type_tfunc(argtypes, vtypes, sv) + rt_rt = return_type_tfunc(interp, argtypes, vtypes, sv) if rt_rt !== nothing return rt_rt end @@ -826,12 +829,12 @@ function abstract_call_known(@nospecialize(f), fargs::Union{Nothing,Vector{Any}} # handle Conditional propagation through !Bool aty = argtypes[2] if isa(aty, Conditional) - abstract_call_gf_by_type(f, Any[Const(f), Bool], Tuple{typeof(f), Bool}, sv) # make sure we've inferred `!(::Bool)` + abstract_call_gf_by_type(interp, f, Any[Const(f), Bool], Tuple{typeof(f), Bool}, sv) # make sure we've inferred `!(::Bool)` return Conditional(aty.var, aty.elsetype, aty.vtype) end elseif la == 3 && istopfunction(f, :!==) # mark !== as exactly a negated call to === - rty = abstract_call_known((===), fargs, argtypes, vtypes, sv) + rty = abstract_call_known(interp, (===), fargs, argtypes, vtypes, sv) if isa(rty, Conditional) return Conditional(rty.var, rty.elsetype, rty.vtype) # swap if-else elseif isa(rty, Const) @@ -847,7 +850,7 @@ function abstract_call_known(@nospecialize(f), fargs::Union{Nothing,Vector{Any}} fargs = nothing end argtypes = Any[typeof(<:), argtypes[3], argtypes[2]] - rty = abstract_call_known(<:, fargs, argtypes, vtypes, sv) + rty = abstract_call_known(interp, <:, fargs, argtypes, vtypes, sv) return rty elseif la == 2 && isa(argtypes[2], Const) && isa(argtypes[2].val, SimpleVector) && istopfunction(f, :length) # mark length(::SimpleVector) as @pure @@ -870,12 +873,12 @@ function abstract_call_known(@nospecialize(f), fargs::Union{Nothing,Vector{Any}} end atype = argtypes_to_type(argtypes) - return abstract_call_gf_by_type(f, argtypes, atype, sv, max_methods) + return abstract_call_gf_by_type(interp, f, argtypes, atype, sv, max_methods) end # call where the function is any lattice element -function abstract_call(fargs::Union{Nothing,Vector{Any}}, argtypes::Vector{Any}, vtypes::VarTable, sv::InferenceState, - max_methods = sv.params.MAX_METHODS) +function abstract_call(interp::AbstractInterpreter, fargs::Union{Nothing,Vector{Any}}, argtypes::Vector{Any}, + vtypes::VarTable, sv::InferenceState, max_methods = InferenceParams(interp).MAX_METHODS) #print("call ", e.args[1], argtypes, "\n\n") ft = argtypes[1] if isa(ft, Const) @@ -890,9 +893,9 @@ function abstract_call(fargs::Union{Nothing,Vector{Any}}, argtypes::Vector{Any}, if typeintersect(widenconst(ft), Builtin) != Union{} return Any end - return abstract_call_gf_by_type(nothing, argtypes, argtypes_to_type(argtypes), sv, max_methods) + return abstract_call_gf_by_type(interp, nothing, argtypes, argtypes_to_type(argtypes), sv, max_methods) end - return abstract_call_known(f, fargs, argtypes, vtypes, sv, max_methods) + return abstract_call_known(interp, f, fargs, argtypes, vtypes, sv, max_methods) end function sp_type_rewrap(@nospecialize(T), linfo::MethodInstance, isreturn::Bool) @@ -933,19 +936,19 @@ function sp_type_rewrap(@nospecialize(T), linfo::MethodInstance, isreturn::Bool) return T end -function abstract_eval_cfunction(e::Expr, vtypes::VarTable, sv::InferenceState) - f = abstract_eval(e.args[2], vtypes, sv) +function abstract_eval_cfunction(interp::AbstractInterpreter, e::Expr, vtypes::VarTable, sv::InferenceState) + f = abstract_eval(interp, e.args[2], vtypes, sv) # rt = sp_type_rewrap(e.args[3], sv.linfo, true) at = Any[ sp_type_rewrap(argt, sv.linfo, false) for argt in e.args[4]::SimpleVector ] pushfirst!(at, f) # this may be the wrong world for the call, # but some of the result is likely to be valid anyways # and that may help generate better codegen - abstract_call(nothing, at, vtypes, sv) + abstract_call(interp, nothing, at, vtypes, sv) nothing end -function abstract_eval(@nospecialize(e), vtypes::VarTable, sv::InferenceState) +function abstract_eval(interp::AbstractInterpreter, @nospecialize(e), vtypes::VarTable, sv::InferenceState) if isa(e, QuoteNode) return AbstractEvalConstant((e::QuoteNode).value) elseif isa(e, SSAValue) @@ -965,22 +968,22 @@ function abstract_eval(@nospecialize(e), vtypes::VarTable, sv::InferenceState) n = length(ea) argtypes = Vector{Any}(undef, n) @inbounds for i = 1:n - ai = abstract_eval(ea[i], vtypes, sv) + ai = abstract_eval(interp, ea[i], vtypes, sv) if ai === Bottom return Bottom end argtypes[i] = ai end - t = abstract_call(ea, argtypes, vtypes, sv) + t = abstract_call(interp, ea, argtypes, vtypes, sv) elseif e.head === :new - t = instanceof_tfunc(abstract_eval(e.args[1], vtypes, sv))[1] + t = instanceof_tfunc(abstract_eval(interp, e.args[1], vtypes, sv))[1] if isconcretetype(t) && !t.mutable args = Vector{Any}(undef, length(e.args)-1) ats = Vector{Any}(undef, length(e.args)-1) anyconst = false allconst = true for i = 2:length(e.args) - at = abstract_eval(e.args[i], vtypes, sv) + at = abstract_eval(interp, e.args[i], vtypes, sv) if !anyconst anyconst = has_nontrivial_const_info(at) end @@ -1010,22 +1013,22 @@ function abstract_eval(@nospecialize(e), vtypes::VarTable, sv::InferenceState) end end elseif e.head === :splatnew - t = instanceof_tfunc(abstract_eval(e.args[1], vtypes, sv))[1] + t = instanceof_tfunc(abstract_eval(interp, e.args[1], vtypes, sv))[1] elseif e.head === :& - abstract_eval(e.args[1], vtypes, sv) + abstract_eval(interp, e.args[1], vtypes, sv) t = Any elseif e.head === :foreigncall - abstract_eval(e.args[1], vtypes, sv) + abstract_eval(interp, e.args[1], vtypes, sv) t = sp_type_rewrap(e.args[2], sv.linfo, true) for i = 3:length(e.args) - if abstract_eval(e.args[i], vtypes, sv) === Bottom + if abstract_eval(interp, e.args[i], vtypes, sv) === Bottom t = Bottom end end elseif e.head === :cfunction t = e.args[1] isa(t, Type) || (t = Any) - abstract_eval_cfunction(e, vtypes, sv) + abstract_eval_cfunction(interp, e, vtypes, sv) elseif e.head === :static_parameter n = e.args[1] t = Any @@ -1035,7 +1038,7 @@ function abstract_eval(@nospecialize(e), vtypes::VarTable, sv::InferenceState) elseif e.head === :method t = (length(e.args) == 1) ? Any : Nothing elseif e.head === :copyast - t = abstract_eval(e.args[1], vtypes, sv) + t = abstract_eval(interp, e.args[1], vtypes, sv) if t isa Const && t.val isa Expr # `copyast` makes copies of Exprs t = Expr @@ -1098,7 +1101,7 @@ function abstract_eval_ssavalue(s::SSAValue, src::CodeInfo) end # make as much progress on `frame` as possible (without handling cycles) -function typeinf_local(frame::InferenceState) +function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) @assert !frame.inferred frame.dont_work_on_me = true # mark that this function is currently on the stack W = frame.ip @@ -1131,7 +1134,7 @@ function typeinf_local(frame::InferenceState) elseif isa(stmt, GotoNode) pc´ = (stmt::GotoNode).label elseif hd === :gotoifnot - condt = abstract_eval(stmt.args[1], s[pc], frame) + condt = abstract_eval(interp, stmt.args[1], s[pc], frame) if condt === Bottom break end @@ -1165,7 +1168,7 @@ function typeinf_local(frame::InferenceState) end elseif hd === :return pc´ = n + 1 - rt = widenconditional(abstract_eval(stmt.args[1], s[pc], frame)) + rt = widenconditional(abstract_eval(interp, stmt.args[1], s[pc], frame)) if !isa(rt, Const) && !isa(rt, Type) && !isa(rt, PartialStruct) # only propagate information we know we can store # and is valid inter-procedurally @@ -1210,7 +1213,7 @@ function typeinf_local(frame::InferenceState) end else if hd === :(=) - t = abstract_eval(stmt.args[2], changes, frame) + t = abstract_eval(interp, stmt.args[2], changes, frame) t === Bottom && break frame.src.ssavaluetypes[pc] = t lhs = stmt.args[1] @@ -1224,7 +1227,7 @@ function typeinf_local(frame::InferenceState) end elseif hd === :inbounds || hd === :meta || hd === :loopinfo else - t = abstract_eval(stmt, changes, frame) + t = abstract_eval(interp, stmt, changes, frame) t === Bottom && break if !isempty(frame.ssavalue_uses[pc]) record_ssa_assign(pc, t, frame) @@ -1279,8 +1282,8 @@ function typeinf_local(frame::InferenceState) end # make as much progress on `frame` as possible (by handling cycles) -function typeinf_nocycle(frame::InferenceState) - typeinf_local(frame) +function typeinf_nocycle(interp::AbstractInterpreter, frame::InferenceState) + typeinf_local(interp, frame) # If the current frame is part of a cycle, solve the cycle before finishing no_active_ips_in_callers = false @@ -1289,10 +1292,10 @@ function typeinf_nocycle(frame::InferenceState) for caller in frame.callers_in_cycle caller.dont_work_on_me && return false # cycle is above us on the stack if caller.pc´´ <= caller.nstmts # equivalent to `isempty(caller.ip)` - # Note that `typeinf_local(caller)` can potentially modify the other frames + # Note that `typeinf_local(interp, caller)` can potentially modify the other frames # `frame.callers_in_cycle`, which is why making incremental progress requires the # outer while loop. - typeinf_local(caller) + typeinf_local(interp, caller) no_active_ips_in_callers = false end if caller.min_valid < frame.min_valid diff --git a/base/compiler/bootstrap.jl b/base/compiler/bootstrap.jl index b1fb73f6b659e..54a8e26ced9da 100644 --- a/base/compiler/bootstrap.jl +++ b/base/compiler/bootstrap.jl @@ -6,7 +6,9 @@ # since we won't be able to specialize & infer them at runtime let fs = Any[typeinf_ext, typeinf, typeinf_edge, pure_eval_call, run_passes], - world = get_world_counter() + world = get_world_counter(), + interp = NativeInterpreter(world) + for x in T_FFUNC_VAL push!(fs, x[3]) end @@ -27,7 +29,7 @@ let fs = Any[typeinf_ext, typeinf, typeinf_edge, pure_eval_call, run_passes], typ[i] = typ[i].ub end end - typeinf_type(m[3], Tuple{typ...}, m[2], Params(world)) + typeinf_type(interp, m[3], Tuple{typ...}, m[2]) end end end diff --git a/base/compiler/compiler.jl b/base/compiler/compiler.jl index b44a60f23d626..6ed55bdc05918 100644 --- a/base/compiler/compiler.jl +++ b/base/compiler/compiler.jl @@ -92,11 +92,11 @@ using .Sort # compiler # ############ +include("compiler/types.jl") include("compiler/utilities.jl") include("compiler/validation.jl") include("compiler/inferenceresult.jl") -include("compiler/params.jl") include("compiler/inferencestate.jl") include("compiler/typeutils.jl") diff --git a/base/compiler/inferenceresult.jl b/base/compiler/inferenceresult.jl index a466a00df89df..40903343e28fe 100644 --- a/base/compiler/inferenceresult.jl +++ b/base/compiler/inferenceresult.jl @@ -2,18 +2,6 @@ const EMPTY_VECTOR = Vector{Any}() -mutable struct InferenceResult - linfo::MethodInstance - argtypes::Vector{Any} - overridden_by_const::BitVector - result # ::Type, or InferenceState if WIP - src #::Union{CodeInfo, OptimizationState, Nothing} # if inferred copy is available - function InferenceResult(linfo::MethodInstance, given_argtypes = nothing) - argtypes, overridden_by_const = matching_cache_argtypes(linfo, given_argtypes) - return new(linfo, argtypes, overridden_by_const, Any, nothing) - end -end - function is_argtype_match(@nospecialize(given_argtype), @nospecialize(cache_argtype), overridden_by_const::Bool) diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index 4caf6b61ec810..527a8988b2014 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -3,7 +3,7 @@ const LineNum = Int mutable struct InferenceState - params::Params # describes how to compute the result + params::InferenceParams result::InferenceResult # remember where to put the result linfo::MethodInstance sptypes::Vector{Any} # types of static parameter @@ -13,6 +13,7 @@ mutable struct InferenceState # info on the state of inference and the linfo src::CodeInfo + world::UInt min_valid::UInt max_valid::UInt nargs::Int @@ -43,7 +44,7 @@ mutable struct InferenceState # src is assumed to be a newly-allocated CodeInfo, that can be modified in-place to contain intermediate results function InferenceState(result::InferenceResult, src::CodeInfo, - cached::Bool, params::Params) + cached::Bool, interp::AbstractInterpreter) linfo = result.linfo code = src.code::Array{Any,1} toplevel = !isa(linfo.def, Method) @@ -91,9 +92,9 @@ mutable struct InferenceState max_valid = src.max_world == typemax(UInt) ? get_world_counter() : src.max_world frame = new( - params, result, linfo, + InferenceParams(interp), result, linfo, sp, slottypes, inmodule, 0, - src, min_valid, max_valid, + src, get_world_counter(interp), min_valid, max_valid, nargs, s_types, s_edges, Union{}, W, 1, n, cur_hand, handler_at, n_handlers, @@ -103,17 +104,17 @@ mutable struct InferenceState #=parent=#nothing, cached, false, false, false) result.result = frame - cached && push!(params.cache, result) + cached && push!(get_inference_cache(interp), result) return frame end end -function InferenceState(result::InferenceResult, cached::Bool, params::Params) +function InferenceState(result::InferenceResult, cached::Bool, interp::AbstractInterpreter) # prepare an InferenceState object for inferring lambda src = retrieve_code_info(result.linfo) src === nothing && return nothing validate_code_in_debug_mode(result.linfo, src, "lowered") - return InferenceState(result, src, cached, params) + return InferenceState(result, src, cached, interp) end function sptypes_from_meth_instance(linfo::MethodInstance) @@ -190,8 +191,7 @@ _topmod(sv::InferenceState) = _topmod(sv.mod) function update_valid_age!(min_valid::UInt, max_valid::UInt, sv::InferenceState) sv.min_valid = max(sv.min_valid, min_valid) sv.max_valid = min(sv.max_valid, max_valid) - @assert(sv.min_valid <= sv.params.world <= sv.max_valid, - "invalid age range update") + @assert(sv.min_valid <= sv.world <= sv.max_valid, "invalid age range update") nothing end diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 38d8a6ea4b377..ee082b8258d35 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -5,32 +5,35 @@ ##################### mutable struct OptimizationState + params::OptimizationParams linfo::MethodInstance calledges::Vector{Any} src::CodeInfo mod::Module nargs::Int + world::UInt min_valid::UInt max_valid::UInt - params::Params sptypes::Vector{Any} # static parameters slottypes::Vector{Any} const_api::Bool - function OptimizationState(frame::InferenceState) + + # TODO: This will be eliminated once optimization no longer needs to do method lookups + interp::AbstractInterpreter + function OptimizationState(frame::InferenceState, params::OptimizationParams, interp::AbstractInterpreter) s_edges = frame.stmt_edges[1] if s_edges === nothing s_edges = [] frame.stmt_edges[1] = s_edges end src = frame.src - return new(frame.linfo, + return new(params, frame.linfo, s_edges::Vector{Any}, src, frame.mod, frame.nargs, - frame.min_valid, frame.max_valid, - frame.params, frame.sptypes, frame.slottypes, false) + frame.world, frame.min_valid, frame.max_valid, + frame.sptypes, frame.slottypes, false, interp) end - function OptimizationState(linfo::MethodInstance, src::CodeInfo, - params::Params) + function OptimizationState(linfo::MethodInstance, src::CodeInfo, params::OptimizationParams, interp::AbstractInterpreter) # prepare src for running optimization passes # if it isn't already nssavalues = src.ssavaluetypes @@ -53,18 +56,18 @@ mutable struct OptimizationState inmodule = linfo.def::Module nargs = 0 end - return new(linfo, + return new(params, linfo, s_edges::Vector{Any}, src, inmodule, nargs, - UInt(1), get_world_counter(), - params, sptypes_from_meth_instance(linfo), slottypes, false) + get_world_counter(), UInt(1), get_world_counter(), + sptypes_from_meth_instance(linfo), slottypes, false, interp) end end -function OptimizationState(linfo::MethodInstance, params::Params) +function OptimizationState(linfo::MethodInstance, params::OptimizationParams, interp::AbstractInterpreter) src = retrieve_code_info(linfo) src === nothing && return nothing - return OptimizationState(linfo, src, params) + return OptimizationState(linfo, src, params, interp) end @@ -104,7 +107,7 @@ _topmod(sv::OptimizationState) = _topmod(sv.mod) function update_valid_age!(min_valid::UInt, max_valid::UInt, sv::OptimizationState) sv.min_valid = max(sv.min_valid, min_valid) sv.max_valid = min(sv.max_valid, max_valid) - @assert(sv.min_valid <= sv.params.world <= sv.max_valid, + @assert(sv.min_valid <= sv.world <= sv.max_valid, "invalid age range update") nothing end @@ -122,10 +125,10 @@ function add_backedge!(li::CodeInstance, caller::OptimizationState) nothing end -function isinlineable(m::Method, me::OptimizationState, bonus::Int=0) +function isinlineable(m::Method, me::OptimizationState, params::OptimizationParams, bonus::Int=0) # compute the cost (size) of inlining this code inlineable = false - cost_threshold = me.params.inline_cost_threshold + cost_threshold = params.inline_cost_threshold if m.module === _topmod(m.module) # a few functions get special treatment name = m.name @@ -140,7 +143,7 @@ function isinlineable(m::Method, me::OptimizationState, bonus::Int=0) end end if !inlineable - inlineable = inline_worthy(me.src.code, me.src, me.sptypes, me.slottypes, me.params, cost_threshold + bonus) + inlineable = inline_worthy(me.src.code, me.src, me.sptypes, me.slottypes, params, cost_threshold + bonus) end return inlineable end @@ -163,7 +166,7 @@ function stmt_affects_purity(@nospecialize(stmt), ir) end # run the optimization work -function optimize(opt::OptimizationState, @nospecialize(result)) +function optimize(opt::OptimizationState, params::OptimizationParams, @nospecialize(result)) def = opt.linfo.def nargs = Int(opt.nargs) - 1 @timeit "optimizer" ir = run_passes(opt.src, nargs, opt) @@ -242,13 +245,13 @@ function optimize(opt::OptimizationState, @nospecialize(result)) else bonus = 0 if result ⊑ Tuple && !isbitstype(widenconst(result)) - bonus = opt.params.inline_tupleret_bonus + bonus = params.inline_tupleret_bonus end if opt.src.inlineable # For functions declared @inline, increase the cost threshold 20x - bonus += opt.params.inline_cost_threshold*19 + bonus += params.inline_cost_threshold*19 end - opt.src.inlineable = isinlineable(def, opt, bonus) + opt.src.inlineable = isinlineable(def, opt, params, bonus) end end nothing @@ -277,7 +280,7 @@ plus_saturate(x::Int, y::Int) = max(x, y, x+y) # known return type isknowntype(@nospecialize T) = (T === Union{}) || isconcretetype(T) -function statement_cost(ex::Expr, line::Int, src::CodeInfo, sptypes::Vector{Any}, slottypes::Vector{Any}, params::Params) +function statement_cost(ex::Expr, line::Int, src::CodeInfo, sptypes::Vector{Any}, slottypes::Vector{Any}, params::OptimizationParams) head = ex.head if is_meta_expr_head(head) return 0 @@ -367,7 +370,7 @@ function statement_cost(ex::Expr, line::Int, src::CodeInfo, sptypes::Vector{Any} end function inline_worthy(body::Array{Any,1}, src::CodeInfo, sptypes::Vector{Any}, slottypes::Vector{Any}, - params::Params, cost_threshold::Integer=params.inline_cost_threshold) + params::OptimizationParams, cost_threshold::Integer=params.inline_cost_threshold) bodycost::Int = 0 for line = 1:length(body) stmt = body[line] diff --git a/base/compiler/params.jl b/base/compiler/params.jl deleted file mode 100644 index 46c086210dbbd..0000000000000 --- a/base/compiler/params.jl +++ /dev/null @@ -1,72 +0,0 @@ -# This file is a part of Julia. License is MIT: https://julialang.org/license - -struct Params - cache::Vector{InferenceResult} - world::UInt - global_cache::Bool - - # optimization - inlining::Bool - ipo_constant_propagation::Bool - aggressive_constant_propagation::Bool - inline_cost_threshold::Int # number of CPU cycles beyond which it's not worth inlining - inline_nonleaf_penalty::Int # penalty for dynamic dispatch - inline_tupleret_bonus::Int # extra willingness for non-isbits tuple return types - - # don't consider more than N methods. this trades off between - # compiler performance and generated code performance. - # typically, considering many methods means spending lots of time - # obtaining poor type information. - # It is important for N to be >= the number of methods in the error() - # function, so we can still know that error() is always Bottom. - MAX_METHODS::Int - # the maximum number of union-tuples to swap / expand - # before computing the set of matching methods - MAX_UNION_SPLITTING::Int - # the maximum number of union-tuples to swap / expand - # when inferring a call to _apply - MAX_APPLY_UNION_ENUM::Int - - # parameters limiting large (tuple) types - TUPLE_COMPLEXITY_LIMIT_DEPTH::Int - - # when attempting to inlining _apply, abort the optimization if the tuple - # contains more than this many elements - MAX_TUPLE_SPLAT::Int - - # reasonable defaults - global function CustomParams(world::UInt, - ; - inlining::Bool = inlining_enabled(), - inline_cost_threshold::Int = DEFAULT_PARAMS.inline_cost_threshold, - inline_nonleaf_penalty::Int = DEFAULT_PARAMS.inline_nonleaf_penalty, - inline_tupleret_bonus::Int = DEFAULT_PARAMS.inline_tupleret_bonus, - ipo_constant_propagation::Bool = true, - aggressive_constant_propagation::Bool = false, - max_methods::Int = DEFAULT_PARAMS.MAX_METHODS, - tupletype_depth::Int = DEFAULT_PARAMS.TUPLE_COMPLEXITY_LIMIT_DEPTH, - tuple_splat::Int = DEFAULT_PARAMS.MAX_TUPLE_SPLAT, - union_splitting::Int = DEFAULT_PARAMS.MAX_UNION_SPLITTING, - apply_union_enum::Int = DEFAULT_PARAMS.MAX_APPLY_UNION_ENUM) - return new(Vector{InferenceResult}(), - world, false, - inlining, ipo_constant_propagation, aggressive_constant_propagation, - inline_cost_threshold, inline_nonleaf_penalty, inline_tupleret_bonus, - max_methods, union_splitting, apply_union_enum, tupletype_depth, - tuple_splat) - end - function Params(world::UInt) - world == typemax(UInt) && (world = get_world_counter()) # workaround for bad callers - @assert world <= get_world_counter() - inlining = inlining_enabled() - return new(Vector{InferenceResult}(), - world, true, - #=inlining, ipo_constant_propagation, aggressive_constant_propagation, inline_cost_threshold, inline_nonleaf_penalty,=# - inlining, true, false, 100, 1000, - #=inline_tupleret_bonus, max_methods, union_splitting, apply_union_enum=# - 400, 4, 4, 8, - #=tupletype_depth, tuple_splat=# - 3, 32) - end -end -const DEFAULT_PARAMS = Params(UInt(0)) diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index bd96133892872..593c0e1bf1790 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -574,11 +574,11 @@ function batch_inline!(todo::Vector{Any}, ir::IRCode, linetable::Vector{LineInfo return ir end -function spec_lambda(@nospecialize(atype), sv::OptimizationState, @nospecialize(invoke_data)) +function spec_lambda(interp::AbstractInterpreter, @nospecialize(atype), sv::OptimizationState, @nospecialize(invoke_data)) min_valid = UInt[typemin(UInt)] max_valid = UInt[typemax(UInt)] if invoke_data === nothing - mi = ccall(:jl_get_spec_lambda, Any, (Any, UInt, Ptr{UInt}, Ptr{UInt}), atype, sv.params.world, min_valid, max_valid) + mi = ccall(:jl_get_spec_lambda, Any, (Any, UInt, Ptr{UInt}, Ptr{UInt}), atype, get_world_counter(interp), min_valid, max_valid) else invoke_data = invoke_data::InvokeData atype <: invoke_data.types0 || return nothing @@ -699,7 +699,7 @@ function analyze_method!(idx::Int, sig::Signature, @nospecialize(metharg), meths # See if there exists a specialization for this method signature mi = specialize_method(method, metharg, methsp, true) # Union{Nothing, MethodInstance} if !isa(mi, MethodInstance) - return spec_lambda(atype_unlimited, sv, invoke_data) + return spec_lambda(sv.interp, atype_unlimited, sv, invoke_data) end isconst, src = find_inferred(mi, atypes, sv, stmttyp) @@ -708,14 +708,14 @@ function analyze_method!(idx::Int, sig::Signature, @nospecialize(metharg), meths return ConstantCase(src, method, Any[methsp...], metharg) end if src === nothing - return spec_lambda(atype_unlimited, sv, invoke_data) + return spec_lambda(sv.interp, atype_unlimited, sv, invoke_data) end src_inferred = ccall(:jl_ast_flag_inferred, Bool, (Any,), src) src_inlineable = ccall(:jl_ast_flag_inlineable, Bool, (Any,), src) if !(src_inferred && src_inlineable) - return spec_lambda(atype_unlimited, sv, invoke_data) + return spec_lambda(sv.interp, atype_unlimited, sv, invoke_data) end # At this point we're committed to performing the inlining, add the backedge @@ -810,7 +810,7 @@ function handle_single_case!(ir::IRCode, stmt::Expr, idx::Int, @nospecialize(cas nothing end -function is_valid_type_for_apply_rewrite(@nospecialize(typ), params::Params) +function is_valid_type_for_apply_rewrite(@nospecialize(typ), params::OptimizationParams) if isa(typ, Const) && isa(typ.val, SimpleVector) length(typ.val) > params.MAX_TUPLE_SPLAT && return false for p in typ.val @@ -880,7 +880,7 @@ function call_sig(ir::IRCode, stmt::Expr) Signature(f, ft, atypes) end -function inline_apply!(ir::IRCode, idx::Int, sig::Signature, params::Params) +function inline_apply!(ir::IRCode, idx::Int, sig::Signature, params::OptimizationParams) stmt = ir.stmts[idx] while sig.f === Core._apply || sig.f === Core._apply_iterate arg_start = sig.f === Core._apply ? 2 : 3 @@ -936,7 +936,7 @@ end # Handles all analysis and inlining of intrinsics and builtins. In particular, # this method does not access the method table or otherwise process generic # functions. -function process_simple!(ir::IRCode, idx::Int, params::Params) +function process_simple!(ir::IRCode, idx::Int, params::OptimizationParams, world::UInt) stmt = ir.stmts[idx] stmt isa Expr || return nothing if stmt.head === :splatnew @@ -967,7 +967,7 @@ function process_simple!(ir::IRCode, idx::Int, params::Params) # Handle invoke invoke_data = nothing if sig.f === Core.invoke && length(sig.atypes) >= 3 - res = compute_invoke_data(sig.atypes, params) + res = compute_invoke_data(sig.atypes, world) res === nothing && return nothing (sig, invoke_data) = res elseif is_builtin(sig) @@ -992,7 +992,7 @@ function assemble_inline_todo!(ir::IRCode, sv::OptimizationState) # todo = (inline_idx, (isva, isinvoke, na), method, spvals, inline_linetable, inline_ir, lie) todo = Any[] for idx in 1:length(ir.stmts) - r = process_simple!(ir, idx, sv.params) + r = process_simple!(ir, idx, sv.params, sv.world) r === nothing && continue stmt = ir.stmts[idx] @@ -1008,7 +1008,7 @@ function assemble_inline_todo!(ir::IRCode, sv::OptimizationState) # Regular case: Perform method matching min_valid = UInt[typemin(UInt)] max_valid = UInt[typemax(UInt)] - meth = _methods_by_ftype(sig.atype, sv.params.MAX_METHODS, sv.params.world, min_valid, max_valid) + meth = _methods_by_ftype(sig.atype, sv.params.MAX_METHODS, sv.world, min_valid, max_valid) if meth === false || length(meth) == 0 # No applicable method, or too many applicable methods continue @@ -1113,7 +1113,7 @@ function linear_inline_eligible(ir::IRCode) return true end -function compute_invoke_data(@nospecialize(atypes), params::Params) +function compute_invoke_data(@nospecialize(atypes), world::UInt) ft = widenconst(atypes[2]) if !isdispatchelem(ft) || has_free_typevars(ft) || (ft <: Builtin) # TODO: this can be rather aggressive at preventing inlining of closures @@ -1132,7 +1132,7 @@ function compute_invoke_data(@nospecialize(atypes), params::Params) min_valid = UInt[typemin(UInt)] max_valid = UInt[typemax(UInt)] invoke_entry = ccall(:jl_gf_invoke_lookup, Any, (Any, UInt), - invoke_types, params.world) # XXX: min_valid, max_valid + invoke_types, world) # XXX: min_valid, max_valid invoke_entry === nothing && return nothing invoke_data = InvokeData(invoke_entry::Core.TypeMapEntry, invoke_types, min_valid[1], max_valid[1]) atype0 = atypes[2] @@ -1150,7 +1150,7 @@ function ispuretopfunction(@nospecialize(f)) istopfunction(f, :promote_type) end -function early_inline_special_case(ir::IRCode, s::Signature, e::Expr, params::Params, +function early_inline_special_case(ir::IRCode, s::Signature, e::Expr, params::OptimizationParams, @nospecialize(etype)) f, ft, atypes = s.f, s.ft, s.atypes if (f === typeassert || ft ⊑ typeof(typeassert)) && length(atypes) == 3 @@ -1290,7 +1290,7 @@ function find_inferred(mi::MethodInstance, @nospecialize(atypes), sv::Optimizati end end if haveconst || improvable_via_constant_propagation(rettype) - inf_result = cache_lookup(mi, atypes, sv.params.cache) # Union{Nothing, InferenceResult} + inf_result = cache_lookup(mi, atypes, sv.interp.cache) # Union{Nothing, InferenceResult} else inf_result = nothing end @@ -1306,7 +1306,7 @@ function find_inferred(mi::MethodInstance, @nospecialize(atypes), sv::Optimizati end end - linfo = inf_for_methodinstance(mi, sv.params.world) + linfo = inf_for_methodinstance(Core.Compiler.NativeInterpreter(sv.world), mi, sv.world) if linfo isa CodeInstance if invoke_api(linfo) == 2 # in this case function can be inlined to a constant diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index d1b2034b7d4ba..4096f92905060 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -1150,21 +1150,21 @@ function apply_type_tfunc(@nospecialize(headtypetype), @nospecialize args...) end add_tfunc(apply_type, 1, INT_INF, apply_type_tfunc, 10) -function invoke_tfunc(@nospecialize(ft), @nospecialize(types), @nospecialize(argtype), sv::InferenceState) +function invoke_tfunc(interp::AbstractInterpreter, @nospecialize(ft), @nospecialize(types), @nospecialize(argtype), sv::InferenceState) argtype = typeintersect(types, argtype) argtype === Bottom && return Bottom argtype isa DataType || return Any # other cases are not implemented below isdispatchelem(ft) || return Any # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below types = rewrap_unionall(Tuple{ft, unwrap_unionall(types).parameters...}, types) argtype = Tuple{ft, argtype.parameters...} - entry = ccall(:jl_gf_invoke_lookup, Any, (Any, UInt), types, sv.params.world) + entry = ccall(:jl_gf_invoke_lookup, Any, (Any, UInt), types, get_world_counter(interp)) if entry === nothing return Any end # XXX: update_valid_age!(min_valid[1], max_valid[1], sv) meth = entry.func (ti, env) = ccall(:jl_type_intersection_with_env, Any, (Any, Any), argtype, meth.sig)::SimpleVector - rt, edge = typeinf_edge(meth::Method, ti, env, sv) + rt, edge = typeinf_edge(interp, meth::Method, ti, env, sv) edge !== nothing && add_backedge!(edge::MethodInstance, sv) return rt end @@ -1301,8 +1301,8 @@ function builtin_nothrow(@nospecialize(f), argtypes::Array{Any, 1}, @nospecializ return _builtin_nothrow(f, argtypes, rt) end -function builtin_tfunction(@nospecialize(f), argtypes::Array{Any,1}, - sv::Union{InferenceState,Nothing}, params::Params = sv.params) +function builtin_tfunction(interp::AbstractInterpreter, @nospecialize(f), argtypes::Array{Any,1}, + sv::Union{InferenceState,Nothing}) isva = !isempty(argtypes) && isvarargtype(argtypes[end]) if f === tuple return tuple_tfunc(argtypes) @@ -1354,7 +1354,7 @@ function builtin_tfunction(@nospecialize(f), argtypes::Array{Any,1}, sigty = nothing end if isa(sigty, Type) && !has_free_typevars(sigty) && sigty <: Tuple - return invoke_tfunc(ft, sigty, argtypes_to_type(argtypes[3:end]), sv) + return invoke_tfunc(interp, ft, sigty, argtypes_to_type(argtypes[3:end]), sv) end end return Any @@ -1428,7 +1428,7 @@ 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 -function return_type_tfunc(argtypes::Vector{Any}, vtypes::VarTable, sv::InferenceState) +function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, vtypes::VarTable, sv::InferenceState) if length(argtypes) == 3 tt = argtypes[3] if isa(tt, Const) || (isType(tt) && !has_free_typevars(tt)) @@ -1441,7 +1441,7 @@ function return_type_tfunc(argtypes::Vector{Any}, vtypes::VarTable, sv::Inferenc if contains_is(argtypes_vec, Union{}) return Const(Union{}) end - rt = abstract_call(nothing, argtypes_vec, vtypes, sv, -1) + rt = abstract_call(interp, nothing, argtypes_vec, vtypes, sv, -1) if isa(rt, Const) # output was computed to be constant return Const(typeof(rt.val)) diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 74f8695e111ec..0d1770bff78f4 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -1,15 +1,15 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license # build (and start inferring) the inference frame for the linfo -function typeinf(result::InferenceResult, cached::Bool, params::Params) - frame = InferenceState(result, cached, params) +function typeinf(interp::AbstractInterpreter, result::InferenceResult, cached::Bool) + frame = InferenceState(result, cached, interp) frame === nothing && return false cached && (result.linfo.inInference = true) - return typeinf(frame) + return typeinf(interp, frame) end -function typeinf(frame::InferenceState) - typeinf_nocycle(frame) || return false # frame is now part of a higher cycle +function typeinf(interp::AbstractInterpreter, frame::InferenceState) + typeinf_nocycle(interp, frame) || return false # frame is now part of a higher cycle # with no active ip's, frame is done frames = frame.callers_in_cycle isempty(frames) && push!(frames, frame) @@ -18,7 +18,7 @@ function typeinf(frame::InferenceState) caller.dont_work_on_me = true end for caller in frames - finish(caller) + finish(caller, interp) end # collect results for the new expanded frame results = InferenceResult[ frames[i].result for i in 1:length(frames) ] @@ -30,8 +30,8 @@ function typeinf(frame::InferenceState) for caller in results opt = caller.src if opt isa OptimizationState - optimize(opt, caller.result) - finish(opt.src) + optimize(opt, OptimizationParams(interp), caller.result) + finish(opt.src, interp) # finish updating the result struct validate_code_in_debug_mode(opt.linfo, opt.src, "optimized") if opt.const_api @@ -64,7 +64,7 @@ function typeinf(frame::InferenceState) caller.src.min_world = min_valid caller.src.max_world = max_valid if cached - cache_result(caller.result, min_valid, max_valid) + cache_result(interp, caller.result, min_valid, max_valid) end if max_valid == typemax(UInt) # if we aren't cached, we don't need this edge @@ -81,7 +81,7 @@ end # inference completed on `me` # update the MethodInstance and notify the edges -function cache_result(result::InferenceResult, min_valid::UInt, max_valid::UInt) +function cache_result(interp::AbstractInterpreter, result::InferenceResult, min_valid::UInt, max_valid::UInt) def = result.linfo.def toplevel = !isa(result.linfo.def, Method) if toplevel @@ -92,7 +92,7 @@ function cache_result(result::InferenceResult, min_valid::UInt, max_valid::UInt) # check if the existing linfo metadata is also sufficient to describe the current inference result # to decide if it is worth caching this already_inferred = !result.linfo.inInference - if inf_for_methodinstance(result.linfo, min_valid, max_valid) isa CodeInstance + if inf_for_methodinstance(interp, result.linfo, min_valid, max_valid) isa CodeInstance already_inferred = true end @@ -140,7 +140,7 @@ function cache_result(result::InferenceResult, min_valid::UInt, max_valid::UInt) nothing end -function finish(me::InferenceState) +function finish(me::InferenceState, interp::AbstractInterpreter) # prepare to run optimization passes on fulltree if me.limited && me.cached && me.parent !== nothing # a top parent will be cached still, but not this intermediate work @@ -155,7 +155,7 @@ function finish(me::InferenceState) if run_optimizer # construct the optimizer for later use, if we're building this IR to cache it # (otherwise, we'll run the optimization passes later, outside of inference) - opt = OptimizationState(me) + opt = OptimizationState(me, OptimizationParams(interp), interp) me.result.src = opt end end @@ -163,7 +163,7 @@ function finish(me::InferenceState) nothing end -function finish(src::CodeInfo) +function finish(src::CodeInfo, interp::AbstractInterpreter) # convert all type information into the form consumed by the cache for inlining and code-generation widen_all_consts!(src) src.inferred = true @@ -454,9 +454,9 @@ function resolve_call_cycle!(linfo::MethodInstance, parent::InferenceState) end # compute (and cache) an inferred AST and return the current best estimate of the result type -function typeinf_edge(method::Method, @nospecialize(atypes), sparams::SimpleVector, caller::InferenceState) +function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize(atypes), sparams::SimpleVector, caller::InferenceState) mi = specialize_method(method, atypes, sparams)::MethodInstance - code = inf_for_methodinstance(mi, caller.params.world) + code = inf_for_methodinstance(interp, mi, get_world_counter(interp)) if code isa CodeInstance # return existing rettype if the code is already inferred update_valid_age!(min_world(code), max_world(code), caller) if isdefined(code, :rettype_const) @@ -476,7 +476,7 @@ function typeinf_edge(method::Method, @nospecialize(atypes), sparams::SimpleVect # completely new mi.inInference = true result = InferenceResult(mi) - frame = InferenceState(result, #=cached=#true, caller.params) # always use the cache for edge targets + frame = InferenceState(result, #=cached=#true, interp) # always use the cache for edge targets if frame === nothing # can't get the source for this, so we know nothing mi.inInference = false @@ -485,7 +485,7 @@ function typeinf_edge(method::Method, @nospecialize(atypes), sparams::SimpleVect if caller.cached || caller.limited # don't involve uncached functions in cycle resolution frame.parent = caller end - typeinf(frame) + typeinf(interp, frame) update_valid_age!(frame, caller) return widenconst_bestguess(frame.bestguess), frame.inferred ? mi : nothing elseif frame === true @@ -506,15 +506,16 @@ end #### entry points for inferring a MethodInstance given a type signature #### # compute an inferred AST and return type -function typeinf_code(method::Method, @nospecialize(atypes), sparams::SimpleVector, run_optimizer::Bool, params::Params) +function typeinf_code(interp::AbstractInterpreter, method::Method, @nospecialize(atypes), sparams::SimpleVector, run_optimizer::Bool) mi = specialize_method(method, atypes, sparams)::MethodInstance ccall(:jl_typeinf_begin, Cvoid, ()) result = InferenceResult(mi) - frame = InferenceState(result, false, params) + frame = InferenceState(result, false, interp) frame === nothing && return (nothing, Any) - if typeinf(frame) && run_optimizer - opt = OptimizationState(frame) - optimize(opt, result.result) + if typeinf(interp, frame) && run_optimizer + opt_params = OptimizationParams(interp) + opt = OptimizationState(frame, opt_params, interp) + optimize(opt, opt_params, result.result) opt.src.inferred = true end ccall(:jl_typeinf_end, Cvoid, ()) @@ -523,11 +524,11 @@ function typeinf_code(method::Method, @nospecialize(atypes), sparams::SimpleVect end # compute (and cache) an inferred AST and return type -function typeinf_ext(mi::MethodInstance, params::Params) +function typeinf_ext(interp::AbstractInterpreter, mi::MethodInstance) method = mi.def::Method for i = 1:2 # test-and-lock-and-test i == 2 && ccall(:jl_typeinf_begin, Cvoid, ()) - code = inf_for_methodinstance(mi, params.world) + code = inf_for_methodinstance(interp, mi, get_world_counter(interp)) if code isa CodeInstance # see if this code already exists in the cache inf = code.inferred @@ -569,23 +570,23 @@ function typeinf_ext(mi::MethodInstance, params::Params) end end mi.inInference = true - frame = InferenceState(InferenceResult(mi), #=cached=#true, params) + frame = InferenceState(InferenceResult(mi), #=cached=#true, interp) frame === nothing && return nothing - typeinf(frame) + typeinf(interp, frame) ccall(:jl_typeinf_end, Cvoid, ()) frame.src.inferred || return nothing return frame.src end # compute (and cache) an inferred AST and return the inferred return type -function typeinf_type(method::Method, @nospecialize(atypes), sparams::SimpleVector, params::Params) +function typeinf_type(interp::AbstractInterpreter, method::Method, @nospecialize(atypes), sparams::SimpleVector) if contains_is(unwrap_unionall(atypes).parameters, Union{}) return Union{} # don't ask: it does weird and unnecessary things, if it occurs during bootstrap end mi = specialize_method(method, atypes, sparams)::MethodInstance for i = 1:2 # test-and-lock-and-test i == 2 && ccall(:jl_typeinf_begin, Cvoid, ()) - code = inf_for_methodinstance(mi, params.world) + code = inf_for_methodinstance(interp, mi, get_world_counter(interp)) if code isa CodeInstance # see if this rettype already exists in the cache i == 2 && ccall(:jl_typeinf_end, Cvoid, ()) @@ -593,16 +594,18 @@ function typeinf_type(method::Method, @nospecialize(atypes), sparams::SimpleVect end end frame = InferenceResult(mi) - typeinf(frame, true, params) + typeinf(interp, frame, true) ccall(:jl_typeinf_end, Cvoid, ()) frame.result isa InferenceState && return nothing return widenconst(frame.result) end -@timeit function typeinf_ext(linfo::MethodInstance, world::UInt) +# This is a bridge for the C code calling `jl_typinf_func()` +typeinf_ext(mi::MethodInstance, world::UInt) = typeinf_ext(NativeInterpreter(world), mi, world) +function typeinf_ext(interp::AbstractInterpreter, linfo::MethodInstance, world::UInt) if isa(linfo.def, Method) # method lambda - infer this specialization via the method cache - src = typeinf_ext(linfo, Params(world)) + src = typeinf_ext(interp, linfo) else src = linfo.uninferred::CodeInfo if !src.inferred @@ -610,8 +613,8 @@ end ccall(:jl_typeinf_begin, Cvoid, ()) if !src.inferred result = InferenceResult(linfo) - frame = InferenceState(result, src, #=cached=#true, Params(world)) - typeinf(frame) + frame = InferenceState(result, src, #=cached=#true, interp) + typeinf(interp, frame) @assert frame.inferred # TODO: deal with this better src = frame.src end @@ -627,19 +630,20 @@ function return_type(@nospecialize(f), @nospecialize(t)) return ccall(:jl_call_in_typeinf_world, Any, (Ptr{Ptr{Cvoid}}, Cint), Any[_return_type, f, t, world], 4) end -function _return_type(@nospecialize(f), @nospecialize(t), world) - params = Params(world) +_return_type(@nospecialize(f), @nospecialize(t), world) = _return_type(NativeInterpreter(), f, t, world) + +function _return_type(interp::AbstractInterpreter, @nospecialize(f), @nospecialize(t), world) rt = Union{} if isa(f, Builtin) - rt = builtin_tfunction(f, Any[t.parameters...], nothing, params) + rt = builtin_tfunction(interp, f, Any[t.parameters...], nothing) if isa(rt, TypeVar) rt = rt.ub else rt = widenconst(rt) end else - for m in _methods(f, t, -1, params.world) - ty = typeinf_type(m[3], m[1], m[2], params) + for m in _methods(f, t, -1, world) + ty = typeinf_type(interp, m[3], m[1], m[2]) ty === nothing && return Any rt = tmerge(rt, ty) rt === Any && break diff --git a/base/compiler/types.jl b/base/compiler/types.jl new file mode 100644 index 0000000000000..7794d8822b271 --- /dev/null +++ b/base/compiler/types.jl @@ -0,0 +1,177 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +""" + AbstractInterpreter + +An abstract base class that allows multiple dispatch to determine the method of +executing Julia code. The native Julia LLVM pipeline is enabled by using the +`TypeInference` concrete instantiatoin of this abstract class, others can be +swapped in as long as they follow the AbstractInterpreter API. + +All AbstractInterpreters are expected to provide at least the following methods: + +- InferenceParams(interp) - return an `InferenceParams` instance +- OptimizationParams(interp) - return an `OptimizationParams` instance +- get_world_counter(interp) - return the world age for this interpreter +- get_inference_cache(interp) - return the runtime inference cache +""" +abstract type AbstractInterpreter; end + + +""" + InferenceResult + +A type that represents the result of running type inference on a chunk of code. +""" +mutable struct InferenceResult + linfo::MethodInstance + argtypes::Vector{Any} + overridden_by_const::BitVector + result # ::Type, or InferenceState if WIP + src #::Union{CodeInfo, OptimizationState, Nothing} # if inferred copy is available + function InferenceResult(linfo::MethodInstance, given_argtypes = nothing) + argtypes, overridden_by_const = matching_cache_argtypes(linfo, given_argtypes) + return new(linfo, argtypes, overridden_by_const, Any, nothing) + end +end + + +""" + OptimizationParams + +Parameters that control optimizer operation. +""" +struct OptimizationParams + inlining::Bool # whether inlining is enabled + inline_cost_threshold::Int # number of CPU cycles beyond which it's not worth inlining + inline_nonleaf_penalty::Int # penalty for dynamic dispatch + inline_tupleret_bonus::Int # extra willingness for non-isbits tuple return types + + # Duplicating for now because optimizer inlining requires it. + # Keno assures me this will be removed in the near future + MAX_METHODS::Int + MAX_TUPLE_SPLAT::Int + MAX_UNION_SPLITTING::Int + + function OptimizationParams(; + inlining::Bool = inlining_enabled(), + inline_cost_threshold::Int = 100, + inline_nonleaf_penalty::Int = 1000, + inline_tupleret_bonus::Int = 400, + max_methods::Int = 4, + tuple_splat::Int = 32, + union_splitting::Int = 4, + ) + return new( + inlining, + inline_cost_threshold, + inline_nonleaf_penalty, + inline_tupleret_bonus, + max_methods, + tuple_splat, + union_splitting, + ) + end +end + +""" + InferenceParams + +Parameters that control type inference operation. +""" +struct InferenceParams + ipo_constant_propagation::Bool + aggressive_constant_propagation::Bool + + # don't consider more than N methods. this trades off between + # compiler performance and generated code performance. + # typically, considering many methods means spending lots of time + # obtaining poor type information. + # It is important for N to be >= the number of methods in the error() + # function, so we can still know that error() is always Bottom. + MAX_METHODS::Int + # the maximum number of union-tuples to swap / expand + # before computing the set of matching methods + MAX_UNION_SPLITTING::Int + # the maximum number of union-tuples to swap / expand + # when inferring a call to _apply + MAX_APPLY_UNION_ENUM::Int + + # parameters limiting large (tuple) types + TUPLE_COMPLEXITY_LIMIT_DEPTH::Int + + # when attempting to inlining _apply, abort the optimization if the tuple + # contains more than this many elements + MAX_TUPLE_SPLAT::Int + + function InferenceParams(; + ipo_constant_propagation::Bool = true, + aggressive_constant_propagation::Bool = false, + max_methods::Int = 4, + union_splitting::Int = 4, + apply_union_enum::Int = 8, + tupletype_depth::Int = 3, + tuple_splat::Int = 32, + ) + return new( + ipo_constant_propagation, + aggressive_constant_propagation, + max_methods, + tupletype_depth, + tuple_splat, + union_splitting, + apply_union_enum, + ) + end +end + +""" + NativeInterpreter + +This represents Julia's native type inference algorithm and codegen backend. +It contains many parameters used by the compilation pipeline. +""" +struct NativeInterpreter <: AbstractInterpreter + # Cache of inference results for this particular interpreter + cache::Vector{InferenceResult} + # The world age we're working inside of + world::UInt + + # Parameters for inference and optimization + inf_params::InferenceParams + opt_params::OptimizationParams + + function NativeInterpreter(world::UInt = get_world_counter(); + inf_params = InferenceParams(), + opt_params = OptimizationParams(), + ) + # Sometimes the caller is lazy and passes typemax(UInt). + # we cap it to the current world age + if world == typemax(UInt) + world = get_world_counter() + end + + # If they didn't pass typemax(UInt) but passed something more subtly + # incorrect, fail out loudly. + @assert world <= get_world_counter() + + + return new( + # Initially empty cache + Vector{InferenceResult}(), + + # world age counter + world, + + # parameters for inference and optimization + inf_params, + opt_params, + ) + end +end + +# Quickly and easily satisfy the AbstractInterpreter API contract +InferenceParams(ni::NativeInterpreter) = ni.inf_params +OptimizationParams(ni::NativeInterpreter) = ni.opt_params +get_world_counter(ni::NativeInterpreter) = ni.world +get_inference_cache(ni::NativeInterpreter) = ni.cache \ No newline at end of file diff --git a/base/compiler/utilities.jl b/base/compiler/utilities.jl index dcb91ac1f952f..fb6d0b9662899 100644 --- a/base/compiler/utilities.jl +++ b/base/compiler/utilities.jl @@ -118,7 +118,7 @@ function retrieve_code_info(linfo::MethodInstance) end end -function inf_for_methodinstance(mi::MethodInstance, min_world::UInt, max_world::UInt=min_world) +function inf_for_methodinstance(interp::AbstractInterpreter, mi::MethodInstance, min_world::UInt, max_world::UInt=min_world) return ccall(:jl_rettype_inferred, Any, (Any, UInt, UInt), mi, min_world, max_world)::Union{Nothing, CodeInstance} end diff --git a/base/reflection.jl b/base/reflection.jl index df3368022fc09..77402f405825a 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -1087,7 +1087,7 @@ function code_typed(@nospecialize(f), @nospecialize(types=Tuple); optimize=true, debuginfo::Symbol=:default, world = get_world_counter(), - params = Core.Compiler.Params(world)) + interp = Core.Compiler.NativeInterpreter(world)) ccall(:jl_is_in_pure_context, Bool, ()) && error("code reflection cannot be used from generated functions") if isa(f, Core.Builtin) throw(ArgumentError("argument is not a generic function")) @@ -1104,7 +1104,7 @@ function code_typed(@nospecialize(f), @nospecialize(types=Tuple); asts = [] for x in _methods(f, types, -1, world) meth = func_for_method_checked(x[3], types, x[2]) - (code, ty) = Core.Compiler.typeinf_code(meth, x[1], x[2], optimize, params) + (code, ty) = Core.Compiler.typeinf_code(interp, meth, x[1], x[2], optimize) code === nothing && error("inference not successful") # inference disabled? debuginfo === :none && remove_linenums!(code) push!(asts, code => ty) @@ -1112,7 +1112,7 @@ function code_typed(@nospecialize(f), @nospecialize(types=Tuple); return asts end -function return_types(@nospecialize(f), @nospecialize(types=Tuple)) +function return_types(@nospecialize(f), @nospecialize(types=Tuple), interp=Core.Compiler.NativeInterpreter()) ccall(:jl_is_in_pure_context, Bool, ()) && error("code reflection cannot be used from generated functions") if isa(f, Core.Builtin) throw(ArgumentError("argument is not a generic function")) @@ -1120,10 +1120,9 @@ function return_types(@nospecialize(f), @nospecialize(types=Tuple)) types = to_tuple_type(types) rt = [] world = get_world_counter() - params = Core.Compiler.Params(world) for x in _methods(f, types, -1, world) meth = func_for_method_checked(x[3], types, x[2]) - ty = Core.Compiler.typeinf_type(meth, x[1], x[2], params) + ty = Core.Compiler.typeinf_type(interp, meth, x[1], x[2]) ty === nothing && error("inference not successful") # inference disabled? push!(rt, ty) end diff --git a/base/show.jl b/base/show.jl index a2eb5664ab223..a2cbd93189f97 100644 --- a/base/show.jl +++ b/base/show.jl @@ -1876,6 +1876,20 @@ function show(io::IO, src::CodeInfo; debuginfo::Symbol=:source) print(io, ")") end +function show(io::IO, inferred::Core.Compiler.InferenceResult) + tt = inferred.linfo.specTypes.parameters[2:end] + tts = join(["::$(t)" for t in tt], ", ") + rettype = inferred.result + if isa(rettype, Core.Compiler.InferenceState) + rettype = rettype.bestguess + end + print(io, "$(inferred.linfo.def.name)($(tts)) => $(rettype)") +end + +function show(io::IO, ::Core.Compiler.NativeInterpreter) + print(io, "Core.Compiler.NativeInterpreter") +end + function dump(io::IOContext, x::SimpleVector, n::Int, indent) if isempty(x) diff --git a/doc/src/devdocs/inference.md b/doc/src/devdocs/inference.md index 6683486f88660..30acb5d1d4813 100644 --- a/doc/src/devdocs/inference.md +++ b/doc/src/devdocs/inference.md @@ -27,12 +27,11 @@ mths = methods(convert, atypes) # worth checking that there is only one m = first(mths) # Create variables needed to call `typeinf_code` -params = Core.Compiler.Params(typemax(UInt)) # parameter is the world age, - # typemax(UInt) -> most recent +interp = Core.Compiler.NativeInterpreter() sparams = Core.svec() # this particular method doesn't have type-parameters optimize = true # run all inference optimizations cached = false # force inference to happen (do not use cached results) -Core.Compiler.typeinf_code(m, atypes, sparams, optimize, cached, params) +Core.Compiler.typeinf_code(interp, m, atypes, sparams, optimize, cached) ``` If your debugging adventures require a `MethodInstance`, you can look it up by @@ -98,7 +97,7 @@ where `f` is your function and `tt` is the Tuple-type of the arguments: f = fill tt = Tuple{Float64, Tuple{Int,Int}} # Create the objects we need to interact with the compiler -params = Core.Compiler.Params(typemax(UInt)) +opt_params = Core.Compiler.OptimizationParams() mi = Base.method_instances(f, tt)[1] ci = code_typed(f, tt)[1][1] opt = Core.Compiler.OptimizationState(mi, params) diff --git a/stdlib/REPL/src/REPLCompletions.jl b/stdlib/REPL/src/REPLCompletions.jl index c35d8e75481bd..04d01b68e5be3 100644 --- a/stdlib/REPL/src/REPLCompletions.jl +++ b/stdlib/REPL/src/REPLCompletions.jl @@ -379,8 +379,8 @@ function get_type_call(expr::Expr) length(mt) == 1 || return (Any, false) m = first(mt) # Typeinference - params = Core.Compiler.Params(world) - return_type = Core.Compiler.typeinf_type(m[3], m[1], m[2], params) + interp = Core.Compiler.NativeInterpreter() + return_type = Core.Compiler.typeinf_type(interp, m[3], m[1], m[2]) return_type === nothing && return (Any, false) return (return_type, true) end diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index b275cdf904757..69b6020edfd05 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -1120,7 +1120,8 @@ function get_linfo(@nospecialize(f), @nospecialize(t)) end function test_const_return(@nospecialize(f), @nospecialize(t), @nospecialize(val)) - linfo = Core.Compiler.inf_for_methodinstance(get_linfo(f, t), Core.Compiler.get_world_counter())::Core.CodeInstance + interp = Core.Compiler.NativeInterpreter() + linfo = Core.Compiler.inf_for_methodinstance(interp, get_linfo(f, t), Core.Compiler.get_world_counter())::Core.CodeInstance # If coverage is not enabled, make the check strict by requiring constant ABI # Otherwise, check the typed AST to make sure we return a constant. if Base.JLOptions().code_coverage == 0 @@ -1420,7 +1421,8 @@ gg13183(x::X...) where {X} = (_false13183 ? gg13183(x, x) : 0) # test the external OptimizationState constructor let linfo = get_linfo(Base.convert, Tuple{Type{Int64}, Int32}), world = UInt(23) # some small-numbered world that should be valid - opt = Core.Compiler.OptimizationState(linfo, Core.Compiler.Params(world)) + interp = Core.Compiler.NativeInterpreter() + opt = Core.Compiler.OptimizationState(linfo, OptimizationParams(interp), interp) # make sure the state of the properties look reasonable @test opt.src !== linfo.def.source @test length(opt.src.slotflags) == linfo.def.nargs <= length(opt.src.slotnames) diff --git a/test/reflection.jl b/test/reflection.jl index f567eb40d55f5..2655c5e42d251 100644 --- a/test/reflection.jl +++ b/test/reflection.jl @@ -531,14 +531,15 @@ let code_typed(f18888, Tuple{}; optimize=false) @test m.specializations isa Core.TypeMapEntry # uncached, but creates the specializations entry mi = Core.Compiler.specialize_method(m, Tuple{ft}, Core.svec()) - @test Core.Compiler.inf_for_methodinstance(mi, world) === nothing + interp = Core.Compiler.NativeInterpreter(world) + @test Core.Compiler.inf_for_methodinstance(interp, mi, world) === nothing @test !isdefined(mi, :cache) code_typed(f18888, Tuple{}; optimize=true) @test !isdefined(mi, :cache) Base.return_types(f18888, Tuple{}) - @test Core.Compiler.inf_for_methodinstance(mi, world) === mi.cache + @test Core.Compiler.inf_for_methodinstance(interp, mi, world) === mi.cache @test mi.cache isa Core.CodeInstance @test !isdefined(mi.cache, :next) end