Stream: helpdesk (published)

Topic: isassigned and Ref


view this post on Zulip Fons van der Plas (Aug 01 2025 at 09:21):

Hey! I'm a bit confused why some Refs have #undef and others have a "zero" value as default:

julia> Ref{Any}()
Base.RefValue{Any}(#undef)

julia> Ref{Float64}()
Base.RefValue{Float64}(0.0)

julia> Ref{Bool}()
Base.RefValue{Bool}(false)

julia> Ref{Char}()
Base.RefValue{Char}('\0')

julia> Ref{Vector}()
Base.RefValue{Vector}(#undef)

Can I get a Ref{Bool} with an undef value?

view this post on Zulip Jakob Nybo Nissen (Aug 01 2025 at 09:26):

They are only #undef if the Ref is implemented with a pointer. #undef represents a null pointer.

view this post on Zulip Jakob Nybo Nissen (Aug 01 2025 at 09:29):

That's a little unsatisfactory because it's an explanation of the internals. The semantics are:

view this post on Zulip Jakob Nybo Nissen (Aug 01 2025 at 09:29):

So the answer is no: You can't have a RefValue{Bool} with an undef value.
Do you need it for something?

view this post on Zulip Fons van der Plas (Aug 01 2025 at 09:38):

Okay thanks!

My use case was:

computed = Ref{Any}()

function expensive()
    isassigned(computed) && return computed[]
    computed[] = let
        # something expensive
    end
end

view this post on Zulip Fons van der Plas (Aug 01 2025 at 09:39):

and changing Ref{Any} to Ref{Bool} broke that code

view this post on Zulip Jakob Nybo Nissen (Aug 01 2025 at 10:04):

Ah. In this case, I would use a Ref{Union{Nothing, Bool}}(nothing)

view this post on Zulip Sukera (Aug 01 2025 at 10:19):

Jakob Nybo Nissen said:

So the answer is no: You can't have a RefValue{Bool} with an undef value.
Do you need it for something?

Are you sure? I'm not aware of there being any special initialization logic for bitstypes, the bitpattern is arbitrary, is it not? Not all bitpatterns are necessarily valid for all bitstypes, so reading this uninitialized memory is very much UB in the general case.

view this post on Zulip Neven Sajko (Aug 01 2025 at 11:10):

reading this uninitialized memory is very much UB in the general case

I think Julia uses the LLVM freeze instruction to prevent UB from reading uninitialized memory. Some merged PRs found after a quick search:

view this post on Zulip Jakob Nybo Nissen (Aug 01 2025 at 11:17):

Yes, it was made non-UB in Julia 1.11. Of course, it can still invoke undefined behaviour yourself if you create your own type which results in undefined behaviour if it doesn't have specifit bitpatterns. I.e. if you do:

struct NeverZero
    x::UInt8

    NeverZero(x::UInt8) = iszero(x) ? error() : new(x)
end

function foo(x::NeverZero)
    if iszero(x.x)
        call_undefined_behaviour()
    else
        x.x + 0x01
    end
end

foo(Ref{NeverZero}()[])

view this post on Zulip Sukera (Aug 01 2025 at 19:23):

I'm wondering now what the possible bitpatterns for Bool in particular are when accessed through that RefValue - I remember there being some cleanup to always make sure to have Bool always be one of 0 or 1 as the bitpattern, but I don't know if that applies to this case as well.

view this post on Zulip Daniel Wennberg (Aug 01 2025 at 20:03):

Even in this example, it's not really correct to say that accessing the value is undefined behavior, is it? However, since the instance you get isn't guaranteed to satisfy invariants enforced by the constructor, using it may result in undefined behavior.

view this post on Zulip Sukera (Aug 02 2025 at 06:47):

Well, it depends. Julia is a little bit more generous than C/C++ (due to optimizing a few things differently/less, ironically), but in principle there is no difference between "accessing" and "using" when it comes to UB. Anything at all may happen, including time travel! So just viewing it from the perspective of "but I haven't used that value for anything yet!" is not sufficient.

view this post on Zulip Jakob Nybo Nissen (Aug 02 2025 at 10:18):

No that's not right. It is not UB to access the value. I.e. if you have a program that accesses the data but does not call an unsafe function, there is no UB. E.g. in my example, the UB is from the call to call_unsafe_behaviour, not from loading the value.

W.r.t booleans, they can be stored as any bitpattern. Loading them will only read the lowest bit. On an LLVM level this happens with loading an i1, on an assembly level the byte is loaded, and then a mask is applied.
I wonder if it's possible to somehow invoke UB using the fact that booleans can be stored in memory as any bitpattern, although when loaded, they must always have 0x01 or 0x00. I can't think of a way, though

view this post on Zulip Jakob Nybo Nissen (Aug 02 2025 at 10:20):

Maybe using unsafe_load? Not sure though.

view this post on Zulip Sukera (Aug 02 2025 at 10:59):

There can still be UB, even if you don't do anything with that value. That's exactly what the link I posted is about. I'm not sure it applies for the exact case of RefValue{Bool} in Julia, but reading something that is not initialized is generally UB. It's still not merged, but https://github.com/JuliaLang/julia/pull/54099/files#diff-cad50cf26296590b1937056ff6064bd99292b8428c0fdff1db0f682e8a48551cR34 explicitly calls out the _existence_ of values other than 0x0/0x1 for a Bool as UB. This is irrespective of what happens later on the LLVM level.

view this post on Zulip Sukera (Aug 02 2025 at 11:00):

We should probably also check what the effects system says about reading from an uninitialized RefValue{Bool} - I suspect the :noub effect ist tainted.

view this post on Zulip Sukera (Aug 02 2025 at 11:03):

Ah, and just a few lines below, it explicitly states that reading uninitialized bitstypes is not UB.. https://github.com/JuliaLang/julia/pull/54099/files#diff-cad50cf26296590b1937056ff6064bd99292b8428c0fdff1db0f682e8a48551cR46

What great consistency.

view this post on Zulip Jakob Nybo Nissen (Aug 02 2025 at 12:12):

I think you're mistaken. Specifically:

view this post on Zulip Jakob Nybo Nissen (Aug 02 2025 at 12:14):

See:

julia> r = Ref{Bool}()
Base.RefValue{Bool}(false)

julia> Base.infer_effects(getindex, Tuple{typeof(r)})
(?c,+e,+n,+t,+s,?m,+u,+o,+r)

view this post on Zulip Jakob Nybo Nissen (Aug 02 2025 at 12:20):

Summa summarum: As far as I can tell there is no way loading uninitialized data in Julia can cause UB, and I think we would need to see an example of this happening before it's worth considering as a problem.
That being said, it would be nice if we had something like: https://github.com/JuliaLang/julia/pull/54099#discussion_r1566982970


Last updated: Aug 14 2025 at 04:51 UTC