From e5586f61b87db21b100b7b069517d5bb31bd4959 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 21 Jun 2026 05:42:48 +0300 Subject: [PATCH] issue 0152 RESOLVED: byte-promote sub-byte (Atomic(bool)) atomic load/store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- examples/1705-atomics-bool-byte-promoted.sx | 20 +++++++++++ .../1705-atomics-bool-byte-promoted.exit | 1 + .../1705-atomics-bool-byte-promoted.stderr | 1 + .../1705-atomics-bool-byte-promoted.stdout | 3 ++ ...atomic-bool-sub-byte-atomic-llvm-reject.md | 29 +++++++++++++++ src/backend/llvm/ops.zig | 36 ++++++++++++++++--- 6 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 examples/1705-atomics-bool-byte-promoted.sx create mode 100644 examples/expected/1705-atomics-bool-byte-promoted.exit create mode 100644 examples/expected/1705-atomics-bool-byte-promoted.stderr create mode 100644 examples/expected/1705-atomics-bool-byte-promoted.stdout diff --git a/examples/1705-atomics-bool-byte-promoted.sx b/examples/1705-atomics-bool-byte-promoted.sx new file mode 100644 index 00000000..b4d1fa84 --- /dev/null +++ b/examples/1705-atomics-bool-byte-promoted.sx @@ -0,0 +1,20 @@ +// Atomic(bool) — a sub-byte (i1) element atomically loaded/stored. LLVM +// rejects a sub-byte atomic ("atomic memory access' size must be byte- +// sized"), so codegen performs the access in the byte storage type (i8) +// and trunc/zext's the value at the boundary. (rmw/cmpxchg on a bool is +// rejected at the sx level — integer-only — so only load/store apply.) +// Regression (issue 0152): Atomic(bool) emitted an i1 atomic that failed +// LLVM verification; Future.canceled: Atomic(bool) in the async layer hit it. +#import "modules/std.sx"; +#import "modules/std/atomic.sx"; + +main :: () { + a := Atomic(bool).init(false); + print("init: {}\n", a.load(.acquire)); // false + + a.store(true, .release); + print("after store: {}\n", a.load(.acquire)); // true + + a.store(false, .seq_cst); + print("after reset: {}\n", a.load(.seq_cst)); // false +} diff --git a/examples/expected/1705-atomics-bool-byte-promoted.exit b/examples/expected/1705-atomics-bool-byte-promoted.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/1705-atomics-bool-byte-promoted.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1705-atomics-bool-byte-promoted.stderr b/examples/expected/1705-atomics-bool-byte-promoted.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1705-atomics-bool-byte-promoted.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/1705-atomics-bool-byte-promoted.stdout b/examples/expected/1705-atomics-bool-byte-promoted.stdout new file mode 100644 index 00000000..88108808 --- /dev/null +++ b/examples/expected/1705-atomics-bool-byte-promoted.stdout @@ -0,0 +1,3 @@ +init: false +after store: true +after reset: false diff --git a/issues/0152-atomic-bool-sub-byte-atomic-llvm-reject.md b/issues/0152-atomic-bool-sub-byte-atomic-llvm-reject.md index 6c7ad553..c9ddff60 100644 --- a/issues/0152-atomic-bool-sub-byte-atomic-llvm-reject.md +++ b/issues/0152-atomic-bool-sub-byte-atomic-llvm-reject.md @@ -1,5 +1,34 @@ # 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 diff --git a/src/backend/llvm/ops.zig b/src/backend/llvm/ops.zig index 208ae795..18d104a8 100644 --- a/src/backend/llvm/ops.zig +++ b/src/backend/llvm/ops.zig @@ -384,14 +384,32 @@ pub const Ops = struct { }; } + // An atomic access type MUST be byte-sized — LLVM rejects a sub-byte + // (i1) atomic load/store/rmw/cmpxchg. `bool` lowers to `i1`, so an + // `Atomic(bool)` op is performed in its byte-sized storage type (`i8`) + // and the value `trunc`/`zext`'d at the boundary. `bool` is the only + // sub-byte scalar in sx (atomics are integer/bool/pointer only), so the + // promotion is a `bool → i8` special-case rather than a general width + // round-up. Returns null when no promotion is needed (the type is + // already byte-sized) — callers then use it as-is. + fn atomicByteType(self: Ops, ir_ty: TypeId) ?c.LLVMTypeRef { + return if (ir_ty == .bool) self.e.cached_i8 else null; + } + pub fn emitAtomicLoad(self: Ops, instruction: *const Inst, a: AtomicLoad) void { const ptr = self.e.resolveRef(a.ptr); const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr)); if (ptr_kind == c.LLVMPointerTypeKind and instruction.ty != .void) { - const llvm_ty = self.e.toLLVMType(instruction.ty); - const result = c.LLVMBuildLoad2(self.e.builder, llvm_ty, ptr, "atomic_load"); - c.LLVMSetOrdering(result, llvmOrdering(a.ordering)); - c.LLVMSetAlignment(result, @intCast(self.e.ir_mod.types.typeSizeBytes(instruction.ty))); + const promoted = self.atomicByteType(instruction.ty); + const llvm_ty = promoted orelse self.e.toLLVMType(instruction.ty); + const raw = c.LLVMBuildLoad2(self.e.builder, llvm_ty, ptr, "atomic_load"); + c.LLVMSetOrdering(raw, llvmOrdering(a.ordering)); + c.LLVMSetAlignment(raw, @intCast(self.e.ir_mod.types.typeSizeBytes(instruction.ty))); + // Narrow the byte-sized load back to the value type (i8 → i1). + const result = if (promoted != null) + c.LLVMBuildTrunc(self.e.builder, raw, self.e.toLLVMType(instruction.ty), "atomic_load.nb") + else + raw; self.e.mapRef(result); } else { self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(if (instruction.ty == .void) .i64 else instruction.ty))); @@ -420,6 +438,9 @@ pub const Ops = struct { const val = self.e.resolveRef(a.operand); const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr)); if (ptr_kind == c.LLVMPointerTypeKind and instruction.ty != .void) { + // No sub-byte promotion here: `Atomic(bool)` rmw is rejected at the + // sx level ("requires an integer type"), so a sub-byte element can + // never reach this emitter (unlike load/store, which bool uses). const is_unsigned = self.e.ir_mod.types.isUnsignedInt(a.val_ty); const result = c.LLVMBuildAtomicRMW(self.e.builder, rmwBinOp(a.kind, is_unsigned), ptr, val, llvmOrdering(a.ordering), 0); self.e.mapRef(result); @@ -441,6 +462,8 @@ pub const Ops = struct { const new = self.e.resolveRef(a.new); const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr)); if (ptr_kind == c.LLVMPointerTypeKind and instruction.ty != .void) { + // No sub-byte promotion: `Atomic(bool)` cmpxchg is rejected at the + // sx level (integer-only), so a sub-byte element never reaches here. const pair = c.LLVMBuildAtomicCmpXchg( self.e.builder, ptr, @@ -497,6 +520,11 @@ pub const Ops = struct { self.e.advanceRefCounter(); return; } + // Widen a sub-byte value (bool/i1) to its byte storage type so the + // atomic store is byte-sized (LLVM rejects an i1 atomic). + if (self.atomicByteType(a.val_ty)) |byte_ty| { + val = c.LLVMBuildZExt(self.e.builder, val, byte_ty, "atomic_store.nb"); + } const st = c.LLVMBuildStore(self.e.builder, val, ptr); c.LLVMSetOrdering(st, llvmOrdering(a.ordering)); c.LLVMSetAlignment(st, @intCast(self.e.ir_mod.types.typeSizeBytes(a.val_ty)));