Julia: Inference incorrectly infers `Nothing` and then incorrectly removes generated code

Created on 4 Oct 2018  ยท  8Comments  ยท  Source: JuliaLang/julia

Consider the following two codes

using Polyhedra
h = HyperPlane([0, 1], 1) โˆฉ HyperPlane([0, 1], -1)
hh = hyperplanes(h)
let
    i = 0
    for a in hh
        i += 1
    end
    println(i)
end

and

using Polyhedra
h = HyperPlane([0, 1], 1) โˆฉ HyperPlane([0, 1], -1)
let
    i = 0
    for a in hyperplanes(h)
        i += 1
    end
    println(i)
end

They re equivalent however the first one prints 2 while the second one prints 0.
Note that when the first one is wrapped inside a function, it also prints 2.

I have tried to debug the second code with gdb and it seems that the error is in typeinf_ext.
The input of typeinf_ext is the following MethodInstance:

Main.<toplevel thunk> -> Core.CodeInfo(code=Array{Any, (17,)}[
  Core.SlotNumber(id=1) = 0,
  Expr(:call, Main.hyperplanes, Main.h),
  Core.SlotNumber(id=2) = Expr(:call, Base.iterate, SSAValue(2)),
  Expr(:call, Core.:(===), Core.SlotNumber(id=2), nothing),
  Expr(:call, Base.not_int, SSAValue(4)),
  Expr(:gotoifnot, SSAValue(5), 16),
  Core.SlotNumber(id=2),
  Core.SlotNumber(id=3) = Expr(:call, Core.getfield, SSAValue(7), 1),
  Expr(:call, Core.getfield, SSAValue(7), 2),
  Core.SlotNumber(id=1) = Expr(:call, Main.:(+), Core.SlotNumber(id=1), 1),
  Core.SlotNumber(id=2) = Expr(:call, Base.iterate, SSAValue(2), SSAValue(9)),
  Expr(:call, Core.:(===), Core.SlotNumber(id=2), nothing),
  Expr(:call, Base.not_int, SSAValue(12)),
  Expr(:gotoifnot, SSAValue(13), 16),
  goto 7,
  Expr(:call, Main.println, Core.SlotNumber(id=1)),
  Expr(:return, SSAValue(16))], codelocs=Array{Int32, (17,)}[1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4], method_for_inference_limit_heuristics=nothing, ssavaluetypes=17, linetable=Array{Any, (4,)}[
  Core.LineInfoNode(mod=Main, method=Symbol("top-level scope"), file=Symbol("/home/blegat/.julia/dev/Polyhedra/ko.jl"), line=4, inlined_at=0),
  Core.LineInfoNode(mod=Main, method=Symbol("top-level scope"), file=Symbol("/home/blegat/.julia/dev/Polyhedra/ko.jl"), line=5, inlined_at=0),
  Core.LineInfoNode(mod=Main, method=Symbol("top-level scope"), file=Symbol("/home/blegat/.julia/dev/Polyhedra/ko.jl"), line=6, inlined_at=0),
  Core.LineInfoNode(mod=Main, method=Symbol("top-level scope"), file=Symbol("/home/blegat/.julia/dev/Polyhedra/ko.jl"), line=8, inlined_at=0)], ssaflags=Array{UInt8, (0,)}[], slotflags=Array{UInt8, (3,)}[0x00, 0x00, 0x10], slotnames=Array{Any, (3,)}[
  :i,
  Symbol("#temp#"),
  :a], inferred=false, inlineable=false, propagate_inbounds=false, pure=false)

and the output is the following MethodInstance

