rustype/notes

Rusty Typestates - Macros

This post is part of a series, you can see an overview of the whole series in [rust-typestate-index].

Introduction

This article picks up where [[rust-typestate]] left off, if you haven’t read it, it is in your best interest to do so beforehand.

Currently, we have a Drone implementation which respects the possible state transitions at compile-time. However, such implementation requires the developer to write a lot of code, and as more states are added, the more and more code is required to be written.

In this article we try to ease that pain using macro_rules! to generate code for the developer. If you are not familiar with Rust’s macro_rules!, I recommend you take a look at their chapter in the Rust book, Rust by example and the Rust reference. If you want a quick and dirty explanation: they’re like C macros, but better.

Finding Ways to Improve

Looking back at our drones and their build process (both the “regular” and “reference-able”), we notice some patterns.

Breaking New Ground

Before we start, it is important to start simple and build incrementally, with that in mind lets build upon the simpler Drone, without references.

Drone and PhantomData

As the good programmers we are, we know that work is evil, and so we want our macro to build the struct for us. To do so, we need the structure name, generic parameter name.

macro_rules! typestate_struct {
    ($struct_name:ident, $state_name:ident) => {
        pub struct $struct_name<$state_name> {
            state: std::marker::PhantomData<$state_name>
        }
    }
}

Using the macro:

typestate_struct!(Drone, DroneState);

Running cargo expand we get:

pub struct Drone<State> {
    state: std::marker::PhantomData<State>,
}

We’re still missing the remaining struct fields. To take care of that we can add a body to our macro:

macro_rules! typestate_struct {
    (($struct_name:ident, $state_name:ident) {$($field:ident:$field_type:ty,)*}) => {
        pub struct $struct_name<$state_name> {
            state: std::marker::PhantomData<$state_name>,
            $($field:$field_type,)*
        }
    }
}

Notice that we added {$($field:ident:$field_type:ty,)*} to our matching logic and $($field:$field_type,)* to our expansion. We also added extra parentheses to the first two parameters, just to clear the usage. Usage now looks like:

typestate_struct!(
    (Drone, State) {
        x: f32,
        y: f32,
    }
);

Which expands to:

pub struct Drone<State> {
    state: std::marker::PhantomData<State>,
    x: f32,
    y: f32,
}

By now we have a simple structure being generated, lets move on to the typestates!

Generating the Typestates

To generate the typestates we have to consider that not all states are used the same way. To cope with it, we categorize the states according to strictness:

Strictness Description Implications
None Allows users to declare the generic argument for any possible type All bets are off, state is completely public
Constrained Users are required to make the type implement the respective State trait Avoids “accidental” implementations over the generic state
Strict Users are not able to extend the automata Avoids the addition of new states to the automata

For this we will write another macro, we will need to declare the strictness level as well as possible states. Let us start with the possible states.

macro_rules! typestates_gen {
   ($($typestate:ident,)+) => {
        $(pub struct $typestate;)+
    }
}

Usage looks like:

typestates_gen!(Idle, Hovering, Flying,);

And expands to:

pub struct Idle;
pub struct Hovering;
pub struct Flying;

Looks good! Moving on to the strictness level, we need to support three levels, we can use the Rust’s macros ability to pattern match specific keywords. Our new macro looks like:

macro_rules! typestates_gen {
    ([$($typestate:ident,)+]) => {
        $(pub struct $typestate;)+
    };
    (limited [$($typestate:ident,)+]) => {
        typestates_gen!([$($typestate,)+]);
        pub trait Limited {}
        $(impl Limited for $typestate {})+
    };
    (strict [$($typestate:ident,)+]) => {
        typestates_gen!([$($typestate,)+]);
        pub trait Limited : sealed::Sealed {}
        $(impl Limited for $typestate {})+
        mod sealed {
            pub trait Sealed {}
            $(impl Sealed for super::$typestate {})+
        }
    };
}

In comparison with the previous macro this is a lot to unpack, lets break it down branch by branch.

([$($typestate:ident,)+]) => {
    $(pub struct $typestate;)+
};

The first branch is the same as the previous macro, we just added square brackets so our keyword can be distinguished from the typestates.

Moving on to the second branch:

(limited [$($typestate:ident,)+]) => {
    typestates_gen!([$($typestate,)+]);
    pub trait Limited {}
    $(impl Limited for $typestate {})+
};

This branch requires the limited keyword and a list of typestates. First, the macro is performs a recursive call to the first branch to generate the typestate structs, then the macro generates a Limited marker trait and implements it for each generated struct.

Finally, we move on to the final branch:

(strict [$($typestate:ident,)+]) => {
    typestates_gen!([$($typestate,)+]);
    pub trait Limited : sealed::Sealed {}
    $(impl Limited for $typestate {})+
    mod sealed {
        pub trait Sealed {}
        $(impl Sealed for super::$typestate {})+
    }
};

The logic behind this branch is the same as for the second, recurse into the first branch to generate the empty structures and then add and implement the necessary marker traits.

Usage looks like:

typestates_gen!(strict[Idle, Hovering, Flying,]);

Which expands to:

pub struct Idle;
pub struct Hovering;
pub struct Flying;
pub trait Limited: sealed::Sealed {}
impl Limited for Idle {}
impl Limited for Hovering {}
impl Limited for Flying {}
mod sealed {
    pub trait Sealed {}
    impl Sealed for super::Idle {}
    impl Sealed for super::Hovering {}
    impl Sealed for super::Flying {}
}

Problems

Currently, our macros are independent of each other, this implies that while we can implement the types for our Drone, like the following snippet:

impl Drone<Idle> {}

We can still write unbounded implementations, such as:

impl Drone<u8> {}

Furthermore, there are several places where the implementation assumes what the user wants, for example, the strict typestates generate a module named sealed, we need to add the ability to customize such details.

🏡