all_is_cubes/universe/
universe_txn.rs

1use alloc::boxed::Box;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4use core::any::Any;
5use core::fmt;
6
7use hashbrown::HashMap as HbHashMap;
8
9use crate::behavior;
10use crate::transaction::{self, CommitError, Equal, Merge, Transaction, Transactional};
11#[cfg(doc)]
12use crate::universe::HandleError;
13use crate::universe::{
14    AnyHandle, ErasedHandle, Handle, InsertError, InsertErrorKind, Name, UBorrowMut, Universe,
15    UniverseId, UniverseMember, UniverseTable,
16};
17
18// Reƫxports for macro-generated types
19#[doc(inline)]
20pub(in crate::universe) use crate::universe::members::{
21    AnyTransaction, AnyTransactionConflict, AnyTransactionMismatch,
22};
23
24/// Conversion from concrete transaction types to [`UniverseTransaction`].
25///
26/// Most code should be able to call [`Transaction::bind`] rather than mentioning this
27/// trait at all; it is an implementation detail of the conversion that unfortunately
28/// cannot be hidden.
29pub trait UTransactional: Transactional + 'static
30where
31    Self: Sized,
32{
33    /// Specify the target of the transaction as a [`Handle`], and erase its type,
34    /// so that it can be combined with other transactions in the same universe.
35    ///
36    /// This is also available as [`Transaction::bind`].
37    fn bind(target: Handle<Self>, transaction: Self::Transaction) -> UniverseTransaction;
38}
39
40/// Pair of a transaction and a [`Handle`] to its target.
41///
42/// [`AnyTransaction`] is a singly-typed wrapper around this.
43///
44/// TODO: Better name.
45#[derive(Debug, Eq)]
46pub(in crate::universe) struct TransactionInUniverse<O: Transactional + 'static> {
47    pub(crate) target: Handle<O>,
48    pub(crate) transaction: O::Transaction,
49}
50
51impl<O> Transaction for TransactionInUniverse<O>
52where
53    O: Transactional + 'static,
54{
55    type Target = ();
56    type CommitCheck = TransactionInUniverseCheck<O>;
57    type Output = <O::Transaction as Transaction>::Output;
58    type Mismatch = <O::Transaction as Transaction>::Mismatch;
59
60    fn check(&self, (): &()) -> Result<Self::CommitCheck, Self::Mismatch> {
61        let guard = self
62            .target
63            .try_borrow_mut()
64            // TODO: return this error
65            .expect("Attempted to execute transaction with target already borrowed");
66        let check = self.transaction.check(&guard)?;
67        Ok(TransactionInUniverseCheck { guard, check })
68    }
69
70    fn commit(
71        &self,
72        _dummy_target: &mut (),
73        mut tu_check: Self::CommitCheck,
74        outputs: &mut dyn FnMut(Self::Output),
75    ) -> Result<(), CommitError> {
76        self.transaction
77            .commit(&mut tu_check.guard, tu_check.check, outputs)
78    }
79}
80
81/// Private to keep the mutable access private.
82pub(in crate::universe) struct TransactionInUniverseCheck<O>
83where
84    O: Transactional + 'static,
85{
86    guard: UBorrowMut<O>,
87    check: <O::Transaction as Transaction>::CommitCheck,
88}
89
90impl<O> Merge for TransactionInUniverse<O>
91where
92    O: Transactional + 'static,
93{
94    type MergeCheck = <O::Transaction as Merge>::MergeCheck;
95    type Conflict = <O::Transaction as Merge>::Conflict;
96
97    fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
98        if self.target != other.target {
99            // This is a panic because it indicates a programming error.
100            panic!("TransactionInUniverse cannot have multiple targets; use UniverseTransaction instead");
101        }
102        self.transaction.check_merge(&other.transaction)
103    }
104
105    fn commit_merge(&mut self, other: Self, check: Self::MergeCheck) {
106        self.transaction.commit_merge(other.transaction, check);
107    }
108}
109
110/// Manual implementation to avoid `O: Clone` bound.
111impl<O> Clone for TransactionInUniverse<O>
112where
113    O: Transactional<Transaction: Clone>,
114{
115    fn clone(&self) -> Self {
116        Self {
117            target: self.target.clone(),
118            transaction: self.transaction.clone(),
119        }
120    }
121}
122/// Manual implementation to avoid `O: PartialEq` bound.
123impl<O> PartialEq for TransactionInUniverse<O>
124where
125    O: Transactional<Transaction: PartialEq>,
126{
127    fn eq(&self, other: &Self) -> bool {
128        self.target == other.target && self.transaction == other.transaction
129    }
130}
131
132/// Not-an-associated-type alias for check values produced by [`AnyTransaction`].
133/// TODO: Make this a newtype struct since we're bothering to name it.
134pub(in crate::universe) type AnyTransactionCheck = Box<dyn Any>;
135
136impl AnyTransaction {
137    fn target_name(&self) -> Option<Name> {
138        self.target_erased().map(ErasedHandle::name)
139    }
140
141    fn universe_id(&self) -> Option<UniverseId> {
142        self.target_erased().and_then(ErasedHandle::universe_id)
143    }
144}
145
146/// Hide the wrapper type entirely since its type is determined entirely by its contents.
147impl fmt::Debug for AnyTransaction {
148    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
149        fmt::Debug::fmt(self.transaction_as_debug(), fmt)
150    }
151}
152
153/// Called from `impl Merge for AnyTransaction`
154pub(in crate::universe) fn anytxn_merge_helper<O>(
155    t1: &mut TransactionInUniverse<O>,
156    t2: TransactionInUniverse<O>,
157    check: AnyTransactionCheck, // contains <TransactionInUniverse<O> as Transaction>::MergeCheck,
158) where
159    O: Transactional,
160    TransactionInUniverse<O>: Transaction,
161{
162    t1.commit_merge(t2, *check.downcast().unwrap())
163}
164
165/// Called from `impl Commit for AnyTransaction`
166pub(in crate::universe) fn anytxn_commit_helper<O>(
167    transaction: &TransactionInUniverse<O>,
168    check: AnyTransactionCheck,
169    outputs: &mut dyn FnMut(<TransactionInUniverse<O> as Transaction>::Output),
170) -> Result<(), CommitError>
171where
172    O: Transactional,
173    TransactionInUniverse<O>: Transaction<Target = ()>,
174{
175    let check: <TransactionInUniverse<O> as Transaction>::CommitCheck =
176        *(check.downcast().map_err(|_| {
177            CommitError::message::<AnyTransaction>("type mismatch in check data".into())
178        })?);
179    transaction.commit(&mut (), check, outputs)
180}
181
182/// A [`Transaction`] which operates on one or more objects in a [`Universe`]
183/// simultaneously.
184///
185/// Construct this by calling [`Transaction::bind`] on other transaction types
186/// and combine them into larger transactions with [`Merge::merge`].
187#[derive(Clone, Default, PartialEq)]
188#[must_use]
189pub struct UniverseTransaction {
190    /// Transactions on existing members or named insertions.
191    /// Invariant: None of the names are [`Name::Pending`].
192    members: HbHashMap<Name, MemberTxn>,
193
194    /// Insertions of anonymous members, kept separate since they do not have unique [`Name`]s.
195    /// Unlike insertions of named members, these cannot fail to merge or commit.
196    ///
197    /// Note that due to concurrent operations on the ref, some of the entries in this
198    /// vector might turn out to have been given names. In that case, the transaction
199    /// should fail. (TODO: Write test verifying that.)
200    ///
201    /// Invariant: Every element of this vector is a `MemberTxn::Insert`.
202    anonymous_insertions: Vec<MemberTxn>,
203
204    behaviors: behavior::BehaviorSetTransaction<Universe>,
205
206    /// Invariant: Has a universe ID if any of the `members` do.
207    universe_id: Equal<UniverseId>,
208}
209
210// TODO: Benchmark cheaper HashMaps / using BTreeMap here
211#[doc(hidden)] // Almost certainly will never need to be used explicitly
212#[derive(Debug)]
213pub struct UniverseMergeCheck {
214    members: HbHashMap<Name, MemberMergeCheck>,
215    behaviors: behavior::MergeCheck,
216}
217#[doc(hidden)] // Almost certainly will never need to be used explicitly
218#[derive(Debug)]
219pub struct UniverseCommitCheck {
220    members: HbHashMap<Name, MemberCommitCheck>,
221    anonymous_insertions: Vec<MemberCommitCheck>,
222    behaviors: behavior::CommitCheck,
223}
224
225/// Transaction precondition error type for [`UniverseTransaction`].
226#[derive(Clone, Debug, PartialEq)]
227#[non_exhaustive]
228pub enum UniverseMismatch {
229    /// The transaction modifies members of a different [`Universe`] than it was applied to.
230    DifferentUniverse {
231        /// The universe mentioned in the transaction.
232        transaction: UniverseId,
233        /// The universe to which the transaction was applied.
234        target: UniverseId,
235    },
236
237    /// The member is not in an appropriate state.
238    Member(transaction::MapMismatch<Name, MemberMismatch>),
239
240    /// Universe transactions may not modify handles that are in the [`Name::Pending`] state.
241    InvalidPending,
242
243    /// The behavior set is not in an appropriate state.
244    Behaviors(<behavior::BehaviorSetTransaction<Universe> as Transaction>::Mismatch),
245}
246
247/// Transaction conflict error type for [`UniverseTransaction`].
248#[derive(Clone, Debug, PartialEq)]
249#[non_exhaustive]
250pub enum UniverseConflict {
251    /// The two transactions modify members of different [`Universe`]s.
252    DifferentUniverse(UniverseId, UniverseId),
253
254    /// The two transactions attempt to modify a member in conflicting ways.
255    Member(transaction::MapConflict<Name, MemberConflict>),
256
257    /// The two transactions attempt to modify a behavior in conflicting ways.
258    Behaviors(behavior::BehaviorTransactionConflict),
259}
260
261impl core::error::Error for UniverseMismatch {
262    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
263        match self {
264            UniverseMismatch::DifferentUniverse { .. } => None,
265            UniverseMismatch::Member(mc) => Some(&mc.mismatch),
266            UniverseMismatch::Behaviors(c) => Some(c),
267            UniverseMismatch::InvalidPending => None,
268        }
269    }
270}
271impl core::error::Error for UniverseConflict {
272    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
273        match self {
274            UniverseConflict::DifferentUniverse(_, _) => None,
275            UniverseConflict::Member(mc) => Some(&mc.conflict),
276            UniverseConflict::Behaviors(c) => Some(c),
277        }
278    }
279}
280
281impl fmt::Display for UniverseMismatch {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        match self {
284            UniverseMismatch::DifferentUniverse { .. } => {
285                write!(
286                    f,
287                    "cannot commit a transaction to a different universe \
288                        than it was constructed for"
289                )
290            }
291            UniverseMismatch::Member(c) => {
292                // details reported via source()
293                write!(
294                    f,
295                    "transaction precondition not met in member {key}",
296                    key = c.key
297                )
298            }
299            UniverseMismatch::InvalidPending => {
300                write!(
301                    f,
302                    "universe transactions may not modify handles that \
303                        are in the [`Name::Pending`] state"
304                )
305            }
306            UniverseMismatch::Behaviors(_) => {
307                // details reported via source()
308                write!(f, "transaction precondition not met in behaviors")
309            }
310        }
311    }
312}
313
314impl fmt::Display for UniverseConflict {
315    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316        match self {
317            UniverseConflict::DifferentUniverse(_, _) => {
318                write!(f, "cannot merge transactions From different universes")
319            }
320            UniverseConflict::Member(c) => {
321                write!(f, "transaction conflict at member {key}", key = c.key)
322            }
323            UniverseConflict::Behaviors(_) => write!(f, "conflict in behaviors"),
324        }
325    }
326}
327
328impl From<transaction::MapConflict<Name, MemberConflict>> for UniverseConflict {
329    fn from(value: transaction::MapConflict<Name, MemberConflict>) -> Self {
330        Self::Member(value)
331    }
332}
333
334impl Transactional for Universe {
335    type Transaction = UniverseTransaction;
336}
337
338impl UniverseTransaction {
339    /// Convert from a transaction on a single member to [`UniverseTransaction`].
340    ///
341    /// The public interface to this is the other methods and [`Transaction::bind()`].
342    fn from_member_txn(name: Name, transaction: MemberTxn) -> Self {
343        UniverseTransaction {
344            universe_id: Equal(transaction.universe_id()),
345            members: HbHashMap::from([(name, transaction)]),
346            anonymous_insertions: Vec::new(),
347            behaviors: Default::default(),
348        }
349    }
350
351    /// Transaction which inserts the given object into the universe under
352    /// the handle's name.
353    ///
354    /// Note that this transaction can only ever succeed once.
355    pub fn insert<T: UniverseMember>(handle: Handle<T>) -> Self {
356        // TODO: fail right away if the ref is already in a universe or if it is Anonym?
357        match handle.name() {
358            Name::Specific(_) | Name::Anonym(_) => Self::from_member_txn(
359                handle.name(),
360                MemberTxn::Insert(UniverseMember::into_any_handle(handle)),
361            ),
362            Name::Pending => Self {
363                members: HbHashMap::new(),
364                anonymous_insertions: vec![MemberTxn::Insert(UniverseMember::into_any_handle(
365                    handle,
366                ))],
367                universe_id: Equal(None),
368                behaviors: Default::default(),
369            },
370        }
371    }
372
373    /// Adds an insertion of a universe member to the transaction.
374    ///
375    /// This is equivalent to [`Self::insert()`] but allows efficiently accumulating a transaction,
376    /// and producing [`InsertError`](crate::universe::InsertError)s on failure rather than a more
377    /// general error.
378    ///
379    /// If the member is to be anonymous, consider using [`Self::insert_anonymous()`] instead.
380    ///
381    /// TODO: Give this a better name by renaming `insert()`.
382    /// TODO: Is `InsertError` actually desirable, or legacy from before transactions?
383    pub fn insert_mut<T: UniverseMember>(&mut self, handle: Handle<T>) -> Result<(), InsertError> {
384        let name = handle.name();
385        let insertion = MemberTxn::Insert(UniverseMember::into_any_handle(handle));
386
387        // TODO: fail right away if the ref is already in a universe or if it is Anonym?
388        match name {
389            name @ (Name::Specific(_) | Name::Anonym(_)) => {
390                match self.members.entry(name.clone()) {
391                    hashbrown::hash_map::Entry::Occupied(_) => {
392                        // Equivalent to how transaction merge would fail
393                        Err(InsertError {
394                            name: name.clone(),
395                            kind: InsertErrorKind::AlreadyExists,
396                        })
397                    }
398                    hashbrown::hash_map::Entry::Vacant(ve) => {
399                        ve.insert(insertion);
400                        Ok(())
401                    }
402                }
403            }
404            Name::Pending => {
405                self.anonymous_insertions.push(insertion);
406                Ok(())
407            }
408        }
409    }
410
411    /// Adds an insertion of an anonymous universe member to the transaction, and returns the
412    /// pending handle to that member.
413    ///
414    /// These steps are combined together because anonymous insertion is special, in that it cannot
415    /// fail to merge with the current state of the transaction, and it is a commonly needed
416    /// operation.
417    pub fn insert_anonymous<T: UniverseMember>(&mut self, value: T) -> Handle<T> {
418        let handle = Handle::new_pending(Name::Pending, value);
419        self.anonymous_insertions
420            .push(MemberTxn::Insert(UniverseMember::into_any_handle(
421                handle.clone(),
422            )));
423        handle
424    }
425
426    /// Delete this member from the universe.
427    ///
428    /// All existing handles will become [`HandleError::Gone`], even if a new member by
429    /// the same name is later added.
430    ///
431    /// This transaction will fail if the member is already gone, is anonymous
432    /// (only named entries can be deleted), or belongs to another universe.
433    /// In the future, there may be a policy such that in-use items cannot be deleted.
434    #[expect(clippy::needless_pass_by_value)] // TODO: by ref or not by ref?
435    pub fn delete<R: ErasedHandle>(member_handle: R) -> Self {
436        Self::from_member_txn(member_handle.name(), MemberTxn::Delete)
437    }
438
439    /// Modify the [`Behavior`](behavior::Behavior)s of the universe.
440    pub fn behaviors(t: behavior::BehaviorSetTransaction<Universe>) -> Self {
441        Self {
442            behaviors: t,
443            members: HbHashMap::new(),
444            anonymous_insertions: Vec::new(),
445            universe_id: Equal(None),
446        }
447    }
448
449    /// If this transaction contains any operations that are on a specific member of a
450    /// universe, then returns the ID of that universe.
451    // TODO: make public?
452    pub fn universe_id(&self) -> Option<UniverseId> {
453        self.universe_id.0
454    }
455}
456
457impl From<AnyTransaction> for UniverseTransaction {
458    fn from(transaction: AnyTransaction) -> Self {
459        if let Some(name) = transaction.target_name() {
460            Self::from_member_txn(name, MemberTxn::Modify(transaction))
461        } else {
462            UniverseTransaction::default()
463        }
464    }
465}
466
467impl Transaction for UniverseTransaction {
468    type Target = Universe;
469    type CommitCheck = UniverseCommitCheck;
470    type Output = transaction::NoOutput;
471    type Mismatch = UniverseMismatch;
472
473    fn check(&self, target: &Universe) -> Result<Self::CommitCheck, UniverseMismatch> {
474        if let Some(universe_id) = self.universe_id() {
475            if universe_id != target.id {
476                return Err(UniverseMismatch::DifferentUniverse {
477                    transaction: universe_id,
478                    target: target.id,
479                });
480            }
481        }
482        let mut member_checks = HbHashMap::with_capacity(self.members.len());
483        for (name, member) in self.members.iter() {
484            match name {
485                Name::Specific(_) | Name::Anonym(_) => {}
486                Name::Pending => {
487                    // TODO: This is a weird place to implement this constraint.
488                    // It would be better (?) to check when the transaction is created,
489                    // but that will be quite a lot of fallibility...
490                    return Err(UniverseMismatch::InvalidPending);
491                }
492            }
493
494            member_checks.insert(
495                name.clone(),
496                member.check(target, name).map_err(|e| {
497                    UniverseMismatch::Member(transaction::MapMismatch {
498                        key: name.clone(),
499                        mismatch: e,
500                    })
501                })?,
502            );
503        }
504
505        let mut insert_checks = Vec::with_capacity(self.anonymous_insertions.len());
506        for insert_txn in self.anonymous_insertions.iter() {
507            insert_checks.push(
508                insert_txn
509                    .check(target, &Name::Pending)
510                    .map_err(|mismatch| {
511                        UniverseMismatch::Member(transaction::MapMismatch {
512                            key: Name::Pending,
513                            mismatch,
514                        })
515                    })?,
516            );
517        }
518
519        Ok(UniverseCommitCheck {
520            members: member_checks,
521            anonymous_insertions: insert_checks,
522            behaviors: self
523                .behaviors
524                .check(&target.behaviors)
525                .map_err(UniverseMismatch::Behaviors)?,
526        })
527    }
528
529    fn commit(
530        &self,
531        target: &mut Universe,
532        checks: Self::CommitCheck,
533        outputs: &mut dyn FnMut(Self::Output),
534    ) -> Result<(), CommitError> {
535        let Self {
536            members,
537            anonymous_insertions,
538            behaviors,
539            universe_id,
540        } = self;
541        let UniverseCommitCheck {
542            members: check_members,
543            anonymous_insertions: check_anon,
544            behaviors: check_behaviors,
545        } = checks;
546
547        // final sanity check so we can't ever modify the wrong universe
548        if universe_id.check(&target.id).is_err() {
549            return Err(CommitError::message::<Self>(
550                "cannot commit a transaction to a different universe \
551                        than it was constructed for"
552                    .into(),
553            ));
554        }
555
556        for (name, check) in check_members {
557            members[&name]
558                .commit(target, &name, check, outputs)
559                .map_err(|e| e.context(format!("universe member {name}")))?;
560        }
561
562        for (new_member, check) in anonymous_insertions.iter().cloned().zip(check_anon) {
563            new_member.commit(target, &Name::Pending, check, outputs)?;
564        }
565
566        behaviors.commit(
567            &mut target.behaviors,
568            check_behaviors,
569            &mut transaction::no_outputs,
570        )?;
571
572        Ok(())
573    }
574}
575
576impl Merge for UniverseTransaction {
577    type MergeCheck = UniverseMergeCheck;
578    type Conflict = UniverseConflict;
579
580    fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
581        // Not using Equal::check_merge() because this lets us report the values easier.
582        if let (Some(id1), Some(id2)) = (self.universe_id(), other.universe_id()) {
583            if id1 != id2 {
584                return Err(UniverseConflict::DifferentUniverse(id1, id2));
585            }
586        }
587        Ok(UniverseMergeCheck {
588            members: self
589                .members
590                .check_merge(&other.members)
591                .map_err(UniverseConflict::Member)?,
592            behaviors: self
593                .behaviors
594                .check_merge(&other.behaviors)
595                .map_err(UniverseConflict::Behaviors)?,
596        })
597    }
598
599    fn commit_merge(&mut self, other: Self, check: Self::MergeCheck) {
600        let Self {
601            members,
602            anonymous_insertions,
603            behaviors,
604            universe_id,
605        } = self;
606
607        members.commit_merge(other.members, check.members);
608        anonymous_insertions.extend(other.anonymous_insertions);
609        behaviors.commit_merge(other.behaviors, check.behaviors);
610        universe_id.commit_merge(other.universe_id, ());
611    }
612}
613
614/// This formatting is chosen to be similar to [`Universe`]'s.
615impl fmt::Debug for UniverseTransaction {
616    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
617        let Self {
618            members,
619            anonymous_insertions,
620            behaviors,
621            universe_id: _, // not printed because it is effectively nondeterministic
622        } = self;
623
624        let mut ds = fmt.debug_struct("UniverseTransaction");
625        for (name, txn) in members {
626            // transaction_as_debug() gives us the type-specific transaction without the redundant
627            // TransactionInUniverse wrapper
628            ds.field(&name.to_string(), txn.transaction_as_debug());
629        }
630        for txn in anonymous_insertions {
631            ds.field("[anonymous pending]", txn.transaction_as_debug());
632        }
633        if !behaviors.is_empty() {
634            ds.field("behaviors", behaviors);
635        }
636        ds.finish()
637    }
638}
639
640/// Transaction for anything that can be done to a single member of a [`Universe`].
641///
642/// Note: This does not implement [`Transaction`] because it needs to refer to an
643/// _entry_ in a Universe. We could kludge around that by having it take the Universe
644/// and embed the Name, but that's unnecessary.
645#[derive(Clone, Debug, Default, PartialEq)]
646enum MemberTxn {
647    /// Mergeable types are required to have a no-operation [`Default`] value,
648    /// though this shouldn't come up much.
649    #[default]
650    Noop,
651    /// Apply given transaction to the existing value.
652    Modify(AnyTransaction),
653    /// Insert the provided [pending](Handle::new_pending) [`Handle`] in the universe.
654    ///
655    /// Note: This transaction can only succeed once, since after the first time it will
656    /// no longer be pending.
657    Insert(AnyHandle),
658    /// Delete this member from the universe.
659    ///
660    /// See [`UniverseTransaction::delete()`] for full documentation.
661    Delete,
662}
663
664#[derive(Debug)]
665struct MemberMergeCheck(Option<AnyTransactionCheck>);
666#[derive(Debug)]
667struct MemberCommitCheck(Option<AnyTransactionCheck>);
668
669impl MemberTxn {
670    fn check(&self, universe: &Universe, name: &Name) -> Result<MemberCommitCheck, MemberMismatch> {
671        match self {
672            MemberTxn::Noop => Ok(MemberCommitCheck(None)),
673            // Kludge: The individual `AnyTransaction`s embed the `Handle<T>` they operate on --
674            // so we don't actually pass anything here.
675            MemberTxn::Modify(txn) => {
676                Ok(MemberCommitCheck(Some(txn.check(&()).map_err(|e| {
677                    MemberMismatch::Modify(ModifyMemberMismatch(e))
678                })?)))
679            }
680            MemberTxn::Insert(pending_handle) => {
681                {
682                    if pending_handle.name() != *name {
683                        return Err(MemberMismatch::Insert(InsertError {
684                            name: name.clone(),
685                            kind: InsertErrorKind::AlreadyInserted,
686                        }));
687                    }
688                }
689
690                // TODO: Deduplicate this check logic vs. Universe::allocate_name
691                match name {
692                    Name::Specific(_) | Name::Pending => {}
693                    Name::Anonym(_) => {
694                        return Err(MemberMismatch::Insert(InsertError {
695                            name: name.clone(),
696                            kind: InsertErrorKind::InvalidName,
697                        }))
698                    }
699                }
700
701                if universe.get_any(name).is_some() {
702                    return Err(MemberMismatch::Insert(InsertError {
703                        name: name.clone(),
704                        kind: InsertErrorKind::AlreadyExists,
705                    }));
706                }
707                // TODO: This has a TOCTTOU problem because it doesn't ensure another thread
708                // couldn't insert the ref in the mean time.
709                pending_handle
710                    .check_upgrade_pending(universe.id)
711                    .map_err(MemberMismatch::Insert)?;
712                Ok(MemberCommitCheck(None))
713            }
714            MemberTxn::Delete => {
715                if let Name::Specific(_) = name {
716                    if universe.get_any(name).is_some() {
717                        Ok(MemberCommitCheck(None))
718                    } else {
719                        Err(MemberMismatch::DeleteNonexistent(name.clone()))
720                    }
721                } else {
722                    Err(MemberMismatch::DeleteInvalid(name.clone()))
723                }
724            }
725        }
726    }
727
728    fn commit(
729        &self,
730        universe: &mut Universe,
731        name: &Name,
732        MemberCommitCheck(check): MemberCommitCheck,
733        outputs: &mut dyn FnMut(core::convert::Infallible), // TODO: placeholder for actual Fluff output
734    ) -> Result<(), CommitError> {
735        match self {
736            MemberTxn::Noop => {
737                assert!(check.is_none());
738                Ok(())
739            }
740            MemberTxn::Modify(txn) => {
741                txn.commit(&mut (), check.expect("missing check value"), outputs)
742            }
743            MemberTxn::Insert(pending_handle) => {
744                pending_handle
745                    .insert_and_upgrade_pending(universe)
746                    .map_err(CommitError::catch::<UniverseTransaction, InsertError>)?;
747                Ok(())
748            }
749            MemberTxn::Delete => {
750                assert!(check.is_none());
751                let did_delete = universe.delete(name);
752                assert!(did_delete);
753                Ok(())
754            }
755        }
756    }
757
758    /// Returns the transaction out of the wrappers.
759    fn transaction_as_debug(&self) -> &dyn fmt::Debug {
760        use MemberTxn::*;
761        match self {
762            Modify(t) => t.transaction_as_debug(),
763            Noop | Insert(_) | Delete => self,
764        }
765    }
766
767    fn universe_id(&self) -> Option<UniverseId> {
768        use MemberTxn::*;
769        match self {
770            Modify(t) => t.universe_id(),
771            Noop | Insert(_) | Delete => None,
772        }
773    }
774}
775
776/// Generic helper function called by macro-generated
777/// [`AnyHandle::insert_and_upgrade_pending()`]
778pub(in crate::universe) fn any_handle_insert_and_upgrade_pending<T>(
779    universe: &mut Universe,
780    pending_handle: &Handle<T>,
781) -> Result<(), InsertError>
782where
783    T: 'static,
784    Universe: UniverseTable<T, Table = super::Storage<T>>,
785{
786    let new_root_handle = pending_handle.upgrade_pending(universe)?;
787
788    UniverseTable::<T>::table_mut(universe).insert(pending_handle.name(), new_root_handle);
789    universe.wants_gc = true;
790
791    Ok(())
792}
793
794impl Merge for MemberTxn {
795    type MergeCheck = MemberMergeCheck;
796    type Conflict = MemberConflict;
797
798    fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
799        use MemberTxn::*;
800        match (self, other) {
801            // Noop merges with anything
802            (Noop, _) | (_, Noop) => Ok(MemberMergeCheck(None)),
803            // Modify merges by merging the transaction
804            (Modify(t1), Modify(t2)) => {
805                let check = t1
806                    .check_merge(t2)
807                    .map_err(|e| MemberConflict::Modify(ModifyMemberConflict(e)))?;
808                Ok(MemberMergeCheck(Some(check)))
809            }
810            // Insert conflicts with everything
811            (Insert(_), _) | (_, Insert(_)) => Err(MemberConflict::InsertVsOther),
812            // Delete merges with itself alone
813            (Delete, Delete) => Ok(MemberMergeCheck(None)),
814            (Delete, _) | (_, Delete) => Err(MemberConflict::DeleteVsOther),
815        }
816    }
817
818    fn commit_merge(&mut self, other: Self, MemberMergeCheck(check): Self::MergeCheck)
819    where
820        Self: Sized,
821    {
822        use MemberTxn::*;
823        match (self, other) {
824            (t1 @ Noop, t2) => {
825                assert!(check.is_none());
826                *t1 = t2;
827            }
828            (_, Noop) => {
829                assert!(check.is_none());
830            }
831            (Modify(t1), Modify(t2)) => {
832                t1.commit_merge(t2, check.expect("missing check value"));
833            }
834            (Delete, Delete) => {}
835            (a @ (Insert(_) | Delete), b) | (a, b @ (Insert(_) | Delete)) => {
836                panic!(
837                    "Invalid merge check: tried to merge {a:?} with {b:?}, \
838                    which are not mergeable"
839                );
840            }
841        }
842    }
843}
844
845/// Transaction conflict error type for a single member in a [`UniverseTransaction`].
846#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
847#[non_exhaustive]
848pub enum MemberConflict {
849    /// cannot simultaneously insert and make another change
850    InsertVsOther,
851
852    /// cannot simultaneously delete and make another change
853    DeleteVsOther,
854
855    /// Tried to make incompatible modifications to the data of the member.
856    #[displaydoc("{0}")]
857    Modify(ModifyMemberConflict),
858}
859
860impl core::error::Error for MemberConflict {
861    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
862        match self {
863            MemberConflict::InsertVsOther => None,
864            MemberConflict::DeleteVsOther => None,
865            MemberConflict::Modify(e) => e.source(),
866        }
867    }
868}
869
870/// Transaction precondition error type for a single member in a [`UniverseTransaction`].
871#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
872#[non_exhaustive]
873pub enum MemberMismatch {
874    /// {0}
875    Insert(InsertError),
876
877    /// member {0} does not exist
878    DeleteNonexistent(Name),
879
880    /// only explicitly named members may be deleted, not {0}
881    DeleteInvalid(Name),
882
883    /// Preconditions of the member transaction weren't met
884    #[displaydoc("{0}")]
885    Modify(ModifyMemberMismatch),
886}
887
888impl core::error::Error for MemberMismatch {
889    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
890        match self {
891            MemberMismatch::Insert(e) => e.source(),
892            MemberMismatch::DeleteNonexistent(_) => None,
893            MemberMismatch::DeleteInvalid(_) => None,
894            MemberMismatch::Modify(e) => e.source(),
895        }
896    }
897}
898
899/// Transaction precondition error type for modifying a [`Universe`] member (something a
900/// [`Handle`] refers to).
901//
902// Public wrapper hiding the details of [`AnyTransactionMismatch`] which is an enum.
903// TODO: Probably this should just _be_ that enum, but let's hold off till a use case
904// shows up.
905#[derive(Clone, Debug, Eq, PartialEq)]
906pub struct ModifyMemberMismatch(AnyTransactionMismatch);
907
908/// Transaction conflict error type for modifying a [`Universe`] member (something a
909/// [`Handle`] refers to).
910//
911// Public wrapper hiding the details of [`AnyTransactionConflict`] which is an enum.
912// TODO: Probably this should just _be_ that enum, but let's hold off till a use case
913// shows up.
914#[derive(Clone, Debug, Eq, PartialEq)]
915pub struct ModifyMemberConflict(AnyTransactionConflict);
916
917impl core::error::Error for ModifyMemberMismatch {
918    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
919        self.0.source()
920    }
921}
922impl core::error::Error for ModifyMemberConflict {
923    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
924        self.0.source()
925    }
926}
927
928impl fmt::Display for ModifyMemberMismatch {
929    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
930        self.0.fmt(f)
931    }
932}
933impl fmt::Display for ModifyMemberConflict {
934    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
935        self.0.fmt(f)
936    }
937}
938
939impl From<AnyTransactionMismatch> for ModifyMemberMismatch {
940    fn from(value: AnyTransactionMismatch) -> Self {
941        Self(value)
942    }
943}
944impl From<AnyTransactionConflict> for ModifyMemberConflict {
945    fn from(value: AnyTransactionConflict) -> Self {
946        Self(value)
947    }
948}
949
950#[cfg(test)]
951mod tests {
952    //! Additional tests of universe transactions also exist in [`super::tests`]
953    //! (where they are parallel with non-transaction behavior tests).
954
955    use super::*;
956    use crate::block::BlockDef;
957    use crate::block::AIR;
958    use crate::content::make_some_blocks;
959    use crate::math::Cube;
960    use crate::space::CubeConflict;
961    use crate::space::Space;
962    use crate::space::SpaceTransaction;
963    use crate::space::SpaceTransactionConflict;
964    use crate::transaction::{ExecuteError, MapConflict, TransactionTester};
965    use crate::universe::{self, HandleError};
966    use alloc::sync::Arc;
967    use indoc::indoc;
968
969    #[test]
970    fn has_default() {
971        assert_eq!(
972            UniverseTransaction::default(),
973            UniverseTransaction {
974                members: HbHashMap::new(),
975                anonymous_insertions: Vec::new(),
976                universe_id: Equal(None),
977                behaviors: behavior::BehaviorSetTransaction::default()
978            }
979        )
980    }
981
982    #[test]
983    fn debug_empty() {
984        let transaction = UniverseTransaction::default();
985
986        pretty_assertions::assert_str_eq!(format!("{transaction:#?}"), "UniverseTransaction");
987    }
988
989    #[test]
990    fn debug_full() {
991        let [block] = make_some_blocks();
992        let mut u = Universe::new();
993        let space = u.insert_anonymous(Space::empty_positive(1, 1, 1));
994
995        // a UniverseBehavior to test inserting it
996        #[derive(Clone, Debug, PartialEq)]
997        struct UTestBehavior {}
998        impl behavior::Behavior<Universe> for UTestBehavior {
999            fn step(
1000                &self,
1001                _: &behavior::Context<'_, Universe>,
1002            ) -> (UniverseTransaction, behavior::Then) {
1003                unimplemented!()
1004            }
1005            fn persistence(&self) -> Option<behavior::Persistence> {
1006                None
1007            }
1008        }
1009        impl universe::VisitHandles for UTestBehavior {
1010            // No handles.
1011            fn visit_handles(&self, _visitor: &mut dyn universe::HandleVisitor) {}
1012        }
1013
1014        // Transaction has all of:
1015        // * a member-modifying part
1016        let mut transaction = SpaceTransaction::set_cube([0, 0, 0], None, Some(block)).bind(space);
1017        // * an anonymous insertion part
1018        transaction.insert_anonymous(BlockDef::new(AIR));
1019        // * a behavior set part
1020        transaction
1021            .merge_from(UniverseTransaction::behaviors(
1022                behavior::BehaviorSetTransaction::insert((), Arc::new(UTestBehavior {})),
1023            ))
1024            .unwrap();
1025
1026        println!("{transaction:#?}");
1027        pretty_assertions::assert_str_eq!(
1028            format!("{transaction:#?}\n"),
1029            indoc! {r#"
1030            UniverseTransaction {
1031                [anonymous #0]: SpaceTransaction {
1032                    (+0, +0, +0): CubeTransaction {
1033                        old: None,
1034                        new: Some(
1035                            Block {
1036                                primitive: Atom {
1037                                    color: Rgba(0.5, 0.5, 0.5, 1.0),
1038                                    collision: Hard,
1039                                },
1040                                modifiers: [
1041                                    BlockAttributes {
1042                                        display_name: "0",
1043                                    },
1044                                ],
1045                            },
1046                        ),
1047                        conserved: true,
1048                    },
1049                },
1050                [anonymous pending]: Insert(
1051                    BlockDef(
1052                        Handle([pending anonymous] in no universe = BlockDef {
1053                            block: Block {
1054                                primitive: Air,
1055                            },
1056                            cache_dirty: Flag(false),
1057                            listeners_ok: true,
1058                            notifier: Notifier(0),
1059                            ..
1060                        }),
1061                    ),
1062                ),
1063                behaviors: BehaviorSetTransaction {
1064                    replace: {},
1065                    insert: [
1066                        UTestBehavior @ (),
1067                    ],
1068                },
1069            }
1070            "#}
1071            .to_string()
1072        );
1073    }
1074
1075    #[test]
1076    fn merge_unrelated() {
1077        let [block_1, block_2] = make_some_blocks();
1078        let mut u = Universe::new();
1079        let s1 = u.insert_anonymous(Space::empty_positive(1, 1, 1));
1080        let s2 = u.insert_anonymous(Space::empty_positive(1, 1, 1));
1081        let t1 = SpaceTransaction::set_cube([0, 0, 0], None, Some(block_1)).bind(s1);
1082        let t2 = SpaceTransaction::set_cube([0, 0, 0], None, Some(block_2)).bind(s2);
1083        let _ = t1.merge(t2).unwrap();
1084        // TODO: check the contents
1085    }
1086
1087    #[test]
1088    fn merge_conflict() {
1089        let [block_1, block_2] = make_some_blocks();
1090        let mut u = Universe::new();
1091        let s = u.insert_anonymous(Space::empty_positive(1, 1, 1));
1092        let t1 = SpaceTransaction::set_cube([0, 0, 0], None, Some(block_1)).bind(s.clone());
1093        let t2 = SpaceTransaction::set_cube([0, 0, 0], None, Some(block_2)).bind(s.clone());
1094
1095        let error = t1.merge(t2).unwrap_err();
1096
1097        let UniverseConflict::Member(MapConflict {
1098            key,
1099            conflict:
1100                MemberConflict::Modify(ModifyMemberConflict(AnyTransactionConflict::Space(
1101                    SpaceTransactionConflict::Cube {
1102                        cube: Cube { x: 0, y: 0, z: 0 },
1103                        conflict:
1104                            CubeConflict {
1105                                old: false,
1106                                new: true,
1107                            },
1108                    },
1109                ))),
1110        }) = error
1111        else {
1112            panic!("not as expected: {error:?}");
1113        };
1114        assert_eq!(key, s.name());
1115    }
1116
1117    #[test]
1118    fn merges_members() {
1119        let [old_block, new_block] = make_some_blocks();
1120        let mut u = Universe::new();
1121        let s = u.insert_anonymous(Space::empty_positive(1, 1, 1));
1122        let t1 =
1123            SpaceTransaction::set_cube([0, 0, 0], None, Some(new_block.clone())).bind(s.clone());
1124        let t2 =
1125            SpaceTransaction::set_cube([0, 0, 0], Some(old_block.clone()), None).bind(s.clone());
1126        let t3 = t1.merge(t2).unwrap();
1127        assert_eq!(
1128            t3,
1129            SpaceTransaction::set_cube([0, 0, 0], Some(old_block), Some(new_block)).bind(s)
1130        );
1131    }
1132
1133    #[test]
1134    fn insert_affects_clones() {
1135        let mut u = Universe::new();
1136        let pending_1 = Handle::new_pending("foo".into(), Space::empty_positive(1, 1, 1));
1137        let pending_2 = pending_1.clone();
1138
1139        UniverseTransaction::insert(pending_2)
1140            .execute(&mut u, &mut drop)
1141            .unwrap();
1142
1143        assert_eq!(pending_1.universe_id(), Some(u.universe_id()));
1144        // TODO: Also verify that pending_1 is not still in the pending state
1145    }
1146
1147    /// Anonymous handles require special handling because, before being inserted, they do
1148    /// not have unique names.
1149    #[test]
1150    fn insert_anonymous() {
1151        let mut u = Universe::new();
1152
1153        // TODO: Cleaner public API for new anonymous?
1154        let foo = Handle::new_pending(Name::Pending, Space::empty_positive(1, 1, 1));
1155        let bar = Handle::new_pending(Name::Pending, Space::empty_positive(2, 2, 2));
1156
1157        UniverseTransaction::insert(foo.clone())
1158            .merge(UniverseTransaction::insert(bar.clone()))
1159            .expect("merge should allow 2 pending")
1160            .execute(&mut u, &mut drop)
1161            .expect("execute");
1162
1163        // Now check all the naming turned out correctly.
1164        assert_eq!(u.get(&foo.name()).unwrap(), foo);
1165        assert_eq!(u.get(&bar.name()).unwrap(), bar);
1166        assert_ne!(foo, bar);
1167    }
1168
1169    #[test]
1170    fn insert_anonymous_equivalence() {
1171        let mut txn = UniverseTransaction::default();
1172        let handle = txn.insert_anonymous(Space::empty_positive(1, 1, 1));
1173
1174        assert_eq!(handle.name(), Name::Pending);
1175        assert_eq!(txn, UniverseTransaction::insert(handle));
1176    }
1177
1178    #[test]
1179    #[ignore] // TODO: figure out how to get this to work w/ insert transactions
1180    fn systematic() {
1181        TransactionTester::new()
1182            // TODO: more transactions of all kinds
1183            .transaction(UniverseTransaction::default(), |_, _| Ok(()))
1184            // TODO: this is going to fail because insert transactions aren't reusable
1185            .transaction(
1186                UniverseTransaction::insert(Handle::new_pending(
1187                    "foo".into(),
1188                    Space::empty_positive(1, 1, 1),
1189                )),
1190                |_, _| Ok(()),
1191            )
1192            .target(Universe::new)
1193            // TODO: target with existing members
1194            .test();
1195    }
1196
1197    #[test]
1198    fn wrong_universe_execute() {
1199        let [block] = make_some_blocks();
1200        let mut u1 = Universe::new();
1201        let mut u2 = Universe::new();
1202        let s1 = u1.insert_anonymous(Space::empty_positive(1, 1, 1));
1203        let t1 = SpaceTransaction::set_cube([0, 0, 0], None, Some(block)).bind(s1);
1204
1205        let e = t1.execute(&mut u2, &mut drop).unwrap_err();
1206        assert!(matches!(e, ExecuteError::Check(_)));
1207    }
1208
1209    #[test]
1210    fn wrong_universe_merge() {
1211        let [block] = make_some_blocks();
1212        let mut u1 = Universe::new();
1213        let mut u2 = Universe::new();
1214        let s1 = u1.insert_anonymous(Space::empty_positive(1, 1, 1));
1215        let s2 = u2.insert_anonymous(Space::empty_positive(1, 1, 1));
1216        let bare_txn = SpaceTransaction::set_cube([0, 0, 0], None, Some(block));
1217        let t1 = bare_txn.clone().bind(s1);
1218        let t2 = bare_txn.bind(s2);
1219
1220        t1.merge(t2).unwrap_err();
1221    }
1222
1223    #[test]
1224    fn insert_named_already_in_different_universe() {
1225        let mut u1 = Universe::new();
1226        let mut u2 = Universe::new();
1227        let handle = u1
1228            .insert("foo".into(), Space::empty_positive(1, 1, 1))
1229            .unwrap();
1230        let txn = UniverseTransaction::insert(handle);
1231
1232        let e = txn.execute(&mut u2, &mut drop).unwrap_err();
1233        assert_eq!(
1234            dbg!(e),
1235            ExecuteError::Check(UniverseMismatch::Member(transaction::MapMismatch {
1236                key: "foo".into(),
1237                mismatch: MemberMismatch::Insert(InsertError {
1238                    name: "foo".into(),
1239                    kind: InsertErrorKind::AlreadyInserted
1240                })
1241            }))
1242        );
1243    }
1244
1245    #[test]
1246    fn insert_anonymous_already_in_different_universe() {
1247        let handle = Handle::new_pending(Name::Pending, Space::empty_positive(1, 1, 1));
1248        let mut u1 = Universe::new();
1249        let mut u2 = Universe::new();
1250        let txn = UniverseTransaction::insert(handle);
1251
1252        txn.execute(&mut u1, &mut drop).unwrap();
1253        let e = txn.execute(&mut u2, &mut drop).unwrap_err();
1254
1255        assert_eq!(
1256            dbg!(e),
1257            ExecuteError::Check(UniverseMismatch::Member(transaction::MapMismatch {
1258                key: Name::Pending,
1259                mismatch: MemberMismatch::Insert(InsertError {
1260                    name: Name::Pending,
1261                    kind: InsertErrorKind::AlreadyInserted
1262                })
1263            }))
1264        );
1265    }
1266
1267    #[test]
1268    fn handle_error_from_handle_execute() {
1269        let e = Handle::<Space>::new_gone("foo".into())
1270            .execute(&SpaceTransaction::default())
1271            .unwrap_err();
1272
1273        assert_eq!(e, ExecuteError::Handle(HandleError::Gone("foo".into())));
1274    }
1275
1276    // This is not specifically desirable, but more work will be needed to avoid it
1277    #[test]
1278    #[should_panic = "Attempted to execute transaction with target already borrowed: Gone(Specific(\"foo\"))"]
1279    fn handle_error_from_universe_txn() {
1280        let mut u = Universe::new();
1281        let txn = SpaceTransaction::default().bind(Handle::<Space>::new_gone("foo".into()));
1282
1283        _ = txn.execute(&mut u, &mut transaction::no_outputs);
1284    }
1285}