Main.<toplevel thunk> -> Core.CodeInfo(code=Array{Any, (5,)}[
  Expr(:call, Main.hyperplanes, Main.h),
  Expr(:call, Base.iterate, SSAValue(1)),
  Core.PiNode(val=0, typ=Int64),
  Expr(:invoke, println(Int64), Main.println, SSAValue(3)),
  Expr(:return, SSAValue(4))], codelocs=Array{Int32, (5,)}[2, 2, 4, 4, 4], method_for_inference_limit_heuristics=nothing, ssavaluetypes=Array{Any, (5,)}[
  Polyhedra.RepIterator{_, _, _} where _ where _ where _,
  Nothing,
  Int64,
  Nothing,
  Any], linetable=Array{Core.LineInfoNode, (4,)}[
  Core.LineInfoNode(mod=Main, method=Symbol("top-level scope"), file=Symbol("/home/blegat/.julia/dev/Polyhedra/ko.jl"), line=4, inlined_at=0),
  Core.LineInfoNode(mod=Main, method=Symbol("top-level scope"), file=Symbol("/home/blegat/.julia/dev/Polyhedra/ko.jl"), line=5, inlined_at=0),
  Core.LineInfoNode(mod=Main, method=Symbol("top-level scope"), file=Symbol("/home/blegat/.julia/dev/Polyhedra/ko.jl"), line=6, inlined_at=0),
  Core.LineInfoNode(mod=Main, method=Symbol("top-level scope"), file=Symbol("/home/blegat/.julia/dev/Polyhedra/ko.jl"), line=8, inlined_at=0)], ssaflags=Array{UInt8, (5,)}[0x00, 0x00, 0x00, 0x00, 0x00], slotflags=Array{UInt8, (0,)}[], slotnames=Array{Any, (0,)}[], inferred=true, inlineable=false, propagate_inbounds=false, pure=false)

