ph-eventing 0.3.0

Deterministic zero-allocation SPSC primitives for no-std embedded targets — ring buffers, a latest-value snapshot channel, condition flags, saturating counters, and complete sample blocks: bounded behaviour, measured cost, Loom-verified orderings
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# ph-eventing


[![Crates.io](https://img.shields.io/crates/v/ph-eventing)](https://crates.io/crates/ph-eventing)
[![docs.rs](https://img.shields.io/docsrs/ph-eventing)](https://docs.rs/ph-eventing)
[![CI: local](https://img.shields.io/badge/CI-local-blue)](scripts/ci.sh)
[![License: MIT](https://img.shields.io/crates/l/ph-eventing)](LICENSE)
[![MSRV](https://img.shields.io/badge/MSRV-1.92.0-blue)](rust-toolchain.toml)
[![no_std](https://img.shields.io/badge/no__std-yes-green)](src/lib.rs)

Deterministic zero-allocation handoff primitives for no-std embedded targets.

## What's in the box


| Type | Use case |
|------|----------|
| [`Block<T, N>` / `BlockBuilder<T, N>`]#complete-blocks | Build complete contiguous sample windows, then compose them with a transport. |
| [`RingBuf<T, N>`]#ringbuf | Single-owner ring buffer — simple, no atomics, `&mut` access. |
| [`SeqRing<T, N>`]#seqring | Lock-free SPSC ring that **overwrites** old entries (lossy, high-throughput). |
| [`EventBuf<T, N>`]#eventbuf | Lock-free SPSC ring with **backpressure** — rejects pushes when full. |
| [`CountedSignal`]#countedsignal | Saturating SPSC count for identical, payload-free events. |
| [`EventFlags`]#eventflags | Coalesced SPSC condition set — 32 payload-free conditions, one atomic hot-path operation. |
| [`LatestBuf<T>`]#latestbuf | Freshness-first SPSC snapshot — retains one newest unread value. |

All types are fixed-size, `#![no_std]`, and zero-allocation. The buffers are
generic over `T: Copy`; `CountedSignal` carries no payload and `EventFlags`
carries an `EventMask(u32)`.

## What this optimises for


`no_std` and no-alloc are the entry fee. What this crate offers past that is
**behaviour you can predict and cost you can measure**:

- **Predictability first.** No unbounded loops, no hidden allocation, and no
  panic reachable from a hot path. For the concurrent types, no data loss that
  cannot be observed either — every drop is reported (`SeqRing`, exact while
  the consumer's resume cursor stays within one sequence span of the newest
  entry; see its section) or prevented (`EventBuf`), or explicitly coalesced
  by contract (`EventFlags`). `RingBuf` is the deliberate exception: it is a single-owner
  window that overwrites silently, with no drop counter and no backpressure.
  Reach for it when losing the oldest entry is the point, not when delivery
  matters.
- **Cost measured on every target, not one.** `scripts/codesize.sh` (added
  alongside this release) reports the
  flash cost of each API shape across 11 targets and 4 ISA families, because a
  design that wins on Cortex-M4 can cost 40–60% more on Cortex-M0+ or ESP32-S2,
  where every atomic becomes an interrupt-disable critical section.
- **Guarantees pinned by tooling.** Loom proves the orderings exhaustively at
  the modelled size, Miri checks UB on 32-bit and big-endian, and a code-size
  row keeps a cheap API from quietly becoming expensive.

**Ergonomics is ranked last, deliberately.** If an API here feels more awkward
than an equivalent `std` type, that is usually a cost being made visible rather
than hidden. Where the awkwardness is not load-bearing, the fix is compile-time
tooling that costs nothing at runtime — not a friendlier API that allocates,
panics, or hides a cost.

## Features

- Three ring buffer flavours plus a freshness-first SPSC snapshot channel.
- Complete contiguous sample blocks with an explicit fill-side builder.
- `EventFlags` for coalesced ISR-to-task condition notification.
- Common `Sink`/`Source`/`Link` traits for writing generic event-processing code.
- `forward(src, snk, max)` utility to bridge any `Source``Sink`.
- No heap, no dynamic dispatch, no required dependencies.
- Optional `portable-atomic` support for targets without native 32-bit atomics.
- Designed for `#![no_std]` environments (std only for tests).

## Compatibility

- MSRV: Rust 1.92.0.
- `SeqRing::new()` and `EventBuf::new()` assert `N > 0`.
- `SeqRing`, `EventBuf`, and `EventFlags` require 32-bit atomics by default.
- `LatestBuf` also requires 32-bit atomics and stores exactly three payload slots.
- For `thumbv6m-none-eabi` (and other no-atomic targets), enable one of:
  - `portable-atomic-unsafe-assume-single-core`
  - `portable-atomic-critical-section` (requires a critical-section implementation in the binary)
- Those two are **mutually exclusive** — they select different portable-atomic backends, and
  enabling both fails inside portable-atomic. Cargo features are additive, so this cannot be
  expressed in the manifest; `build.rs` detects the combination and explains it.
- Consequently **`--all-features` does not work for this crate** and cannot be made to. Check
  combinations individually; `scripts/ci.sh` enumerates the supported set.

## Usage


### Complete blocks


`BlockBuilder<T, N>` privately accumulates sequenced samples and yields a
`Block<T, N>` only when all `N` contiguous samples are present. A gap is
returned to the caller without changing the partial block, and clearing or
dropping a partial builder publishes nothing. Timestamping is payload policy:
use a timestamped type for `T` when required.

`Block` is deliberately not another queue. Compose it with the overload policy
you need: `EventBuf<Block<T, N>, Q>` queues complete blocks and rejects the
newest when full; `LatestBuf<Block<T, N>>` retains only the
latest complete block.

**Budget the composition before choosing it.** Publication copies the
complete block, so cost scales with block bytes (150–8,651 reference
instructions across the measured 2/8/16-byte × N = 8/32/128 grid), a
rejected push costs nearly as much as an accepted one (the complete block
is preserved and returned, within 2–25 instructions), and RAM is multiple
complete blocks — `Q` slots plus the private builder. Small windows can
invert the economics (per-sample publication beats blocks at the
8/16-byte `N = 8` corners), and DMA integrations currently cannot avoid
the double copy in ISR context — the builder's storage is deliberately
private, so either budget both copies or publish from task context. The
`block` module docs carry the full measured disclosure.

```rust
use ph_eventing::{BlockBuilder, EventBuf};

let mut fill = BlockBuilder::<i16, 4>::new();
for (sequence, sample) in [(10, 1), (11, 2), (12, 3)] {
    assert!(fill.push(sequence, sample).expect("contiguous").is_none());
}
let block = fill.push(13, 4).expect("contiguous").expect("complete");

let queue = EventBuf::<_, 2>::new();
let producer = queue.try_producer().expect("no producer taken yet");
let consumer = queue.try_consumer().expect("no consumer taken yet");
// Backpressure is returned, never unwrapped: a full queue hands the
// complete block back through `Err` for the caller's policy.
assert!(producer.push(block).is_ok());
assert_eq!(consumer.pop().expect("one block queued").samples(), &[1, 2, 3, 4]);
```

### RingBuf


A straightforward, single-owner ring buffer for collecting values when you
don't need cross-thread access. When full, new pushes silently overwrite the
oldest entry. Requires only `T: Copy` — no `Default`.

```rust
use ph_eventing::RingBuf;

let mut ring = RingBuf::<u32, 4>::new();
ring.push(1);
ring.push(2);
ring.push(3);
assert_eq!(ring.latest(), Some(3));
assert_eq!(ring.get(0),  Some(1)); // oldest

// iterate oldest → newest
for val in ring.iter() {
    // 1, 2, 3
}
```

### SeqRing


A lock-free SPSC ring for high-rate telemetry. The producer never blocks;
the consumer reports drops when it lags behind by more than `N`.

```rust
use ph_eventing::SeqRing;

let ring = SeqRing::<u32, 64>::new();
let producer = ring.try_producer().expect("no producer taken yet");
let mut consumer = ring.try_consumer().expect("no consumer taken yet");

producer.push(123);
assert_eq!(consumer.poll_one_value(), Some((1, 123)));
// hook form still available:
// consumer.poll_one(|seq, v| { ... });
```

### LatestBuf


A three-slot SPSC snapshot channel for state where freshness dominates FIFO
delivery. Publishing never rejects; it reports whether an unread value was
replaced. Taking returns the newest complete value with generation and skipped
counts. Exact skipped counts are guaranteed within one non-zero `u32` wrap
span; beyond it the count **under-counts, and a gap of exactly one or more
whole cycles reports `skipped = 0`** — silence there is not evidence that
nothing was lost. The boundary is a rate × take-interval property (~49.7
days between takes at 1 kHz publishing, ~72 minutes at 1 MHz); if the
count itself is your requirement, carry a wider producer-assigned sequence
in `T`, and if consumer liveness is, use a watchdog — the
`LatestItem::skipped` docs carry the full disclosure. The
consumer intentionally implements `LatestSource`, not `Source`, so gap evidence
is not silently discarded. `T` may be one sample or a complete block. Empty
polls use an Acquire load rather than an atomic RMW; pending polls transfer
ownership with one `AcqRel` swap. The all-zero initial representation keeps a
const-initialized channel in `.bss` with no payload-proportional flash or
startup-copy cost.

```rust
use ph_eventing::LatestBuf;

let channel = LatestBuf::<u32>::new();
let producer = channel.try_producer().expect("producer");
let consumer = channel.try_consumer().expect("consumer");

let _ = producer.publish(10);
assert!(producer.publish(20).replaced_unread);
let item = consumer.take_latest().expect("latest");
assert_eq!((item.value, item.generation, item.skipped), (20, 2, 1));
```

### EventBuf


A bounded SPSC queue with backpressure. When the buffer is full, `push`
returns `Err(val)` so the producer can decide what to do — no data is
silently lost.

```rust
use ph_eventing::EventBuf;

let buf = EventBuf::<u32, 2>::new();
let producer = buf.try_producer().expect("no producer taken yet");
let consumer = buf.try_consumer().expect("no consumer taken yet");

assert!(producer.push(1).is_ok());
assert!(producer.push(2).is_ok());
assert_eq!(producer.push(3), Err(3)); // full — value returned

assert_eq!(consumer.peek(), Some(1));  // copy, no advance
assert_eq!(consumer.pop(), Some(1));
assert!(producer.push(3).is_ok());     // space freed
```

### CountedSignal


A saturating count for repeated events whose payload and ordering do not
matter. The sole producer is load-bearing: it permits exact saturation with a
fixed source-level sequence that treats observed `u32::MAX` as maybe-stale and
confirms it through a no-op RMW re-read — an RMW observes the latest value in
modification order, so there is no compare-exchange and no algorithmic retry;
the contract discloses how each single RMW is realised per ISA.

```rust
use ph_eventing::CountedSignal;

let signal = CountedSignal::new();
let producer = signal.try_producer().expect("no producer taken yet");
let consumer = signal.try_consumer().expect("no consumer taken yet");

producer.increment();
producer.increment();
let snapshot = consumer.take_count();
assert_eq!(snapshot.count(), 2);
assert!(!snapshot.is_saturated());
```

### EventFlags


A coalesced condition set for ISR-to-task notification. Repeated raises of one
condition may merge; a take returns and clears every condition that occurred at
least once since the preceding take.

```rust
use ph_eventing::{EventFlags, EventMask};

const DATA_READY: EventMask = EventMask::from_bits(1 << 0);
const OVERFLOW: EventMask = EventMask::from_bits(1 << 1);

let flags = EventFlags::new();
let producer = flags.try_producer().expect("no producer taken yet");
let consumer = flags.try_consumer().expect("no consumer taken yet");

producer.raise(DATA_READY);
producer.raise(DATA_READY); // coalesces
producer.raise(OVERFLOW);

assert_eq!(consumer.take_all(), DATA_READY | OVERFLOW);
assert!(consumer.take_all().is_empty());
```

`EventFlags` deliberately does not implement the stream traits below: a
coalesced condition set is not a sequence of items, and destructive take plus
a rejecting downstream sink could silently lose the mask.

### Common Traits


The ring-buffer producers implement `Sink<T>` and their consumers
implement `Source<T>`, so generic code works with any combination of the
listed handles. Signal types such as `CountedSignal` are outside that
stream vocabulary (no `T` payload), and `EventFlags` is condition
signalling, not a payload stream — its handles deliberately implement
neither (see its section above). `LatestBuf` deliberately stands
outside it as well: its consumer implements `LatestSource<T>` (and its
producer `LatestSink<T>`), because `try_pop` cannot report the
displacement that is this channel's designed overload behaviour — a
generic `Source` bound will not compile against it, by decision D2:

```rust
use ph_eventing::{SeqRing, EventBuf};
use ph_eventing::traits::{Source, Sink, forward};

// bridge a SeqRing producer → EventBuf consumer
let seq = SeqRing::<u32, 8>::new();
let sp = seq.try_producer().expect("no producer taken yet");
let mut sc = seq.try_consumer().expect("no consumer taken yet");

sp.push(1); sp.push(2);

let eb = EventBuf::<u32, 8>::new();
let mut ep = eb.try_producer().expect("no producer taken yet");

let (n, err) = forward(&mut sc, &mut ep, 10);
assert_eq!(n, 2);
assert!(err.is_none());
```

| Trait | Role | Implementors |
|-------|------|--------------|
| `Sink<T>` | Accept events | `RingBuf`, `seq_ring::Producer`, `event_buf::Producer` |
| `Source<T>` | Yield events | `seq_ring::Consumer`, `event_buf::Consumer` |
| `Link<In,Out>` | Both | Blanket impl for `Sink<In> + Source<Out>` |
| `LatestSink<T>` | Publish latest | `latest_buf::Producer` (reports replacement) |
| `LatestSource<T>` | Take latest | `latest_buf::Consumer` (reports generation + skipped) |

### Declarative static bring-up


`static_spsc!` names the handle types for you, so a signature does not have to
spell out `ph_eventing::event_buf::Producer<'static, u32, 64>`:

```rust
ph_eventing::static_spsc! {
    pub mod telemetry: EventBuf<u32, 64>;
}

fn on_sample(tx: &telemetry::Tx, v: u32) { let _ = tx.push(v); }

let (tx, rx) = telemetry::take().expect("first take");
```

It expands to a `static`, two type aliases, and an all-or-nothing `take()` —
no allocation, no indirection, and no instruction that would not be there
written by hand. `SeqRing` works the same way. Note the handles are
`Send + !Sync` by design, so they cannot themselves live in a `static`; taking
them is a runtime step and always will be.

## Semantics


### RingBuf

- Single-owner (`&mut self` to push).
- `get(i)` returns the `i`-th element where `0` is the oldest.
- `latest()` returns the most recently pushed element.
- `iter()` yields elements oldest → newest.
- `new()` is a `const fn`; `N == 0` fails at compile time.

### SeqRing

- Sequence numbers are monotonically increasing `u32` values; `0` is reserved for "empty".
- When the producer wraps the ring, old values are overwritten.
- `poll_one` and `poll_up_to` drain in-order and return `PollStats` (`read`, `dropped`, `newest`).
- `poll_one_value` / `latest_value` return `(seq, T)` without a hook.
- `latest` reads the newest value without advancing the consumer cursor.
- `skip_to_latest` discards the backlog so the next poll returns the newest item.
- If the consumer lags by more than `N`, it skips ahead and reports drops via `PollStats`.
- Once every `2^32 - 1` pushes the sequence counter wraps and a few extra entries are dropped —
  exactly one for a power-of-two `N`, none if `N` divides `2^32 - 1`, up to `N - 1` otherwise. They
  are reported as ordinary drops; no stale or torn value is returned (within the span bound
  below). See [Choosing `N`]#choosing-n.
- Sequence arithmetic is modular over that `2^32 - 1` span, and both headline guarantees carry
  its bound: a whole-span gap from the consumer's resume cursor aliases to "nothing new" and
  reports **zero** drops, and the torn-copy re-check shares the same counter-width ABA limit for
  a consumer stalled mid-read. Reachability arithmetic and the structural escape hatches are in
  the rustdoc ("Known limitation: whole-span sequence aliasing").

### EventBuf

- FIFO order: `pop` always returns the oldest item.
- `peek` copies the oldest item without advancing the cursor.
- `push` returns `Ok(())` on success or `Err(val)` when the buffer is full.
- `drain(max, hook)` consumes up to `max` items through a callback and returns the count.
- No data is silently lost — the producer always knows when the buffer cannot accept more.

### CountedSignal

- Counts below `u32::MAX` are exact; the counter saturates rather than wrapping.
- `take_count` atomically clears the counter and reports whether it saturated.
- A concurrent increment belongs wholly to the current take or the next one.
- The sole `Send + !Sync` producer handle is part of the correctness contract.
- Count operations do not publish unrelated application memory; payload data
  needs a separate synchronization mechanism.
- The reference Cortex-M3 probe measures `increment` at 8 retired
  instructions on the below-`MAX` hot path and 9 on the saturated sentinel
  arm, and `take_count` at 9 (rustc 1.92.0, QEMU 10.0.11, measured on the
  assembled 0.3.0 tree). The third arm — a
  stale `MAX` re-read below `MAX` after a take — is the saturated arm plus
  one `fetch_add` by construction; all rows are uncontended single-pass
  counts (the contract discloses the per-ISA RMW realisation).

### EventFlags

- `raise(mask)` unions conditions into the pending set; duplicate bits may coalesce.
- `take_all()` atomically returns and clears every pending condition.
- Conditions are unordered and carry no payload or multiplicity.
- `EventMask` is exactly 32 bits; `from_index` rejects out-of-range indices without panicking.
- A take that observes a raise also observes memory writes sequenced before it.
- There is no non-clearing peek and no stream/signal trait implementation in the initial surface.

## Safety and Concurrency

- `RingBuf` has no atomics and no interior mutability — standard Rust borrow rules apply. It stores slots as `MaybeUninit<T>` and reads only live entries, so it does contain `unsafe`.
- `SeqRing`, `EventBuf`, `EventFlags`, `CountedSignal`, and `LatestBuf` are SPSC by design: exactly one producer and one consumer may be
  active. Use `try_producer()`/`try_consumer()`, which return `None` rather than panicking —
  on a microcontroller a panic is a reset, and the panic machinery costs flash you may not have.
  The panicking `producer()`/`consumer()`, deprecated since 0.2.0, **were removed in
  0.3.0**. Using unsafe to bypass `SeqRing`/`EventBuf`/`LatestBuf` ownership can be undefined
  behavior. Forging or concurrently sharing a `CountedSignal` producer breaks
  its bounded no-wrap contract; the handle is `!Sync` to prevent that in safe Rust.
  Forging a second `LatestBuf` producer or consumer similarly breaks the three-slot
  exclusive-ownership exchange.
- `T: Copy` is required by all payload-carrying types to avoid allocation and return values by copy.
- `EventFlags` has no unsafe slot access and passes Miri with the race detector enabled.
- `EventBuf` is race-free by construction: its producer and consumer never touch the same slot,
  and it passes Miri with the data-race detector enabled.
- `SeqRing` is a seqlock and carries a **known formal data race** — the consumer may copy a slot
  the producer is overwriting, then discard the copy when the sequence re-check fails. A raced
  copy is discarded and never becomes an invalid value within the whole-span bound (the re-check
  compares `u32` sequences, so a consumer stalled mid-read for a full `2^32 − 1` publications can
  pass both checks against a rewritten slot; see the SeqRing section above and the rustdoc). The
  access itself is undefined behaviour by the letter of the memory model.
  - **This affects your tooling, not just ours:** if you run `cargo miri test` over a test that
    drives `SeqRing` from two threads, Miri will report UB pointing into this crate. That is the
    known deviation, not a new bug.
  - It is a deliberate trade. A ring restricted to a word-sized payload could store it in an
    atomic and be fully race-free; accepting any `T: Copy` is what rules that out. Generality was
    chosen over formal soundness.
  - `EventBuf` has no such caveat and passes Miri with the detector on — but it is **not a
    drop-in**, since it applies backpressure instead of overwriting.
  - Full reasoning, including the alternatives and why each was rejected, is in the `seq_ring`
    module docs.

## Using it across contexts


The typical embedded shape is a producer in an interrupt handler and a consumer
in a task loop. That works, with three things to know:

- **The primitive is shared; the handles are owned.** `SeqRing<T, N>`,
  `EventBuf<T, N>`, and `LatestBuf<T>` are `Sync` when `T: Send`, and `EventFlags` and
  `CountedSignal` are `Sync`, so the primitive can be handed to both
  contexts. `Producer` and `Consumer` are `Send + !Sync` — move each one into
  the context that owns it, and never share a single handle between contexts.
  There is no way to get a second `Producer` while one is live:
  `try_producer` / `try_consumer` return `None` rather than handing out a
  duplicate.
- **The buffer must outlive both handles.** The handles borrow it, so the usual
  answer is to own the buffer where it lives longest.
- **`new()` is a `const fn`** on the normal build, so
  `static BUF: EventBuf<u32, 64> = EventBuf::new();` works. (Under `--cfg loom`
  it is non-const because Loom's atomics are not const-constructible.) Handles
  still borrow the buffer, so an ISR / task split typically pairs the `static`
  with a `StaticCell` or similar for the handles themselves.

### Choosing `N`


`N` is the slot count, fixed at compile time, and the whole buffer lives inline
— `N * size_of::<T>()` bytes of stack or static, with no allocation.

- For `EventBuf`, `N` is your backpressure threshold: the point at which `push`
  starts returning `Err`. Size it for the largest burst you are willing to
  absorb between drains.
- For `SeqRing`, `N` is how far the consumer may lag before it starts losing
  entries. Size it for the worst-case gap between polls, not for the average.
- `N` need not be a power of two — no indexing or capacity logic requires it — but for `SeqRing`
  a power of two is still the better default. `SeqRing` addresses slots by `(seq - 1) % N` while
  `push` skips the reserved sequence `0`, so a full cycle is `2^32 - 1` sequences and the slot walk
  only lines up across the wrap when `N` divides `2^32 - 1`. What that costs, once per wrap:

  | `N` | Entries dropped at the wrap |
  |-----|-----------------------------|
  | A power of two | Exactly 1 |
  | A divisor of `2^32 - 1` (3, 5, 15, 17, 51, 85, 255, 257, 65537, …) | 0 |
  | Anything else | Up to `N - 1``N = 48` drops 15, `N = 96` drops 33, `N = 121` drops 58 |

  These are reported through `PollStats` like any other drop, and no stale or torn value is
  returned (within the whole-span bound stated in the SeqRing section and rustdoc) — it is a
  data-loss bound, not a correctness one. One lost entry per `2^32` pushes is
  beneath the noise floor for anything that already tolerates overwrite, so a power of two is
  almost always the right call. `EventBuf` has no wrap boundary of this kind.

## Quality and verification


The concurrent primitives are atomic, so a green test run on x86 is weak
evidence — a strongly-ordered host cannot exhibit the ordering bugs that appear
on ARM and RISC-V. What backs this crate, in descending order of strength:

| Evidence | What it establishes |
|----------|---------------------|
| [Loom]https://github.com/tokio-rs/loom models | Exhaustive: every interleaving and every legal relaxed-load value, for the modelled size |
| [Miri]https://github.com/rust-lang/miri | UB, data races, and weak-memory behaviour; also run on 32-bit and big-endian targets |
| 103 unit + 13 doctests + 11 compile-fail | Behaviour, including threaded stress tests for all SPSC types; `N == 0` rejected at compile time on the three buffers and `BlockBuilder`; `LatestBuf`'s absent `Source` impl and handle `!Sync` pinned (D2/H2); CountedSignal and EventFlags handle `!Sync` pinned |
| 3 embedded targets | `thumbv6m` / `thumbv7em` / `riscv32imac` compile checks |
| Code-size baseline | Flash cost gated in CI across 8 pinned targets; growth past +16 bytes fails |
| QEMU instruction counts | Hot-path cost is constant w.r.t. occupancy, measured per instruction |

All of it is reproducible: `./scripts/verify.sh` runs the full matrix inside one
pinned Docker environment (`scripts/verify/Dockerfile`), so the numbers above
can be checked rather than believed.

**One known deviation.** `SeqRing` is a seqlock and carries a formal data race —
see [Safety and Concurrency](#safety-and-concurrency) above. `EventBuf` is
race-free by construction and passes Miri with the detector enabled.

Coverage is around 94% of lines, though it is a weak signal here: what matters
is ordering and interleaving, which line coverage cannot see.

Contributors: [CONTRIBUTING.md](CONTRIBUTING.md) has the commands for running
all of the above locally. CI runs on every PR, but it covers only part of that
list — **coverage, Miri, and Loom are local-only**, so a green check is not a
clean matrix.

## License

MIT. See `LICENSE`.