gpp_solver/
lib.rs

1//! Generic Push-Pull Solver.
2//!
3//! This crate implements a generic solver for anything that can have a clear dependency graph.
4//! The implementation is a mix of push (eager) and pull (lazy) architectures with user-driven
5//! recursion.
6//!
7//! Functionality is centered on the [`Solver`] struct. Users record all *fragments* they want to
8//! evaluate and only those. Fragments are represented by an integral [`FragmentId`], but what
9//! *is* a fragment is arbitrary and the solver does not care. It may represent a variable, an
10//! action, an object, or anything else.
11//!
12//! Users must also implement the [`Problem`] trait, which defines a dependency graph and an
13//! interface for evaluating fragments that the solver finds are both solvable and required. This
14//! dependency graph does not need to be complete or explicit, as long as implementors can return
15//! the direct dependencies of specified fragments as the solver explores the dependency graph.
16//!
17//! [`Solver::run`] and [`Solver::step`] will incrementally explore the depedency graph and call
18//! [`Problem::evaluate`] on fragments that have all of its dependencies met.
19//!
20//! In the end, all requested fragments will either have been evaluated or will be proven to be
21//! part of a dependency cycle. The user may choose to report cycles as errors, or break them with
22//! [`Solver::assume_evaluated`] or [`Solver::clone_with_evaluation_assumptions`]. See also
23//! [`Solver::status`].
24//!
25//! [`Solver::punted_iter`] will return an iterator yielding all fragments that have been *punted*
26//! so far. A punted fragment is one that has been considered for evaluation but its dependencies
27//! haven't been met yet. If the solver is done, punted fragments must be part of at least one
28//! cycle.
29//!
30//! # Concurrency
31//!
32//! [`Solver`] is fully asynchronous but the core algorithm is not parallel at the moment. Running
33//! multiple [`Solver::step`] concurrently or calling [`Solver::run`] with `concurrency > 1` is
34//! safe but will not make the solver itself run faster. What this does allow is for multiple
35//! [`Problem::direct_dependencies`] and [`Problem::evaluate`] calls to run concurrently.
36//!
37//! # Build Features
38//!
39//! This crate has multiple features. From those, there are three where users must specify exactly
40//! one of: `futures-lock`, `tokio-lock`, or `async-std-lock`. Use whichever is most convenient.
41//!
42//! ## `std`
43//!
44//! Use std. [`Solver::run`] will be unavailable if `std` is disabled. **TODO**: actually make
45//! this assertion true.
46//!
47//! ## `js-bindings`
48//!
49//! Build the JavaScript API if building for WASM.
50//!
51//! ## `futures-lock`
52//!
53//! Use the locks implemented by the `futures` crate.
54//!
55//! ## `tokio-lock`
56//!
57//! Use the locks implemented by the `tokio` crate.
58//!
59//! ## `async-std-lock`
60//!
61//! Use the locks implemented by the `async-lock` crate.
62//!
63//! # Internals
64//!
65//! [`Solver`] implements a hybrid push-pull architecture. Fragments are only evaluated if needed
66//! (pull, lazy evaluation), but instead of evaluating dependencies recursively, this process will
67//! only evaluate fragments that already have all of its *direct* dependencies met. If that's not
68//! the case, the fragment will be *punted*: stored away and only considered again if *all* its
69//! dependencies are met sometime in the future.
70//!
71//! On the other hand, if a fragment is successfully evaluated, punted fragments that depend on it
72//! will be evaluated eagerly (push) if all other dependencies have also been evaluated.
73//!
74//! This architecture has two major advantages:
75//!
76//! - It is lazy. Only fragments that are explicitly requested to be evaluated, and the fragments
77//!   those depend on, will be evaluated. And never more than once.
78//! - There is no need to explicitly detect nor handle cycles, unlike both pure push and pure
79//!   pull. Fragments that are part of cycles will naturally be punted and never considered again.
80//!   Unless the cycle is explicitly broken with [`Solver::assume_evaluated`] or
81//!   [`Solver::clone_with_evaluation_assumptions`]. This enables a much simpler implementation.
82
83#![cfg_attr(not(feature = "std"), no_std)]
84
85// Only used when testing
86#[cfg(test)]
87macro_rules! family_cfg {
88    (for $name:literal; $($item:item)*) => {
89        $(
90            #[cfg(target_family = $name)]
91            $item
92        )*
93    };
94    (for !$name:literal; $($item:item)*) => {
95        $(
96            #[cfg(not(target_family = $name))]
97            $item
98        )*
99    };
100}
101
102macro_rules! feature_cfg {
103    (for $name:literal; $($item:item)*) => {
104        $(
105            #[cfg(feature = $name)]
106            $item
107        )*
108    };
109    (for !$name:literal; $($item:item)*) => {
110        $(
111            #[cfg(not(feature = $name))]
112            $item
113        )*
114    };
115}
116
117use crate::reexported::{iter, Box, Map, Mutex, NonZeroUsize, Set, Vec};
118use async_trait::async_trait;
119use derive_more::{From, Into};
120use futures::stream::{FuturesUnordered, StreamExt};
121
122pub mod reexported;
123
124#[cfg(all(feature = "js-bindings", target_family = "wasm"))]
125mod js;
126
127#[cfg(test)]
128mod test;
129
130/// Trait implemented by objects that define a specific problem to be solved by the [`Solver`].
131///
132/// Use [`mod@async_trait`] to implement this trait.
133#[async_trait]
134pub trait Problem {
135    /// Error type for [`Problem::evaluate`].
136    type Error;
137
138    /// Fill `dependencies` with the direct dependencies of `id`. The output vector is guaranteed
139    /// to be empty when this method is called.
140    async fn direct_dependencies(
141        &self,
142        id: FragmentId,
143        dependecies: &mut Vec<FragmentId>,
144    );
145
146    /// Called by the solver to signal that a fragment has had all of its dependencies evaluated.
147    /// Thus, the fragment should be evaluated too.
148    ///
149    /// See [`Solver::run`] and [`Solver::step`] on how evaluation failures are handled.
150    ///
151    /// This method is never called more than once with the same fragment.
152    async fn evaluate(&self, id: FragmentId) -> Result<(), Self::Error>;
153}
154
155/// ID of a fragment.
156// TODO: allow `Problem` implementors to define their own ID type
157#[derive(
158    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, From, Into,
159)]
160pub struct FragmentId(pub usize);
161
162/// Hybrid push-pull solver.
163pub struct Solver<P> {
164    state: Mutex<State>,
165    // This is a scratch vector we store here to reduce allocations
166    dependencies: Mutex<Vec<FragmentId>>,
167    problem_instance: P,
168}
169
170// POD struct
171struct State {
172    // TODO: these should be an intrusive copy-on-write to make cloning and testing alternatives
173    // cheap
174    to_solve: Set<FragmentId>,
175    pending_on: Map<FragmentId, Vec<FragmentId>>,
176    punted: Map<FragmentId, usize>,
177    solved: Set<FragmentId>,
178}
179
180impl<P> Solver<P> {
181    /// Create a new [`Solver`] instance for a [`Problem`].
182    pub fn new(problem_instance: P) -> Self {
183        Self {
184            state: Mutex::new(State {
185                to_solve: Set::new(),
186                pending_on: Map::new(),
187                punted: Map::new(),
188                solved: Set::new(),
189            }),
190            dependencies: Mutex::new(Vec::new()),
191            problem_instance,
192        }
193    }
194
195    /// Consume `self` and return the wrapped [`Problem`] instance.
196    pub fn into_problem_instance(self) -> P {
197        self.problem_instance
198    }
199
200    /// Get the current [`Status`] of the solver.
201    pub async fn status(&self) -> Status {
202        let state = self.state.lock().await;
203
204        if state.to_solve.is_empty() {
205            if state.punted.is_empty() {
206                Status::Done
207            } else {
208                Status::DoneWithCycles
209            }
210        } else {
211            Status::Pending
212        }
213    }
214
215    /// Enqueue a fragment to be solved.
216    ///
217    /// Only fragments enqueued through this method and their transitive dependencies will be
218    /// considered for evaluation.
219    pub async fn enqueue_fragment(&self, id: FragmentId) -> &Self {
220        self.state.lock().await.to_solve.insert(id);
221
222        self
223    }
224
225    /// Get an interator to all fragments that are currently punted. Interpretation of punted
226    /// fragments depends on the current [status](Solver::status):
227    ///
228    /// - [`Status::Pending`]: fragments are pending on dependencies.
229    /// - [`Status::DoneWithCycles`]: fragments are part of one or more cycles.
230    /// - [`Status::Done`]: the returned iterator will be empty.
231    pub async fn punted_iter(&self) -> Vec<FragmentId> {
232        self.state.lock().await.punted.keys().copied().collect()
233    }
234}
235
236impl<P> Solver<P>
237where
238    P: Problem,
239{
240    /// Assume the given fragment is already evaluated.
241    pub async fn assume_evaluated(&self, id: FragmentId) -> &Self {
242        self.mark_solved(id, &mut *self.state.lock().await);
243
244        self
245    }
246
247    /* TODO: rethink about cloning in general
248    /// Create a clone of `self` that assumes some fragments are already evaluated.
249    ///
250    /// This method is useful for trying out assumptions that may need to be discarted.
251    pub async fn clone_with_evaluation_assumptions<A>(
252        &self,
253        assume_evaluated: A,
254    ) -> Self
255    where
256        A: IntoIterator<Item = FragmentId>,
257        P: Clone,
258    {
259        let clone = self.clone();
260        for id in assume_evaluated {
261            clone.assume_evaluated(id).await;
262        }
263
264        clone
265    }
266    */
267
268    /// Run the solver until all enqueued fragments and their transitive dependencies are either
269    /// solved or proven to be part of at least one cycle. See the module docs for the limitations
270    /// when `concurrency > 1`.
271    ///
272    /// Returns an interator with all fragments that are part of at least one cycle, if any. See
273    /// [`Solver::punted_iter`].
274    ///
275    /// Returns an error if any evaluation returns an error.
276    ///
277    /// # Known Issues
278    ///
279    /// - If [`Solver::enqueue_fragment`] is called while [`Solver::run`] is executing, those new
280    ///   fragments may not be solved.
281    /// - If [`Solver::run`] returns with an error, the [`Solver`] may be left in an inconsistent
282    ///   state.
283    pub async fn run(
284        &self,
285        concurrency: NonZeroUsize,
286    ) -> Result<Vec<FragmentId>, P::Error> {
287        let mut steps = iter::repeat_with(|| self.step())
288            .take(concurrency.into())
289            .collect::<FuturesUnordered<_>>();
290        loop {
291            // Run a `parallelism` number of `step`s until one of them errors out or we evaluate
292            // all fragments
293            match steps.next().await.unwrap() {
294                Ok(false) => break,
295                Ok(true) => steps.push(self.step()),
296                Err(err) => return Err(err),
297            }
298        }
299        while let Some(res) = steps.next().await {
300            // Make sure all pending `step`s are evaluated to completion
301            if let Err(err) = res {
302                return Err(err);
303            }
304        }
305
306        Ok(self.punted_iter().await)
307    }
308
309    /// Run a single solver step for a single fragment.
310    ///
311    /// Returns `false` if there are no more fragments that can be evaluated.
312    ///
313    /// Returns an error if [`Problem::evaluate`] was called and evaluation returned an error.
314    ///
315    /// # Known Issues
316    ///
317    /// - If [`Solver::step`] is not run to completion the [`Solver`] may be left in an
318    ///   inconsistent state.
319    pub async fn step(&self) -> Result<bool, P::Error> {
320        let item = {
321            let mut state = self.state.lock().await;
322
323            state
324                .to_solve
325                .iter()
326                .next()
327                .copied()
328                .map(|x| state.to_solve.take(&x).unwrap())
329        };
330
331        match item {
332            Some(id) => {
333                let mut dependencies = self.dependencies.lock().await;
334                dependencies.clear();
335                self.problem_instance
336                    .direct_dependencies(id, &mut dependencies)
337                    .await;
338                let mut state = self.state.lock().await;
339                dependencies.retain(|x| !state.solved.contains(x));
340
341                if dependencies.is_empty() {
342                    // Drop all locks before calling `evaluate`to allow other calls to `step` to
343                    // progress while `evaluate` is running. And we only need to lock `self.state`
344                    // again if `evaluate` is successful
345                    drop(dependencies);
346                    drop(state);
347
348                    match self.problem_instance.evaluate(id).await {
349                        Ok(()) => {
350                            // TODO: take a deeper look here to make sure there are no possible
351                            // race condition between dropping the state lock and locking it again
352                            // here
353                            self.mark_solved(id, &mut *self.state.lock().await);
354
355                            Ok(true)
356                        }
357                        Err(err) => Err(err),
358                    }
359                } else {
360                    self.mark_punted(id, &dependencies, &mut state);
361
362                    Ok(true)
363                }
364            }
365            None => Ok(false),
366        }
367    }
368
369    fn mark_solved(&self, id: FragmentId, state: &mut State) {
370        state.solved.insert(id);
371
372        if let Some(dependents) = state.pending_on.remove(&id) {
373            for dependent in dependents {
374                if *state.punted.get(&dependent).unwrap() == 1 {
375                    state.punted.remove(&dependent);
376                    state.to_solve.insert(dependent);
377                } else {
378                    *state.punted.get_mut(&dependent).unwrap() -= 1;
379                }
380            }
381        }
382    }
383
384    fn mark_punted(
385        &self,
386        id: FragmentId,
387        dependencies: &[FragmentId],
388        state: &mut State,
389    ) {
390        state.punted.insert(id, dependencies.len());
391
392        for dependency in dependencies.iter().copied() {
393            if dependency != id
394                && !state.solved.contains(&dependency)
395                && !state.punted.contains_key(&dependency)
396            {
397                state.to_solve.insert(dependency);
398            }
399            state.pending_on.entry(dependency).or_default().push(id);
400        }
401    }
402}
403
404/// Current status of a [`Solver`] instance.
405#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
406pub enum Status {
407    /// All fragments have been successfully evaluated and no cycles were found.
408    Done,
409
410    /// All fragments that could be evaluated were evaluated, but some fragments were part of at
411    /// least one dependency cycle and thus could not be evaluated.
412    DoneWithCycles,
413
414    /// The solver is still running and there are still fragments that may be evaluated.
415    Pending,
416}