1use std::sync::atomic::{AtomicI64, AtomicU8, AtomicU32, Ordering};
14
15pub const MAX_ASSERTION_SLOTS: usize = 128;
17
18const SLOT_MSG_LEN: usize = 64;
20
21pub const ASSERTION_TABLE_MEM_SIZE: usize =
25 8 + MAX_ASSERTION_SLOTS * std::mem::size_of::<AssertionSlot>();
26
27#[repr(u8)]
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum AssertKind {
31 Always = 0,
33 AlwaysOrUnreachable = 1,
35 Sometimes = 2,
37 Reachable = 3,
39 Unreachable = 4,
41 NumericAlways = 5,
43 NumericSometimes = 6,
45 BooleanSometimesAll = 7,
47}
48
49impl AssertKind {
50 #[must_use]
52 pub fn from_u8(v: u8) -> Option<Self> {
53 match v {
54 0 => Some(Self::Always),
55 1 => Some(Self::AlwaysOrUnreachable),
56 2 => Some(Self::Sometimes),
57 3 => Some(Self::Reachable),
58 4 => Some(Self::Unreachable),
59 5 => Some(Self::NumericAlways),
60 6 => Some(Self::NumericSometimes),
61 7 => Some(Self::BooleanSometimesAll),
62 _ => None,
63 }
64 }
65}
66
67#[repr(u8)]
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum AssertCmp {
71 Gt = 0,
73 Ge = 1,
75 Lt = 2,
77 Le = 3,
79}
80
81#[repr(C)]
85pub struct AssertionSlot {
86 pub msg_hash: u32,
88 pub kind: u8,
90 pub must_hit: u8,
92 pub maximize: u8,
94 pub split_triggered: u8,
96 pub pass_count: u64,
98 pub fail_count: u64,
100 pub watermark: i64,
102 pub split_watermark: i64,
104 pub frontier: u8,
106 pad: [u8; 7],
108 pub msg: [u8; SLOT_MSG_LEN],
110}
111
112impl AssertionSlot {
113 #[must_use]
115 pub fn msg_str(&self) -> &str {
116 let len = self
117 .msg
118 .iter()
119 .position(|&b| b == 0)
120 .unwrap_or(SLOT_MSG_LEN);
121 std::str::from_utf8(&self.msg[..len]).unwrap_or("???")
122 }
123}
124
125#[must_use]
127pub fn msg_hash(msg: &str) -> u32 {
128 let mut h: u32 = 0x811c_9dc5;
129 for b in msg.bytes() {
130 h ^= u32::from(b);
131 h = h.wrapping_mul(0x0100_0193);
132 }
133 h
134}
135
136unsafe fn find_or_alloc_slot(
145 table_ptr: *mut u8,
146 hash: u32,
147 kind: AssertKind,
148 must_hit: u8,
149 maximize: u8,
150 msg: &str,
151) -> (*mut AssertionSlot, usize) {
152 unsafe {
153 let next_atomic = &*table_ptr.cast::<()>().cast::<AtomicU32>();
154 let count = next_atomic.load(Ordering::Acquire) as usize;
155 let base = table_ptr.add(8).cast::<()>().cast::<AssertionSlot>();
156
157 for i in 0..count.min(MAX_ASSERTION_SLOTS) {
159 let slot = base.add(i);
160 let h = &*std::ptr::addr_of!((*slot).msg_hash).cast::<AtomicU32>();
161 if h.load(Ordering::Acquire) == hash {
162 return (slot, i);
163 }
164 }
165
166 let new_idx = next_atomic.fetch_add(1, Ordering::AcqRel) as usize;
168 if new_idx >= MAX_ASSERTION_SLOTS {
169 next_atomic.fetch_sub(1, Ordering::AcqRel);
170 return (std::ptr::null_mut(), 0);
171 }
172
173 let slot = base.add(new_idx);
177 let hash_atomic = &*std::ptr::addr_of!((*slot).msg_hash).cast::<AtomicU32>();
178 hash_atomic.store(hash, Ordering::Release);
179
180 for i in 0..new_idx {
183 let existing = base.add(i);
184 let existing_hash = &*std::ptr::addr_of!((*existing).msg_hash).cast::<AtomicU32>();
185 if existing_hash.load(Ordering::Acquire) == hash {
186 hash_atomic.store(0, Ordering::Release);
188 std::ptr::write_bytes(slot.cast::<u8>(), 0, std::mem::size_of::<AssertionSlot>());
189 return (existing, i);
190 }
191 }
192
193 let mut msg_buf = [0u8; SLOT_MSG_LEN];
195 let n = msg.len().min(SLOT_MSG_LEN - 1);
196 msg_buf[..n].copy_from_slice(&msg.as_bytes()[..n]);
197
198 (*slot).kind = kind as u8;
199 (*slot).must_hit = must_hit;
200 (*slot).maximize = maximize;
201 (*slot).split_triggered = 0;
202 (*slot).pass_count = 0;
203 (*slot).fail_count = 0;
204 (*slot).watermark = if maximize == 1 { i64::MIN } else { i64::MAX };
205 (*slot).split_watermark = if maximize == 1 { i64::MIN } else { i64::MAX };
206 (*slot).frontier = 0;
207 (*slot).pad = [0; 7];
208 (*slot).msg = msg_buf;
209
210 (slot, new_idx)
211 }
212}
213
214fn assertion_split(slot_idx: usize, hash: u32) {
219 let bm_ptr = crate::context::COVERAGE_BITMAP_PTR.with(std::cell::Cell::get);
220 let vm_ptr = crate::context::EXPLORED_MAP_PTR.with(std::cell::Cell::get);
221
222 if !bm_ptr.is_null() {
223 let bm = unsafe { crate::coverage::CoverageBitmap::new(bm_ptr) };
225 bm.set_bit(hash as usize);
226 if !vm_ptr.is_null() {
227 let vm = unsafe { crate::coverage::ExploredMap::new(vm_ptr) };
229 vm.merge_from(&bm);
230 }
231 }
232
233 if crate::context::explorer_is_active() {
234 crate::split_loop::dispatch_split("", slot_idx % MAX_ASSERTION_SLOTS);
235 }
236}
237
238pub fn assertion_bool(kind: AssertKind, must_hit: bool, condition: bool, msg: &str) {
246 let table_ptr = crate::context::assertion_table_ptr();
247 if table_ptr.is_null() {
248 return;
249 }
250
251 let hash = msg_hash(msg);
252 let must_hit_u8 = u8::from(must_hit);
253
254 let (slot, slot_idx) =
256 unsafe { find_or_alloc_slot(table_ptr, hash, kind, must_hit_u8, 0, msg) };
257 if slot.is_null() {
258 return;
259 }
260
261 unsafe {
263 match kind {
264 AssertKind::Always | AssertKind::AlwaysOrUnreachable | AssertKind::NumericAlways => {
265 if condition {
266 let pc = &*(&raw const (*slot).pass_count).cast::<AtomicI64>();
267 pc.fetch_add(1, Ordering::Relaxed);
268 } else {
269 let fc = &*(&raw const (*slot).fail_count).cast::<AtomicI64>();
270 let prev = fc.fetch_add(1, Ordering::Relaxed);
271 if prev == 0 {
272 eprintln!("[ASSERTION FAILED] {msg} (kind={kind:?})");
273 }
274 }
275 }
276 AssertKind::Sometimes | AssertKind::Reachable => {
277 if condition {
278 let pc = &*(&raw const (*slot).pass_count).cast::<AtomicI64>();
279 pc.fetch_add(1, Ordering::Relaxed);
280
281 let ft = &*(&raw const (*slot).split_triggered).cast::<AtomicU8>();
283 if ft
284 .compare_exchange(0, 1, Ordering::Relaxed, Ordering::Relaxed)
285 .is_ok()
286 {
287 assertion_split(slot_idx, hash);
288 }
289 } else {
290 let fc = &*(&raw const (*slot).fail_count).cast::<AtomicI64>();
291 fc.fetch_add(1, Ordering::Relaxed);
292 }
293 }
294 AssertKind::Unreachable => {
295 let pc = &*(&raw const (*slot).pass_count).cast::<AtomicI64>();
298 let prev = pc.fetch_add(1, Ordering::Relaxed);
299 if prev == 0 {
300 eprintln!("[UNREACHABLE REACHED] {msg}");
301 }
302 }
303 _ => {}
304 }
305 }
306}
307
308pub fn assertion_numeric(
319 kind: AssertKind,
320 cmp: AssertCmp,
321 maximize: bool,
322 left: i64,
323 right: i64,
324 msg: &str,
325) {
326 let table_ptr = crate::context::assertion_table_ptr();
327 if table_ptr.is_null() {
328 return;
329 }
330
331 let hash = msg_hash(msg);
332 let maximize_u8 = u8::from(maximize);
333
334 let (slot, slot_idx) =
336 unsafe { find_or_alloc_slot(table_ptr, hash, kind, 1, maximize_u8, msg) };
337 if slot.is_null() {
338 return;
339 }
340
341 let passes = match cmp {
343 AssertCmp::Gt => left > right,
344 AssertCmp::Ge => left >= right,
345 AssertCmp::Lt => left < right,
346 AssertCmp::Le => left <= right,
347 };
348
349 unsafe {
351 if passes {
352 let pc = &*(&raw const (*slot).pass_count).cast::<AtomicI64>();
353 pc.fetch_add(1, Ordering::Relaxed);
354 } else {
355 let fc = &*(&raw const (*slot).fail_count).cast::<AtomicI64>();
356 let prev = fc.fetch_add(1, Ordering::Relaxed);
357 if kind == AssertKind::NumericAlways && prev == 0 {
358 eprintln!(
359 "[NUMERIC ASSERTION FAILED] {msg} (left={left}, right={right}, cmp={cmp:?})"
360 );
361 }
362 }
363
364 let wm = &*(&raw const (*slot).watermark).cast::<AtomicI64>();
366 let mut current = wm.load(Ordering::Relaxed);
367 loop {
368 let is_better = if maximize {
369 left > current
370 } else {
371 left < current
372 };
373 if !is_better {
374 break;
375 }
376 match wm.compare_exchange_weak(current, left, Ordering::Relaxed, Ordering::Relaxed) {
377 Ok(_) => break,
378 Err(actual) => current = actual,
379 }
380 }
381
382 if kind == AssertKind::NumericSometimes {
384 let fw = &*(&raw const (*slot).split_watermark).cast::<AtomicI64>();
385 let mut fork_current = fw.load(Ordering::Relaxed);
386 loop {
387 let is_better = if maximize {
388 left > fork_current
389 } else {
390 left < fork_current
391 };
392 if !is_better {
393 break;
394 }
395 match fw.compare_exchange_weak(
396 fork_current,
397 left,
398 Ordering::Relaxed,
399 Ordering::Relaxed,
400 ) {
401 Ok(_) => {
402 assertion_split(slot_idx, hash);
403 break;
404 }
405 Err(actual) => fork_current = actual,
406 }
407 }
408 }
409 }
410}
411
412pub fn assertion_sometimes_all(msg: &str, named_bools: &[(&str, bool)]) {
419 let table_ptr = crate::context::assertion_table_ptr();
420 if table_ptr.is_null() {
421 return;
422 }
423
424 let hash = msg_hash(msg);
425
426 let (slot, slot_idx) =
428 unsafe { find_or_alloc_slot(table_ptr, hash, AssertKind::BooleanSometimesAll, 1, 0, msg) };
429 if slot.is_null() {
430 return;
431 }
432
433 let true_count =
437 u8::try_from(named_bools.iter().filter(|(_, v)| *v).count()).unwrap_or(u8::MAX);
438
439 unsafe {
441 let pc = &*(&raw const (*slot).pass_count).cast::<AtomicI64>();
443 pc.fetch_add(1, Ordering::Relaxed);
444
445 let fr = &*(&raw const (*slot).frontier).cast::<AtomicU8>();
447 let mut current = fr.load(Ordering::Relaxed);
448 loop {
449 if true_count <= current {
450 break;
451 }
452 match fr.compare_exchange_weak(
453 current,
454 true_count,
455 Ordering::Relaxed,
456 Ordering::Relaxed,
457 ) {
458 Ok(_) => {
459 assertion_split(slot_idx, hash);
460 break;
461 }
462 Err(actual) => current = actual,
463 }
464 }
465 }
466}
467
468#[must_use]
472pub fn assertion_read_all() -> Vec<AssertionSlotSnapshot> {
473 let table_ptr = crate::context::assertion_table_ptr();
474 if table_ptr.is_null() {
475 return Vec::new();
476 }
477
478 unsafe {
485 let count = (*table_ptr.cast::<()>().cast::<u32>()) as usize;
486 let count = count.min(MAX_ASSERTION_SLOTS);
487 let base = table_ptr.add(8).cast::<()>().cast::<AssertionSlot>();
488
489 (0..count)
490 .filter_map(|i| {
491 let slot = &*base.add(i);
492 if slot.msg_hash == 0 {
494 return None;
495 }
496 Some(AssertionSlotSnapshot {
497 msg: slot.msg_str().to_string(),
498 kind: slot.kind,
499 must_hit: slot.must_hit,
500 pass_count: slot.pass_count,
501 fail_count: slot.fail_count,
502 watermark: slot.watermark,
503 frontier: slot.frontier,
504 })
505 })
506 .collect()
507 }
508}
509
510#[derive(Debug, Clone)]
512pub struct AssertionSlotSnapshot {
513 pub msg: String,
515 pub kind: u8,
517 pub must_hit: u8,
519 pub pass_count: u64,
521 pub fail_count: u64,
523 pub watermark: i64,
525 pub frontier: u8,
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn test_msg_hash_deterministic() {
535 let h1 = msg_hash("test_assertion");
536 let h2 = msg_hash("test_assertion");
537 assert_eq!(h1, h2);
538 }
539
540 #[test]
541 fn test_msg_hash_no_collision() {
542 let names = ["a", "b", "c", "timeout", "connect", "retry"];
543 let hashes: Vec<u32> = names.iter().map(|n| msg_hash(n)).collect();
544 for i in 0..hashes.len() {
545 for j in (i + 1)..hashes.len() {
546 assert_ne!(
547 hashes[i], hashes[j],
548 "{} and {} collide",
549 names[i], names[j]
550 );
551 }
552 }
553 }
554
555 #[test]
556 fn test_slot_size_stable() {
557 assert_eq!(std::mem::size_of::<AssertionSlot>(), 112);
562 }
563
564 #[test]
565 fn test_assertion_bool_noop_when_inactive() {
566 assertion_bool(AssertKind::Sometimes, true, true, "test");
568 assertion_bool(AssertKind::Always, true, false, "test2");
569 }
570
571 #[test]
572 fn test_assertion_numeric_noop_when_inactive() {
573 assertion_numeric(
575 AssertKind::NumericAlways,
576 AssertCmp::Gt,
577 false,
578 10,
579 5,
580 "test",
581 );
582 }
583
584 #[test]
585 fn test_assertion_read_all_when_inactive() {
586 let slots = assertion_read_all();
588 assert!(slots.is_empty());
589 }
590
591 #[test]
592 fn test_assert_kind_from_u8() {
593 assert_eq!(AssertKind::from_u8(0), Some(AssertKind::Always));
594 assert_eq!(
595 AssertKind::from_u8(7),
596 Some(AssertKind::BooleanSometimesAll)
597 );
598 assert_eq!(AssertKind::from_u8(8), None);
599 }
600}