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}