Does `@atomic` syntax support non-primitive-type fields in a `mutable struct`?

I was reading the documentation on per-field atomics, and it seems that @atomic can be put in front of any type of field (unlike Base.Threads.Atomic) as long as the struct is mutable. I also tested it with the following code which ran just fine:

julia> mutable struct TT{T}
           @atomic a::T
       end

julia> t1 = TT(fill(1))
TT{Array{Int64, 0}}(fill(1))

julia> @atomic t1.a = fill(2)
0-dimensional Array{Int64, 0}:
2

Does this mean that @atomic recursively applies the atomic version of setproperty! to the fields of a composite-type field object? Or does it fall back to thread-unsafe version when the field is not of a primitive type?

Thanks!

Does this mean that @atomic recursively applies the atomic version of setproperty! to the fields of a composite-type field object?

Not quite there is nothing recursive about the use of @atomic here.
t1.a contains a reference to an array. t1.a = b replaces that reference with another one. Only the update of the reference needs to be atomic.

Now if you have:

t1 = TT((1,2))

@atomic t1.a = (2, 1)

Tuples are stored inline

julia> sizeof(TT((1,2)))
32

and so the question is. How does Julia make sure that this
is done atomically?

julia> f(t1, b) =  @atomic t1.a = b 
f (generic function with 1 method)

julia> @code_llvm f(TT((1,2)), (2, 3))
; Function Signature: f(Main.TT{Tuple{Int64, Int64}}, Tuple{Int64, Int64})
;  @ REPL[4]:1 within `f`
define void @julia_f_1185(ptr noalias nocapture noundef nonnull sret([2 x i64]) align 8 dereferenceable(16) %sret_return, ptr noundef nonnull align 8 dereferenceable(32) %"t1::TT", ptr nocapture noundef nonnull readonly align 8 dereferenceable(16) %"b::Tuple") #0 {
top:
; ┌ @ Base.jl:75 within `setproperty!`
   %.a_ptr = getelementptr inbounds i8, ptr %"t1::TT", i64 16
   call void @jl_lock_value(ptr nonnull %"t1::TT")
   call void @llvm.memcpy.p0.p0.i64(ptr noundef nonnull align 8 dereferenceable(16) %.a_ptr, ptr noundef nonnull align 8 dereferenceable(16) %"b::Tuple", i64 16, i1 false)
   call void @jl_unlock_value(ptr nonnull %"t1::TT")
   call void @llvm.memcpy.p0.p0.i64(ptr noundef nonnull align 8 dereferenceable(16) %sret_return, ptr noundef nonnull align 8 dereferenceable(16) %"b::Tuple", i64 16, i1 false)
   ret void
; └
}

So if we can’t use an atomic instruction, because your CPU may not support the use of them, Julia falls back to using locks transparently.

Cool! Thanks for the explanation!

So I can safely assume that whatever field is annotated by @atomic should be thread-safe (regardless the underlying implementation is an optimized atomic operation or a basic lock mechanism), as long as the evaluation of the mutation expression also annotated by @atomic does not prompt an error, right?