Does `Transitionable` exist?

Imagine:

  1. a state machine that transitions from one state to the next by value.
  2. some external API that forces you to use the state machine somewhere you can only access it through a mutable reference &mut Machine.

In order to transition the states by value you can wrap them in an Option:

struct Machine {
  state: Option<State>,
}

Now we can do:

// machine: &mut Machine
machine.state = Some(machine.state.take().unwrap().transition());

That works, but what doesn't sit right with me is the fact that the option should never be None unless we caught and recovered from a panic in the transition method.

I found a library called takeable but it's API allows take and takeable-option which is fairly bare.

I want to limit the API to transitioning and borrowing. If we do that, I think we can optimize the case where panic = "abort" by duplicating the held value temporarily.

Here is a short version of the code:

#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Transitionable<T> {
    Ok(T),
    #[cfg(not(panic = "abort"))]
    Poisoned,
}

impl<T> Transitionable<T> {
    pub fn new(value: T) -> Self {
        Self::Ok(value)
    }

    pub fn try_into_inner(transitionable: Self) -> Result<T, PoisonError> {
        match transitionable {
            Transitionable::Ok(value) => Ok(value),
            #[cfg(not(panic = "abort"))]
            Transitionable::Poisoned => Err(PoisonError {}),
        }
    }

    pub fn try_transition<F: FnOnce(T) -> T>(
        transitionable: &mut Self,
        f: F,
    ) -> Result<&mut Self, PoisonError> {
        #[cfg(not(panic = "abort"))]
        {
            let value = match std::mem::replace(transitionable, Self::Poisoned {}) {
                Self::Ok(value) => Ok(value),
                Self::Poisoned => return Err(PoisonError {}),
            }?;
            *transitionable = Self::Ok(f(value));
        }
        #[cfg(panic = "abort")]
        {
            // SAFETY: Duplicating the value is safe because the process aborts if the transition
            // function panics.
            unsafe {
                // TODO: Determine if we need to mess with ManuallyDrop, perhaps transitionable::Ok should
                // contain a ManuallyDrop<T>.
                let Self::Ok(value) = std::ptr::read(transitionable);
                std::ptr::write(transitionable, Self::Ok(f(value)));
            }
        }
        Ok(transitionable)
    }
}

#[cfg(test)]
mod test {
    use std::panic::catch_unwind;
    use std::panic::AssertUnwindSafe;

    use super::*;

    #[test]
    fn transition_works() {
        struct A;
        let mut t = Transitionable::new(A);
        Transitionable::transition(&mut t, |a: A| a);
    }
}

I know the code needs to be documented, more tests, and I can mark some things #[inline] and #[track_caller]. The full code implements (try_)get(_mut) and Deref(Mut).

The main questions I have are:

  1. did I miss an existing library? (probably)
  2. is the panic = "abort" code sound?
  3. how can run tests with panic = "abort", I'm getting warning: `panic` setting is ignored for `test` profile.

This doesn’t directly do what your Transitionable does, but it can replace your unsafe code.

1 Like

Thanks for the reference kpreid! It seems like their implementation is doing the same thing as mine in case of panic = "abort":

Transitionable is now available on crates.io.