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.
109 lines
4.6 KiB
Markdown
109 lines
4.6 KiB
Markdown
# 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).
|
|
|
|
**Fix** — `emitAtomicLoad`/`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`:
|
|
|
|
```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`).
|
|
|
|
### Possibly-related secondary symptom (verify after the i1 fix)
|
|
|
|
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.
|