ph_eventing/latest_buf.rs
1//! Freshness-first SPSC snapshot channel.
2//!
3//! [`LatestBuf`] retains at most one unread publication. A producer always
4//! publishes the newest complete `T`; if an older unread value is displaced,
5//! [`PublishReport::replaced_unread`] reports it. The consumer takes only the
6//! latest value and receives its generation plus the exact number skipped.
7//!
8//! The channel uses three slots with exclusive ownership: one producer
9//! slot, one consumer slot, and one slot named by an atomic exchange state.
10//! An endpoint accesses a slot only after acquiring it through a single
11//! atomic swap. Producer and consumer therefore never touch the same payload
12//! bytes concurrently, unlike a seqlock.
13//!
14//! Endpoint cursors live in the channel rather than the handles. Handles are
15//! stateless and dropping/reacquiring one continues its slot ownership,
16//! generation sequence, and skipped accounting.
17//!
18//! An empty consumer poll first Acquire-loads the ready bit and returns without
19//! an atomic read-modify-write. A pending poll still performs the same `AcqRel`
20//! swap that transfers slot ownership. The private initial role indices are
21//! encoded as zero so a const-initialized channel lands in `.bss` rather than
22//! carrying its three payload slots in the flash-backed `.data` image.
23//!
24//! # Decision status
25//!
26//! Decision D1 (wrap-ambiguity policy) is **closed** as documented
27//! approximation plus payload escape hatch (contract §9, non-promise X6):
28//! skipped counts are exact while fewer than `u32::MAX` non-zero
29//! generations separate successful takes, and beyond that full wrap span
30//! the `u32` result is a documented under-count —
31//! [`LatestItem::skipped`] carries the full disclosure.
32//!
33//! Decision D2 (`Source<T>` policy) is **closed**: [`Consumer`] does not
34//! implement [`crate::Source`], because `try_pop` cannot report the
35//! displacement that is this channel's *designed* overload behaviour —
36//! [`crate::LatestSource`] is the consumer's designed contract surface
37//! (contract §9, non-promise X7), and the absent impl is pinned by a
38//! `compile_fail` doctest on [`Consumer`].
39//!
40//! Decision D3 (first deliverable form) is **closed**: `T` stays generic
41//! by decision, not default — a complete block is a payload
42//! (`LatestBuf<Block<T, N>>` via the BlockBuf composition),
43//! sample-versus-block is release scheduling and RAM, and no separate
44//! latest-block type exists (contract §9).
45//!
46//! Review caveat A.3 (handle-state continuation) is **closed** the same
47//! way this implementation works: role state is channel-resident in
48//! role-owned storage and handles are stateless, so a drop-and-reacquire
49//! continues by construction (contract H4; proposal Appendix A.3).
50//! Contract non-promise X8 states the role-recovery boundary: the role is
51//! held until the handle is dropped, handle lifetime is an application
52//! property, and there is deliberately no out-of-band role reset.
53//!
54//! Every LatestBuf decision (D1–D3, A.3) is closed; the contract §9 and
55//! proposal Appendix A.3 carry the records.
56//!
57//! # Example
58//!
59//! ```
60//! use ph_eventing::LatestBuf;
61//!
62//! let channel = LatestBuf::<u32>::new();
63//! let producer = channel.try_producer().expect("producer");
64//! let consumer = channel.try_consumer().expect("consumer");
65//!
66//! assert!(!producer.publish(10).replaced_unread);
67//! assert!(producer.publish(20).replaced_unread);
68//! let item = consumer.take_latest().expect("latest value");
69//! assert_eq!((item.value, item.generation, item.skipped), (20, 2, 1));
70//! ```
71
72use crate::sync::{AtomicBool, AtomicU32, Ordering, TrackedCell};
73use core::cell::Cell;
74use core::marker::PhantomData;
75use core::mem::MaybeUninit;
76
77const SLOT_MASK: u32 = 0b11;
78const READY_BIT: u32 = 0b100;
79// Encode the initially owned slots as zero so a const-initialized channel is
80// an all-zero image and lands in `.bss`. XOR is its own inverse, and these
81// role-owned fields never enter the shared exchange state.
82const PRODUCER_SLOT_XOR: u32 = 1;
83const CONSUMER_SLOT_XOR: u32 = 2;
84
85#[derive(Clone, Copy)]
86struct Entry<T: Copy> {
87 generation: u32,
88 value: T,
89}
90
91#[derive(Clone, Copy)]
92struct ProducerState {
93 back_encoded: u32,
94 next_generation: u32,
95}
96
97#[derive(Clone, Copy)]
98struct ConsumerState {
99 front_encoded: u32,
100 last_generation: u32,
101}
102
103#[cfg(not(loom))]
104const fn slot_array<T: Copy>() -> [TrackedCell<MaybeUninit<Entry<T>>>; 3] {
105 [const { TrackedCell::new(MaybeUninit::uninit()) }; 3]
106}
107
108#[cfg(loom)]
109fn slot_array<T: Copy>() -> [TrackedCell<MaybeUninit<Entry<T>>>; 3] {
110 core::array::from_fn(|_| TrackedCell::new(MaybeUninit::uninit()))
111}
112
113/// Result of one successful latest-value publication.
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115#[must_use]
116pub struct PublishReport {
117 /// Non-zero transport generation assigned to this publication.
118 pub generation: u32,
119 /// Whether this publication displaced an unread older publication.
120 pub replaced_unread: bool,
121}
122
123/// A value claimed from a [`LatestBuf`].
124#[derive(Clone, Copy, Debug, Eq, PartialEq)]
125#[must_use]
126pub struct LatestItem<T> {
127 /// Complete value copied from the claimed publication.
128 pub value: T,
129 /// Non-zero transport generation assigned when the value was published.
130 pub generation: u32,
131 /// Publications assigned after the prior take and before this one.
132 ///
133 /// One formula in every case (contract C3): the wrap-aware generation
134 /// distance from the previously taken value to this one, minus one,
135 /// saturated at zero. Exact while fewer than `u32::MAX` non-zero
136 /// generations — one full wrap span — separate two successful takes.
137 /// Beyond that span the same formula under-counts: by whole spans
138 /// while the endpoints differ, and a gap of exactly one or more full
139 /// cycles reports zero — a silent under-count, accepted and
140 /// documented by decision D1 (contract non-promise X6).
141 ///
142 /// The boundary is a rate × take-interval property: it is crossed
143 /// exactly when a full span of publications occurs between two
144 /// successful takes — whether because the consumer stopped (fault)
145 /// or because the deployment deliberately takes rarely against a
146 /// fast producer (design). At representative rates that is roughly
147 /// 49.7 days between takes at continuous 1 kHz publishing, or
148 /// 72 minutes at 1 MHz; the crate bounds neither rate nor cadence,
149 /// so this arithmetic is the caller's to run. The channel
150 /// deliberately does not detect the crossing; detection would price
151 /// wider hot-path state into every operation and still could not
152 /// recover the lost count. If the count itself is the requirement
153 /// (audit, metering, loss accounting), carry a wider
154 /// producer-assigned sequence inside `T`; if consumer liveness is,
155 /// use a watchdog — each catches its condition sooner and correctly.
156 pub skipped: u32,
157}
158
159/// Three-slot freshness-first SPSC snapshot channel.
160///
161/// `LatestBuf<T>` has fixed storage for three `T` values and never allocates.
162/// Publishing always succeeds and takes one atomic exchange. Taking is also
163/// bounded to one load, at most one exchange, and at most one payload copy.
164pub struct LatestBuf<T: Copy> {
165 exchange: AtomicU32,
166 slots: [TrackedCell<MaybeUninit<Entry<T>>>; 3],
167 producer_state: TrackedCell<ProducerState>,
168 consumer_state: TrackedCell<ConsumerState>,
169 producer_taken: AtomicBool,
170 consumer_taken: AtomicBool,
171}
172
173// SAFETY: the handle-acquisition flags enforce one endpoint per role. Each
174// payload slot belongs exclusively to the producer, consumer, or atomic
175// exchange state, and AcqRel swaps transfer both ownership and visibility.
176// The role-state cells are accessed only by their unique active handle; the
177// Release drop / AcqRel acquisition pair orders access across reacquisition.
178unsafe impl<T: Copy + Send> Sync for LatestBuf<T> {}
179
180impl<T: Copy> LatestBuf<T> {
181 /// Create an empty channel.
182 ///
183 /// On normal builds this is `const`, so a channel may live in a `static`.
184 /// Under `--cfg loom` it is non-const because Loom's primitives are not
185 /// const-constructible.
186 #[cfg(not(loom))]
187 pub const fn new() -> Self {
188 Self {
189 exchange: AtomicU32::new(0),
190 slots: slot_array(),
191 producer_state: TrackedCell::new(ProducerState {
192 back_encoded: 0,
193 next_generation: 0,
194 }),
195 consumer_state: TrackedCell::new(ConsumerState {
196 front_encoded: 0,
197 last_generation: 0,
198 }),
199 producer_taken: AtomicBool::new(false),
200 consumer_taken: AtomicBool::new(false),
201 }
202 }
203
204 /// Create an empty channel for Loom model checking.
205 #[cfg(loom)]
206 pub fn new() -> Self {
207 Self {
208 exchange: AtomicU32::new(0),
209 slots: slot_array(),
210 producer_state: TrackedCell::new(ProducerState {
211 back_encoded: 0,
212 next_generation: 0,
213 }),
214 consumer_state: TrackedCell::new(ConsumerState {
215 front_encoded: 0,
216 last_generation: 0,
217 }),
218 producer_taken: AtomicBool::new(false),
219 consumer_taken: AtomicBool::new(false),
220 }
221 }
222
223 /// Try to acquire the unique producer role.
224 ///
225 /// Returns `None` while another producer handle is active. Dropping the
226 /// active handle makes the role available without resetting its state.
227 ///
228 /// The role is held until the active handle is *dropped* — a handle
229 /// that is forgotten, or owned by an execution context destroyed
230 /// without running destructors, leaves the role held with channel
231 /// state intact. There is deliberately no out-of-band role reset: a
232 /// forced release could free a role while a live handle still exists,
233 /// defeating the exclusive ownership that soundness rests on
234 /// (contract non-promise X8). Handle lifetime is the application's
235 /// property; the channel observes only acquisition and drop.
236 #[inline]
237 pub fn try_producer(&self) -> Option<Producer<'_, T>> {
238 if self.producer_taken.swap(true, Ordering::AcqRel) {
239 None
240 } else {
241 Some(Producer {
242 buf: self,
243 _not_sync: PhantomData,
244 })
245 }
246 }
247
248 /// Try to acquire the unique consumer role.
249 ///
250 /// Returns `None` while another consumer handle is active. Dropping the
251 /// active handle makes the role available without resetting its state.
252 ///
253 /// Role recovery follows the same boundary as [`Self::try_producer`]:
254 /// held until dropped, no out-of-band reset, handle lifetime owned by
255 /// the application (contract non-promise X8).
256 #[inline]
257 pub fn try_consumer(&self) -> Option<Consumer<'_, T>> {
258 if self.consumer_taken.swap(true, Ordering::AcqRel) {
259 None
260 } else {
261 Some(Consumer {
262 buf: self,
263 _not_sync: PhantomData,
264 })
265 }
266 }
267
268 #[inline(always)]
269 const fn encode(slot: u32, ready: bool) -> u32 {
270 slot | if ready { READY_BIT } else { 0 }
271 }
272
273 #[inline(always)]
274 const fn slot(state: u32) -> usize {
275 (state & SLOT_MASK) as usize
276 }
277
278 #[inline(always)]
279 const fn ready(state: u32) -> bool {
280 state & READY_BIT != 0
281 }
282
283 #[inline(always)]
284 const fn producer_slot(encoded: u32) -> u32 {
285 encoded ^ PRODUCER_SLOT_XOR
286 }
287
288 #[inline(always)]
289 const fn encode_producer_slot(slot: u32) -> u32 {
290 slot ^ PRODUCER_SLOT_XOR
291 }
292
293 #[inline(always)]
294 const fn consumer_slot(encoded: u32) -> u32 {
295 encoded ^ CONSUMER_SLOT_XOR
296 }
297
298 #[inline(always)]
299 const fn encode_consumer_slot(slot: u32) -> u32 {
300 slot ^ CONSUMER_SLOT_XOR
301 }
302
303 #[inline(always)]
304 const fn next_generation(current: u32) -> u32 {
305 match current.wrapping_add(1) {
306 0 => 1,
307 generation => generation,
308 }
309 }
310
311 /// Assigned-generation distance in `(from, to]`, excluding reserved zero.
312 #[inline(always)]
313 const fn generation_distance(from: u32, to: u32) -> u32 {
314 let raw = to.wrapping_sub(from);
315 if to < from { raw - 1 } else { raw }
316 }
317}
318
319impl<T: Copy> Default for LatestBuf<T> {
320 fn default() -> Self {
321 Self::new()
322 }
323}
324
325impl<T: Copy> core::fmt::Debug for LatestBuf<T> {
326 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
327 f.debug_struct("LatestBuf").finish_non_exhaustive()
328 }
329}
330
331/// Unique, stateless write handle for a [`LatestBuf`].
332///
333/// This handle is `Send` **when `T: Send`** (the handle can move a payload
334/// across contexts, so a non-`Send` payload correctly pins it — `T: Copy`
335/// alone does not imply `T: Send`) and always `!Sync`: it may move into an
336/// ISR or another execution context, but it may not be shared between
337/// contexts. Sole-producer ownership is load-bearing for the exclusive-slot
338/// soundness argument (contract H2).
339///
340/// ```compile_fail,E0277
341/// use ph_eventing::latest_buf::Producer;
342///
343/// fn assert_sync<T: Sync>() {}
344/// assert_sync::<Producer<'static, u32>>();
345/// ```
346pub struct Producer<'a, T: Copy> {
347 buf: &'a LatestBuf<T>,
348 _not_sync: PhantomData<Cell<()>>,
349}
350
351impl<T: Copy> Producer<'_, T> {
352 /// Publish a complete value as the newest channel state.
353 ///
354 /// This always succeeds, executes one payload write and one atomic swap,
355 /// and never waits for consumer progress.
356 #[inline]
357 pub fn publish(&self, value: T) -> PublishReport {
358 // SAFETY: producer_taken grants the unique producer handle exclusive
359 // access to producer_state for its lifetime. Reacquisition is ordered
360 // by the handle's Release drop and AcqRel acquisition.
361 let (back, generation) = self.buf.producer_state.with_mut(|state| unsafe {
362 let state = &mut *state;
363 let generation = LatestBuf::<T>::next_generation(state.next_generation);
364 state.next_generation = generation;
365 (
366 LatestBuf::<T>::producer_slot(state.back_encoded),
367 generation,
368 )
369 });
370
371 // SAFETY: `back` is exclusively producer-owned. The producer does not
372 // relinquish it until the following atomic exchange.
373 self.buf.slots[back as usize].with_mut(|slot| unsafe {
374 (*slot).write(Entry { generation, value });
375 });
376
377 let previous = self
378 .buf
379 .exchange
380 .swap(LatestBuf::<T>::encode(back, true), Ordering::AcqRel);
381
382 // SAFETY: the exchange transferred its previous slot exclusively to
383 // the producer. No other producer handle exists.
384 self.buf.producer_state.with_mut(|state| unsafe {
385 (*state).back_encoded = LatestBuf::<T>::encode_producer_slot(previous & SLOT_MASK);
386 });
387
388 PublishReport {
389 generation,
390 replaced_unread: LatestBuf::<T>::ready(previous),
391 }
392 }
393}
394
395impl<T: Copy> Drop for Producer<'_, T> {
396 fn drop(&mut self) {
397 self.buf.producer_taken.store(false, Ordering::Release);
398 }
399}
400
401impl<T: Copy> core::fmt::Debug for Producer<'_, T> {
402 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
403 f.debug_struct("latest_buf::Producer").finish()
404 }
405}
406
407/// Unique, stateless read handle for a [`LatestBuf`].
408///
409/// This handle is `Send` **when `T: Send`** (a non-`Send` payload correctly
410/// pins it to one context) and always `!Sync`: it may move into a consumer
411/// context, but it may not be shared between contexts (contract H2).
412///
413/// ```compile_fail,E0277
414/// use ph_eventing::latest_buf::Consumer;
415///
416/// fn assert_sync<T: Sync>() {}
417/// assert_sync::<Consumer<'static, u32>>();
418/// ```
419///
420/// # No `Source<T>` implementation — by decision, not omission
421/// `Source::try_pop` cannot report the displacement that is this channel's
422/// designed overload behaviour, so a generic pipeline would silently
423/// discard loss evidence during *normal* operation (decision D2; contract
424/// non-promise X7). [`crate::LatestSource`] is the consumer's designed
425/// contract surface. A caller whose domain genuinely permits discarding
426/// the skipped count writes its own adapter, so the discard is signed in
427/// application code, never by the transport. This `compile_fail` doctest
428/// pins the absent impl so a convenience `Source` cannot arrive silently,
429/// and pinning the error code keeps it honest:
430///
431/// ```compile_fail,E0277
432/// use ph_eventing::Source;
433/// let channel = ph_eventing::LatestBuf::<u32>::new();
434/// let mut consumer = channel.try_consumer().unwrap();
435/// let _ = Source::try_pop(&mut consumer);
436/// ```
437pub struct Consumer<'a, T: Copy> {
438 buf: &'a LatestBuf<T>,
439 _not_sync: PhantomData<Cell<()>>,
440}
441
442impl<T: Copy> Consumer<'_, T> {
443 /// Claim and copy the latest unread publication.
444 ///
445 /// Returns `None` when no publication is pending. The operation performs
446 /// an `Acquire` load first, so an empty poll returns without an atomic
447 /// read-modify-write. A pending publication is still claimed with the
448 /// ownership-transferring `AcqRel` swap.
449 #[inline]
450 pub fn take_latest(&self) -> Option<LatestItem<T>> {
451 // A false load can linearize before a concurrent publication: the
452 // publication remains pending for the next poll. This path transfers
453 // no slot and therefore must not access or update either owned role.
454 // A true load is only a hint; the AcqRel swap below remains the actual
455 // ownership transfer and may claim a newer replacement publication.
456 if !LatestBuf::<T>::ready(self.buf.exchange.load(Ordering::Acquire)) {
457 return None;
458 }
459
460 // SAFETY: consumer_taken grants this handle exclusive role-state
461 // access, ordered across reacquisition by Release/AcqRel.
462 let front = self
463 .buf
464 .consumer_state
465 .with(|state| unsafe { LatestBuf::<T>::consumer_slot((*state).front_encoded) });
466
467 let previous = self
468 .buf
469 .exchange
470 .swap(LatestBuf::<T>::encode(front, false), Ordering::AcqRel);
471 let claimed = previous & SLOT_MASK;
472
473 // SAFETY: the exchange transferred `claimed` exclusively to this
474 // consumer; preserving it even on the empty path maintains the three
475 // disjoint ownership roles.
476 self.buf.consumer_state.with_mut(|state| unsafe {
477 (*state).front_encoded = LatestBuf::<T>::encode_consumer_slot(claimed);
478 });
479
480 if !LatestBuf::<T>::ready(previous) {
481 return None;
482 }
483
484 // SAFETY: the ready bit means the producer initialized this Entry
485 // before publishing it. AcqRel swap acquired both ownership and the
486 // initialized bytes, and no producer can reacquire the slot until a
487 // later exchange relinquishes it.
488 let entry = self.buf.slots[LatestBuf::<T>::slot(previous)]
489 .with(|slot| unsafe { (*slot).assume_init_read() });
490
491 // SAFETY: this unique consumer handle exclusively owns consumer_state.
492 let skipped = self.buf.consumer_state.with_mut(|state| unsafe {
493 let state = &mut *state;
494 let distance =
495 LatestBuf::<T>::generation_distance(state.last_generation, entry.generation);
496 state.last_generation = entry.generation;
497 distance.saturating_sub(1)
498 });
499
500 Some(LatestItem {
501 value: entry.value,
502 generation: entry.generation,
503 skipped,
504 })
505 }
506}
507
508impl<T: Copy> Drop for Consumer<'_, T> {
509 fn drop(&mut self) {
510 self.buf.consumer_taken.store(false, Ordering::Release);
511 }
512}
513
514impl<T: Copy> core::fmt::Debug for Consumer<'_, T> {
515 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
516 f.debug_struct("latest_buf::Consumer").finish()
517 }
518}
519
520impl<T: Copy> crate::traits::LatestSink<T> for Producer<'_, T> {
521 #[inline]
522 fn publish_latest(&mut self, value: T) -> PublishReport {
523 self.publish(value)
524 }
525}
526
527impl<T: Copy> crate::traits::LatestSource<T> for Consumer<'_, T> {
528 #[inline]
529 fn try_take_latest(&mut self) -> Option<LatestItem<T>> {
530 self.take_latest()
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537
538 #[test]
539 fn starts_empty_and_takes_each_publication_at_most_once() {
540 let channel = LatestBuf::<u32>::new();
541 let producer = channel.try_producer().unwrap();
542 let consumer = channel.try_consumer().unwrap();
543 assert_eq!(consumer.take_latest(), None);
544 assert_eq!(producer.publish(7).generation, 1);
545 assert_eq!(
546 consumer.take_latest(),
547 Some(LatestItem {
548 value: 7,
549 generation: 1,
550 skipped: 0
551 })
552 );
553 assert_eq!(consumer.take_latest(), None);
554 }
555
556 #[test]
557 fn replacement_is_reported_on_both_endpoints() {
558 let channel = LatestBuf::<u32>::new();
559 let producer = channel.try_producer().unwrap();
560 let consumer = channel.try_consumer().unwrap();
561 assert!(!producer.publish(10).replaced_unread);
562 assert!(producer.publish(20).replaced_unread);
563 assert!(producer.publish(30).replaced_unread);
564 assert_eq!(
565 consumer.take_latest(),
566 Some(LatestItem {
567 value: 30,
568 generation: 3,
569 skipped: 2
570 })
571 );
572 }
573
574 #[test]
575 fn handle_reacquisition_continues_role_state() {
576 let channel = LatestBuf::<u32>::new();
577 {
578 let producer = channel.try_producer().unwrap();
579 assert_eq!(producer.publish(1).generation, 1);
580 }
581 let producer = channel.try_producer().unwrap();
582 assert_eq!(
583 producer.publish(2),
584 PublishReport {
585 generation: 2,
586 replaced_unread: true
587 }
588 );
589
590 {
591 let consumer = channel.try_consumer().unwrap();
592 assert_eq!(consumer.take_latest().unwrap().skipped, 1);
593 }
594 let _ = producer.publish(3);
595 let _ = producer.publish(4);
596 let consumer = channel.try_consumer().unwrap();
597 assert_eq!(
598 consumer.take_latest(),
599 Some(LatestItem {
600 value: 4,
601 generation: 4,
602 skipped: 1
603 })
604 );
605 }
606
607 #[test]
608 fn role_acquisition_is_unique_and_handles_are_send() {
609 fn assert_send<T: Send>() {}
610 assert_send::<Producer<'_, u32>>();
611 assert_send::<Consumer<'_, u32>>();
612
613 let channel = LatestBuf::<u32>::new();
614 let producer = channel.try_producer().unwrap();
615 let consumer = channel.try_consumer().unwrap();
616 assert!(channel.try_producer().is_none());
617 assert!(channel.try_consumer().is_none());
618 drop(producer);
619 drop(consumer);
620 assert!(channel.try_producer().is_some());
621 assert!(channel.try_consumer().is_some());
622 }
623
624 #[cfg(not(loom))]
625 #[test]
626 fn static_channel_yields_static_sendable_handles() {
627 static CHANNEL: LatestBuf<u32> = LatestBuf::new();
628 fn producer() -> Producer<'static, u32> {
629 CHANNEL.try_producer().unwrap()
630 }
631 fn consumer() -> Consumer<'static, u32> {
632 CHANNEL.try_consumer().unwrap()
633 }
634 let producer = producer();
635 let consumer = consumer();
636 let _ = producer.publish(42);
637 assert_eq!(consumer.take_latest().unwrap().value, 42);
638 }
639
640 #[test]
641 fn generation_wrap_skips_zero_and_counts_gap_exactly() {
642 let channel = LatestBuf::<u32>::new();
643 // SAFETY: no producer handle exists while the test seeds role state.
644 channel.producer_state.with_mut(|state| unsafe {
645 (*state).next_generation = u32::MAX - 1;
646 });
647 // SAFETY: no consumer handle exists while the test seeds role state.
648 channel.consumer_state.with_mut(|state| unsafe {
649 (*state).last_generation = u32::MAX - 1;
650 });
651 let producer = channel.try_producer().unwrap();
652 let consumer = channel.try_consumer().unwrap();
653 assert_eq!(producer.publish(1).generation, u32::MAX);
654 assert_eq!(producer.publish(2).generation, 1);
655 assert_eq!(
656 consumer.take_latest(),
657 Some(LatestItem {
658 value: 2,
659 generation: 1,
660 skipped: 1
661 })
662 );
663 assert_eq!(LatestBuf::<u32>::generation_distance(u32::MAX, 1), 1);
664 }
665
666 #[test]
667 fn full_generation_cycle_uses_documented_approximation() {
668 // Equal endpoints are indistinguishable from no progress after a full
669 // generation cycle. D1 is closed as documented approximation plus
670 // payload escape hatch (contract C3/X6); this test is the closure's
671 // named pin for the full-cycle modular result — including the take
672 // path, not only the pure distance helper.
673 assert_eq!(LatestBuf::<u32>::generation_distance(17, 17), 0);
674
675 let channel = LatestBuf::<u32>::new();
676 // SAFETY: no handles exist while the test seeds role state.
677 channel.producer_state.with_mut(|state| unsafe {
678 // Next publish assigns generation 17 (skipping reserved 0).
679 (*state).next_generation = 16;
680 });
681 // SAFETY: no handles exist while the test seeds role state.
682 channel.consumer_state.with_mut(|state| unsafe {
683 // Resume cursor already at 17: a wrap-aliased publish of 17 looks
684 // like "nothing skipped" even though a full span was lost.
685 (*state).last_generation = 17;
686 });
687 let producer = channel.try_producer().unwrap();
688 let consumer = channel.try_consumer().unwrap();
689 assert_eq!(producer.publish(99).generation, 17);
690 assert_eq!(
691 consumer.take_latest(),
692 Some(LatestItem {
693 value: 99,
694 generation: 17,
695 skipped: 0,
696 })
697 );
698 }
699
700 #[test]
701 fn generic_payload_can_be_a_complete_block() {
702 let channel = LatestBuf::<[u16; 4]>::new();
703 let producer = channel.try_producer().unwrap();
704 let consumer = channel.try_consumer().unwrap();
705 let _ = producer.publish([1, 2, 3, 4]);
706 assert_eq!(consumer.take_latest().unwrap().value, [1, 2, 3, 4]);
707 }
708
709 #[test]
710 fn concurrent_publication_never_returns_torn_value() {
711 let channel = LatestBuf::<[u32; 4]>::new();
712 let total = crate::test_support::iterations(50_000);
713 std::thread::scope(|scope| {
714 scope.spawn(|| {
715 let producer = channel.try_producer().unwrap();
716 for value in 1..=total {
717 let _ = producer.publish([value; 4]);
718 }
719 });
720 let consumer = channel.try_consumer().unwrap();
721 let mut last_generation = 0;
722 while last_generation < total {
723 if let Some(item) = consumer.take_latest() {
724 assert!(item.generation > last_generation);
725 assert_eq!(item.value, [item.value[0]; 4]);
726 last_generation = item.generation;
727 } else {
728 std::thread::yield_now();
729 }
730 }
731 });
732 }
733}