MWE of what I think might be an issue in Julia itself:
mutable struct Complete
head::Union{Nothing,Int}
end
const null = Complete(nothing)
function anydefined_const(inds, maybes)
for i in eachindex(inds)
if !isnothing(get(maybes, inds[i], null).head)
return true
end
end
return false
end
function anydefined_internalconst(inds, maybes)
_null = Complete(nothing)
for i in eachindex(inds)
if !isnothing(get(maybes, inds[i], _null).head)
return true
end
end
return false
end
# correctly returns false
anydefined_const([1,2,3,4,5], Dict(2 => Complete(nothing), 5 => Complete(nothing), 10 => Complete(nothing)))
# incorrectly returns true!
anydefined_internalconst([1,2,3,4,5], Dict(2 => Complete(nothing), 5 => Complete(nothing), 10 => Complete(nothing)))
(Unless I’m missing something), these two functions are identical besides where the null default value is defined, and in both cases, nothing in the code should be mutating the null variable?
Both function versions (correctly) return false when the null type is immutable. In a less reduced MWE, I also often observed inconsistent return values, but the MWE seems to be much more consistently wrong.
Wow, that’s pretty scary! I can reproduce, albeit not deterministically, on Julia 1.11, 1.12-beta, and 1.13-nightly, but Julia 1.10 gets it right. Below are the results using a script that builds on your MWE and counts the number of times I get true in 100_000 tries:
$ julia +lts --startup-file=no bug.jl # 1.10.9
ntrue = 0
$ julia +release --startup-file=no bug.jl # 1.11.5
ntrue = 20340
$ julia +beta --startup-file=no bug.jl # 1.12.0-beta3
ntrue = 37781
$ julia +nightly --startup-file=no bug.jl # 1.13.0-DEV.590
ntrue = 36954
In interactive mode, the error is more frequent, but 1.10 is still solid:
mutable struct Complete
head::Union{Nothing,Int}
end
function anydefined_internalconst(inds, maybes)
_null = Complete(nothing)
for i in eachindex(inds)
if !isnothing(get(maybes, inds[i], _null).head)
return true
end
end
return false
end
ntrue::Int = 0
for i in 1:100000
if anydefined_internalconst([1, 2, 3, 4, 5], Dict(2 => Complete(nothing), 5 => Complete(nothing), 10 => Complete(nothing)))
global ntrue += 1
end
end
@show ntrue
(Btw. your MWE code has an opening parenthesis too many on the last line. Substitute anydefined_internalconst(( => anydefined_internalconst(.)
That does look like a bug. I also get inconsistent return values, and also two different values for get(maybes, index, null).head.
julia> function bug(indices, maybes)
null = Complete(nothing)
for index ∈ indices
x = get(maybes, index, null).head
isnothing(x) || @show x
end
end;
julia> bug([1], Dict(2 => Complete(nothing)))
x = 140106805534776
Same, which actually makes it worse. I added @show to the get call and some runs print all nothings before returning false, and some return true right after an uninitialized Int value, which given the inputs must be _null. Weirdly, adding @show to the _null line seems to fix the bug.
So what’s supposed to happen is that the underlying type tag for the head field stores a 0. If we provided an integer instead, the type tag stores a 1 AND we store the integer to the field.
The highlighted portion is just gone in the @code_llvm anydefined_internalconst..., so I suppose an uninitialized type tag somehow has the same effect as setting either 0 or 1 instead of outright breaking (maybe a cutoff?). Trying to reduce the method further optimized away the Complete instantiation entirely so that’s no help.