Files
sx/issues/0152-atomic-bool-sub-byte-atomic-llvm-reject.md
agra e5586f61b8 issue 0152 RESOLVED: byte-promote sub-byte (Atomic(bool)) atomic load/store
LLVM rejects a sub-byte atomic memory access (must be byte-sized), so
Atomic(bool) — bool lowers to i1 — failed verification on load/store. The
atomic emitters in src/backend/llvm/ops.zig now perform a sub-byte access in
its byte storage type (i8) and trunc/zext the value at the boundary (new
atomicByteType helper: i8 for .bool, null otherwise). rmw/cmpxchg are left
as-is on purpose — a bool rmw/CAS is rejected at the sx level (integer-only),
so a sub-byte element never reaches those emitters.

Regression test examples/1705-atomics-bool-byte-promoted.sx. Suite green 729/0.
Unblocks Future.canceled: Atomic(bool) in the B1.2 async layer.
2026-06-21 05:42:48 +03:00

4.6 KiB

0152 — Atomic(bool) emits a sub-byte (i1) atomic load/store that LLVM rejects

RESOLVED (2026-06-21)

Root cause — the atomic load/store emitters in src/backend/llvm/ops.zig used toLLVMType(ty) directly as the atomic access type. For a bool element that is i1, which LLVM rejects for atomics (size must be a byte multiple).

FixemitAtomicLoad/emitAtomicStore now promote a sub-byte element to its byte storage type (i8) for the atomic access, and trunc/zext the value at the boundary (a new atomicByteType helper returns i8 for .bool, null otherwise). rmw/cmpxchg were left unchanged on purpose: a bool rmw/CAS is rejected at the sx level (atomic.sx — "requires an integer type"), so a sub-byte element can never reach those emitters (a comment records this). bool is the only sub-byte scalar in sx.

Verified — the repro prints yes; regression test examples/1705-atomics-bool-byte-promoted.sx (init / store / reset round- trip on Atomic(bool)). Full suite green (729/0).

Downstream (NOT this bug): with Atomic(bool) fixed, the B1.2 async examples surfaced ANOTHER, separate bug — a generic value-failable ($R, !E) fn reached through a re-export alias loses its ! error channel at the call site (typed as a plain tuple), so await(...) or { … } builds a malformed PHI. io.sx's await/IoErr are re-exported via std.sx, so the async surface is now blocked on THAT (filed as a new issue), not on 0152.


Symptom

Atomic(bool) lowers bool to LLVM i1 and emits the atomic load/store at that type. LLVM requires atomic memory accesses to be byte-sized, so codegen fails verification:

LLVM verification failed: atomic memory access' size must be byte-sized
 i1  store atomic i1 %load5, ptr %gep release, align 1
 i1  %atomic_load = load atomic i1, ptr %gep11 acquire, align 1
  • Atomic(i64), Atomic(i32), … → fine (byte-sized).
  • Atomic(bool) → LLVM verifier error (i1 is 1 bit, not byte-sized).

Reproduction

Standalone — depends on no project symbols beyond modules/std.sx + modules/std/atomic.sx:

#import "modules/std.sx";
#import "modules/std/atomic.sx";

main :: () -> i32 {
    a := Atomic(bool).init(false);
    a.store(true, .release);
    if a.load(.acquire) { print("yes\n"); } else { print("no\n"); }
    return 0;
}

Expected: prints yes. Actual: LLVM verification failure (i1 atomic).

Impact

Blocks the B1.2 async surface. library/modules/std/io.sx's Future($R) carries a canceled: Atomic(bool) cancellation flag (atomic so a future scheduler thread can flip it). async/cancel/await all touch it (Atomic(bool).init, .store(true, .release), .load(.acquire)), so the async examples (1805/1806) cannot build. This is independent of issue 0151 (generic inference) — that is now fixed, which is what newly exposed this codegen path.

Investigation prompt

The atomic load/store emitters in src/backend/llvm/ops.zig (emitAtomicLoad ~line 387, emitAtomicStore, emitAtomicRmw, emitAtomicCmpxchg) use toLLVMType(instruction.ty) directly as the atomic access type. For a bool element that is i1, which LLVM rejects for atomics (must be a byte-multiple).

The fix should promote a sub-byte atomic to its byte-sized storage type: load/store as i8 (the ABI storage type for bool) and trunc/zext between i1 and i8 at the value boundary — mirroring how a non-atomic bool field is already stored as a byte. Apply consistently across load / store / rmw / cmpxchg so an Atomic(bool) round-trips. Confirm the alignment (LLVMSetAlignment) uses the promoted byte size.

Possible alternative: have Atomic($T) (in library/modules/std/atomic.sx) constrain / widen a bool element to a byte-sized integer in the type itself — but the codegen-level promotion is more robust (any i1-typed atomic, however it arises, becomes legal).

Verification: run the repro above; expect yes. Then restore examples/1805-concurrency-io-blocking-async.sx (+ add 1806 cancel) per Stream B1 / CHECKPOINT-FIBERS and confirm the async surface builds and runs (sum: 42 / double: 42, cancel → .canceled).

The same async probe also tripped an or-merge PHI type mismatch (%bp = phi i1 [ true, … ], [ 0, … ]) when f.await() or { <i64> } was lowered. A minimal (i64, !E) + or { -1 } does NOT reproduce it, so this may be entangled with the malformed Atomic(bool) field in the Future struct rather than a second bug. Re-check once the i1 atomic is fixed; if it persists, file separately.