Current solution is just to put your closure in let block
function test(a)
b = a
if a < 0
b = -a
end
let b = b
f(x) = x + b
f(10)
end
end
julia> @code_warntype test(1)
MethodInstance for test(::Int64)
from test(a) in Main at REPL[3]:1
Arguments
#self#::Core.Const(test)
a::Int64
Locals
b@_3::Int64
f::var"#f#4"{Int64}
b@_5::Int64
Body::Int64
I am not an expert, but I think you are right, capturing is the most reasonable explanation. IIRC it is a scope problem: variable b is changed outside of closure, so compiler puts it in a Box to trace these changes (presumably it is the same trick as Ref). When new scope is introduced with the help of the let block, compiler is happy and not boxing anything. But I may be wrong, so take it with a grain of salt.
The b in the let scope is never changed. Thus there is no boxing, right? It is the first time I knew that closure capturing can lead to type instability.