What seems to happen is that the return type inferred for Expr(:call, Base.iterate, SSAValue(1)) is Nothing and then all the for the for loop is optimized out.
However, the inference is wrong, it does not always return nothing (otherwise, the first code wouldn't print 2).

I have tried debugging typeinf_ext as follows but the bug does not occur even if the input I give to typeinf_ext seems to be exactly the same.
Any idea ?

Cfr https://github.com/JuliaPolyhedra/Polyhedra.jl/issues/123

bug types and dispatch

Most helpful comment

I have extracted a minimal failing example from the bug in Polyhedra. It does not require any packages:

abstract type A{T} end
abstract type B{T} end
const C{T} = Union{B{T}, A{T}}
struct MyVec <: B{Int}
    elements::Vector{Int}
end
function startindex(elements::Vector)
    if isempty(elements)
        return nothing
    else
        return 1
    end
end
struct Iterator{PT<:Tuple{C}}
    ps::PT
    function Iterator(ps::PT) where {PT}
        new{PT}(ps)
    end
end
element_and_index(it, idx::Nothing) = nothing
element_and_index(it::Iterator, idx::Int) = it.ps[1].elements[idx], idx
function Base.iterate(it::Iterator{Tuple{T}}) where {T}
    idx = startindex(it.ps[1].elements)
    return element_and_index(it, idx)
end

h = MyVec([1])
let
    i = 0
    for a in Iterator((h,))
        i += 1 # never gets executed
    end
    println(i) # Prints 0 instead of 1
end

It seems that every element is important in this example. Whenever I try to simplify it, the bug disappears :-P

All 8 comments

I have extracted a minimal failing example from the bug in Polyhedra. It does not require any packages:

abstract type A{T} end
abstract type B{T} end
const C{T} = Union{B{T}, A{T}}
struct MyVec <: B{Int}
    elements::Vector{Int}
end
function startindex(elements::Vector)
    if isempty(elements)
        return nothing
    else
        return 1
    end
end
struct Iterator{PT<:Tuple{C}}
    ps::PT
    function Iterator(ps::PT) where {PT}
        new{PT}(ps)
    end
end
element_and_index(it, idx::Nothing) = nothing
element_and_index(it::Iterator, idx::Int) = it.ps[1].elements[idx], idx
function Base.iterate(it::Iterator{Tuple{T}}) where {T}
    idx = startindex(it.ps[1].elements)
    return element_and_index(it, idx)
end

h = MyVec([1])
let
    i = 0
    for a in Iterator((h,))
        i += 1 # never gets executed
    end
    println(i) # Prints 0 instead of 1
end

It seems that every element is important in this example. Whenever I try to simplify it, the bug disappears :-P

Thank you, @blegat, for reducing the example. That's tremendously helpful!

Further reduction:

julia> struct Foo end

julia> struct Bar{PT<:Tuple{Foo}}
           function Bar(ps::PT) where {PT}
               new{PT}()
           end
       end

julia> baz(it::Bar) = "baz called"
baz (generic function with 1 method)

julia> bork1(it::Bar{Tuple{T}}) where {T} = return baz(it)
bork1 (generic function with 1 method)

julia> bork2(it::Bar{Tuple{T}}) where {T<:Foo} = return baz(it)
bork2 (generic function with 1 method)

julia> h = Foo()
Foo()

julia> bar = Bar((h,))
Bar{Tuple{Foo}}()

julia> # this is OK:
       code_warntype(bork1, Tuple{typeof(bar)})
Body::String
1 1 โ”€     return "baz called"                                                                       โ”‚

julia> # but if h could not be inferred in Bar((h,)), bar is inferred as
       T_bar = Core.Compiler.return_type(Bar, Tuple{Tuple{Any}})
Bar{_1} where _1

julia> bar isa T_bar
true

julia> # and this is not OK:
       code_warntype(bork1, Tuple{T_bar})
Body::Union{}
1 1 โ”€     (Main.baz)(it)                                                                            โ”‚
  โ””โ”€โ”€     $(Expr(:unreachable))                                                                     โ”‚

julia> # although
       code_warntype(baz, Tuple{T_bar})
Body::String
1 1 โ”€     return "baz called"                                                                       โ”‚

julia> # OTOH, this is OK:
       code_warntype(bork2, Tuple{T_bar})
Body::String
1 1 โ”€     return "baz called"     

As an aside, inference of apply_type in

julia> code_warntype(Bar, Tuple{Tuple{Any}})
Body::Bar{_1} where _1
3 1 โ”€ %1 = (Core.apply_type)(Main.Bar, $(Expr(:static_parameter, 1)))::Type{Bar{_1}} where _1       โ”‚
  โ”‚   %2 = %new(%1)::Bar{_1} where _1                                                               โ”‚
  โ””โ”€โ”€      return %2  

could probably be improved to Type{Bar{T}} where T<:Tuple{Foo}, which would presumable also solve this particular example, but not the underlying issue.

Hm, I have a branch where Bar(::Tuple{Any}) is inferred as Bar{_1} where _1<:Tuple{Foo} which looks good to me. But I noticed (and this is the case on master, too):

julia> methods(bork1, Tuple{Bar{_1} where _1<:Tuple{Foo}})
# 0 methods for generic function "bork1":

Huh?
OTOH:

julia> methods(bork1, Tuple{Bar{_1} where _1<:Tuple{Any}})
# 1 method for generic function "bork1":
[1] bork1(it::Bar{Tuple{T}}) where T in Main at REPL[5]:1

This is consistent with

julia> typeintersect(Bar{Tuple{T}} where T, Bar{_1} where _1<:Tuple{Foo})
Union{}

julia> typeintersect(Bar{Tuple{T}} where T, Bar{_1} where _1<:Tuple{Any})
Bar{Tuple{T}} where T

where I'd expect the former to yield Bar{Tuple{T}} where T<:Foo.

I've dug a bit into the type intersection bug (which I suspect to be at least related to the underlying cause here) and when calling typeintersect(Bar{Tuple{T}} where T, Bar{_1} where _1<:Tuple{Foo}), after peeling off the outer stuff, we end up in intersect_invariant(Tuple{T}, _1<:Tuple{Foo}). Here, the recursive intersection correctly yields Tuple{Foo}, but only updates the environment to fix _1 at Tuple{Foo}, while T is left at Union{} <: T <: Any. Then the check that Tuple{T} is a subtype of Tuple{Foo} in the environment fails, and the intersection is assumed to be empty. Can you take a look at this, @JeffBezanson?

Verified that when looking up matching methods during inference, the intersection between Tuple{typeof(baz), Bar{Tuple{T}} where T} and Tuple{typeof(baz), Bar{PT} where PT<:Tuple{Foo}} is wrongly computed as Union{}, making inference miss the method.

Should be fixed by #31747, but please confirm with the original example if possible.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Keno picture Keno  ยท  3Comments

sbromberger picture sbromberger  ยท  3Comments

felixrehren picture felixrehren  ยท  3Comments

tkoolen picture tkoolen  ยท  3Comments

i-apellaniz picture i-apellaniz  ยท  3Comments