#![cfg(loom)]
use loom::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use loom::sync::{Arc, Mutex};
use loom::thread;
const STATE_LIVE: u64 = 0;
const STATE_ABANDONED: u64 = 1;
const STATE_MASK: u64 = 0x1;
const GEN_SHIFT: u32 = 32;
const fn pack(state: u64, gen: u32) -> u64 {
(state & STATE_MASK) | ((gen as u64) << GEN_SHIFT)
}
const fn unpack_state(word: u64) -> u64 {
word & STATE_MASK
}
const fn unpack_gen(word: u64) -> u32 {
(word >> GEN_SHIFT) as u32
}
struct Segment {
owner_state: AtomicU64,
tfs_pushed: AtomicUsize,
adopted_by: AtomicUsize,
}
impl Segment {
fn new() -> Self {
Self {
owner_state: AtomicU64::new(pack(STATE_LIVE, 0)),
tfs_pushed: AtomicUsize::new(0),
adopted_by: AtomicUsize::new(usize::MAX), }
}
}
struct AbandonedStack {
inner: Mutex<Option<Arc<Segment>>>,
}
impl AbandonedStack {
fn new() -> Self {
Self {
inner: Mutex::new(None),
}
}
fn push(&self, seg: Arc<Segment>) {
let mut g = self.inner.lock().unwrap();
*g = Some(seg);
}
fn pop(&self) -> Option<Arc<Segment>> {
self.inner.lock().unwrap().take()
}
}
fn adopt_correct(seg: &Segment, adopter_id: usize) -> bool {
let cur = seg.owner_state.load(Ordering::Acquire);
if unpack_state(cur) != STATE_ABANDONED {
return false; }
let new_gen = unpack_gen(cur).wrapping_add(1);
let new_word = pack(STATE_LIVE, new_gen);
match seg
.owner_state
.compare_exchange(cur, new_word, Ordering::AcqRel, Ordering::Relaxed)
{
Ok(_) => {
seg.adopted_by.store(adopter_id, Ordering::Release);
true
}
Err(_) => false, }
}
fn adopt_naive_broken(seg: &Segment, adopter_id: usize) -> bool {
let cur = seg.owner_state.load(Ordering::Acquire);
if unpack_state(cur) != STATE_ABANDONED {
return false;
}
let new_gen = unpack_gen(cur).wrapping_add(1);
let new_word = pack(STATE_LIVE, new_gen);
seg.owner_state.store(new_word, Ordering::Release);
seg.adopted_by.store(adopter_id, Ordering::Relaxed);
true
}
#[test]
fn correct_adopt_is_exactly_once() {
let mut builder = loom::model::Builder::new();
builder.preemption_bound = Some(3);
builder.check(|| {
let seg = Arc::new(Segment::new());
let stack = Arc::new(AbandonedStack::new());
let cur = seg.owner_state.load(Ordering::Acquire);
let gen = unpack_gen(cur);
let _ = seg.owner_state.compare_exchange(
cur,
pack(STATE_ABANDONED, gen),
Ordering::AcqRel,
Ordering::Relaxed,
);
stack.push(Arc::clone(&seg));
seg.tfs_pushed.fetch_add(1, Ordering::Release);
let s1 = Arc::clone(&stack);
let adopted_count_1 = Arc::new(AtomicUsize::new(0));
let adopted_count_2 = Arc::clone(&adopted_count_1);
let seg_for_1 = Arc::clone(&seg);
let t1 = thread::spawn(move || {
if let Some(popped) = s1.pop() {
if adopt_correct(&popped, 0) {
adopted_count_2.fetch_add(1, Ordering::Relaxed);
let _ = popped.tfs_pushed.load(Ordering::Acquire);
}
}
let _ = seg_for_1;
});
let s2 = Arc::clone(&stack);
let adopted_count_3 = Arc::clone(&adopted_count_1);
let seg_for_2 = Arc::clone(&seg);
let t2 = thread::spawn(move || {
if let Some(popped) = s2.pop() {
if adopt_correct(&popped, 1) {
adopted_count_3.fetch_add(1, Ordering::Relaxed);
let _ = popped.tfs_pushed.load(Ordering::Acquire);
}
}
let _ = seg_for_2;
});
t1.join().unwrap();
t2.join().unwrap();
let adoptions = adopted_count_1.load(Ordering::Acquire);
assert!(
adoptions <= 1,
"M9 violated: double-adoption — {adoptions} adopters won (expected ≤ 1)"
);
if adoptions == 1 {
let pushed = seg.tfs_pushed.load(Ordering::Acquire);
assert!(
pushed >= 1,
"the adopter lost the remote-pushed block (expected ≥ 1, got {pushed})"
);
assert_eq!(
unpack_state(seg.owner_state.load(Ordering::Acquire)),
STATE_LIVE,
"the adopted segment must be LIVE after a successful adoption"
);
}
});
}
#[test]
fn correct_adopt_direct_contention_is_exactly_once() {
let mut builder = loom::model::Builder::new();
builder.preemption_bound = Some(3);
builder.check(|| {
let seg = Arc::new(Segment::new());
seg.owner_state
.store(pack(STATE_ABANDONED, 5), Ordering::Release);
let adopted = Arc::new(AtomicUsize::new(0));
let seg1 = Arc::clone(&seg);
let adopted1 = Arc::clone(&adopted);
let t1 = thread::spawn(move || {
if adopt_correct(&seg1, 0) {
adopted1.fetch_add(1, Ordering::Relaxed);
}
});
let seg2 = Arc::clone(&seg);
let adopted2 = Arc::clone(&adopted);
let t2 = thread::spawn(move || {
if adopt_correct(&seg2, 1) {
adopted2.fetch_add(1, Ordering::Relaxed);
}
});
t1.join().unwrap();
t2.join().unwrap();
let adoptions = adopted.load(Ordering::Acquire);
assert_eq!(
adoptions, 1,
"M9 violated: exactly one adopter must win under direct contention \
(got {adoptions})"
);
});
}
#[test]
#[should_panic(expected = "M9 violated")]
fn counterfactual_naive_adopt_double_owns() {
let mut builder = loom::model::Builder::new();
builder.preemption_bound = Some(3);
builder.check(|| {
let seg = Arc::new(Segment::new());
seg.owner_state
.store(pack(STATE_ABANDONED, 5), Ordering::Release);
let adopted = Arc::new(AtomicUsize::new(0));
let seg1 = Arc::clone(&seg);
let adopted1 = Arc::clone(&adopted);
let t1 = thread::spawn(move || {
if adopt_naive_broken(&seg1, 0) {
adopted1.fetch_add(1, Ordering::Relaxed);
}
});
let seg2 = Arc::clone(&seg);
let adopted2 = Arc::clone(&adopted);
let t2 = thread::spawn(move || {
if adopt_naive_broken(&seg2, 1) {
adopted2.fetch_add(1, Ordering::Relaxed);
}
});
t1.join().unwrap();
t2.join().unwrap();
let adoptions = adopted.load(Ordering::Acquire);
assert_eq!(
adoptions, 1,
"M9 violated: the naive non-CAS adopt double-adopts \
(got {adoptions} adoptions, expected exactly 1) — loom caught the bug"
);
});
}
#[test]
fn reader_observes_consistent_state() {
let mut builder = loom::model::Builder::new();
builder.preemption_bound = Some(3);
builder.check(|| {
let seg = Arc::new(Segment::new());
seg.owner_state
.store(pack(STATE_ABANDONED, 1), Ordering::Release);
let seg_r = Arc::clone(&seg);
let reader = thread::spawn(move || {
let word = seg_r.owner_state.load(Ordering::Acquire);
let state = unpack_state(word);
assert!(
state == STATE_LIVE || state == STATE_ABANDONED,
"reader observed an invalid owner_state ({state}) — data race / torn read"
);
});
let seg_a = Arc::clone(&seg);
let adopter = thread::spawn(move || {
let _ = adopt_correct(&seg_a, 0);
});
reader.join().unwrap();
adopter.join().unwrap();
});
}