# Unmutated mutable type being corrupted?

**URL:** <https://discourse.julialang.org/t/unmutated-mutable-type-being-corrupted/129161>\
**Category:** General Usage\
**Created:** [May 20, 2025, 2:51am UTC](https://discourse.julialang.org/t/unmutated-mutable-type-being-corrupted/129161 "2025-05-20T02:51:41Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![halleysfifthinc](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/halleysfifthinc/32/206280_2.png) [@halleysfifthinc](https://discourse.julialang.org/u/halleysfifthinc)\
**Post date:** [May 20, 2025, 2:51am UTC](https://discourse.julialang.org/t/unmutated-mutable-type-being-corrupted/129161/1 "2025-05-20T02:51:41Z")

</div>

MWE of what I think might be an issue in Julia itself:

```julia
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.

---

<div class="post-metadata">

**Author:** ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)\
**Post date:** [May 20, 2025, 3:52am UTC](https://discourse.julialang.org/t/unmutated-mutable-type-being-corrupted/129161/2 "2025-05-20T03:52:06Z")

</div>

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:

```sh
$ 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:

```julia-repl
$ julia +lts --startup-file=no -q # 1.10.9
julia> include("bug.jl"); exit()
ntrue = 0
$ julia +release --startup-file=no -q # 1.11.5
julia> include("bug.jl"); exit()
ntrue = 43730
$ julia +beta --startup-file=no -q # 1.12-beta3
julia> include("bug.jl"); exit()
ntrue = 66663
$ julia +nightly --startup-file=no -q # 1.13.0-DEV.590
julia> include("bug.jl"); exit()
ntrue = 58372

```

Here’s the script:

```julia
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(`.)

---

<div class="post-metadata">

**Author:** ![jakobjpeters](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jakobjpeters/32/207797_2.png) [@jakobjpeters](https://discourse.julialang.org/u/jakobjpeters)\
**Post date:** [May 20, 2025, 3:52am UTC](https://discourse.julialang.org/t/unmutated-mutable-type-being-corrupted/129161/3 "2025-05-20T03:52:56Z")

</div>

That does look like a bug. I also get inconsistent return values, and also two different values for `get(maybes, index, null).head`.

```julia-repl
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

```

---

<div class="post-metadata">

**Author:** ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)\
**Post date:** [May 20, 2025, 5:01am UTC](https://discourse.julialang.org/t/unmutated-mutable-type-being-corrupted/129161/4 "2025-05-20T05:01:12Z")

</div>

I couldn’t find a relevant issue, so I took the liberty: [`Union{Nothing,Int}` field in mutable struct set to `nothing` sometimes reads as garbage `Int` in \>=1.11 · Issue #58470 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/issues/58470)

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [May 20, 2025, 5:41am UTC](https://discourse.julialang.org/t/unmutated-mutable-type-being-corrupted/129161/5 "2025-05-20T05:41:27Z")

</div>

> [@danielwe](#):
>
> albeit not deterministically

Same, which actually makes it worse. I added `@show` to the `get` call and some runs print all `nothing`s 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.

```julia
julia> @code_llvm Complete(nothing)
...
define nonnull ptr @julia_Complete_2959() #0 {
top:
...
  %"new::Complete.head.tindex_ptr" = getelementptr inbounds i8, ptr %"new::Complete", i64 8
  store i8 0, ptr %"new::Complete.head.tindex_ptr", align 8
  ret ptr %"new::Complete"
}

julia> @code_llvm Complete(1)
...
define nonnull ptr @julia_Complete_2963(i64 signext %"head::Int64") #0 {
top:
...
  %"new::Complete.head.tindex_ptr" = getelementptr inbounds i8, ptr %"new::Complete", i64 8
  store i8 1, ptr %"new::Complete.head.tindex_ptr", align 8
  store i64 %"head::Int64", ptr %"new::Complete", align 8
  ret ptr %"new::Complete"
}

```

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.
