diff --git a/base/boot.jl b/base/boot.jl index 65dc5137d34cb8..2d0645443c9eca 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -424,6 +424,7 @@ eval(Core, :(CodeInstance(mi::MethodInstance, @nospecialize(rettype), @nospecial mi, rettype, inferred_const, inferred, const_flags, min_world, max_world))) 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, :(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 7d07e94ed26fe6..19c51f6f9877d0 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, @@ -191,6 +191,8 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), end end end + + @assert !(rettype isa Conditional) "invalid lattice element returned from inter-procedural context" #print("=> ", rettype, "\n") return CallMeta(rettype, info) end @@ -1035,9 +1037,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 + 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 abstract_call_known(interp, f, fargs, argtypes, sv, max_methods) + return callinfo end function sp_type_rewrap(@nospecialize(T), linfo::MethodInstance, isreturn::Bool) @@ -1279,6 +1301,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 @@ -1321,12 +1346,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 !== false @@ -1340,15 +1361,42 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) end elseif isa(stmt, ReturnNode) pc´ = n + 1 - rt = widenconditional(abstract_eval_value(interp, stmt.val, 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 + bestguess = frame.bestguess + rt = abstract_eval_value(interp, stmt.val, s[pc], frame) + 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 inter-procedurally + if isa(rt, Conditional) + rt = InterConditional(slot_id(rt.var), rt.vtype, rt.elsetype) + elseif !isa(rt, Const) && !isa(rt, Type) && !isa(rt, PartialStruct) rt = widenconst(rt) 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 @@ -1452,6 +1500,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 07356eed211f09..ed41f1549d6205 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -286,20 +286,24 @@ end function CodeInstance(result::InferenceResult, @nospecialize(inferred_result::Any), valid_worlds::WorldRange) local const_flags::Int32 + res = result.result if inferred_result isa Const # use constant calling convention rettype_const = (result.src::Const).val const_flags = 0x3 inferred_result = nothing else - if isa(result.result, Const) - rettype_const = (result.result::Const).val + if isa(res, Const) + rettype_const = res.val const_flags = 0x2 - elseif isconstType(result.result) - rettype_const = result.result.parameters[1] + elseif isconstType(res) + rettype_const = res.parameters[1] const_flags = 0x2 - elseif isa(result.result, PartialStruct) - rettype_const = (result.result::PartialStruct).fields + elseif isa(res, PartialStruct) + rettype_const = res.fields + const_flags = 0x2 + elseif isa(res, InterConditional) + rettype_const = res const_flags = 0x2 else rettype_const = nothing @@ -307,7 +311,7 @@ function CodeInstance(result::InferenceResult, @nospecialize(inferred_result::An end end return CodeInstance(result.linfo, - widenconst(result.result), rettype_const, inferred_result, + widenconst(res), rettype_const, inferred_result, const_flags, first(valid_worlds), last(valid_worlds)) end @@ -724,14 +728,18 @@ 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 + rettype_const = code.rettype_const + if isa(rettype_const, InterConditional) + return rettype_const, mi + elseif isa(rettype_const, Vector{Any}) && !(Vector{Any} <: rettype) + return PartialStruct(rettype, 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 db9fbce7d59fbd..55fd67190f2e76 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 @@ -90,11 +101,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 @@ -103,9 +113,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 @@ -124,14 +136,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) @@ -205,7 +217,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) @@ -238,7 +250,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 ce264b576f6b3d..100930f7db064c 100644 --- a/base/compiler/typelimits.jl +++ b/base/compiler/typelimits.jl @@ -314,7 +314,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 @@ -327,6 +327,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 076e1c4fc44f21..b6309ff1988a17 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -1608,6 +1608,7 @@ void jl_init_primitives(void) JL_GC_DISABLED add_builtin("Argument", (jl_value_t*)jl_argument_type); add_builtin("Const", (jl_value_t*)jl_const_type); add_builtin("PartialStruct", (jl_value_t*)jl_partial_struct_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 76e0984798522e..7f67fe6995e2e5 100644 --- a/src/jl_exported_data.inc +++ b/src/jl_exported_data.inc @@ -71,6 +71,7 @@ XX(jl_nothing_type) \ XX(jl_number_type) \ XX(jl_partial_struct_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 6da3211dbd3746..73fce479edceb4 100644 --- a/src/jltypes.c +++ b/src/jltypes.c @@ -2302,6 +2302,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 8acaade3cc2962..75e919d70d4860 100644 --- a/src/julia.h +++ b/src/julia.h @@ -619,6 +619,7 @@ extern JL_DLLIMPORT jl_datatype_t *jl_typedslot_type JL_GLOBALLY_ROOTED; 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_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 5a67ebd7eb3130..c3a4f50165e8a9 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -68,6 +68,7 @@ jl_value_t **const*const get_tags(void) { INSERT_TAG(jl_returnnode_type); INSERT_TAG(jl_const_type); INSERT_TAG(jl_partial_struct_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 5a2d806b237aea..26c5dff18f78c5 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 @@ -1260,7 +1261,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) @@ -1727,6 +1738,93 @@ for expr25261 in opt25261[i:end] end @test foundslot +# some tests below only work for functions defined in toplevel, but not for closures +macro evaltoplevel(ex) + m = Core.eval(__module__, :(module $(gensym()) end))::Module + return QuoteNode(Core.eval(m, ex)) +end + +@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] + @test @evaltoplevel begin + isaint2(a::Int) = true + isaint2(@nospecialize(_)) = false + + Base.return_types((Any,)) do a + isaint2(a) && return a # a::Int + return 0 + end == Any[Int] + end + @test @evaltoplevel begin + ispositive2(a::Int) = a > 0 + ispositive2(a) = false + Base.return_types((Any,)) do a + ispositive2(a) && return a # a::Int + return 0 + end == Any[Int] + end + + # type constraints from multiple constant boolean return types + @test @evaltoplevel begin + function f(x) + isa(x, Int) && return true + isa(x, Symbol) && return true + return false + end + Base.return_types((Any,)) do x + f(x) && return x # ::Union{Int,Symbol} + return nothing + end == Any[Union{Int,Symbol,Nothing}] + end + + # 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 Base.return_types((Any,)) do x + Meta.isexpr(x, :call) && return x # x::Expr + return nothing + end == Any[Union{Nothing,Expr}] + + # TODO: back-propagate multiple conditional constraints + # e.g. for the case below, currently inference picks up and propagates the constraint + # on the second argument (i.e. `ex.head ==== head`), which is less interesting than + # the constraint of the first (`isa(ex, Expr)`) + # we can fix this by back-propagating multiple conditional constraints, + # or by more appropriately choosing a constraint to propagate + @test_broken @evaltoplevel begin + # old and natural `isexpr` definition + isexpr(@nospecialize(ex), head::Symbol) = isa(ex, Expr) && ex.head === head + + Base.return_types((Any,)) do x + isexpr(x, :call) && return x # x::Expr, ideally + return nothing + end == Any[Union{Nothing,Expr}] + end +end + function f25579(g) h = g[] t = (h === nothing)