1
use std::collections::VecDeque;
2
use std::sync::OnceLock;
3
use std::sync::atomic::AtomicBool;
4
use std::sync::atomic::AtomicUsize;
5
use std::sync::atomic::Ordering;
6

            
7
use crossbeam_deque::Steal;
8
use crossbeam_deque::Stealer;
9
use crossbeam_deque::Worker;
10
use crossbeam_utils::Backoff;
11
use rand::RngExt;
12
use rand::SeedableRng;
13
use rand::rngs::SmallRng;
14

            
15
use merc_lts::StateIndex;
16
use merc_utilities::MercError;
17
use merc_utilities::Timing;
18

            
19
use crate::DiscoveredSet;
20
use crate::LPS;
21
use crate::SequenceForestContext;
22
use crate::StateRef;
23
use crate::Summand;
24

            
25
/// Order in which discovered states are explored.
26
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
28
#[derive(Default)]
29
pub enum ExplorationStrategy {
30
    /// Depth-first exploration.
31
    #[default]
32
    Dfs,
33
    /// Breadth-first exploration.
34
    Bfs,
35
}
36

            
37
/// Explores the state space of `lps`, invoking caller-supplied closures for
38
/// each discovered state and transition.
39
///
40
/// # Closures
41
///
42
/// The mutable caller context `ctx`  is passed into
43
/// every closure invocation so the closures themselves can stay
44
/// non-capturing of the outer mutable state.
45
///
46
/// - `on_state(ctx, state_index, &state_info)` is called exactly once per
47
///   discovered state, in the order the state was popped from the working set,
48
///   after the implementation has been prepared for that state.
49
/// - `on_transition(ctx, from, &label, to)` is called for every transition
50
///   produced by the summands.
51
///
52
/// Returns the [`StateIndex`] assigned to the initial state of `lps`. The
53
/// caller is responsible for finalising any builder it owns; counting states
54
/// and transitions (and any progress reporting) is left to the closures.
55
66
pub fn explore<P, Ctx, OnState, OnTransition>(
56
66
    lps: &P,
57
66
    strategy: ExplorationStrategy,
58
66
    timing: &Timing,
59
66
    ctx: &mut Ctx,
60
66
    mut on_state: OnState,
61
66
    mut on_transition: OnTransition,
62
66
) -> Result<StateIndex, MercError>
63
66
where
64
66
    P: LPS,
65
66
    OnState: FnMut(&mut Ctx, StateIndex, &P::StateInfo) -> Result<(), MercError>,
66
66
    OnTransition: FnMut(&mut Ctx, StateIndex, &P::Label, StateIndex) -> Result<(), MercError>,
