Skip to main content

moonpool_assertions/
hooks.rs

1//! Discovery hook: the one-way coupling surface to an exploration backend.
2//!
3//! The accounting layer ([`crate::slots`], [`crate::buckets`]) calls these hooks
4//! at the exact points where something "new" happens — a first Sometimes/Reachable
5//! pass, a numeric watermark improvement, a frontier advance, or an each-bucket
6//! hit. With no hook installed (the default — wasm, macOS, plain native runs) the
7//! calls are no-ops and accounting is pure. An exploration backend
8//! (`moonpool-explorer`) installs hooks that mark coverage and dispatch forks.
9//!
10//! This mirrors `moonpool-explorer`'s own `set_rng_hooks` pattern, inverted: there
11//! the sim hands the explorer RNG hooks; here the explorer hands this crate its
12//! discovery hooks. The function pointers are stored in a thread-local `Cell` set
13//! before forking, so forked children inherit them.
14
15use std::cell::Cell;
16
17/// Discovery callbacks installed by an exploration backend.
18///
19/// Each field is a plain function pointer (defaults are no-ops). The accounting
20/// layer never knows what these do — coverage bitmaps and fork dispatch live
21/// entirely behind these pointers.
22#[derive(Clone, Copy)]
23pub struct DiscoveryHooks {
24    /// A slot assertion (bool Sometimes/Reachable first pass, numeric watermark
25    /// improvement, or frontier advance) made a discovery. Receives the slot
26    /// index and the message hash.
27    pub on_slot_discovery: fn(slot_idx: usize, hash: u32),
28    /// An each-bucket assertion was hit (called on every invocation). Receives
29    /// the bucket hash for coverage marking.
30    pub on_bucket_mark: fn(hash: u32),
31    /// An each-bucket assertion made a first discovery or quality improvement.
32    /// Receives the message label and a slot index.
33    pub on_bucket_split: fn(label: &str, slot_idx: usize),
34}
35
36fn noop_slot(_: usize, _: u32) {}
37fn noop_mark(_: u32) {}
38fn noop_split(_: &str, _: usize) {}
39
40impl DiscoveryHooks {
41    /// Hooks that do nothing — pure accounting, no exploration.
42    pub const NOOP: Self = Self {
43        on_slot_discovery: noop_slot,
44        on_bucket_mark: noop_mark,
45        on_bucket_split: noop_split,
46    };
47}
48
49impl Default for DiscoveryHooks {
50    fn default() -> Self {
51        Self::NOOP
52    }
53}
54
55thread_local! {
56    static HOOKS: Cell<DiscoveryHooks> = const { Cell::new(DiscoveryHooks::NOOP) };
57}
58
59/// Install the discovery hooks. Must be called before forking; children inherit
60/// the hooks via thread-local storage.
61pub fn set_discovery_hooks(hooks: DiscoveryHooks) {
62    HOOKS.with(|h| h.set(hooks));
63}
64
65/// Remove any installed hooks, reverting to pure accounting.
66pub fn clear_discovery_hooks() {
67    HOOKS.with(|h| h.set(DiscoveryHooks::NOOP));
68}
69
70pub(crate) fn on_slot_discovery(slot_idx: usize, hash: u32) {
71    HOOKS.with(|h| (h.get().on_slot_discovery)(slot_idx, hash));
72}
73
74pub(crate) fn on_bucket_mark(hash: u32) {
75    HOOKS.with(|h| (h.get().on_bucket_mark)(hash));
76}
77
78pub(crate) fn on_bucket_split(label: &str, slot_idx: usize) {
79    HOOKS.with(|h| (h.get().on_bucket_split)(label, slot_idx));
80}