Why is the return type of the following inplace broadcast operation not inferred?

julia> @descend_code_warntype (Y -> Y .+= Any[rand(size(Y))][1])(rand(2))
(::var"#3#4")(Y) @ Main REPL[4]:1
┌ Warning: couldn't retrieve source of (::var"#3#4")(Y) @ Main REPL[4]:1
└ @ TypedSyntax ~/.julia/packages/TypedSyntax/vbZHi/src/node.jl:31
Variables
  #self#::Core.Const(var"#3#4"())
  Y::Vector{Float64}

Body::Any
    @ REPL[4]:1 within `#3`
1 ─ %1 = Main.:+::Core.Const(+)
│   %2 = Main.Any::Core.Const(Any)
│   %3 = Main.size(Y)::Tuple{Int64}
│   %4 = Main.rand(%3)::Int64
│   %5 = Base.getindex(%2, %4)::Vector{Any}
│   %6 = Base.getindex(%5, 1)::Any
│   %7 = Base.broadcasted(%1, Y, %6)::Any
│   %8 = Base.materialize!(Y, %7)::Any
└──      return %8
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 • %3 = size(::Vector{Float64})::Tuple{Int64}
   %4 = rand(::Tuple{Int64})::Int64
   %5 = getindex(::Type{Any},::Int64)::Vector{Any}
   %6 = getindex(::Vector{Any},::Int64)::Any
   %7 = broadcasted(::typeof(+),::Vector{Float64},::Any)::Any
   %8 = call → materialize!(::Vector{Float64},::Any)::Any

The input array is the destination and is returned, so its type should be known, and the return type of Base.materialize! should be known.

We don’t know know what the result of Base.materialize!(::Vector{Float64}, ::Any) could be. Someone could have overloaded Base.materialize!(::Vector{Float64}, ::SomeNewType) to return anything.

1 Like

Ah, brainfade

Is it reasonable to change the lowering to include a return Y at the end? This should not be breaking, as materialize! being the last step is an implementation detail at best. This can help quite a bit in badly inferred code.

That sounds reasonable to me.

julia> @code_warntype (Y -> (Y .+= Any[rand(size(Y))][1]))(rand(2))
MethodInstance for (::var"#15#16")(::Vector{Float64})
  from (::var"#15#16")(Y) @ Main REPL[7]:1
Arguments
  #self#::Core.Const(var"#15#16"())
  Y::Vector{Float64}
Body::Any
1 ─ %1 = Main.:+::Core.Const(+)
│   %2 = Main.Any::Core.Const(Any)
│   %3 = Main.size(Y)::Tuple{Int64}
│   %4 = Main.rand(%3)::Int64
│   %5 = Base.getindex(%2, %4)::Vector{Any}
│   %6 = Base.getindex(%5, 1)::Any
│   %7 = Base.broadcasted(%1, Y, %6)::Any
│   %8 = Base.materialize!(Y, %7)::Any
└──      return %8


julia> @code_warntype (Y -> (Y .+= Any[rand(size(Y))][1]; return Y))(rand(2))
MethodInstance for (::var"#17#18")(::Vector{Float64})
  from (::var"#17#18")(Y) @ Main REPL[8]:1
Arguments
  #self#::Core.Const(var"#17#18"())
  Y::Vector{Float64}
Body::Vector{Float64}
1 ─ %1 = Main.:+::Core.Const(+)
│   %2 = Main.Any::Core.Const(Any)
│   %3 = Main.size(Y)::Tuple{Int64}
│   %4 = Main.rand(%3)::Int64
│   %5 = Base.getindex(%2, %4)::Vector{Any}
│   %6 = Base.getindex(%5, 1)::Any
│   %7 = Base.broadcasted(%1, Y, %6)::Any
│        Base.materialize!(Y, %7)
└──      return Y
1 Like

It seems like broadcast!, which is supposed to be equivalent to the in-place dot syntax, handles this by returning the destination array. In the case where it’s a function call in another function, I’m still not entirely sure how the runtime dispatch of broadcast! is inferred properly even when I @noinline it , I thought it would’ve been an unknown call with unknown return type like with materialize!: because the compiler does check the sole method’s return type. You can ruin it by type pirating more applicable methods for the runtime call: for T in (Bool, Float64, Float32, ComplexF64) @eval Base.broadcast!(f::Tf, dest, A1::Vector{Float64}, As::Vararg{$T, N}) where {Tf, N} = $(zero(T)) end # no methods for the Int example.

julia> function test1(a::Vector{Any} = Any[2])
         two = a[begin] # what the original code always results in
         dest = rand(2)
         @noinline broadcast!(+, dest, dest, two)
       end
test1 (generic function with 2 methods)

julia> @code_warntype test1(Any[2])
MethodInstance for test1(::Vector{Any})
  from test1(a::Vector{Any}) in Main at REPL[7]:1
Arguments
  #self#::Core.Const(test1)
  a::Vector{Any}
Locals
  val::Vector{Float64}
  dest::Vector{Float64}
  two::Any
Body::Vector{Float64}
1 ─ %1 = Base.firstindex(a)::Core.Const(1)
│        (two = Base.getindex(a, %1))
│        (dest = Main.rand(2))
│        nothing
│        (val = Main.broadcast!(Main.:+, dest, dest, two))
│        nothing
└──      return val

compared to

julia> function test2(a::Vector{Any})
         two = a[begin] # what the original code always results in
         dest = rand(2)
         dest .+= two
       end
test2 (generic function with 1 method)

julia> @code_warntype test2(Any[2])
MethodInstance for test2(::Vector{Any})
  from test2(a::Vector{Any}) in Main at REPL[32]:1
Arguments
  #self#::Core.Const(test2)
  a::Vector{Any}
Locals
  dest::Vector{Float64}
  two::Any
Body::Any
1 ─ %1 = Base.firstindex(a)::Core.Const(1)
│        (two = Base.getindex(a, %1))
│        (dest = Main.rand(2))
│   %4 = dest::Vector{Float64}
│   %5 = Base.broadcasted(Main.:+, dest, two)::Any
│   %6 = Base.materialize!(%4, %5)::Any
└──      return %6