Puzzling inference with inhomogeneous tuples

Consider this reduced example:

f(term, sets...) = (sets..., zero(term)), zero(term)

g(sets, term, terms...) = g(_g(sets, term), terms...)
g(sets) = sets
function _g(sets, term)
    newsets, newset = f(term, sets...)
    return newsets
end

function main(terms)
    sets = g((), terms...)
    return sets
end

using StaticArrays
ss = (SVector{2,Int}, SVector{1,Float64}, SVector{2,Int})

I want to ensure type-stability of a related (but more complicated) piece of code. But I see this (v1.0.2 and v1.1)

julia> @code_warntype main(ss)
Body::Tuple
....

while g((), ss...) that is called by main is inferred just fine!

julia> @code_warntype g((), ss...)
Body::Tuple{SArray{Tuple{2},Int64,1,2},SArray{Tuple{1},Float64,1,1},SArray{Tuple{2},Int64,1,2}}
...

Any help to make main inferable would be very appreciated!

julia> typeof(ss)
Tuple{DataType,DataType,DataType}

If you where given this type for the input ss to main, could you predict the output?

2 Likes

Oh, how interesting! I see the origin of the problem now, thanks! Any solution you can think of? (I can give more details of what I try to do if required.)
EDIT: The solution is obviously to rethink my design :smiley:

Mmm, I think there might be more to this. Following your comment I now make (the equivalent of) ss to be concrete-typed in my code. Inference still fails for main

f(term, sets...) = (sets..., term), term

g(sets, term, terms...) = g(_g(sets, term), terms...)
g(sets) = sets
function _g(sets, term)
    newsets, newset = f(term, sets...)
    return newsets
end

function main(terms)
    sets = g((), terms...)
    return sets
end

using StaticArrays
ss = zero.((SVector{2,Int}, SVector{1,Float64}, SVector{2,Int}))
julia> typeof(ss)
Tuple{SArray{Tuple{2},Int64,1,2},SArray{Tuple{1},Float64,1,1},SArray{Tuple{2},Int64,1,2}}

julia> @code_warntype main(ss)
Body::Tuple
...

julia> @code_warntype g((), ss...)
Body::Tuple{SArray{Tuple{2},Int64,1,2},SArray{Tuple{1},Float64,1,1},SArray{Tuple{2},Int64,1,2}}
...

Any further suggestion?

Seems there is some inference caching problems going on here. If we exectue @code_warntype g((), ss...) before @code_warntype main(ss) then it infers:

julia> @code_warntype g((), ss...)
Body::Tuple{SArray{Tuple{2},Int64,1,2},SArray{Tuple{1},Float64,1,1},SArray{Tuple{2},Int64,1,2}}
...
julia> @code_warntype main(ss)
Body::Tuple{SArray{Tuple{2},Int64,1,2},SArray{Tuple{1},Float64,1,1},SArray{Tuple{2},Int64,1,2}}
...
1 Like

:open_mouth:
That is weird!

If I put the code in a module, however, there is no caching anomaly, only in global REPL scope.

Also: version 0.6 infers this without problem, independent of the order. I guess this could be some lingering bug in the new optimiser?

I’ll open an issue.

https://github.com/JuliaLang/julia/issues/30976

For anyone interested, I found a workaround that seems to be good enough. It involves not starting with an empty tuple in the first g call inside main. This is what seems to trip inference. The following equivalent version infers correctly

f(newset, sets...) = (sets..., newset), newset
g(term, terms...) = _g(first(f(term)), terms...)
_g(sets, term, terms...) = _g(_g(sets, term), terms...)
_g(sets, term) = first(f(term, sets...))
_g(sets) = sets

function main(terms)
    sets = g(terms...)
    return sets
end

using StaticArrays
ss = zero.((SVector{2,Int}, SVector{1,Float64}, SVector{2,Int}))
julia> @code_warntype main(ss)
Body::Tuple{SArray{Tuple{1},Float64,1,1},SArray{Tuple{2},Int64,1,2},SArray{Tuple{2},Int64,1,2},SArray{Tuple{1},Float64,1,1},SArray{Tuple{2},Int64,1,2}}
...

This seems to be good enough for my real-world case, so I’ll mark this as solved, although there is probably still a bug in the Julia inference code, somewhere.

Just wanted to pop by and thank @kristoffer.carlsson once more. I was really stuck with a real-world problem due to this, and his first comment was crucial to solve it. What a difference do such things make in Julia! But geez, it takes some serious training to get the hang of it. I sometime feel Julia is truly a language for academics. Great fun, powerful, but deep and subtle as hell!

1 Like

This is “just” a bug though.

1 Like

Yes, the second bit is indeed a bug. I was referring to your first comment about dispatching with tuples of Types. I was naively expecting I could, but:

julia> f(::Tuple{Type{S},Type{T}}) where {S,T} = 2
f (generic function with 1 method)
julia> f(::Tuple{Type{S},Type{S}}) where {S} = 1
f (generic function with 2 methods)
julia> f((Int,Float64))
2
julia> f((Int,Int))
2

I found that really puzzling. It of course makes sense after your comment, but I find it subtle!

julia> typeof((Int,Float64)) === typeof((Int,Int))
true

Ah, tuple types are written using the Tuple{...} syntax. They aren’t really normal tuples though.

julia> Tuple{Int, Int}
Tuple{Int64,Int64}

I see. I was wanting to use proper tuples to be able to splat two “tuples of types” into one and similar things. Which I guess you cannot do with “tuple types”. Anyway, I ended doing this, which I find quite cool

julia> struct TypeWrapper{T}
       end
julia> TypeWrapper(::Type{T}) where {T} = TypeWrapper{T}()
TypeWrapper
julia> f(::Tuple{TypeWrapper{T}, TypeWrapper{S}}) where {S,T} = 2
f (generic function with 1 method)
julia> f(::Tuple{TypeWrapper{S}, TypeWrapper{S}}) where {S} = 1
f (generic function with 2 methods)
julia> f((TypeWrapper(Int), TypeWrapper(Int)))
1
julia> f((TypeWrapper(Int), TypeWrapper(Float64)))
2