all_is_cubes/
transaction.rs

1//! The [`Transaction`] trait, for modifying game objects.
2
3#![allow(
4    unused_assignments,
5    reason = "nightly FP <https://github.com/rust-lang/rust/issues/147648>"
6)]
7
8use alloc::string::String;
9use alloc::sync::Arc;
10use core::any::type_name;
11use core::error::Error;
12use core::fmt;
13
14use crate::universe::{Handle, HandleError, UTransactional, UniverseTransaction};
15
16mod equal;
17pub(crate) use equal::*;
18
19mod generic;
20pub use generic::*;
21
22#[cfg(test)]
23mod tester;
24#[cfg(test)]
25pub(crate) use tester::*;
26
27/// A mutation that is to be performed atomically.
28///
29/// A `Transaction` is a description of a mutation to an object or collection thereof that
30/// should occur in a logically atomic fashion (all or nothing), with a set of
31/// preconditions for it to happen at all. (Here we mean [“atomic” in the database sense][atomic],
32/// not in the CPU instruction sense.)
33///
34/// Transactions are used:
35///
36/// * to enable game objects to have effects on their containers in a way compatible
37///   with Rust's ownership rules,
38/// * to avoid bugs of the “item duplication” sort, by checking all preconditions before making
39///   any changes, and
40/// * to avoid update-order-dependent game mechanics by applying effects in batches.
41///
42/// A [`Transaction`] is not consumed by committing it; it may be used repeatedly. Future
43/// work may include building on this to provide undo/redo functionality.
44///
45/// If a transaction implements [`Default`], then the default value should be a
46/// transaction which has no effects and always succeeds, and is cheap to create.
47///
48/// [atomic]: https://en.wikipedia.org/wiki/Atomicity_(database_systems)
49#[must_use]
50pub trait Transaction: Merge {
51    /// Type of the transaction’s target (what it can be used to mutate).
52    type Target;
53
54    /// Type of a value passed from [`Transaction::check`] to [`Transaction::commit`].
55    /// This may be used to pass precalculated values to speed up the commit phase,
56    /// or even lock guards or similar, but also makes it slightly harder to accidentally
57    /// call `commit` without `check`.
58    type CommitCheck: 'static;
59
60    /// Data which must be passed when checking the transaction.
61    /// Use `()` if none is needed.
62    type Context<'a>: Copy;
63
64    /// The results of a [`Transaction::commit()`] or [`Transaction::execute()`].
65    /// Each commit may produce any number of these messages.
66    ///
67    /// The [`Transaction`] trait imposes no requirements on this value, but it may be
68    /// a change-notification message which could be redistributed via the target's
69    /// owner's [`Notifier`](crate::listen::Notifier).
70    type Output;
71
72    /// Error type describing a precondition not met, returned by [`Self::check()`].
73    ///
74    /// This type should be cheap to construct and drop (hopefully `Copy`) if at all
75    /// possible, because checks may be done very frequently during simulation; not every
76    /// such failure is an error of interest to the user.
77    ///
78    /// Accordingly, it might not describe the _entire_ set of unmet preconditions,
79    /// but only one example from it, so as to avoid needing to allocate a
80    /// data structure of arbitrary size.
81    type Mismatch: Error + 'static;
82
83    /// Checks whether the target's current state meets the preconditions and returns
84    /// [`Err`] if it does not.
85    ///
86    /// If the preconditions are met, returns [`Ok`] containing data to be passed to
87    /// [`Transaction::commit()`].
88    fn check(
89        &self,
90        target: &Self::Target,
91        context: Self::Context<'_>,
92    ) -> Result<Self::CommitCheck, Self::Mismatch>;
93
94    /// Perform the mutations specified by this transaction. The `check` value should have
95    /// been created by a prior call to [`Transaction::check()`].
96    ///
97    /// Returns [`Ok`] if the transaction completed normally, and [`Err`] if there was a
98    /// problem which was not detected as a precondition; in this case the transaction may
99    /// have been partially applied, since that problem was detected too late, by
100    /// definition. No [`Err`]s should be seen unless there is a bug.
101    ///
102    /// The `outputs` callback function is called to produce information resulting from
103    /// the transaction; what that information is is up to the individual transaction type.
104    ///
105    /// The target should not be mutated between the call to [`Transaction::check()`] and
106    /// [`Transaction::commit()`] (including via interior mutability, however that applies
107    /// to the particular `Target` type). The consequences of doing so may include mutating the
108    /// wrong components, signaling an error partway through the transaction, or merely
109    /// committing the transaction while its preconditions do not hold.
110    fn commit(
111        self,
112        target: &mut Self::Target,
113        check: Self::CommitCheck,
114        outputs: &mut dyn FnMut(Self::Output),
115    ) -> Result<(), CommitError>;
116
117    /// Convenience method to execute a transaction in one step. Implementations should not
118    /// need to override this. Equivalent to:
119    ///
120    /// ```rust
121    /// # use all_is_cubes::transaction::{Transaction, ExecuteError, no_outputs};
122    /// # use all_is_cubes::universe::{Universe, UniverseTransaction};
123    /// # let transaction = UniverseTransaction::default();
124    /// # let target = &mut Universe::new();
125    /// # let context = ();
126    /// # let outputs = &mut no_outputs;
127    /// let check = transaction.check(target, context).map_err(ExecuteError::Check)?;
128    /// transaction.commit(target, check, outputs).map_err(ExecuteError::Commit)?;
129    /// # Ok::<(), ExecuteError<UniverseTransaction>>(())
130    /// ```
131    ///
132    /// See also: [`Transactional::transact()`], for building a transaction through mutations.
133    fn execute(
134        self,
135        target: &mut Self::Target,
136        context: Self::Context<'_>,
137        outputs: &mut dyn FnMut(Self::Output),
138    ) -> Result<(), ExecuteError<Self>> {
139        let check = self.check(target, context).map_err(ExecuteError::Check)?;
140        self.commit(target, check, outputs).map_err(ExecuteError::Commit)
141    }
142
143    /// Specify the target of this transaction as a [`Handle`], and erase its type,
144    /// so that it can be combined with other transactions in the same universe.
145    ///
146    /// This is a convenience wrapper around [`UTransactional::bind`].
147    fn bind(self, target: Handle<Self::Target>) -> UniverseTransaction
148    where
149        Self: Sized,
150        Self::Target: UTransactional<Transaction = Self>,
151    {
152        UTransactional::bind(target, self)
153    }
154}
155
156/// Merging two transactions (or other values) to produce one result “with
157/// the effect of both”.
158///
159/// Merging is a commutative, fallible operation.
160/// The exact conditions under which it fails are up to the specific type, but generally,
161/// it will fail whenever there is no outcome which reasonably corresponds to
162/// the pair of transactions being executed “simultaneously”, and it will succeed when
163/// the pair of transactions modify independent parts of their target.
164///
165/// This is a separate trait from [`Transaction`] because some components of transactions
166/// are mergeable but not executable in isolation.
167///
168/// TODO: Generalize to different RHS types for convenient combination?
169pub trait Merge: Sized {
170    /// Type of a value passed from [`Merge::check_merge`] to [`Merge::commit_merge`].
171    /// This may be used to pass precalculated values to speed up the merge phase,
172    /// but also makes it difficult to accidentally merge without checking.
173    type MergeCheck: 'static;
174
175    /// Error type giving the reason why a merge was not possible.
176    ///
177    /// This type should be cheap to construct and drop (hopefully `Copy`) if at all possible,
178    /// because merges may be attempted very frequently during simulation; not every such
179    /// failure is an error of interest to the user.
180    ///
181    /// Accordingly, it might not describe the _entire_ area of the conflict
182    /// but only one example from it, so as to avoid needing to allocate a
183    /// data structure of arbitrary size.
184    type Conflict: Error + 'static;
185
186    /// Checks whether two transactions can be merged into a single transaction.
187    /// If so, returns [`Ok`] containing data which may be passed to [`Self::commit_merge()`].
188    ///
189    /// Generally, “can be merged” means that the two transactions do not have mutually
190    /// exclusive preconditions and are not specify conflicting mutations. However, the
191    /// definition of conflict is type-specific; for example, merging two “add 1 to
192    /// velocity” transactions may produce an “add 2 to velocity” transaction.
193    ///
194    /// This is not necessarily the same as either ordering of applying the two
195    /// transactions sequentially. See [`Self::commit_merge()`] for more details.
196    fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict>;
197
198    /// Combines `other` into `self` so that it has both effects simultaneously.
199    /// This operation must be commutative and have [`Default::default()`] as the identity.
200    ///
201    /// May panic if `check` is not the result of a previous call to
202    /// `self.check_merge(&other)` or if either transaction was mutated in the intervening
203    /// time.
204    fn commit_merge(&mut self, other: Self, check: Self::MergeCheck);
205
206    /// Combines two transactions into one which has both effects simultaneously, if possible.
207    ///
208    /// This is a shortcut for calling [`Self::check_merge`] followed by [`Self::commit_merge`].
209    /// It should not be necessary to override the provided implementation.
210    fn merge(mut self, other: Self) -> Result<Self, Self::Conflict> {
211        self.merge_from(other)?;
212        Ok(self)
213    }
214
215    /// Combines two transactions into one which has both effects simultaneously, if possible.
216    ///
217    /// If successful, then `self` now includes `other`. If unsuccessful, `self` is unchanged.
218    ///
219    /// This is a shortcut for calling [`Self::check_merge`] followed by [`Self::commit_merge`].
220    /// It should not be necessary to override the provided implementation.
221    fn merge_from(&mut self, other: Self) -> Result<(), Self::Conflict> {
222        let check = self.check_merge(&other)?;
223        self.commit_merge(other, check);
224        Ok(())
225    }
226}
227
228/// Error type from [`Transaction::execute()`] and [`Transactional::transact()`].
229#[expect(clippy::exhaustive_enums)]
230pub enum ExecuteError<Txn: Transaction = UniverseTransaction> {
231    /// A conflict was discovered between parts that were to be assembled into the transaction.
232    ///
233    /// This error cannot be produced by [`Transaction::execute()`], but only by
234    /// [`Transactional::transact()`].
235    Merge(<Txn as Merge>::Conflict),
236
237    /// The transaction's preconditions were not met; it does not apply to the current
238    /// state of the target. No change has been made.
239    Check(<Txn as Transaction>::Mismatch),
240
241    /// An unexpected error occurred while applying the transaction's effects.
242    /// See the documentation of [`Transaction::commit()`] for the unfortunate
243    /// implications of this.
244    Commit(CommitError),
245
246    /// Executing the transaction required accessing a [`Handle`] that was unavailable.
247    ///
248    /// The [`HandleError`] will include the name of the problematic handle.
249    ///
250    /// This error may be transient, and
251    /// unlike [`ExecuteError::Commit`], does not indicate data corruption,
252    /// but code which triggers it should generally be considered incorrect.
253    ///
254    /// Note that not all cases of failure due to handle access will return this error;
255    /// those means of transaction execution which are specifically asked to
256    /// act on a handle do, whereas handles read during the check phase will produce
257    /// [`ExecuteError::Check`], and so on. This may change in the future.
258    Handle(HandleError),
259}
260
261// Manual impl required to set proper associated type bounds.
262impl<Txn> Clone for ExecuteError<Txn>
263where
264    Txn: Transaction<Mismatch: Clone> + Merge<Conflict: Clone>,
265{
266    fn clone(&self) -> Self {
267        match self {
268            Self::Merge(e) => Self::Merge(e.clone()),
269            Self::Check(e) => Self::Check(e.clone()),
270            Self::Commit(e) => Self::Commit(e.clone()),
271            Self::Handle(e) => Self::Handle(e.clone()),
272        }
273    }
274}
275
276impl<Txn> Error for ExecuteError<Txn>
277where
278    Txn: Transaction<Mismatch: Error + 'static> + Merge<Conflict: Error + 'static>,
279{
280    fn source(&self) -> Option<&(dyn Error + 'static)> {
281        match self {
282            ExecuteError::Merge(e) => e.source(),
283            ExecuteError::Check(e) => e.source(),
284            ExecuteError::Commit(e) => e.source(),
285            ExecuteError::Handle(e) => e.source(),
286        }
287    }
288}
289
290impl<Txn> fmt::Debug for ExecuteError<Txn>
291where
292    Txn: Transaction<Mismatch: fmt::Debug> + Merge<Conflict: fmt::Debug>,
293{
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        match self {
296            Self::Merge(e) => f.debug_tuple("Merge").field(e).finish(),
297            Self::Check(e) => f.debug_tuple("Check").field(e).finish(),
298            Self::Commit(e) => f.debug_tuple("Commit").field(e).finish(),
299            Self::Handle(e) => f.debug_tuple("Handle").field(e).finish(),
300        }
301    }
302}
303
304impl<Txn> fmt::Display for ExecuteError<Txn>
305where
306    Txn: Transaction<Mismatch: fmt::Display> + Merge<Conflict: fmt::Display>,
307{
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        match self {
310            ExecuteError::Merge(e) => e.fmt(f),
311            ExecuteError::Check(e) => e.fmt(f),
312            ExecuteError::Commit(e) => e.fmt(f),
313            ExecuteError::Handle(e) => e.fmt(f),
314        }
315    }
316}
317
318/// Note: [`ExecuteError::Commit`] never compares equal, because it contains
319/// arbitrary errors which may not implement [`PartialEq`].
320/// TODO: push this down to `impl PartialEq for CommitError` for more precision.
321impl<Txn> PartialEq for ExecuteError<Txn>
322where
323    Txn: Transaction<Mismatch: PartialEq> + Merge<Conflict: PartialEq>,
324{
325    fn eq(&self, other: &Self) -> bool {
326        match (self, other) {
327            (Self::Merge(a), Self::Merge(b)) => a == b,
328            (Self::Check(a), Self::Check(b)) => a == b,
329            (Self::Commit(_), Self::Commit(_)) => false,
330            (Self::Handle(a), Self::Handle(b)) => a == b,
331            _ => false,
332        }
333    }
334}
335
336/// Type of “unexpected errors” from [`Transaction::commit()`].
337//---
338// Design note: `CommitError` doesn't need to be cheap because it should never happen
339// during normal game operation; it exists because we want to do better than panicking
340// if it does, and give a report that's detailed enough that someone might be able to
341// fix the underlying bug.
342#[derive(Clone, Debug, displaydoc::Display)]
343#[displaydoc("Unexpected error while committing a transaction")]
344pub struct CommitError(CommitErrorKind);
345
346#[derive(Clone, Debug, displaydoc::Display)]
347enum CommitErrorKind {
348    #[displaydoc("{transaction_type}::commit() failed")]
349    Leaf {
350        transaction_type: &'static str,
351        error: Arc<dyn Error + Send + Sync>,
352    },
353    #[displaydoc("{transaction_type}::commit() failed: {message}")]
354    LeafMessage {
355        transaction_type: &'static str,
356        message: String,
357    },
358    /// One of the component transactions in this transaction failed.
359    #[displaydoc("in transaction part '{component}'")]
360    Context {
361        component: String,
362        error: Arc<CommitError>, // must box recursion, might as well Arc
363    },
364}
365
366impl CommitError {
367    /// Wrap an arbitrary unexpected error as a [`CommitError`].
368    /// `T` should be the type of the transaction that caught it.
369    #[must_use]
370    pub fn catch<T, E: Error + Send + Sync + 'static>(error: E) -> Self {
371        CommitError(CommitErrorKind::Leaf {
372            transaction_type: type_name::<T>(),
373            error: Arc::new(error),
374        })
375    }
376
377    /// Construct a [`CommitError`] with a string description.
378    /// `T` should be the type of the transaction that detected the problem.
379    #[must_use]
380    pub fn message<T>(message: String) -> Self {
381        CommitError(CommitErrorKind::LeafMessage {
382            transaction_type: type_name::<T>(),
383            message,
384        })
385    }
386
387    /// Report an error propagating up from an inner transaction.
388    /// `component` should describe which part of the current transaction
389    /// returned the error from its `commit()`.
390    #[must_use]
391    pub fn context(self, component: String) -> Self {
392        CommitError(CommitErrorKind::Context {
393            component,
394            error: Arc::new(self),
395        })
396    }
397}
398
399impl Error for CommitError {
400    fn source(&self) -> Option<&(dyn Error + 'static)> {
401        match &self.0 {
402            CommitErrorKind::Leaf { error, .. } => Some(error),
403            CommitErrorKind::LeafMessage { .. } => None,
404            CommitErrorKind::Context { error, .. } => Some(error),
405        }
406    }
407}
408
409/// Specifies a canonical [`Transaction`] type for the implementing type.
410///
411/// For a given `T`, [`Transaction<Target = T>`] may be implemented by multiple types,
412/// but there can be at most one `<T as Transactional>::Transaction`.
413pub trait Transactional {
414    /// The type of transaction which should be used with `Self`.
415    type Transaction: Transaction<Target = Self>;
416
417    /// Convenience method for building and then applying a transaction to `self`,
418    /// equivalent to the following steps:
419    ///
420    /// 1. Call [`default()`](Default::default()) to create the transaction.
421    /// 2. Mutate the transaction using the function `f`.
422    /// 3. Call [`Transaction::execute()`] with `self`.
423    ///
424    /// `f` is given an empty transaction to write into,
425    /// and a reference to `self` in case it is needed.
426    /// It may return merge conflict errors, or a successful return value of any type.
427    ///
428    /// The transaction must not have outputs.
429    ///
430    /// # Design note
431    ///
432    /// Ideally, we would have an `async` version of this function too, but that
433    /// is not possible, because the required borrowing pattern is not currently
434    /// expressible when writing the future-returning closure it would require.
435    fn transact<'c, F, O>(&mut self, f: F) -> Result<O, ExecuteError<Self::Transaction>>
436    where
437        F: FnOnce(
438            &mut Self::Transaction,
439            &Self,
440        ) -> Result<O, <Self::Transaction as Merge>::Conflict>,
441        Self::Transaction:
442            Transaction<Target = Self, Context<'c> = (), Output = NoOutput> + Default,
443    {
444        let mut transaction = Self::Transaction::default();
445        let output = f(&mut transaction, self).map_err(ExecuteError::Merge)?;
446        transaction.execute(self, (), &mut no_outputs)?;
447        Ok(output)
448    }
449}
450
451/// Type of `Output` for a [`Transaction`] that never produces any outputs.
452pub type NoOutput = core::convert::Infallible; // TODO: use `!` never type if it stabilizes
453
454/// Output callback function for committing a [`Transaction`] whose `Output` type is
455/// [`NoOutput`] and therefore cannot produce any outputs.
456pub fn no_outputs(_: NoOutput) {}