inference: inter-procedural conditional constraint back-propagation
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