Interesting example. Learning with you. Note that the instability is not dependent on the order of the assignments:
julia> function g(x)
g(_) = y
y = (1,2)
y = [1,2]
return g(x)
end
g (generic function with 1 method)
julia> @code_warntype g(1)
Variables
#self#::Core.Const(g)
x::Int64
y::Core.Box
g::var"#g#15"
Body::Any
1 ─ (y = Core.Box())
│ (g = %new(Main.:(var"#g#15"), y))
│ %3 = Core.tuple(1, 2)::Core.Const((1, 2))
│ Core.setfield!(y, :contents, %3)
│ %5 = Base.vect(1, 2)::Vector{Int64}
│ Core.setfield!(y, :contents, %5)
│ %7 = (g)(x)::Any
└── return %7
Thus, to be stable, the compiler should have noticed that wherever g
is called y
is constant. It is understandable that this type inference is tricky. What if g
was called twice, should two specialized versions of g
be compiled (but not specialized to arguments, but to captured variables)?
Of course that is solved if y
is now a parameter of g
(i. e. g(y) = y
).
What is happening here appears to be related to the discussion in this thread. From what I understand, the possibility of changing the value of a captured variable in a closure is a feature in some sense, but can lead to these problems. Probably the good advice here is that one should never change the value of a captured variable except if, for some reason, the purpose of the closure is to modify that variable. Probably in most cases, one should consider these as a kind of “avoid non-constant globals” performance tip, but in this case the non-constant variable is local to the scope of the enclosing function.
.