From 9be2edbc41364bc8d5668b4a1dd5b07aaff254d0 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Sat, 16 Jan 2021 15:34:09 +0900 Subject: [PATCH 1/3] inference: inter-procedural conditional constraint back-propagation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR propagates `Conditional`s inter-procedurally when a `Conditional` at return site imposes a constraint on the call arguments. When inference exits local frame and the return type is annotated as `Conditional`, it will be converted into `InterConditional` object, which is implemented in `Core` and can be directly put into the global cache. Finally after going back to caller frame, `InterConditional` will be re-converted into `Conditional` in the context of the caller frame. ## improvements So now some simple "is-wrapper" functions will propagate its constraint as expected, e.g.: ```julia isaint(a) = isa(a, Int) @test Base.return_types((Any,)) do a isaint(a) && return a # a::Int return 0 end == Any[Int] isaint2(::Any) = false isaint2(::Int) = true @test Base.return_types((Any,)) do a isaint2(a) && return a # a::Int return 0 end == Any[Int] function isa_int_or_float64(a) isa(a, Int) && return true isa(a, Float64) && return true return false end @test Base.return_types((Any,)) do a isa_int_or_float64(a) && return a # a::Union{Float64,Int} 0 end == Any[Union{Float64,Int}] ``` (and now we don't need something like #38636) ## benchmarks A compile time comparison: > on the current master (82d79ce18f88923c14d322b70699da43a72e6b32) ``` Sysimage built. Summary: Total ─────── 55.295376 seconds Base: ─────── 23.359226 seconds 42.2444% Stdlibs: ──── 31.934773 seconds 57.7531% JULIA usr/lib/julia/sys-o.a Generating REPL precompile statements... 29/29 Executing precompile statements... 1283/1283 Precompilation complete. Summary: Total ─────── 91.129162 seconds Generation ── 68.800937 seconds 75.4983% Execution ─── 22.328225 seconds 24.5017% LINK usr/lib/julia/sys.dylib ``` > on this PR (37e279bce7136e48f159811641b68143412c3881) ``` Sysimage built. Summary: Total ─────── 51.694730 seconds Base: ─────── 21.943914 seconds 42.449% Stdlibs: ──── 29.748987 seconds 57.5474% JULIA usr/lib/julia/sys-o.a Generating REPL precompile statements... 29/29 Executing precompile statements... 1357/1357 Precompilation complete. Summary: Total ─────── 88.956226 seconds Generation ── 67.077710 seconds 75.4053% Execution ─── 21.878515 seconds 24.5947% LINK usr/lib/julia/sys.dylib ``` Here is a sample code that benefits from this PR: ```julia function summer(ary) r = 0 for a in ary if ispositive(a) r += a end end r end ispositive(a) = isa(a, Int) && a > 0 ary = Any[] for _ in 1:100_000 if rand(Bool) push!(ary, rand(-100:100)) elseif rand(Bool) push!(ary, rand('a':'z')) else push!(ary, nothing) end end using BenchmarkTools @btime summer($(ary)) ``` > on the current master (82d79ce18f88923c14d322b70699da43a72e6b32) ``` ❯ julia summer.jl 1.214 ms (24923 allocations: 389.42 KiB) ``` > on this PR (37e279bce7136e48f159811641b68143412c3881) ``` ❯ julia summer.jl 421.223 μs (0 allocations: 0 bytes) ``` ## caveats Within the `Conditional`/`InterConditional` framework, only a single constraint can be back-propagated inter-procedurally. This PR implements a naive heuristic to "pick up" a constraint to be propagated when a return type is a boolean. The heuristic may fail to select an "interesting" constraint in some cases. For example, we may expect `::Expr` constraint to be imposed on the first argument of `Meta.isexpr`, but the current heuristic ends up picking up a constraint on the second argument (i.e. `ex.head === head`). ```julia isexpr(@nospecialize(ex), head::Symbol) = isa(ex, Expr) && ex.head === head @test_broken Base.return_types((Any,)) do x Meta.isexpr(x, :call) && return x # x::Expr, ideally return nothing end == Any[Union{Nothing,Expr}] ``` I think We can get rid of this limitation by extending `Conditional` and `InterConditional` so that they can convey multiple constraints, but I'd like to leave this as a future work. --- - closes #38636 - closes #37342 --- base/boot.jl | 1 + base/compiler/abstractinterpretation.jl | 100 ++++++++++++++++++++---- base/compiler/typeinfer.jl | 19 +++-- base/compiler/typelattice.jl | 40 ++++++---- base/compiler/typelimits.jl | 32 +++++++- src/builtins.c | 1 + src/jl_exported_data.inc | 1 + src/jltypes.c | 4 + src/julia.h | 1 + src/staticdata.c | 3 +- test/compiler/inference.jl | 73 ++++++++++++++++- 11 files changed, 236 insertions(+), 39 deletions(-) diff --git a/base/boot.jl b/base/boot.jl index 0b6eee2fcf3f0..0d4c6cc57438e 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -425,6 +425,7 @@ eval(Core, :(CodeInstance(mi::MethodInstance, @nospecialize(rettype), @nospecial eval(Core, :(Const(@nospecialize(v)) = $(Expr(:new, :Const, :v)))) eval(Core, :(PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields)))) eval(Core, :(PartialOpaque(@nospecialize(typ), @nospecialize(env), isva::Bool, parent::MethodInstance, source::Method) = $(Expr(:new, :PartialOpaque, :typ, :env, :isva, :parent, :source)))) +eval(Core, :(InterConditional(slot::Int, @nospecialize(vtype), @nospecialize(elsetype)) = $(Expr(:new, :InterConditional, :slot, :vtype, :elsetype)))) eval(Core, :(MethodMatch(@nospecialize(spec_types), sparams::SimpleVector, method::Method, fully_covers::Bool) = $(Expr(:new, :MethodMatch, :spec_types, :sparams, :method, :fully_covers)))) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 9588e2160f62a..1df48642fc67e 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -24,8 +24,8 @@ function is_improvable(@nospecialize(rtype)) # already at Bottom return rtype !== Union{} end - # Could be improved to `Const` or a more precise PartialStruct - return isa(rtype, PartialStruct) + # Could be improved to `Const` or a more precise wrapper + return isa(rtype, PartialStruct) || isa(rtype, InterConditional) end function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), argtypes::Vector{Any}, @nospecialize(atype), sv::InferenceState, @@ -186,6 +186,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), rettype = Any end add_call_backedges!(interp, rettype, edges, fullmatch, mts, atype, sv) + @assert !(rettype isa Conditional) "invalid lattice element returned from inter-procedural context" #print("=> ", rettype, "\n") if rettype isa LimitedAccuracy union!(sv.pclimitations, rettype.causes) @@ -1098,9 +1099,29 @@ function abstract_call(interp::AbstractInterpreter, fargs::Union{Nothing,Vector{ add_remark!(interp, sv, "Could not identify method table for call") return CallMeta(Any, false) end - return abstract_call_gf_by_type(interp, nothing, argtypes, argtypes_to_type(argtypes), sv, max_methods) + callinfo = abstract_call_gf_by_type(interp, nothing, argtypes, argtypes_to_type(argtypes), sv, max_methods) + return callinfo_from_interprocedural(callinfo, fargs) end - return abstract_call_known(interp, f, fargs, argtypes, sv, max_methods) + callinfo = abstract_call_known(interp, f, fargs, argtypes, sv, max_methods) + return callinfo_from_interprocedural(callinfo, fargs) +end + +function callinfo_from_interprocedural(callinfo::CallMeta, ea::Union{Nothing,Vector{Any}}) + rt = callinfo.rt + if isa(rt, InterConditional) + if ea !== nothing + # convert inter-procedural conditional constraint from callee into the constraint + # on slots of the current frame; `InterConditional` only comes from a "valid" + # `abstract_call` as such its slot should always be within the bound of this + # call arguments `ea` + e = ea[rt.slot] + if isa(e, Slot) + return CallMeta(Conditional(e, rt.vtype, rt.elsetype), callinfo.info) + end + end + return CallMeta(widenconditional(rt), callinfo.info) + end + return callinfo end function sp_type_rewrap(@nospecialize(T), linfo::MethodInstance, isreturn::Bool) @@ -1375,17 +1396,43 @@ function abstract_eval_ssavalue(s::SSAValue, src::CodeInfo) return typ end -function widenreturn(@nospecialize rt) +function widenreturn(@nospecialize(rt), @nospecialize(bestguess), isva::Bool, nargs::Int, changes::VarTable) + if isva + # give up inter-procedural constraint back-propagation from vararg methods + # because types of same slot may differ between callee and caller + rt = widenconditional(rt) + else + if isa(rt, Conditional) && !(1 ≤ slot_id(rt.var) ≤ nargs) + # discard this `Conditional` imposed on non-call arguments, + # since it's not interesting in inter-procedural context; + # we may give constraints on other call argument + rt = widenconditional(rt) + end + if !isa(rt, Conditional) && rt ⊑ Bool + if isa(bestguess, InterConditional) + # if the bestguess so far is already `Conditional`, try to convert + # this `rt` into `Conditional` on the slot to avoid overapproximation + # due to conflict of different slots + rt = boolean_rt_to_conditional(rt, changes, bestguess.slot) + elseif nargs ≥ 1 + # pick up the first "interesting" slot, convert `rt` to its `Conditional` + # TODO: this is very naive heuristic, ideally we want `Conditional` + # and `InterConditional` to convey constraints on multiple slots + rt = boolean_rt_to_conditional(rt, changes, nargs > 1 ? 2 : 1) + end + end + end + # only propagate information we know we can store # and is valid and good inter-procedurally - rt = widenconditional(rt) + isa(rt, Conditional) && return InterConditional(slot_id(rt.var), rt.vtype, rt.elsetype) isa(rt, Const) && return rt isa(rt, Type) && return rt if isa(rt, PartialStruct) fields = copy(rt.fields) haveconst = false for i in 1:length(fields) - a = widenreturn(fields[i]) + a = widenreturn(fields[i], bestguess, isva, nargs, changes) if !haveconst && has_const_info(a) # TODO: consider adding && const_prop_profitable(a) here? haveconst = true @@ -1407,6 +1454,9 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) W = frame.ip s = frame.stmt_types n = frame.nstmts + nargs = frame.nargs + def = frame.linfo.def + isva = isa(def, Method) && def.isva while frame.pc´´ <= n # make progress on the active ip set local pc::Int = frame.pc´´ # current program-counter @@ -1461,12 +1511,8 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) frame.handler_at[l] = frame.cur_hand changes_else = changes if isa(condt, Conditional) - if condt.elsetype !== Any && condt.elsetype !== changes[slot_id(condt.var)] - changes_else = StateUpdate(condt.var, VarState(condt.elsetype, false), changes_else) - end - if condt.vtype !== Any && condt.vtype !== changes[slot_id(condt.var)] - changes = StateUpdate(condt.var, VarState(condt.vtype, false), changes) - end + changes_else = conditional_changes(changes_else, condt.elsetype, condt.var) + changes = conditional_changes(changes, condt.vtype, condt.var) end newstate_else = stupdate!(s[l], changes_else) if newstate_else !== nothing @@ -1480,7 +1526,8 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) end elseif isa(stmt, ReturnNode) pc´ = n + 1 - rt = widenreturn(abstract_eval_value(interp, stmt.val, changes, frame)) + bestguess = frame.bestguess + rt = widenreturn(abstract_eval_value(interp, stmt.val, changes, frame), bestguess, isva, nargs, changes) # copy limitations to return value if !isempty(frame.pclimitations) union!(frame.limitations, frame.pclimitations) @@ -1489,9 +1536,9 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) if !isempty(frame.limitations) rt = LimitedAccuracy(rt, copy(frame.limitations)) end - if tchanged(rt, frame.bestguess) + if tchanged(rt, bestguess) # new (wider) return type for frame - frame.bestguess = tmerge(frame.bestguess, rt) + frame.bestguess = tmerge(bestguess, rt) for (caller, caller_pc) in frame.cycle_backedges # notify backedges of updated type information typeassert(caller.stmt_types[caller_pc], VarTable) # we must have visited this statement before @@ -1600,6 +1647,27 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) nothing end +function conditional_changes(changes::VarTable, @nospecialize(typ), var::Slot) + if typ ⊑ (changes[slot_id(var)]::VarState).typ + return StateUpdate(var, VarState(typ, false), changes) + end + return changes +end + +function boolean_rt_to_conditional(@nospecialize(rt), state::VarTable, slot_id::Int) + typ = widenconditional((state[slot_id]::VarState).typ) # avoid nested conditional + if isa(rt, Const) + if rt.val === true + return Conditional(SlotNumber(slot_id), typ, Bottom) + elseif rt.val === false + return Conditional(SlotNumber(slot_id), Bottom, typ) + end + elseif rt === Bool + return Conditional(SlotNumber(slot_id), typ, typ) + end + return rt +end + # make as much progress on `frame` as possible (by handling cycles) function typeinf_nocycle(interp::AbstractInterpreter, frame::InferenceState) typeinf_local(interp, frame) diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index bda8ce7017a52..d17bbe44291ae 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -298,6 +298,9 @@ function CodeInstance(result::InferenceResult, @nospecialize(inferred_result::An elseif isa(result_type, PartialStruct) rettype_const = result_type.fields const_flags = 0x2 + elseif isa(result_type, InterConditional) + rettype_const = result_type + const_flags = 0x2 else rettype_const = nothing const_flags = 0x00 @@ -773,16 +776,20 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize code = get(code_cache(interp), mi, nothing) if code isa CodeInstance # return existing rettype if the code is already inferred update_valid_age!(caller, WorldRange(min_world(code), max_world(code))) + rettype = code.rettype if isdefined(code, :rettype_const) - if isa(code.rettype_const, Vector{Any}) && !(Vector{Any} <: code.rettype) - return PartialStruct(code.rettype, code.rettype_const), mi - elseif code.rettype <: Core.OpaqueClosure && isa(code.rettype_const, PartialOpaque) - return code.rettype_const, mi + rettype_const = code.rettype_const + if isa(rettype_const, Vector{Any}) && !(Vector{Any} <: rettype) + return PartialStruct(rettype, rettype_const), mi + elseif rettype <: Core.OpaqueClosure && isa(rettype_const, PartialOpaque) + return rettype_const, mi + elseif isa(rettype_const, InterConditional) + return rettype_const, mi else - return Const(code.rettype_const), mi + return Const(rettype_const), mi end else - return code.rettype, mi + return rettype, mi end end if ccall(:jl_get_module_infer, Cint, (Any,), method.module) == 0 diff --git a/base/compiler/typelattice.jl b/base/compiler/typelattice.jl index 60e91391f4888..eb8acf0848028 100644 --- a/base/compiler/typelattice.jl +++ b/base/compiler/typelattice.jl @@ -4,7 +4,7 @@ # structs/constants # ##################### -# N.B.: Const/PartialStruct are defined in Core, to allow them to be used +# N.B.: Const/PartialStruct/InterConditional are defined in Core, to allow them to be used # inside the global code cache. # # # The type of a value might be constant @@ -18,7 +18,6 @@ # end import Core: Const, PartialStruct - # The type of this value might be Bool. # However, to enable a limited amount of back-propagagation, # we also keep some information about how this Bool value was created. @@ -45,6 +44,18 @@ struct Conditional end end +# # Similar to `Conditional`, but conveys inter-procedural constraints imposed on call arguments. +# # This is separate from `Conditional` to catch logic errors: the lattice element name is InterConditional +# # while processing a call, then Conditional everywhere else. Thus InterConditional does not appear in +# # CompilerTypes—these type's usages are disjoint—though we define the lattice for InterConditional. +# struct InterConditional +# slot::Int +# vtype +# elsetype +# end +import Core: InterConditional +const AnyConditional = Union{Conditional,InterConditional} + struct PartialTypeVar tv::TypeVar # N.B.: Currently unused, but would allow turning something back @@ -101,11 +112,10 @@ const CompilerTypes = Union{MaybeUndef, Const, Conditional, NotFound, PartialStr # lattice logic # ################# -function issubconditional(a::Conditional, b::Conditional) - avar = a.var - bvar = b.var - if (isa(avar, Slot) && isa(bvar, Slot) && slot_id(avar) === slot_id(bvar)) || - (isa(avar, SSAValue) && isa(bvar, SSAValue) && avar === bvar) +# `Conditional` and `InterConditional` are valid in opposite contexts +# (i.e. local inference and inter-procedural call), as such they will never be compared +function issubconditional(a::C, b::C) where {C<:AnyConditional} + if is_same_conditionals(a, b) if a.vtype ⊑ b.vtype if a.elsetype ⊑ b.elsetype return true @@ -114,9 +124,11 @@ function issubconditional(a::Conditional, b::Conditional) end return false end +is_same_conditionals(a::Conditional, b::Conditional) = slot_id(a.var) === slot_id(b.var) +is_same_conditionals(a::InterConditional, b::InterConditional) = a.slot === b.slot -maybe_extract_const_bool(c::Const) = isa(c.val, Bool) ? c.val : nothing -function maybe_extract_const_bool(c::Conditional) +maybe_extract_const_bool(c::Const) = (val = c.val; isa(val, Bool)) ? val : nothing +function maybe_extract_const_bool(c::AnyConditional) (c.vtype === Bottom && !(c.elsetype === Bottom)) && return false (c.elsetype === Bottom && !(c.vtype === Bottom)) && return true nothing @@ -145,14 +157,14 @@ function ⊑(@nospecialize(a), @nospecialize(b)) b === Union{} && return false @assert !isa(a, TypeVar) "invalid lattice item" @assert !isa(b, TypeVar) "invalid lattice item" - if isa(a, Conditional) - if isa(b, Conditional) + if isa(a, AnyConditional) + if isa(b, AnyConditional) return issubconditional(a, b) elseif isa(b, Const) && isa(b.val, Bool) return maybe_extract_const_bool(a) === b.val end a = Bool - elseif isa(b, Conditional) + elseif isa(b, AnyConditional) return false end if isa(a, PartialStruct) @@ -251,7 +263,7 @@ function is_lattice_equal(@nospecialize(a), @nospecialize(b)) return a ⊑ b && b ⊑ a end -widenconst(c::Conditional) = Bool +widenconst(c::AnyConditional) = Bool function widenconst(c::Const) if isa(c.val, Type) if isvarargtype(c.val) @@ -286,7 +298,7 @@ end @inline schanged(@nospecialize(n), @nospecialize(o)) = (n !== o) && (o === NOT_FOUND || (n !== NOT_FOUND && !issubstate(n, o))) widenconditional(@nospecialize typ) = typ -function widenconditional(typ::Conditional) +function widenconditional(typ::AnyConditional) if typ.vtype === Union{} return Const(false) elseif typ.elsetype === Union{} diff --git a/base/compiler/typelimits.jl b/base/compiler/typelimits.jl index 9f6a67179a0a8..3de2627f42569 100644 --- a/base/compiler/typelimits.jl +++ b/base/compiler/typelimits.jl @@ -334,7 +334,7 @@ function tmerge(@nospecialize(typea), @nospecialize(typeb)) end end if isa(typea, Conditional) && isa(typeb, Conditional) - if typea.var === typeb.var + if is_same_conditionals(typea, typeb) vtype = tmerge(typea.vtype, typeb.vtype) elsetype = tmerge(typea.elsetype, typeb.elsetype) if vtype != elsetype @@ -347,6 +347,36 @@ function tmerge(@nospecialize(typea), @nospecialize(typeb)) end return Bool end + # type-lattice for InterConditional wrapper, InterConditional will never be merged with Conditional + if isa(typea, InterConditional) && isa(typeb, Const) + if typeb.val === true + typeb = InterConditional(typea.slot, Any, Union{}) + elseif typeb.val === false + typeb = InterConditional(typea.slot, Union{}, Any) + end + end + if isa(typeb, InterConditional) && isa(typea, Const) + if typea.val === true + typea = InterConditional(typeb.slot, Any, Union{}) + elseif typea.val === false + typea = InterConditional(typeb.slot, Union{}, Any) + end + end + if isa(typea, InterConditional) && isa(typeb, InterConditional) + if is_same_conditionals(typea, typeb) + vtype = tmerge(typea.vtype, typeb.vtype) + elsetype = tmerge(typea.elsetype, typeb.elsetype) + if vtype != elsetype + return InterConditional(typea.slot, vtype, elsetype) + end + end + val = maybe_extract_const_bool(typea) + if val isa Bool && val === maybe_extract_const_bool(typeb) + return Const(val) + end + return Bool + end + # type-lattice for Const and PartialStruct wrappers if (isa(typea, PartialStruct) || isa(typea, Const)) && (isa(typeb, PartialStruct) || isa(typeb, Const)) && widenconst(typea) === widenconst(typeb) diff --git a/src/builtins.c b/src/builtins.c index 10e2092a0be78..2fcf6781fd442 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -1616,6 +1616,7 @@ void jl_init_primitives(void) JL_GC_DISABLED add_builtin("Const", (jl_value_t*)jl_const_type); add_builtin("PartialStruct", (jl_value_t*)jl_partial_struct_type); add_builtin("PartialOpaque", (jl_value_t*)jl_partial_opaque_type); + add_builtin("InterConditional", (jl_value_t*)jl_interconditional_type); add_builtin("MethodMatch", (jl_value_t*)jl_method_match_type); add_builtin("IntrinsicFunction", (jl_value_t*)jl_intrinsic_type); add_builtin("Function", (jl_value_t*)jl_function_type); diff --git a/src/jl_exported_data.inc b/src/jl_exported_data.inc index d65de126b86a8..1f10f6ef43991 100644 --- a/src/jl_exported_data.inc +++ b/src/jl_exported_data.inc @@ -72,6 +72,7 @@ XX(jl_number_type) \ XX(jl_partial_struct_type) \ XX(jl_partial_opaque_type) \ + XX(jl_interconditional_type) \ XX(jl_phicnode_type) \ XX(jl_phinode_type) \ XX(jl_pinode_type) \ diff --git a/src/jltypes.c b/src/jltypes.c index 6e7dc8f5232d4..0cbdb576f4f6e 100644 --- a/src/jltypes.c +++ b/src/jltypes.c @@ -2308,6 +2308,10 @@ void jl_init_types(void) JL_GC_DISABLED jl_perm_symsvec(2, "typ", "fields"), jl_svec2(jl_any_type, jl_array_any_type), 0, 0, 2); + jl_interconditional_type = jl_new_datatype(jl_symbol("InterConditional"), core, jl_any_type, jl_emptysvec, + jl_perm_symsvec(3, "slot", "vtype", "elsetype"), + jl_svec(3, jl_long_type, jl_any_type, jl_any_type), 0, 0, 3); + jl_method_match_type = jl_new_datatype(jl_symbol("MethodMatch"), core, jl_any_type, jl_emptysvec, jl_perm_symsvec(4, "spec_types", "sparams", "method", "fully_covers"), jl_svec(4, jl_type_type, jl_simplevector_type, jl_method_type, jl_bool_type), 0, 0, 4); diff --git a/src/julia.h b/src/julia.h index 0ef4d7cb4bfd3..4f0fa5de397f0 100644 --- a/src/julia.h +++ b/src/julia.h @@ -634,6 +634,7 @@ extern JL_DLLIMPORT jl_datatype_t *jl_argument_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_datatype_t *jl_const_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_datatype_t *jl_partial_struct_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_datatype_t *jl_partial_opaque_type JL_GLOBALLY_ROOTED; +extern JL_DLLIMPORT jl_datatype_t *jl_interconditional_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_datatype_t *jl_method_match_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_datatype_t *jl_simplevector_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_typename_t *jl_tuple_typename JL_GLOBALLY_ROOTED; diff --git a/src/staticdata.c b/src/staticdata.c index 46cdadf7e03b0..44b6f33d05e7a 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -30,7 +30,7 @@ extern "C" { // TODO: put WeakRefs on the weak_refs list during deserialization // TODO: handle finalizers -#define NUM_TAGS 146 +#define NUM_TAGS 147 // An array of references that need to be restored from the sysimg // This is a manually constructed dual of the gvars array, which would be produced by codegen for Julia code, for C. @@ -69,6 +69,7 @@ jl_value_t **const*const get_tags(void) { INSERT_TAG(jl_const_type); INSERT_TAG(jl_partial_struct_type); INSERT_TAG(jl_partial_opaque_type); + INSERT_TAG(jl_interconditional_type); INSERT_TAG(jl_method_match_type); INSERT_TAG(jl_pinode_type); INSERT_TAG(jl_phinode_type); diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index 05be8670c94fb..cdb96b82d1d1f 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -109,6 +109,7 @@ tmerge_test(Tuple{}, Tuple{Complex, Vararg{Union{ComplexF32, ComplexF64}}}, @test Core.Compiler.tmerge(Vector{Int}, Core.Compiler.tmerge(Vector{String}, Vector{Bool})) == Vector @test Core.Compiler.tmerge(Base.BitIntegerType, Union{}) === Base.BitIntegerType @test Core.Compiler.tmerge(Union{}, Base.BitIntegerType) === Base.BitIntegerType +@test Core.Compiler.tmerge(Core.Compiler.InterConditional(1, Int, Union{}), Core.Compiler.InterConditional(2, String, Union{})) === Core.Compiler.Const(true) struct SomethingBits x::Base.BitIntegerType @@ -1255,7 +1256,17 @@ end push!(constvec, 10) @test @inferred(sizeof_constvec()) == sizeof(Int) * 4 -test_const_return((x)->isdefined(x, :re), Tuple{ComplexF64}, true) +let + f = x->isdefined(x, :re) + t = Tuple{ComplexF64} + interp = Core.Compiler.NativeInterpreter() + linfo = get_linfo(f, t) + ci = Core.Compiler.getindex(Core.Compiler.code_cache(interp), get_linfo(f, t)) + rc = ci.rettype_const + @test isa(rc, Core.InterConditional) + @test rc.vtype === ComplexF64 && rc.elsetype === Union{} +end + isdefined_f3(x) = isdefined(x, 3) @test @inferred(isdefined_f3(())) == false @test find_call(first(code_typed(isdefined_f3, Tuple{Tuple{Vararg{Int}}})[1]), isdefined, 3) @@ -1722,6 +1733,66 @@ for expr25261 in opt25261[i:end] end @test foundslot +@testset "inter-procedural conditional constraint propagation" begin + # simple cases + isaint(a) = isa(a, Int) + @test Base.return_types((Any,)) do a + isaint(a) && return a # a::Int + return 0 + end == Any[Int] + eqnothing(a) = a === nothing + @test Base.return_types((Union{Nothing,Int},)) do a + eqnothing(a) && return 0 + return a # a::Int + end == Any[Int] + + # more complicated cases + ispositive(a) = isa(a, Int) && a > 0 + @test Base.return_types((Any,)) do a + ispositive(a) && return a # a::Int + return 0 + end == Any[Int] + global isaint2 + isaint2(a::Int) = true + isaint2(@nospecialize(_)) = false + @test Base.return_types((Any,)) do a + isaint2(a) && return a # a::Int + return 0 + end == Any[Int] + global ispositive2 + ispositive2(a::Int) = a > 0 + ispositive2(@nospecialize(_)) = false + @test Base.return_types((Any,)) do a + ispositive2(a) && return a # a::Int + return 0 + end == Any[Int] + + # type constraints from multiple constant boolean return types + function f(x) + isa(x, Int) && return true + isa(x, Symbol) && return true + return false + end + @test Base.return_types((Any,)) do x + f(x) && return x # x::Union{Int,Symbol} + return nothing + end == Any[Union{Int,Symbol,Nothing}] + + # with Base functions + @test Base.return_types((Any,)) do a + Base.Fix2(isa, Int)(a) && return sin(a) # a::Float64 + return 0.0 + end == Any[Float64] + @test Base.return_types((Union{Nothing,Int},)) do a + isnothing(a) && return 0 + return a # a::Int + end == Any[Int] + @test_broken Base.return_types((Any,)) do x + Meta.isexpr(x, :call) && return x # x::Expr, ideally + return nothing + end == Any[Union{Nothing,Expr}] +end + function f25579(g) h = g[] t = (h === nothing) From cf634a826d4af578c3c269dac27fbf421f903c1e Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Wed, 20 Jan 2021 12:33:57 +0900 Subject: [PATCH 2/3] inference: callsite conditional argument type refinement - within a callee (i.e. `typeinf_local`), we widen conditional return type if it doesn't refine input argument type (for better cache) - within a caller (i.e. `abstract_call_gf_by_type`), we re-form a conditional if needed, which allows us to choose a propagation target more appropriately this commit implements the "pick up" logic within a caller (i.e. within `abstract_call_gf_by_type`), which allows us to choose a constraint more appropriately, and now the `Meta.isexpr` case is fixed. Still there is a limitation of multiple conditional constraint back-propagation; the following broken test case will explain the future work. ```julia is_int_and_int(a, b) = isa(a, Int) && isa(b, Int) @test_broken Base.return_types((Any,Any)) do a, b is_int_and_int(a, b) && return a, b # (a::Int, b::Int) ideally, but (a::Any, b::Int) 0, 0 end == Any[Tuple{Int,Int}] ``` --- base/compiler/abstractinterpretation.jl | 260 +++++++++++++++++------- base/compiler/typelattice.jl | 3 + test/compiler/inference.jl | 32 +-- 3 files changed, 212 insertions(+), 83 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 1df48642fc67e..58040fe5911ce 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -28,8 +28,9 @@ function is_improvable(@nospecialize(rtype)) return isa(rtype, PartialStruct) || isa(rtype, InterConditional) end -function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), argtypes::Vector{Any}, @nospecialize(atype), sv::InferenceState, - max_methods::Int = InferenceParams(interp).MAX_METHODS) +function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), + fargs::Union{Nothing,Vector{Any}}, argtypes::Vector{Any}, @nospecialize(atype), + sv::InferenceState, max_methods::Int = InferenceParams(interp).MAX_METHODS) if sv.params.unoptimize_throw_blocks && sv.currpc in sv.throw_blocks return CallMeta(Any, false) end @@ -98,8 +99,9 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), rettype = Bottom edgecycle = false edges = MethodInstance[] - nonbot = 0 # the index of the only non-Bottom inference result if > 0 - seen = 0 # number of signatures actually inferred + conditionals = nothing # keeps refinement information of call argument types when the return type is boolean + nonbot = 0 # the index of the only non-Bottom inference result if > 0 + seen = 0 # number of signatures actually inferred multiple_matches = napplicable > 1 if f !== nothing && napplicable == 1 && is_method_pure(applicable[1]::MethodMatch) @@ -121,18 +123,18 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), break end sigtuple = unwrap_unionall(sig)::DataType - splitunions = false this_rt = Bottom + splitunions = false # TODO: splitunions = 1 < unionsplitcost(sigtuple.parameters) * napplicable <= InferenceParams(interp).MAX_UNION_SPLITTING - # currently this triggers a bug in inference recursion detection + # this used to trigger a bug in inference recursion detection, and is unmaintained now if splitunions splitsigs = switchtupleunion(sig) for sig_n in splitsigs rt, edgecycle1, edge = abstract_call_method(interp, method, sig_n, svec(), multiple_matches, sv) + edgecycle |= edgecycle1::Bool if edge !== nothing push!(edges, edge) end - edgecycle |= edgecycle1::Bool this_rt = tmerge(this_rt, rt) if bail_out_call(interp, this_rt, sv) break @@ -145,6 +147,9 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), push!(edges, edge) end end + this_conditional = ignorelimited(this_rt) + this_rt = widenwrappedconditional(this_rt) + @assert !(this_conditional isa Conditional) "invalid lattice element returned from inter-procedural context" if this_rt !== Bottom if nonbot === 0 nonbot = i @@ -157,6 +162,26 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), if bail_out_call(interp, rettype, sv) break end + if this_conditional !== Bottom && is_lattice_bool(rettype) && fargs !== nothing + if conditionals === nothing + conditionals = Any[Bottom for _ in 1:length(argtypes)], + Any[Bottom for _ in 1:length(argtypes)] + end + condval = maybe_extract_const_bool(this_conditional) + for i = 1:length(argtypes) + fargs[i] isa Slot || continue + if this_conditional isa InterConditional && this_conditional.slot == i + vtype = this_conditional.vtype + elsetype = this_conditional.elsetype + else + elsetype = vtype = tmeet(argtypes[i], fieldtype(sig, i)) + condval === true && (elsetype = Union{}) + condval === false && (vtype = Union{}) + end + conditionals[1][i] = tmerge(conditionals[1][i], vtype) + conditionals[2][i] = tmerge(conditionals[2][i], elsetype) + end + end end # try constant propagation if only 1 method is inferred to non-Bottom # this is in preparation for inlining, or improving the return result @@ -165,16 +190,99 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), is_improvable(rettype) && 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 + # TODO: refactor this, enable constant propagation for each (union-split) signature + match = applicable[nonbot]::MethodMatch const_rettype, result = abstract_call_method_with_const_args(interp, rettype, f, argtypes, applicable[nonbot]::MethodMatch, sv, edgecycle) - if const_rettype ⊑ rettype - # use the better result, if it's a refinement of rettype + const_conditional = ignorelimited(const_rettype) + @assert !(const_conditional isa Conditional) "invalid lattice element returned from inter-procedural context" + const_rettype = widenwrappedconditional(const_rettype) + if ignorelimited(const_rettype) ⊑ rettype + # use the better result, if it is a refinement of rettype rettype = const_rettype + if const_conditional isa InterConditional && conditionals === nothing && fargs !== nothing + arg = fargs[const_conditional.slot] + if arg isa Slot + rettype = Conditional(arg, const_conditional.vtype, const_conditional.elsetype) + if const_rettype isa LimitedAccuracy + rettype = LimitedAccuracy(rettype, const_rettype.causes) + end + end + end end if result !== nothing info = ConstCallInfo(info, result) end + # and update refinements with the InterConditional info too + # (here we ignorelimited, since there isn't much below this in the + # lattice, particularly when we're already using tmeet) + if const_conditional isa InterConditional && conditionals !== nothing + let i = const_conditional.slot, + vtype = const_conditional.vtype, + elsetype = const_conditional.elsetype + if !(vtype ⊑ conditionals[1][i]) + vtype = tmeet(conditionals[1][i], widenconst(vtype)) + end + if !(elsetype ⊑ conditionals[2][i]) + elsetype = tmeet(conditionals[2][i], widenconst(elsetype)) + end + conditionals[1][i] = vtype + conditionals[2][i] = elsetype + end + end + end + if rettype isa LimitedAccuracy + union!(sv.pclimitations, rettype.causes) + rettype = rettype.typ + end + # if we have argument refinement information, apply that now to get the result + if is_lattice_bool(rettype) && conditionals !== nothing && fargs !== nothing + slot = 0 + vtype = elsetype = Any + condval = maybe_extract_const_bool(rettype) + for i in 1:length(fargs) + # find the first argument which supports refinment, + # and intersect all equvalent arguments with it + arg = fargs[i] + arg isa Slot || continue # can't refine + old = argtypes[i] + old isa Type || continue # unlikely to refine + id = slot_id(arg) + if slot == 0 || id == slot + new_vtype = conditionals[1][i] + if condval === false + vtype = Union{} + elseif new_vtype ⊑ vtype + vtype = new_vtype + else + vtype = tmeet(vtype, widenconst(new_vtype)) + end + new_elsetype = conditionals[2][i] + if condval === true + elsetype = Union{} + elseif new_elsetype ⊑ elsetype + elsetype = new_elsetype + else + elsetype = tmeet(elsetype, widenconst(new_elsetype)) + end + if (slot > 0 || condval !== false) && !(old ⊑ vtype) # essentially vtype ⋤ old + slot = id + elseif (slot > 0 || condval !== true) && !(old ⊑ elsetype) # essentially elsetype ⋤ old + slot = id + else # reset: no new useful information for this slot + vtype = elsetype = Any + if slot > 0 + slot = 0 + end + end + end + end + if vtype === Bottom && elsetype === Bottom + rettype = Bottom # accidentally proved this call to be dead / throw ! + elseif slot > 0 + rettype = Conditional(SlotNumber(slot), vtype, elsetype) # record a Conditional improvement to this slot + end end + @assert !(rettype isa InterConditional) "invalid lattice element returned from inter-procedural context" if is_unused && !(rettype === Bottom) add_remark!(interp, sv, "Call result type was widened because the return value is unused") # We're mainly only here because the optimizer might want this code, @@ -186,21 +294,19 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), rettype = Any end add_call_backedges!(interp, rettype, edges, fullmatch, mts, atype, sv) - @assert !(rettype isa Conditional) "invalid lattice element returned from inter-procedural context" - #print("=> ", rettype, "\n") - if rettype isa LimitedAccuracy - union!(sv.pclimitations, rettype.causes) - rettype = rettype.typ - end if !isempty(sv.pclimitations) # remove self, if present delete!(sv.pclimitations, sv) for caller in sv.callers_in_cycle delete!(sv.pclimitations, caller) end end + #print("=> ", rettype, "\n") return CallMeta(rettype, info) end +widenwrappedconditional(@nospecialize(typ)) = widenconditional(typ) +widenwrappedconditional(typ::LimitedAccuracy) = LimitedAccuracy(widenconditional(typ.typ), typ.causes) + function add_call_backedges!(interp::AbstractInterpreter, @nospecialize(rettype), edges::Vector{MethodInstance}, @@ -1016,7 +1122,7 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), # handle Conditional propagation through !Bool aty = argtypes[2] if isa(aty, Conditional) - call = abstract_call_gf_by_type(interp, f, Any[Const(f), Bool], Tuple{typeof(f), Bool}, sv) # make sure we've inferred `!(::Bool)` + call = abstract_call_gf_by_type(interp, f, fargs, Any[Const(f), Bool], Tuple{typeof(f), Bool}, sv) # make sure we've inferred `!(::Bool)` return CallMeta(Conditional(aty.var, aty.elsetype, aty.vtype), call.info) end elseif la == 3 && istopfunction(f, :!==) @@ -1060,7 +1166,7 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), return CallMeta(val === false ? Type : val, MethodResultPure()) end atype = argtypes_to_type(argtypes) - return abstract_call_gf_by_type(interp, f, argtypes, atype, sv, max_methods) + return abstract_call_gf_by_type(interp, f, fargs, argtypes, atype, sv, max_methods) end function abstract_call_opaque_closure(interp::AbstractInterpreter, closure::PartialOpaque, argtypes::Vector{Any}, sv::InferenceState) @@ -1099,29 +1205,9 @@ function abstract_call(interp::AbstractInterpreter, fargs::Union{Nothing,Vector{ add_remark!(interp, sv, "Could not identify method table for call") return CallMeta(Any, false) end - callinfo = abstract_call_gf_by_type(interp, nothing, argtypes, argtypes_to_type(argtypes), sv, max_methods) - return callinfo_from_interprocedural(callinfo, fargs) - end - callinfo = abstract_call_known(interp, f, fargs, argtypes, sv, max_methods) - return callinfo_from_interprocedural(callinfo, fargs) -end - -function callinfo_from_interprocedural(callinfo::CallMeta, ea::Union{Nothing,Vector{Any}}) - rt = callinfo.rt - if isa(rt, InterConditional) - if ea !== nothing - # convert inter-procedural conditional constraint from callee into the constraint - # on slots of the current frame; `InterConditional` only comes from a "valid" - # `abstract_call` as such its slot should always be within the bound of this - # call arguments `ea` - e = ea[rt.slot] - if isa(e, Slot) - return CallMeta(Conditional(e, rt.vtype, rt.elsetype), callinfo.info) - end - end - return CallMeta(widenconditional(rt), callinfo.info) + return abstract_call_gf_by_type(interp, nothing, fargs, argtypes, argtypes_to_type(argtypes), sv, max_methods) end - return callinfo + return abstract_call_known(interp, f, fargs, argtypes, sv, max_methods) end function sp_type_rewrap(@nospecialize(T), linfo::MethodInstance, isreturn::Bool) @@ -1396,29 +1482,46 @@ function abstract_eval_ssavalue(s::SSAValue, src::CodeInfo) return typ end -function widenreturn(@nospecialize(rt), @nospecialize(bestguess), isva::Bool, nargs::Int, changes::VarTable) - if isva - # give up inter-procedural constraint back-propagation from vararg methods - # because types of same slot may differ between callee and caller +function widenreturn(@nospecialize(rt), @nospecialize(bestguess), nslots::Int, slottypes::Vector{Any}, changes::VarTable) + if !(bestguess ⊑ Bool) || bestguess === Bool + # give up inter-procedural constraint back-propagation + # when tmerge would widen the result anyways (as an optimization) rt = widenconditional(rt) else - if isa(rt, Conditional) && !(1 ≤ slot_id(rt.var) ≤ nargs) - # discard this `Conditional` imposed on non-call arguments, - # since it's not interesting in inter-procedural context; - # we may give constraints on other call argument - rt = widenconditional(rt) + if isa(rt, Conditional) + id = slot_id(rt.var) + if 1 ≤ id ≤ nslots + old_id_type = widenconditional(slottypes[id]) # same as `((s[1]::VarTable)[id]::VarState).typ` + if (!(rt.vtype ⊑ old_id_type) || old_id_type ⊑ rt.vtype) && + (!(rt.elsetype ⊑ old_id_type) || old_id_type ⊑ rt.elsetype) + # discard this `Conditional` since it imposes + # no new constraint on the argument type + # (the caller will recreate it if needed) + rt = widenconditional(rt) + end + else + # discard this `Conditional` imposed on non-call arguments, + # since it's not interesting in inter-procedural context; + # we may give constraints on other call argument + rt = widenconditional(rt) + end end - if !isa(rt, Conditional) && rt ⊑ Bool + if isa(rt, Conditional) + rt = InterConditional(slot_id(rt.var), rt.vtype, rt.elsetype) + elseif is_lattice_bool(rt) if isa(bestguess, InterConditional) # if the bestguess so far is already `Conditional`, try to convert # this `rt` into `Conditional` on the slot to avoid overapproximation # due to conflict of different slots - rt = boolean_rt_to_conditional(rt, changes, bestguess.slot) - elseif nargs ≥ 1 + rt = bool_rt_to_conditional(rt, slottypes, changes, bestguess.slot) + else # pick up the first "interesting" slot, convert `rt` to its `Conditional` - # TODO: this is very naive heuristic, ideally we want `Conditional` - # and `InterConditional` to convey constraints on multiple slots - rt = boolean_rt_to_conditional(rt, changes, nargs > 1 ? 2 : 1) + # TODO: ideally we want `Conditional` and `InterConditional` to convey + # constraints on multiple slots + for slot_id in 1:nslots + rt = bool_rt_to_conditional(rt, slottypes, changes, slot_id) + rt isa InterConditional && break + end end end end @@ -1426,13 +1529,14 @@ function widenreturn(@nospecialize(rt), @nospecialize(bestguess), isva::Bool, na # only propagate information we know we can store # and is valid and good inter-procedurally isa(rt, Conditional) && return InterConditional(slot_id(rt.var), rt.vtype, rt.elsetype) + isa(rt, InterConditional) && return rt isa(rt, Const) && return rt isa(rt, Type) && return rt if isa(rt, PartialStruct) fields = copy(rt.fields) haveconst = false for i in 1:length(fields) - a = widenreturn(fields[i], bestguess, isva, nargs, changes) + a = widenreturn(fields[i], bestguess, nslots, slottypes, changes) if !haveconst && has_const_info(a) # TODO: consider adding && const_prop_profitable(a) here? haveconst = true @@ -1457,6 +1561,8 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) nargs = frame.nargs def = frame.linfo.def isva = isa(def, Method) && def.isva + nslots = nargs - isva + slottypes = frame.slottypes while frame.pc´´ <= n # make progress on the active ip set local pc::Int = frame.pc´´ # current program-counter @@ -1527,7 +1633,19 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) elseif isa(stmt, ReturnNode) pc´ = n + 1 bestguess = frame.bestguess - rt = widenreturn(abstract_eval_value(interp, stmt.val, changes, frame), bestguess, isva, nargs, changes) + rt = abstract_eval_value(interp, stmt.val, changes, frame) + rt = widenreturn(rt, bestguess, nslots, slottypes, changes) + # narrow representation of bestguess slightly to prepare for tmerge with rt + if rt isa InterConditional && bestguess isa Const + let slot_id = rt.slot + old_id_type = slottypes[slot_id] + if bestguess.val === true && rt.elsetype !== Bottom + bestguess = InterConditional(slot_id, old_id_type, Bottom) + elseif bestguess.val === false && rt.vtype !== Bottom + bestguess = InterConditional(slot_id, Bottom, old_id_type) + end + end + end # copy limitations to return value if !isempty(frame.pclimitations) union!(frame.limitations, frame.pclimitations) @@ -1538,7 +1656,9 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) end if tchanged(rt, bestguess) # new (wider) return type for frame - frame.bestguess = tmerge(bestguess, rt) + bestguess = tmerge(bestguess, rt) + # TODO: if bestguess isa InterConditional && !interesting(bestguess); bestguess = widenconditional(bestguess); end + frame.bestguess = bestguess for (caller, caller_pc) in frame.cycle_backedges # notify backedges of updated type information typeassert(caller.stmt_types[caller_pc], VarTable) # we must have visited this statement before @@ -1654,16 +1774,20 @@ function conditional_changes(changes::VarTable, @nospecialize(typ), var::Slot) return changes end -function boolean_rt_to_conditional(@nospecialize(rt), state::VarTable, slot_id::Int) - typ = widenconditional((state[slot_id]::VarState).typ) # avoid nested conditional - if isa(rt, Const) - if rt.val === true - return Conditional(SlotNumber(slot_id), typ, Bottom) - elseif rt.val === false - return Conditional(SlotNumber(slot_id), Bottom, typ) - end - elseif rt === Bool - return Conditional(SlotNumber(slot_id), typ, typ) +function bool_rt_to_conditional(@nospecialize(rt), slottypes::Vector{Any}, state::VarTable, slot_id::Int) + old = slottypes[slot_id] + new = widenconditional((state[slot_id]::VarState).typ) # avoid nested conditional + if new ⊑ old && !(old ⊑ new) + if isa(rt, Const) + val = rt.val + if val === true + return InterConditional(slot_id, new, Bottom) + elseif val === false + return InterConditional(slot_id, Bottom, new) + end + elseif rt === Bool + return InterConditional(slot_id, new, new) + end end return rt end diff --git a/base/compiler/typelattice.jl b/base/compiler/typelattice.jl index eb8acf0848028..17ff193db24c0 100644 --- a/base/compiler/typelattice.jl +++ b/base/compiler/typelattice.jl @@ -124,9 +124,12 @@ function issubconditional(a::C, b::C) where {C<:AnyConditional} end return false end + is_same_conditionals(a::Conditional, b::Conditional) = slot_id(a.var) === slot_id(b.var) is_same_conditionals(a::InterConditional, b::InterConditional) = a.slot === b.slot +is_lattice_bool(@nospecialize(typ)) = typ !== Bottom && typ ⊑ Bool + maybe_extract_const_bool(c::Const) = (val = c.val; isa(val, Bool)) ? val : nothing function maybe_extract_const_bool(c::AnyConditional) (c.vtype === Bottom && !(c.elsetype === Bottom)) && return false diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index cdb96b82d1d1f..890512c93d5f1 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -1256,16 +1256,7 @@ end push!(constvec, 10) @test @inferred(sizeof_constvec()) == sizeof(Int) * 4 -let - f = x->isdefined(x, :re) - t = Tuple{ComplexF64} - interp = Core.Compiler.NativeInterpreter() - linfo = get_linfo(f, t) - ci = Core.Compiler.getindex(Core.Compiler.code_cache(interp), get_linfo(f, t)) - rc = ci.rettype_const - @test isa(rc, Core.InterConditional) - @test rc.vtype === ComplexF64 && rc.elsetype === Union{} -end +test_const_return(x->isdefined(x, :re), Tuple{ComplexF64}, true) isdefined_f3(x) = isdefined(x, 3) @test @inferred(isdefined_f3(())) == false @@ -1778,17 +1769,28 @@ end return nothing end == Any[Union{Int,Symbol,Nothing}] + # constraint on non-vararg argument of `isva` method + isaint_isvapositive(a, va...) = isa(a, Int) && sum(va) > 0 + @test Base.return_types((Any,Int,Int)) do a, b, c + isaint_isvapositive(a, b, c) && return a # a::Int + 0 + end == Any[Int] + # with Base functions @test Base.return_types((Any,)) do a - Base.Fix2(isa, Int)(a) && return sin(a) # a::Float64 - return 0.0 - end == Any[Float64] + Base.Fix2(isa, Int)(a) && return a # a::Int + return 0 + end == Any[Int] @test Base.return_types((Union{Nothing,Int},)) do a isnothing(a) && return 0 return a # a::Int end == Any[Int] - @test_broken Base.return_types((Any,)) do x - Meta.isexpr(x, :call) && return x # x::Expr, ideally + @test Base.return_types((Union{Missing,Int},)) do a + ismissing(a) && return 0 + return a # a::Int + end == Any[Int] + @test Base.return_types((Any,)) do x + Meta.isexpr(x, :call) && return x # x::Expr return nothing end == Any[Union{Nothing,Expr}] end From 36e7e8a10f4f3d0ddf85ff15bd7439c3927622bf Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Wed, 20 Jan 2021 02:13:00 +0900 Subject: [PATCH 3/3] tweaks for inter-procedural constraint back-prop' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit note that the changes for `isnothing` and `ismissing` aren't necessary, but they reduce the number of method definitions for good reason; the less the number of methods they have, the better we can back-propagate type constraints, because even after a package defines their own new methods for them we can keep to use our `InterConditional` logic as far as far as the number of methods is [≤3](https://github.com/JuliaLang/julia/blob/5c6e21edbfd8f0c7d16ea01c91d1c75c30d4eaa1/base/compiler/types.jl#L119) --- base/essentials.jl | 3 +-- base/show.jl | 2 +- base/some.jl | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/base/essentials.jl b/base/essentials.jl index 4c7e68a7e6ac6..62a63afa55576 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -817,8 +817,7 @@ const missing = Missing() Indicate whether `x` is [`missing`](@ref). """ -ismissing(::Any) = false -ismissing(::Missing) = true +ismissing(x) = x === missing function popfirst! end diff --git a/base/show.jl b/base/show.jl index 6db1a414f53d3..b668296f875b4 100644 --- a/base/show.jl +++ b/base/show.jl @@ -1382,7 +1382,7 @@ function operator_associativity(s::Symbol) end is_expr(@nospecialize(ex), head::Symbol) = isa(ex, Expr) && (ex.head === head) -is_expr(@nospecialize(ex), head::Symbol, n::Int) = is_expr(ex, head) && length((ex::Expr).args) == n +is_expr(@nospecialize(ex), head::Symbol, n::Int) = is_expr(ex, head) && length(ex.args) == n is_quoted(ex) = false is_quoted(ex::QuoteNode) = true diff --git a/base/some.jl b/base/some.jl index 1f5624502a583..272ed0e00ce31 100644 --- a/base/some.jl +++ b/base/some.jl @@ -65,8 +65,7 @@ Return `true` if `x === nothing`, and return `false` if not. !!! compat "Julia 1.1" This function requires at least Julia 1.1. """ -isnothing(::Any) = false -isnothing(::Nothing) = true +isnothing(x) = x === nothing """