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.
This commit is contained in:
20
examples/1705-atomics-bool-byte-promoted.sx
Normal file
20
examples/1705-atomics-bool-byte-promoted.sx
Normal file
@@ -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
|
||||||
|
}
|
||||||
1
examples/expected/1705-atomics-bool-byte-promoted.exit
Normal file
1
examples/expected/1705-atomics-bool-byte-promoted.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
examples/expected/1705-atomics-bool-byte-promoted.stderr
Normal file
1
examples/expected/1705-atomics-bool-byte-promoted.stderr
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
3
examples/expected/1705-atomics-bool-byte-promoted.stdout
Normal file
3
examples/expected/1705-atomics-bool-byte-promoted.stdout
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
init: false
|
||||||
|
after store: true
|
||||||
|
after reset: false
|
||||||
@@ -1,5 +1,34 @@
|
|||||||
# 0152 — `Atomic(bool)` emits a sub-byte (i1) atomic load/store that LLVM rejects
|
# 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
|
## Symptom
|
||||||
|
|
||||||
`Atomic(bool)` lowers `bool` to LLVM `i1` and emits the atomic load/store
|
`Atomic(bool)` lowers `bool` to LLVM `i1` and emits the atomic load/store
|
||||||
|
|||||||
@@ -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 {
|
pub fn emitAtomicLoad(self: Ops, instruction: *const Inst, a: AtomicLoad) void {
|
||||||
const ptr = self.e.resolveRef(a.ptr);
|
const ptr = self.e.resolveRef(a.ptr);
|
||||||
const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr));
|
const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr));
|
||||||
if (ptr_kind == c.LLVMPointerTypeKind and instruction.ty != .void) {
|
if (ptr_kind == c.LLVMPointerTypeKind and instruction.ty != .void) {
|
||||||
const llvm_ty = self.e.toLLVMType(instruction.ty);
|
const promoted = self.atomicByteType(instruction.ty);
|
||||||
const result = c.LLVMBuildLoad2(self.e.builder, llvm_ty, ptr, "atomic_load");
|
const llvm_ty = promoted orelse self.e.toLLVMType(instruction.ty);
|
||||||
c.LLVMSetOrdering(result, llvmOrdering(a.ordering));
|
const raw = c.LLVMBuildLoad2(self.e.builder, llvm_ty, ptr, "atomic_load");
|
||||||
c.LLVMSetAlignment(result, @intCast(self.e.ir_mod.types.typeSizeBytes(instruction.ty)));
|
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);
|
self.e.mapRef(result);
|
||||||
} else {
|
} else {
|
||||||
self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(if (instruction.ty == .void) .i64 else instruction.ty)));
|
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 val = self.e.resolveRef(a.operand);
|
||||||
const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr));
|
const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr));
|
||||||
if (ptr_kind == c.LLVMPointerTypeKind and instruction.ty != .void) {
|
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 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);
|
const result = c.LLVMBuildAtomicRMW(self.e.builder, rmwBinOp(a.kind, is_unsigned), ptr, val, llvmOrdering(a.ordering), 0);
|
||||||
self.e.mapRef(result);
|
self.e.mapRef(result);
|
||||||
@@ -441,6 +462,8 @@ pub const Ops = struct {
|
|||||||
const new = self.e.resolveRef(a.new);
|
const new = self.e.resolveRef(a.new);
|
||||||
const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr));
|
const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr));
|
||||||
if (ptr_kind == c.LLVMPointerTypeKind and instruction.ty != .void) {
|
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(
|
const pair = c.LLVMBuildAtomicCmpXchg(
|
||||||
self.e.builder,
|
self.e.builder,
|
||||||
ptr,
|
ptr,
|
||||||
@@ -497,6 +520,11 @@ pub const Ops = struct {
|
|||||||
self.e.advanceRefCounter();
|
self.e.advanceRefCounter();
|
||||||
return;
|
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);
|
const st = c.LLVMBuildStore(self.e.builder, val, ptr);
|
||||||
c.LLVMSetOrdering(st, llvmOrdering(a.ordering));
|
c.LLVMSetOrdering(st, llvmOrdering(a.ordering));
|
||||||
c.LLVMSetAlignment(st, @intCast(self.e.ir_mod.types.typeSizeBytes(a.val_ty)));
|
c.LLVMSetAlignment(st, @intCast(self.e.ir_mod.types.typeSizeBytes(a.val_ty)));
|
||||||
|
|||||||
Reference in New Issue
Block a user