Description
Our longstanding policy on forward progress guarantees has basically been that
we expose the LLVM behavior and follow the C/C++ world by considering infinite
loops to be undefined behavior, leading to behavior like:
julia> f(x) = x ? true : while true; end
f (generic function with 1 method)
julia> f(true)
true
julia> f(false)
true
This might not seem to bad, but it can lead to unsoundness and security vulnerabilities ("nice error check you have there, would be a shame if somebody were to see the UB before it")
julia> @noinline function my_checkbounds(Alen::Int, i::Int)
if i < 0 || i > Alen
while true; end # A bad error path, maybe inlined from somewhere
return false
end
return true
end
my_checkbounds (generic function with 2 methods)
julia> function my_getindex(A::Vector{Int}, i::Int)
my_checkbounds(length(A), i) || error("Out of bounds!")
@inbounds A[i]
end
my_getindex (generic function with 1 method)
julia> my_getindex(Int[1], 100000000)
signal (11): Segmentation fault
For this reason, more modern languages tend to not make forward progress assumptions. The Rust folks in particular took this quite seriously and recently changed LLVM to require the mustprogress
attribute in order to top into forward progress guarantees (https://reviews.llvm.org/D85393). We should carefully chose what to do here. Since we don't set the new LLVM attribute, LLVM actually no longer makes these forward progress assumptions on our code. However, we do still make such assumptions in our own inference code.
Now that LLVM is fixed, my preference would be to follow Rust here and stop making any forward progress assumptions.