# \[a,b,c,d\] for a,b,c,d::Union{Float64,Int} fails type prediction at seemingly no benefit

**URL:** <https://discourse.julialang.org/t/a-b-c-d-for-a-b-c-d-union-float64-int-fails-type-prediction-at-seemingly-no-benefit/139515>\
**Category:** Internals & Design\
**Created:** [September 17, 2026, 2:26pm UTC](https://discourse.julialang.org/t/a-b-c-d-for-a-b-c-d-union-float64-int-fails-type-prediction-at-seemingly-no-benefit/139515 "2026-09-17T14:26:55Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![skraemer](https://avatars.discourse-cdn.com/v4/letter/s/4bbf92/32.png) [@skraemer](https://discourse.julialang.org/u/skraemer)\
**Post date:** [September 17, 2026, 2:26pm UTC](https://discourse.julialang.org/t/a-b-c-d-for-a-b-c-d-union-float64-int-fails-type-prediction-at-seemingly-no-benefit/139515/1 "2026-09-17T14:26:55Z")

</div>

I have recently already asked related questions, but type prediction for union types has unfortunately become a gift for me that keeps on giving.

Replacing `Base.promote_typeof` by a simpler implementation seems at no loss for me, but makes an `v = [a,b,c,d]` equivalent quite a lot faster when for instance `a,b,c,d::Union{Float64,Int}` is inferred. It also leads to perfect type prediction `v::Union{Vector{Float64}, Vector{Int}}` whereas `Base` implementation does not.

I was in particular suprised that use of `X,Y,Z` in `(x::X, y::Y, z::Z) where {X,Y,Z}` deteriorates type prediction compared to `typeof(x),typeof(y),typeof(z)`, although I am not sure if this is strictly related to the rest.

I thought at first this could be some intentional avoided specialization, but experiments do not seem to support this. Maybe you can help me out.

What is unfortunate, but a compromise?  
Or has something gone unnoticed?  
Or what did I just get wrong?

_Quite a long code example, so the `@code_warntype` output is each directly after the function definition. Please note that the `k[]` construction is just for demonstrational purposes. Also `Float64` and `Int` just mean to represent an actual implementation example._

_The most relevant lines taken from the full code are:_

```julia
bar2(::X, ::Y, ::Z) where {X,Y,Z} = k[]::promote_type(promote_type(X, Y), Z) # ::Any (!)
bar4_4(x, y, z) = k[]::Base.promote_typeof(x, y, z, z) # ::Any (!)
bar5_4(x, y, z) = k[]::nosp_promote_typeof(x, y, z, z) # ::Union{Float64,Int}

sp_promote_typeof(x, args::Vararg{Any,N}) where N = promote_type(typeof(x), sp_promote_typeof(args...))
nosp_promote_typeof(x, args...) = promote_type(typeof(x), nosp_promote_typeof(args...))

sp_v = sp_vect(a, b, c, d, e, f) # Union{Vector{Float64}, Vector{Int64}}
nosp_v = nosp_vect(a, b, c, d, e, f) # Union{Vector{Float64}, Vector{Int64}}
base_v = base_vect(a, b, c, d, e, f) # Vector

## (case for large difference in compilation time for the three versions)
# 0.000568 seconds (3.23 k allocations: 169.469 KiB, 42.16% compilation time)
# 0.000708 seconds (2.00 k allocations: 109.375 KiB)
# 0.005754 seconds (15.93 k allocations: 603.641 KiB, 80.30% compilation time)

## (case for large difference in runtime for the three versions)
# 0.000252 seconds (2.00 k allocations: 109.375 KiB)
# 0.000707 seconds (2.00 k allocations: 109.375 KiB)
# 0.000984 seconds (6.00 k allocations: 203.125 KiB)

```

_Full code:_

```julia
## promote_type(of)
k = Ref{Any}()
nosp_promote_typeof(x) = typeof(x)
nosp_promote_typeof(x, args...) = promote_type(typeof(x), nosp_promote_typeof(args...))

bar1(::X, ::Y, ::Z) where {X,Y,Z} = k[]::promote_type(X, Y, Z) # ::Any
bar2(::X, ::Y, ::Z) where {X,Y,Z} = k[]::promote_type(promote_type(X, Y), Z) # ::Any (!)
bar3(x, y, z) = k[]::promote_type(typeof(x), typeof(y), typeof(z)) # ::Union{Float64,Int}
bar4(x, y, z) = k[]::Base.promote_typeof(x, y, z) # ::Union{Float64,Int}
bar4_4(x, y, z) = k[]::Base.promote_typeof(x, y, z, z) # ::Any (!)
bar5(x, y, z) = k[]::nosp_promote_typeof(x, y, z) # ::Union{Float64,Int}
bar5_4(x, y, z) = k[]::nosp_promote_typeof(x, y, z, z) # ::Union{Float64,Int}

function test3(f::F) where F<:Function
    a = rand((1, 1.0))
    b = rand((1, 1.0))
    c = rand((1, 1.0))
    result = f(a, b, c)
end
for f = (bar1, bar2, bar3, bar4, bar4_4, bar5, bar5_4)
    println(f)
    @code_warntype test3(f)
end

## union behaves simiarly
k = Ref{Any}()
union(::Type{X}, ::Type{Y}) where {X,Y} = Union{X,Y}
union(::Type{X}, ::Type{Y}, ::Type{Z}) where {X,Y,Z} = union(union(X, Y), Z)

foo1(::X, ::Y, ::Z) where {X,Y,Z} = k[]::Union{X,Y,Z} # ::Any
foo2(::X, ::Y, ::Z) where {X,Y,Z} = k[]::union(union(X, Y), Z) # ::Any
foo3(::X, ::Y, ::Z) where {X,Y,Z} = k[]::union(X, Y, Z) # ::Any
foo4(x, y, z) = k[]::Union{typeof(x),typeof(y),typeof(z)} # ::Any
foo5(x, y, z) = k[]::union(typeof(x), typeof(y), typeof(z)) # ::Union{Float64, Int64}

for f = (foo1, foo2, foo3, foo4, foo5)
    println(f)
    @code_warntype test3(f)
end

## vectors
sp_promote_typeof(x) = typeof(x)
sp_promote_typeof(x, args::Vararg{Any,N}) where N = promote_type(typeof(x), sp_promote_typeof(args...))

function sp_vect(x...)
    T = sp_promote_typeof(x...)
    v = Vector{T}(undef, length(x))
    for i = eachindex(x)
        v[i] = x[i]
    end
    return v
end

function nosp_vect(x...)
    T = nosp_promote_typeof(x...)
    v = Vector{T}(undef, length(x))
    for i = eachindex(x)
        v[i] = x[i]
    end
    return v
end

function base_vect(x...)
    T = Base.promote_typeof(x...)
    v = Vector{T}(undef, length(x))
    for i = eachindex(x)
        v[i] = x[i]
    end
    return v
end

function test3()
    a = rand((1, 1.0))
    b = rand((1, 1.0))
    c = rand((1, 1.0))
    sp_v = sp_vect(a, b, c) # Union{Vector{Float64}, Vector{Int64}}
    nosp_v = nosp_vect(a, b, c) # Union{Vector{Float64}, Vector{Int64}}
    base_v = base_vect(a, b, c) # Union{Vector{Float64}, Vector{Int64}}
end
@code_warntype test3()

function test6()
    a = rand((1, 1.0))
    b = rand((1, 1.0))
    c = rand((1, 1.0))
    d = rand((1, 1.0))
    e = rand((1, 1.0))
    f = rand((1, 1.0))
    sp_v = sp_vect(a, b, c, d, e, f) # Union{Vector{Float64}, Vector{Int64}}
    nosp_v = nosp_vect(a, b, c, d, e, f) # Union{Vector{Float64}, Vector{Int64}}
    base_v = base_vect(a, b, c, d, e, f) # Vector
end
@code_warntype test6()

function time6()
    a = rand((1, 1.0))
    b = rand((1, 1.0))
    c = rand((1, 1.0))
    d = rand((1, 1.0))
    e = rand((1, 1.0))
    f = rand((1, 1.0))
    @time for i = 1:1000
        sp_vect(a, b, c, d, e, f)
    end
    @time for i = 1:1000
        nosp_vect(a, b, c, d, e, f)
    end
    @time for i = 1:1000
        base_vect(a, b, c, d, e, f)
    end
    return nothing
end
for i = 1:4
    println("---")
    time6()
end
# ---
# 0.019506 seconds (22.79 k allocations: 1.087 MiB, 98.36% compilation time)
# 0.019999 seconds (14.85 k allocations: 727.273 KiB, 96.17% compilation time)
# 0.018908 seconds (70.11 k allocations: 3.387 MiB, 94.52% compilation time)
# ---
# 0.008233 seconds (11.40 k allocations: 564.055 KiB, 96.47% compilation time)
# 0.008273 seconds (6.96 k allocations: 352.367 KiB, 92.64% compilation time)
# 0.017095 seconds (21.27 k allocations: 927.219 KiB, 93.83% compilation time)
# ---
# 0.000508 seconds (3.24 k allocations: 169.859 KiB, 46.22% compilation time)
# 0.000699 seconds (2.00 k allocations: 109.375 KiB)
# 0.009744 seconds (14.13 k allocations: 647.312 KiB, 89.49% compilation time)
# ---
# 0.000494 seconds (3.24 k allocations: 169.453 KiB, 46.85% compilation time)
# 0.004030 seconds (3.25 k allocations: 168.812 KiB, 86.05% compilation time)
# 0.011168 seconds (63.04 k allocations: 2.987 MiB, 90.28% compilation time)

## after some reruns
# ---
# 0.000470 seconds (2.30 k allocations: 123.906 KiB, 37.92% compilation time)
# 0.000556 seconds (2.00 k allocations: 109.375 KiB)
# 0.001050 seconds (8.00 k allocations: 234.375 KiB)
# ---
# 0.000316 seconds (2.30 k allocations: 123.844 KiB, 14.97% compilation time)
# 0.000533 seconds (2.00 k allocations: 109.375 KiB)
# 0.001026 seconds (10.00 k allocations: 265.625 KiB)
# ---
# 0.000263 seconds (2.00 k allocations: 109.375 KiB)
# 0.000638 seconds (2.00 k allocations: 109.375 KiB)
# 0.001049 seconds (6.00 k allocations: 203.125 KiB)
# ---
# 0.000252 seconds (2.00 k allocations: 109.375 KiB)
# 0.000707 seconds (2.00 k allocations: 109.375 KiB)
# 0.000984 seconds (6.00 k allocations: 203.125 KiB)

## another first time run
# ---
# 0.020731 seconds (19.28 k allocations: 943.086 KiB, 98.19% compilation time)
# 0.020742 seconds (17.21 k allocations: 842.578 KiB, 95.89% compilation time)
# 0.020964 seconds (73.27 k allocations: 3.470 MiB, 94.22% compilation time)
# ---
# 0.009201 seconds (11.40 k allocations: 564.258 KiB, 96.52% compilation time)
# 0.013132 seconds (8.20 k allocations: 411.664 KiB, 95.84% compilation time)
# 0.020222 seconds (68.17 k allocations: 3.229 MiB, 94.24% compilation time)
# ---
# 0.000467 seconds (2.93 k allocations: 154.438 KiB, 36.04% compilation time)
# 0.000622 seconds (2.00 k allocations: 109.375 KiB)
# 0.007555 seconds (56.36 k allocations: 2.620 MiB, 85.31% compilation time)
# ---
# 0.000568 seconds (3.23 k allocations: 169.469 KiB, 42.16% compilation time)
# 0.000708 seconds (2.00 k allocations: 109.375 KiB)
# 0.005754 seconds (15.93 k allocations: 603.641 KiB, 80.30% compilation time)

```

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [September 17, 2026, 6:41pm UTC](https://discourse.julialang.org/t/a-b-c-d-for-a-b-c-d-union-float64-int-fails-type-prediction-at-seemingly-no-benefit/139515/2 "2026-09-17T18:41:21Z")

</div>

My guess as an outsider is that unbounded recursion like this:

> [@skraemer](#):
>
> ```julia-auto
> nosp_promote_typeof(x) = typeof(x)
> nosp_promote_typeof(x, args...) = promote_type(typeof(x), nosp_promote_typeof(args...))
> ...
> sp_promote_typeof(x) = typeof(x)
> sp_promote_typeof(x, args::Vararg{Any,N}) where N = promote_type(typeof(x), sp_promote_typeof(args...))
> 
> ```

…was deliberately avoided. If inlined, the amount of compiled code scales with the number of inputs, and if not inlined, each step is put onto a limited stack. Compared that with the 1.12.7 implementation:

```julia-auto
promote_typeof(x) = typeof(x)
promote_typeof(x, y) = (@inline; promote_type(typeof(x), typeof(y)))
promote_typeof(x, y, z) = (@inline; promote_type(typeof(x), typeof(y), typeof(z)))
promote_typeof(x, y, z, a...) = (@inline; afoldl(((::Type{T}, y) where {T}) -> promote_type(T, typeof(y)), promote_typeof(x, y, z), a...))

```

…where `promote_typeof` calling itself doesn’t loop back to a method. How much this actually matters, I have no idea. I’m not very aware of how many n-ary promotions are done, maybe in the n-ary operations like `+` or `*`?

> [@skraemer](#):
>
> I was in particular suprised that use of `X,Y,Z` in `(x::X, y::Y, z::Z) where {X,Y,Z}` deteriorates type prediction compared to `typeof(x),typeof(y),typeof(z)`

Suboptimal type inference, I think. If the type inference reflection can be trusted, then compare `code_warntype(bar1, Tuple(fill(Union{Float64, Int64}, 3)))` and `code_warntype(bar3, Tuple(fill(Union{Float64, Int64}, 3)))`. The static parameters are annotated with a red `$(Expr(:static_parameter, 1))::Type{X} where X<:Union{Float64, Int64}` whereas the inputs are annotated with a yellow `(%4)(x)::Union{Type{Float64}, Type{Int64}}`. The former is actually wider and includes `Type{Union{}}`, or maybe it’s the iterated union itself throwing the inference algorithm off.

---

<div class="post-metadata">

**Author:** ![skraemer](https://avatars.discourse-cdn.com/v4/letter/s/4bbf92/32.png) [@skraemer](https://discourse.julialang.org/u/skraemer)\
**Post date:** [October 2, 2026, 2:24pm UTC](https://discourse.julialang.org/t/a-b-c-d-for-a-b-c-d-union-float64-int-fails-type-prediction-at-seemingly-no-benefit/139515/3 "2026-10-02T14:24:56Z")

</div>

I get the point about bounded recursions, but it seems no problem to me to set a limit depending on `N` here. I guess the current `afoldl` does deliberately not use specialization on `op` and `bs...`, but for small numbers of inputs of `promote_typeof`, stopping at two inputs seems to break reasonable tradeoffs.

On how much this matters, see for instance what this does for concatenation of just two union inferred instances, when one of them is used twice:

```julia
function test()
    v = rand(([1], [1.0]))
    x = rand((1, 1.0)) # same phenomenon for another union inferred vector
    vx = [v; x] # vcat
    vxv = [v; x; v] # vcat
end
# @code_warntype test()
# (...)
# Locals
# vxv::Vector
# vx::Union{Vector{Float64}, Vector{Int64}}
# x::Union{Float64, Int64}
# v::Union{Vector{Float64}, Vector{Int64}}

```
