Everything here operates on a naturally aligned 64-bit cell at an address you
own. That is on purpose: atomics are only interesting between threads, and
threads in this library share state through a raw context pointer, so the
useful shape is a view over somebody else's memory rather than a value you hold.
AtomicCounter and AtomicFlag are both such views — copying one copies the
view, not the cell. The seven free functions below are that view expressed as
std.atomic calls against a Cell the caller's pointer is bitcast to; a Cell
is laid out exactly like the Int64 it aliases, which the comptime asserts in
_cell pin down so that a future stdlib change to Atomic fails the build
rather than silently scribbling past the cell.
Orderings are spelled into the function names (_relaxed, _acquire,
_release) rather than passed as a parameter, because the ordering has to be
selected at compile time anyway and a named function is both clearer at the call
site and cheaper to read than a comptime if ladder. fetch_add is
sequentially consistent; that is what a work-queue counter wants.
Why Cell comes from outside this file
std.atomic.Atomic changed its parameter kind under this tin once already:
| toolchain |
declaration |
| Mojo 1.0.0 |
struct Atomic[dtype: DType, *, scope: StaticString = ""] |
| Mojo 1.2.0, nightly |
struct Atomic[T: Deinitable & Movable, *, scope: ...] |
so Atomic[DType.int64] was the only spelling 1.0.0 accepted and Atomic[Int64]
is the only spelling the two toolchains supported today accept. Everything else
was identical — the same fetch_add[ordering=…], load[ordering=…],
store[ordering=…] and fence calls compile unchanged against any of them.
The divergence was one type expression and nothing more.
Mojo offers no way to pick a type at module scope from a compiler-version
predicate: a comptime if around a comptime Cell = … does not parse on either
toolchain, and -D defines (get_defined_bool plus a comptime if) can only
branch inside a function body. So the choice is made by the include path
instead. compat/threads_compat.mojo declares Cell and nothing else, and a
build says so:
mojo build … -I src -I compat
Dropping Mojo 1.0.0 left that directory with one occupant rather than two: 1.2.0
and the 1.3.0.dev nightlies agree on every spelling here. The seam stays because the
next divergence is a matter of when, not whether, and re-splitting compat/ into
one directory per toolchain is then a two-line change in this tin and a path edit
in each consumer.
This used to be solved one level down instead, by reaching past std.atomic
to the pop.atomic.rmw / pop.load / pop.store intrinsics it is itself
written on top of, on the reasoning that a compiler primitive would be steadier
than a stdlib signature. That held until it did not: nightlies from
26.6.0.dev2026090105 onward reject the __mlir_attr syntax those calls need
(error: invalid MLIR attribute: expected '<', filed as modular/modular#7094),
and because this was the only file in the whole stack touching raw MLIR it was
single-handedly capping every downstream tin below that nightly. Two spellings
of one alias, in a directory the caller selects, is a far smaller and more
legible surface than a private compiler ABI — and when it breaks it breaks in
Mojo the language rather than in an attribute grammar nobody documents.
That particular regression was fixed upstream the same day it was reported, so
the intrinsics would compile again today. This module is not going back to
them. The reasoning that sent it there — that a compiler primitive is steadier
than a stdlib signature — was wrong in the way that matters: std.atomic is
the interface with a compatibility promise, and pop.* is an implementation
detail of it. Two spellings of one alias is the cost of being on the supported
side of that line, and it is a cost worth paying whether or not the
undocumented side happens to work this week.
Expect compat/ to grow again. Stable and nightly diverge as readily as they
converge, and async is the next thing this tin will want that they may well
spell differently. The pattern to follow is the one here: the divergent file
holds the declaration that differs and nothing whatsoever besides, so that a
reader can see the entire delta between the two toolchains at a glance and every
line that actually does work stays single-sourced.