67
{
68
66
    let discovered: DiscoveredSet<P::Value> = DiscoveredSet::new();
69
66
    let (initial_ref, _) = discovered.insert(&lps.initial_state());
70

            
71
66
    let mut working: VecDeque<StateRef> = VecDeque::from([initial_ref]);
72
    // Reusable buffer holding the current state vector reconstructed from the
73
    // discovered set, avoiding an allocation per explored state.
74
66
    let mut current_state: Vec<P::Value> = Vec::new();
75
    // Reused interning scratch buffers, avoiding a reallocation per inserted
76
    // state.
77
66
    let mut forest_context = SequenceForestContext::new();
78
    // Per-thread enumeration context owning the backend and scratch buffers.
79
    // Single-threaded loop, so one context suffices.
80
66
    let mut enumerate_context = lps.create_context();
81

            
82
66
    timing.measure("explore", || -> Result<(), MercError> {
83
        loop {
84
1033
            let current = match strategy {
85
888
                ExplorationStrategy::Dfs => working.pop_back(),
86
145
                ExplorationStrategy::Bfs => working.pop_front(),
87
            };
88
1033
            let Some(current) = current else { break };
89

            
90
            // Reconstruct the state vector into the reusable buffer so
91
            // `discovered` can be mutated by inserts inside the summand callback
92
            // below.
93
968
            let found = discovered.get_into(current, &mut current_state);
94
968
            debug_assert!(found, "StateRef from working queue must be valid");
95
968
            let from = StateIndex::new(current.index());
96
968
            let summands_to_explore = lps.prepare(&mut enumerate_context, &current_state);
97

            
98
968
            let info = lps.state_info(&current_state);
99
968
            on_state(ctx, from, &info)?;
100

            
101
968
            let summands = lps.summands();
102
2891
            for index in summands_to_explore {
103
2891
                summands[index].enumerate(&mut enumerate_context, &current_state, |label, next_state| {
104
2331
                    let (target_ref, is_new) = discovered.insert_with(next_state, &mut forest_context);
105
2331
                    let to = StateIndex::new(target_ref.index());
106
2331
                    on_transition(ctx, from, label, to)?;
107
2330
                    if is_new {
108
902
                        working.push_back(target_ref);
109
1428
                    }
110
2330
                    Ok(())
111
2331
                })?;
112
            }
113
        }
114

            
115
65
        Ok(())
116
66
    })?;
117

            
118
65
    Ok(StateIndex::new(initial_ref.index()))
119
66
}
120

            
121
/// Explores the state space of `lps` in parallel using continuous, work-stealing
122
/// breadth-/depth-first search driven by `rayon`.
123
///
124
/// The interface is similar to [`explore`], but an extra closure is supplied to
125
/// produce a fresh per-thread accumulator for each worker.
126
///
127
/// - `make_local()` produces a fresh accumulator for each worker.
128
///
129
/// The returned `Vec<Local>` holds one accumulator per worker (one per pool
130
/// thread), each carrying the merged result of every state that worker
131
/// processed.
132
///
133
/// # State numbering
134
///
135
/// The [`StateIndex`] values passed to the closures come from the shared
136
/// [`DiscoveredSet`], whose backing store hands each worker a whole block of
137
/// consecutive indices at once to keep index allocation contention-free. This
138
/// means that the returned state indices are not dense.
139
28
pub fn explore_parallel<P, Local, MakeLocal, OnState, OnTransition>(
140
28
    lps: &P,
141
28
    make_local: MakeLocal,
142
28
    on_state: OnState,
143
28
    on_transition: OnTransition,
144
28
) -> Result<(StateIndex, Vec<Local>), MercError>
145
28
where
146
28
    P: LPS + Sync,
147
28
    P::Value: Send + Sync,
148
28
    <P::Summand as Summand>::Context: Send,
149
28
    Local: Send,
150
28
    MakeLocal: Fn() -> Local + Sync,
151
28
    OnState: Fn(&mut Local, StateIndex, &P::StateInfo) -> Result<(), MercError> + Sync,
152
28
    OnTransition: Fn(&mut Local, StateIndex, &P::Label, StateIndex) -> Result<(), MercError> + Sync,
