Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type check in callee does not propagate to caller #37342

Closed
yuyichao opened this issue Sep 2, 2020 · 0 comments · Fixed by #38905
Closed

Type check in callee does not propagate to caller #37342

yuyichao opened this issue Sep 2, 2020 · 0 comments · Fixed by #38905
Assignees
Labels
compiler:inference Type inference compiler:optimizer Optimization passes (mostly in base/compiler/ssair/)

Comments

@yuyichao
Copy link
Contributor

yuyichao commented Sep 2, 2020

Somewhat similar to #25632 but more general and maybe more relavant as well.

julia> isType(@nospecialize t) = isa(t, DataType) && t.name === Core.Compiler._TYPE_NAME
isType (generic function with 1 method)

julia> function f(r)
           t = r[]
           if isType(t)
               return t.parameters[1]
           end
       end
f (generic function with 1 method)

julia> @code_typed f(Ref{Any}())
CodeInfo(
1%1  = Base.getfield(r, :x)::Any%2  = (%1 isa Main.DataType)::Bool
└──       goto #3 if not %2
2%4  = π (%1, DataType)
│   %5  = Base.getfield(%4, :name)::Core.TypeName%6  = Core.Compiler::Module%7  = Base.getfield(%6, :_TYPE_NAME)::Core.TypeName%8  = (%5 === %7)::Bool
└──       goto #4
3 ─       goto #4
4%11 = φ (#2 => %8, #3 => false)::Bool
└──       goto #6 if not %11
5%13 = Base.getproperty(%1, :parameters)::Any%14 = Base.getindex(%13, 1)::Any
└──       return %14
6return nothing
) => Any

Note that the access of the parameters field and subsequent calls are not inferred. If isType is manually inlined then the function works just fine. (The @nospecialize is due to copy-and-paste from type inference and does not have any effect on the issue.)


Noticed this when testing #37303 a while ago. Dynamic dispatch like this happens everywhere in inference/optimizer code (including most callers of typeutils.jl functions) and I've verified from LLVM IR dump that this is actually happening in the final code. I don't know how much performance issue it costs but could be significant... (note that it won't be as bad as the code above since Core.Compiler.getproperty === getfield). Adding type assertions back in to inference code could be a temporary solution but fixing the inference itself would be better...

@yuyichao yuyichao added compiler:inference Type inference compiler:optimizer Optimization passes (mostly in base/compiler/ssair/) labels Sep 2, 2020
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 16, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 16, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 17, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 17, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 18, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 18, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 19, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 19, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/LoweredCodeUtils.jl that referenced this issue Dec 21, 2020
- `@isssa`/`@isslotnum` hacks circumvent JuliaLang/julia#37342
  and gets rid of type assertions and accompanying inference overheads
  (they are really, really minor and negligible though)
  NOTE: well, we won't need them once JuliaLang/julia#38905
  gets merged
- improve inferrability around `pcexec` within `selective_eval!`
aviatesk added a commit to aviatesk/LoweredCodeUtils.jl that referenced this issue Dec 21, 2020
- `@isssa`/`@isslotnum` hacks circumvent 
JuliaLang/julia#37342
  and gets rid of type assertions and accompanying inference overheads
  (I left type assertions themselves though, in order to make them work
  in older versions of Julia)
  NOTE: well, we won't need them once 
JuliaLang/julia#38905
  gets merged
- improve inferrability around `pcexec` within `selective_eval!`
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 21, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 22, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 25, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 26, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 27, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 30, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 30, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 31, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 31, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Dec 31, 2020
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 2, 2021
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 5, 2021
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 5, 2021
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.

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 == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

There're certain limitations around. One of the biggest ones would be
that it can propagate constrains only on a single argument, and it fails
to back-propagate constrains when there're multiple conditions on
different slots, e.g. `Meta.isexpr` can't still propagate its type
constraint to the caller:
```julia
@test_broken Base.return_types((Any,)) do x
    Meta.isexpr(x, :call) && return x # still x::Any but ideally x::Expr
    return nothing
end == [Nothing,Expr]
```
(and because of this reason, this PR can't close JuliaLang#37342)
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 7, 2021
## improvements

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.

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 == [Int]

isaint2(::Any) = false
isaint2(::Int) = true
@test Base.return_types((Any,)) do a
    isaint2(a) && return a # a::Int
    return 0
end == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

---

## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 with the original definition of `Meta.isexpr`, the
heuristic ends up picking up a constraint on the second argument
(i.e. `ex.head === head`).
```julia
# original definition of `Meta.isexpr`
isexpr(@nospecialize(ex), head::Symbol) = isa(ex, Expr) && ex.head === 
head

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 8, 2021
## improvements

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.

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 == [Int]

isaint2(::Any) = false
isaint2(::Int) = true
@test Base.return_types((Any,)) do a
    isaint2(a) && return a # a::Int
    return 0
end == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

---

## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 with the original definition of `Meta.isexpr`, the
heuristic ends up picking up a constraint on the second argument
(i.e. `ex.head === head`).
```julia
# original definition of `Meta.isexpr`
isexpr(@nospecialize(ex), head::Symbol) = isa(ex, Expr) && ex.head === 
head

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 11, 2021
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.

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 == [Int]

isaint2(::Any) = false
isaint2(::Int) = true
@test Base.return_types((Any,)) do a
    isaint2(a) && return a # a::Int
    return 0
end == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 13, 2021
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.

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 == [Int]

isaint2(::Any) = false
isaint2(::Int) = true
@test Base.return_types((Any,)) do a
    isaint2(a) && return a # a::Int
    return 0
end == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 15, 2021
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.

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 == [Int]

isaint2(::Any) = false
isaint2(::Int) = true
@test Base.return_types((Any,)) do a
    isaint2(a) && return a # a::Int
    return 0
end == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 15, 2021
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.

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 == [Int]

isaint2(::Any) = false
isaint2(::Int) = true
@test Base.return_types((Any,)) do a
    isaint2(a) && return a # a::Int
    return 0
end == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 16, 2021
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.

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 == [Int]

isaint2(::Any) = false
isaint2(::Int) = true
@test Base.return_types((Any,)) do a
    isaint2(a) && return a # a::Int
    return 0
end == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 19, 2021
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.

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 == [Int]

isaint2(::Any) = false
isaint2(::Int) = true
@test Base.return_types((Any,)) do a
    isaint2(a) && return a # a::Int
    return 0
end == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 19, 2021
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.

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 == [Int]

isaint2(::Any) = false
isaint2(::Int) = true
@test Base.return_types((Any,)) do a
    isaint2(a) && return a # a::Int
    return 0
end == [Int]
```

This PR also tweaks `isnothing` and `ismissing` so that there is no
longer any inferrability penalties to use them instead of
`x === nothing` or `x === missing` e.g.:
```julia
@test Base.return_types((Union{Nothing,Int},)) do a
    isnothing(a) && return 0
    return a # a::Int
end == [Int]
```
(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 20, 2021
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.

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
    0
end == Any[Union{Float64,Int}]
```

(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 20, 2021
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.

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
    0
end == Any[Union{Float64,Int}]
```

(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 20, 2021
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.

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
    0
end == Any[Union{Float64,Int}]
```

(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 21, 2021
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.

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
    0
end == Any[Union{Float64,Int}]
```

(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 21, 2021
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.

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
    0
end == Any[Union{Float64,Int}]
```

(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 21, 2021
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.

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
    0
end == Any[Union{Float64,Int}]
```

(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
@aviatesk aviatesk self-assigned this Jan 25, 2021
aviatesk added a commit to aviatesk/julia that referenced this issue Jan 30, 2021
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.

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
    0
end == Any[Union{Float64,Int}]
```

(and now we don't need something like JuliaLang#38636)

---

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ julia summer.jl
  421.223 μs (0 allocations: 0 bytes)
```

---

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 with the original definition of `Meta.isexpr`, the
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

x::Any
if isexpr(x, :call)
    x.args # ideally `x::Expr`
end
```

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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Feb 3, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Feb 5, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Feb 6, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Feb 10, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Feb 15, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Feb 16, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Feb 17, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Feb 18, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
aviatesk added a commit to aviatesk/julia that referenced this issue Feb 24, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
ElOceanografo pushed a commit to ElOceanografo/julia that referenced this issue May 4, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
antoine-levitt pushed a commit to antoine-levitt/julia that referenced this issue May 9, 2021
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 JuliaLang#38636)

 ## benchmarks

A compile time comparison:
> on the current master (82d79ce)
```
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 (37e279b)
```
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 (82d79ce)
```
❯ julia summer.jl
  1.214 ms (24923 allocations: 389.42 KiB)
```

> on this PR (37e279b)
```
❯ 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 JuliaLang#38636
- closes JuliaLang#37342
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler:inference Type inference compiler:optimizer Optimization passes (mostly in base/compiler/ssair/)
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants