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) {}