153
{
154
28
    let discovered: DiscoveredSet<P::Value> = DiscoveredSet::new();
155
28
    let (initial_ref, _) = discovered.insert(&lps.initial_state());
156

            
157
28
    let num_workers = rayon::current_num_threads().max(1);
158

            
159
    // One publication slot per worker.
160
112
    let stealers: Vec<OnceLock<Stealer<StateRef>>> = (0..num_workers).map(|_| OnceLock::new()).collect();
161

            
162
    // Count of states discovered but not yet fully processed; exploration is
163
    // complete once it reaches zero.
164
28
    let pending = AtomicUsize::new(1);
165
    // Set when any worker's callback returns an error so the others stop promptly.
166
28
    let aborted = AtomicBool::new(false);
167

            
168
112
    let results = rayon::broadcast(|broadcast| -> (Local, Option<MercError>) {
169
112
        let me = broadcast.index();
170

            
171
        // Each worker owns its deque, created here on its own thread
172
        // Publish its stealer so peers can balance against it.
173
112
        let local_deque: Worker<StateRef> = Worker::new_lifo();
174
112
        let _ = stealers[me].set(local_deque.stealer());
175

            
176
        // The first worker seeds the initial state onto its own deque; the
177
        // others pick it up by stealing.
178
112
        if me == 0 {
179
28
            local_deque.push(initial_ref);
180
84
        }
181

            
182
        // Thread-affine per-worker scratch and enumeration backend, created
183
        // and dropped on this same physical worker thread.
184
112
        let mut context = lps.create_context();
185
112
        let mut forest_context = SequenceForestContext::new();
186
112
        let mut state_buf: Vec<P::Value> = Vec::new();
187
        // Successors discovered while processing one state, counted in a
188
        // single atomic update and pushed onto the deque only afterwards.
189
112
        let mut successors: Vec<StateRef> = Vec::new();
190
112
        let mut local = make_local();
191
112
        let mut first_error: Option<MercError> = None;
192

            
193
        // Per-worker work-stealing instrumentation, emitted at debug level
194
        // when this worker finishes.
195
112
        let mut stats = StealMetrics::default();
196

            
197
        // Per-worker RNG driving randomized victim selection while stealing.
198
112
        let mut rng = SmallRng::from_rng(&mut rand::rng());
199

            
200
112
        let backoff = Backoff::new();
201
        loop {
202
1729
            if aborted.load(Ordering::Relaxed) {
203
3
                break;
204
1726
            }
205

            
206
1726
            let Some(state_ref) = find_task(&local_deque, &stealers, me, &mut rng, &mut stats) else {
207
                // No work found this sweep. Stop once every state has been
208
                // processed, otherwise keep trying: a busy peer may still
209
                // discover successors that become stealable.
210
1317
                if pending.load(Ordering::Acquire) == 0 {
211
108
                    break;
212
1209
                }
213
1209
                stats.idle_sweeps += 1;
214
1209
                backoff.snooze();
215
1209
                continue;
216
            };
217
409
            backoff.reset();
218

            
219
409
            let result = (|| -> Result<(), MercError> {
220
409
                let found = discovered.get_into(state_ref, &mut state_buf);
221
409
                debug_assert!(found, "StateRef from work queue must be valid");
222
409
                let from = StateIndex::new(state_ref.index());
223
409
                let summands_to_explore = lps.prepare(&mut context, &state_buf);
224

            
225
409
                let info = lps.state_info(&state_buf);
226
409
                on_state(&mut local, from, &info)?;
227

            
228
409
                let summands = lps.summands();
229
1228
                for index in summands_to_explore {
230
1228
                    summands[index].enumerate(&mut context, &state_buf, |label, next_state| {
231
991
                        let (target_ref, is_new) = discovered.insert_with(next_state, &mut forest_context);
232
991
                        let to = StateIndex::new(target_ref.index());
233
991
                        on_transition(&mut local, from, label, to)?;
234
990
                        if is_new {
235
381
                            successors.push(target_ref);
236
609
                        }
237
990
                        Ok(())
238
991
                    })?;
239
                }
240
408
                Ok(())
241
            })();
242

            
243
409
            match result {
244
                Ok(()) => {
245
                    // Apply this state's net effect on the outstanding-work
246
                    // count in a single atomic update.
247
408
                    let produced = successors.len();
248
408
                    if produced == 0 {
249
153
                        pending.fetch_sub(1, Ordering::AcqRel);
250
153
                    } else {
251
255
                        pending.fetch_add(produced - 1, Ordering::AcqRel);
252
381
                        for successor in successors.drain(..) {
253
381
                            local_deque.push(successor);
254
381
                        }
255
                    }
256
                }
257
1
                Err(err) => {
258
                    // Abort: the pending counter is left as-is since `aborted`
259
                    // now drives every worker to stop, independent of it.
260
1
                    first_error = Some(err);
261
1
                    aborted.store(true, Ordering::Relaxed);
262
1
                    break;
263
                }
264
            }
265
        }
266

            
267
112
        stats.log(me);
268

            
269
112
        (local, first_error)
270
112
    });
271

            
272
    // Surface the first error in worker-index order, otherwise return one
273
    // accumulator per worker.
274
28
    let mut first_error: Option<MercError> = None;
275
28
    let mut locals = Vec::with_capacity(results.len());
276
112
    for (local, error) in results {
277
112
        if first_error.is_none() {
278
110
            first_error = error;
279
110
        }
280
112
        locals.push(local);
281
    }
282
28
    if let Some(error) = first_error {
283
1
        return Err(error);
284
27
    }
285

            
286
27
    Ok((StateIndex::new(initial_ref.index()), locals))
287
28
}
288

            
289
/// Per-worker work-stealing counters, collected while a worker runs and logged
290
/// at debug level when it finishes. Used to diagnose load balancing: a healthy
291
/// run keeps the stolen fraction modest and idle sweeps low.
292
#[derive(Default)]
293
struct StealMetrics {
294
    /// Tasks taken from the worker's own deque.
295
    local_pops: u64,
296
    /// Tasks obtained by stealing a batch from a peer.
297
    steals: u64,
298
    /// `Steal::Retry` results observed while stealing (CAS contention with peers).
299
    steal_retries: u64,
300
    /// Sweeps that found no work anywhere, each followed by a snooze.
301
    idle_sweeps: u64,
302
}
303

            
304
impl StealMetrics {
305
    /// Emits this worker's counters at debug level.
306
560
    fn log(&self, worker: usize) {
307
560
        let processed = self.local_pops + self.steals;
308
560
        let stolen_pct = if processed == 0 {
309
410
            0.0
310
        } else {
311
150
            100.0 * self.steals as f64 / processed as f64
312
        };
313
560
        log::debug!(
314
            "explore worker {worker}: processed {processed} states ({} local, {} stolen = {stolen_pct:.1}%), \
315
             {} steal retries, {} idle sweeps",
316
            self.local_pops,
317
            self.steals,
318
            self.steal_retries,
319
            self.idle_sweeps,
320
        );
321
560
    }
322
}
323

            
324
/// Finds the next state for a worker to process: its own deque first, then a
325
/// batch stolen from a peer worker. Each attempt samples a random peer index
326
/// (`me` is skipped). Stops at the first peer that yields a task.
327
///
328
/// On a sweep with no successful steal the function returns `None` regardless of
329
/// whether peers were empty or merely contended (`Steal::Retry`). The caller's
330
/// loop then re-checks the `pending` counter and `aborted` flag before snoozing
331
/// and trying again, so retries are paced by the outer backoff and a worker can
332
/// never spin here in isolation while the run is shutting down.
333
1726
fn find_task<T>(
334
1726
    local: &Worker<T>,
335
1726
    stealers: &[OnceLock<Stealer<T>>],
336
1726
    me: usize,
337
1726
    rng: &mut SmallRng,
338
1726
    stats: &mut StealMetrics,
339
1726
) -> Option<T> {
340
1726
    if let Some(task) = local.pop() {
341
401
        stats.local_pops += 1;
342
401
        return Some(task);
343
1325
    }
344

            
345
    // Try up to one random peer per worker; some peers may be sampled more
346
    // than once and others not at all, which is fine since a sweep that finds
347
    // nothing is retried by the caller.
348
1325
    for _ in 0..stealers.len() {
349
5285
        let index = rng.random_range(0..stealers.len());
350
5285
        if index == me {
351
1323
            continue;
352
3962
        }
353

            
354
3962
        let Some(stealer) = stealers[index].get() else {
355
1723
            continue;
356
        };
357

            
358
2239
        match stealer.steal_batch_and_pop(local) {
359
8
            Steal::Success(task) => {
360
8
                stats.steals += 1;
361
8
                return Some(task);
362
            }
363
1
            Steal::Retry => stats.steal_retries += 1,
364
2230
            Steal::Empty => {}
365
        }
366
    }
367

            
368
1317
    None
369
1726
}