Hello Mesozoic World: Enums and Macros

The world sucks,[citation needed] but you know that it would suck a whole lot less if only there were a definitive, cross-platform Pterosaur Classification app.

So, dutiful citizen that you are, you roll up your sleeves and get to work.

The first five taxonomic levels practically write themselves:

/// # Pterosaur Domain.
pub const DOMAIN: &str =  "Eukarya";

/// # Pterosaur Kingdom.
pub const KINGDOM: &str = "Animalia";

/// # Pterosaur Phylum.
pub const PHYLUM: &str =  "Chordata";

/// # Pterosaur Class.
pub const CLASS: &str =   "Reptilia";

/// # Pterosaur Order.
pub const ORDER: &str =   "Pterosauria";

The remaining levels are more diverse, of course, but there are only so many families, genera, and species to go around, so they should just be simple enums, right?

Right.

The definition for Family is clean and straightforward. The obligatory as_str helper method winds up being more of the same. The equal and opposite FromStr trait implementation, likewise, more of the same…

Hmmm.

There's nothing wrong with repetition in and of itself, but how many times can a person reasonably be expected to type out "Rhamphorhynchidae" correctly?!

Would you even notice if you didn't?

If it were outright missing from one of the lists, would you notice that?

Would it help or hurt if there were, say, 15-20x as many variants?

Problems and Solutions.

You (the reader) might be thinking that you (the programmer) is just a big baby who needs to suck it up and type everything correctly because that's what programming is.

And, well, sure, but that's not really the point.

Typos and omissions and (un)maintainability are secondary problems. They aren't ideal, of course, but they're merely symptoms of something more… specific:1

Human beings are not particularly good at dealing with repetitive taskwork, at least not compared with machines.[citation needed]

The problem, in a nutshell, is who's doing what.

You (the programmer) already know that Rust code can be used to tell a computer what to do — see e.g. this program — but Rust code can also be used to generate Rust code.

This sort of abstraction is called a macro, and as luck should have it, it can solve all of our enum-wrangling problems, including a whole bunch that you hadn't even noticed yet.

Rust technically supports two different kinds of macro, but for our purposes, it's sufficient to say that one of them2 is the traditional and obvious choice for this particular situation, and the other3 is what we'll be using. Haha.

Let's see how we can make things better for our fine fuzzy friends, shall we?

To/From String Conversion.

When setting up the Family object, the most obviously pointless repetitions emerged in the as_str/from_str conversion helpers.

Conversions of this sort are common when working with enums and relatively easy to solve with declarative macros, so are as good a place to start as any.

Pterosaur families are depressingly patchwork,4 so any attempt to build an example around these groupings would be laughably outdated in no time at all.

For the remainder of the article, let's focus on the genera instead, where the ground is a little firmer. That there are hundreds of them,5 so much the better for illustrating the need for this kind of automation.

Before we dive in, let's get the lay of the land.

Linnaean genera happen to follow the same casing conventions as Rust variant names, so their literals are literally identical, at least insofar as as_str will be concerned:

…
Self::Dsungaripterus => "Dsungaripterus",
…

The reverse from_str operation, however, will be a little more complicated because we'll need to:

  1. Match case-insensitively;6
  2. Map synonymized/corrected names to their valid contemporaries;
  3. Trigger appropriate errors for historical misidentifications and dubia;
  4. Trigger a generic error for everything else;

In short, we'll need an error type and a (re)casing helper method in addition to the FromStr trait implementation itself.

Let's knock out the first two things real quick since they're just so much background detail:

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
/// # Classification Error Kind.
///
/// This error is used to classify errors of classification at the level of
/// [`Family`], [`Genus`], and [`Species`].
pub enum LinnaErrorKind {
    /// # Just Plain Wrong.
    Invalid,

    /// # Misidentified (Not a Pterosaur).
    Misidentification,

    /// # Doubtful.
    NomenDubium,

    /// # Inadequately Documented.
    NomenNudum,

    /// # Name Already Taken (Second Choice Still TBD).
    Preoccupied,
}

/// # Fix Case for [`Family`]/[`Genus`] Matching.
///
/// The families and genera are all formatted like "Foobarus", i.e. capital
/// first, lowercase rest.
///
/// This method checks the input case, adjusting if/as necessary for fairer
/// (subsequent) matching.
///
/// ## Errors
///
/// Returns an error if empty or the first character isn't ASCII to save the
/// trouble of pointless matching.
fn to_sentence_case(src: &str) -> Result<Cow<'_, str>, LinnaErrorKind> {
    let src = src.trim();
    let mut chars = src.chars();

    // If there's no first character, the string is empty and won't match.
    let first = chars.next().ok_or(LinnaErrorKind::Invalid)?;

    // No adjustments necessary.
    if first.is_ascii_uppercase() && chars.all(|c| c.is_ascii_lowercase()) {
        Ok(Cow::Borrowed(src))
    }
    // The casing is wrong; let's fix it up.
    else if first.is_ascii() {
        let mut out = src.to_ascii_lowercase();
        out[..1].make_ascii_uppercase();
        Ok(Cow::Owned(out))
    }
    // Nothing in our database starts with a unicode character, so whatever
    // this is, it won't match.
    else {
        Err(LinnaErrorKind::Invalid)
    }
}

Macro time!

Declarative macros are declared by invoking the macro_rules macro. (The name says it all, really.)

The basic setup includes a name (for your macro) and some number of conditional (pattern) => (output); branches.

Around this point, it is customary for macro tutorials to freak out and fly off the rails attempting to introduce the entirety of the syntax, every last rule and constraint, etc., all in one go.

We'll get a lot farther by disseminating information on a need-to-know basis instead, so let's just plop down the initial declaration, sans output, and explain what it's doing.

macro_rules! genera {
    ($( $k:ident $( $alias:literal )* ),+ $(,)?) => (
        …
    );
}

The $()+, $()*, and $()? are "repetition wrappers". Reminiscent of regular expressions, the +, *, and ? indicate that their wrappees may appear one or more times, zero or more times, and zero or once, repsectively.

The $k:ident means we're capturing an argument called $k that, whatever we wind up using it for, would be valid as something like a variable or function name, or enum variant.

The $alias:literal is also capturing an argument — $alias — but this one has to be a valid string or byte literal.

The keen-eyed among you might have spotted the comma in the middle of ),+. That means the repetitions, if any, must have commas between them.

Were we to end it there, a trailing comma would trigger a compiler error. The $(,)? is there to give us the option.

(If we wanted mandatory trailing commas, we could drop the $(,)? and replace ),+ with , )+, making the comma a (required) part of the wrappee.)

Similar to other Rust code, whitespace in macro definitions — and invocations — is pretty much dealer's choice.

Putting it all together, this pattern expects:

Got it? Good!

Now let's add the output, but since there's a lot of it, let's let the documentation handle the explaining:

/// # Helper: Pterosaur Genera.
macro_rules! genera {
    ($( $k:ident $( $alias:literal )* ),+ $(,)?) => (
        #[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
        pub enum Genus {
            // Repeating captures must be "unwrapped" if you want to use them.
            // The same $()+ are used for this, but everything else can be
            // changed as needed, so long as at least one of the repeating
            // variables appears inside.
            //
            // In this spot, we're just listing all the idents, separated by
            // commas. That comma, by the way, has moved inside the
            // parentheses because I feel better knowing that there'll be a
            // trailing one, but we could have left it on the outside.
            $( $k, )+
        }

        impl TryFrom<&str> for Genus {
            type Error = LinnaErrorKind;

            /// # From String Slice.
            ///
            /// Case-insensitively match a string slice to a [`Genus`],
            /// returning it.
            ///
            /// Synonymized and corrected labels map to their contemporary
            /// equivalents.
            ///
            /// ## Errors.
            ///
            /// An error is returned if the value is unrecognized or has been
            /// rejected since its introduction.
            fn try_from(src: &str) -> Result<Self, Self::Error> {
                match to_sentence_case(src)?.as_ref() {
                    // Valid names and aliases.
                    //
                    // As with the definition, we're unwrapping things again,
                    // but this time we're adding a whole bunch of other
                    // markup to each "loop" because that's what the code
                    // being generated requires.
                    //
                    // The "stringify" macro is part of the standard library.
                    // It helpfully converts our $k ident to a "$k" literal.
                    //
                    // We're also unwrapping the nested $alias literals,
                    // prefixed with a "|" so that if present, we get an match
                    // arm like "Apples" | "Bananas" | "Carrots".
                    //
                    // Finally, we're printing $k again, but as an enum
                    // variant this time, complete with "Self::" prefix to
                    // make it a valid return type.
                    //
                    // Oh, and each arm ends with a comma because the comma is
                    // part of the wrappee.
                    $( stringify!($k) $( | $alias)* => Ok(Self::$k), )+

                    // Specific errors.
                    //
                    // This is just manual code, same as you'd write outside
                    // a macro.
                    //
                    // There isn't much point in automating this since this
                    // dumbassery appears nowhere else, and isn't actually part
                    // of the enum itself.
                    "Aidachar" =>        Err(LinnaErrorKind::Misidentification),
                    "Apatomerus" =>      Err(LinnaErrorKind::Misidentification),
                    "Belonochasma" =>    Err(LinnaErrorKind::Misidentification),
                    "Cimoliornis" =>     Err(LinnaErrorKind::NomenNudum),
                    "Comodactylus" =>    Err(LinnaErrorKind::NomenDubium),
                    "Daitingopterus" =>  Err(LinnaErrorKind::NomenNudum),
                    "Dermodactylus" =>   Err(LinnaErrorKind::NomenDubium),
                    "Dolicorhamphus" =>  Err(LinnaErrorKind::NomenDubium),
                    "Doratorhynchus" =>  Err(LinnaErrorKind::NomenDubium),
                    "Faxinalipterus" =>  Err(LinnaErrorKind::Misidentification),
                    "Gwawinapterus" =>   Err(LinnaErrorKind::Misidentification),
                    "Huaxiapterus" =>    Err(LinnaErrorKind::NomenDubium),
                    "Laopteryx" =>       Err(LinnaErrorKind::NomenDubium),
                    "Lithosteornis" =>   Err(LinnaErrorKind::NomenNudum),
                    "Oolithorhynchus" => Err(LinnaErrorKind::NomenNudum),
                    "Ornithodesmus" =>   Err(LinnaErrorKind::Misidentification),
                    "Osteornis" =>       Err(LinnaErrorKind::NomenNudum),
                    "Palaeornis" =>      Err(LinnaErrorKind::Preoccupied),
                    "Pricesaurus" =>     Err(LinnaErrorKind::NomenNudum),
                    "Ptenodactylus" =>   Err(LinnaErrorKind::Preoccupied),
                    "Rhabdopelix" =>     Err(LinnaErrorKind::Misidentification),
                    "Rhamphocephalus" => Err(LinnaErrorKind::Misidentification),
                    "Sultanuvaisia" =>   Err(LinnaErrorKind::Misidentification),
                    "Tribelesodon" =>    Err(LinnaErrorKind::Misidentification),
                    "Wyomingopteryx" =>  Err(LinnaErrorKind::NomenNudum),

                    // Everything else.
                    _ => Err(LinnaErrorKind::Invalid),
                }
            }
        }
        
        impl Genus {
            #[must_use]
            /// # As String Slice.
            ///
            /// Return the variant as a static string slice.
            pub const fn as_str(self) -> &'static str {
                match self {
                    // As with the other automated bits, we're just unwrapping
                    // the repeating $k again, mapping its ident self to an
                    // equivalent literal, helpfully generated by "stringify".
                    $( Self::$k => stringify!($k), )+
                }
            }
        }
    );
}

Neat, huh?

Of course, Rust won't even bother evaluating our macro code unless we invoke it somewhere, so we need to add some code to run the code that writes the code.

As promised, this list is hilariously large:

genera!{
    Aerodactylus,
    Aerodraco,
    Aerotitan,
    Aetodactylus,
    Afrotapejara,
    Akharhynchus,
    Alamodactylus,
    Alanqa,
    Albadraco,
    Alcione,
    Allkaruen,
    Altmuehlopterus,
    Amblydectes,
    Angustinaripterus,
    Anhanguera,
    Anurognathus         "Paranurognathus",
    Apatorhamphus,
    Arambourgiania       "Titanopteryx",
    Aralazhdarcho,
    Araripedactylus,
    Araripesaurus,
    Archaeoistiodactylus,
    Arcticodactylus,
    Ardeadactylus,
    Argentinadraco,
    Arthurdactylus,
    Aurorazhdarcho,
    Aussiedraco,
    Austriadactylus,
    Austriadraco,
    Aymberedactylus,
    Azhdarcho,
    Bakonydraco,
    Balaenognathus,
    Barbaridactylus,
    Barbosania,
    Batrachognathus,
    Beipiaopterus,
    Bellubrunnus,
    Bennettazhia,
    Bogolubovia,
    Boreopterus,
    Brasileodactylus,
    Cacibupteryx,
    Caelestiventus,
    Caiuajara,
    Camposipterus,
    Campylognathoides    "Campylognathus",
    Carniadactylus       "Bergamodactylus",
    Cascocauda,
    Cathayopterus,
    Caulkicephalus,
    Caupedactylus,
    Caviramus,
    Cearadactylus,
    Ceoptera,
    Changchengopterus,
    Chaoyangopterus,
    Cimoliopterus,
    Coloborhynchus,
    Cratonopterus,
    Cretornis,
    Cryodrakon,
    Ctenochasma          "Ptenodracon",
    Cuspicephalus,
    Cycnorhamphus        "Gallodactylus",
    Daohugoupterus,
    Darwinopterus,
    Dawndraco,
    Dearc,
    Dendrorhynchoides    "Dendrorhynchus",
    Dimorphodon,
    Diopecephalus,
    Domeykodactylus,
    Dorygnathus,
    Douzhanopterus,
    Draigwenia,
    Dsungaripterus,
    Elanodactylus,
    Eoazhdarcho,
    Eopteranodon,
    Eosipterus,
    Eotephradactylus,
    Epapatelo,
    Eudimorphodon,
    Europejara,
    Eurazhdarcho,
    Eurolimnornis,
    Feilongus,
    Fenghuangopterus,
    Ferrodraco,
    Forfexopterus,
    Garudapterus,
    Gegepterus,
    Germanodactylus,
    Gladocephaloideus,
    Gnathosaurus,
    Guidraco,
    Haliskia,
    Hamipterus,
    Haopterus            "Avgodectes",
    Harpactognathus,
    Hatzegopteryx,
    Herbstosaurus,
    Hongshanopterus,
    Huanhepterus,
    Huaxiadraco,
    Iberodactylus,
    Ikrandraco,
    Inabtanin,
    Infernodrakon,
    Istiodactylus,
    Jeholopterus,
    Jianchangnathus,
    Jianchangopterus,
    Jidapterus,
    Kariridraco,
    Kepodactylus,
    Keresdrakon,
    Klobiodon,
    Kunpengopterus,
    Kryptodrakon,
    Lacusovagus,
    Leptostomia,
    Liaodactylus,
    Liaoningopterus,
    Liaoxipterus,
    Linlongopterus,
    Lingyuanopterus,
    Lonchodectes,
    Lonchodraco,
    Lonchognathosaurus,
    Longchengpterus,
    Luchibang,
    Ludodactylus,
    Luopterus,
    Lusognathus,
    Maaradactylus,
    Meilifeilong,
    Melkamter,
    Mesadactylus,
    Microtuban,
    Mimodactylus,
    Mistralazhdarcho,
    Moganopterus,
    Montanazhdarcho,
    Muzquizopteryx,
    Mythunga,
    Navajodactylus,
    Nemicolopterus,
    Nesodactylus         "Nesodon",
    Nicorhynchus,
    Ningchengopterus,
    Nipponopterus,
    Noripterus           "Phobetor",
    Normannognathus,
    Nurhachius,
    Nyctosaurus          "Nyctodactylus",
    Orientognathus,
    Ordosipterus,
    Ornithocheirus       "Criorhynchus",
    Ornithostoma,
    Otogopterus,
    Pachagnathus,
    Palaeocursornis      "Limnornis",
    Pangupterus,
    Parapsicephalus,
    Peteinosaurus,
    Petrodactyle,
    Phosphatodraco,
    Piksi,
    Plataleorhynchus,
    Prejanopterus,
    Preondactylus,
    Propterodactylus,
    Pteranodon           "Geosternbergia",
    Pterodactylus        "Macrotrachelus" "Ornithocephalus" "Pterotherium" "Ptéro-dactyle",
    Pterodaustro,
    Pterofiltrus,
    Pterorhynchus,
    Puntanipterus,
    Qinglongopterus,
    Quetzalcoatlus,
    Radiodactylus,
    Raeticodactylus,
    Rhamphinion,
    Rhamphorhynchus      "Odontorhynchus" "Ornithopterus" "Pteromonodactylus",
    Samrukia,
    Santanadactylus,
    Saratovia,
    Scaphognathus        "Brachytrachelus" "Pachyrhamphus",
    Seazzadactylus,
    Sericipterus,
    Serradraco,
    Shenzhoupterus,
    Simurghia,
    Sinomacrops,
    Sinopterus,
    Siroccopteryx,
    Skiphosoura,
    Sordes,
    Spathagnathus,
    Tacuadactylus,
    Tapejara,
    Targaryendraco,
    Tendaguripterus,
    Tethydraco,
    Thalassodromeus      "Banguela",
    Thanatosdrakon,
    Thapunngaka,
    Torukjara,
    Tropeognathus,
    Tupandactylus        "Ingridia",
    Tupuxuara,
    Uktenadactylus,
    Unwindia,
    Utahdactylus,
    Vectidraco,
    Vesperopterylus,
    Volgadraco,
    Wellnhopterus        "Javelinadactylus",
    Wenupteryx,
    Wightia,
    Wukongopterus,
    Xericeps,
    Yelaphomte,
    Yixianopterus,
    Zhejiangopterus,
    Zhenyuanopterus,
}

Before moving on, let's reflect on what we've already accomplished.

The macro setup itself is relatively minimal, no more than a dozen lines however you want to count them.

The code output by the macro is almost identical to the code we would have manually written anyway, except for the key fact that our gigantic list of genera only appears once.

By removing that list from both the as_str and from_str methods, we've reduced the overall module size by more than four hundred lines.

More importantly, though, we've bound the variants and literals together. It is now impossible for an output from as_str to not map back to the original variant if fed back into from_str.

What's more, we've also made it impossible for any variants to be outright missing from from_str. (It now has the same "exhaustiveness" as the as_str method.7)

Typos are still a risk, but down nearer background levels since there's only one place left for them to live.

While more subtle, moving the aliases inline with their valid betters has made that aspect of the code much more maintainable and, if you dig the columnation, more readable too.

Not a bad start!

Iteration.

Being enumerated, you'd think there'd be some native way to iterate through enum variants, but alas, we're on our own for that too.

Most third-party solutions involve generating whole new structs with custom Iterator trait implementations, but that's overkill for Copy-friendly enums like Genus.

We just need an array.

If you don't mind hardcoding the array size, our existing macro already has everything it needs:

/// # Helper: Pterosaur Genera.
macro_rules! genera {
    ($( $k:ident $( $alias:literal )* ),+ $(,)?) => (
        …
        impl Genus {
            /// # All Genera.
            ///
            /// This array contains all genera, sorted and ready to fly.
            ///
            /// ```
            /// use pterosaur::Genus;
            ///
            /// assert!(Genus::ALL.is_sorted());
            /// ```
            pub const ALL: [Self; 229] = [
                // Unwrap and loop through $k, prefixing each with `Self::` to
                // make them proper types. Commas for separation.
                $( Self::$k, )+
            ];
        }
    );
}

Magic numbers aren't ideal, but this one is actually quite harmless since Rust will verify the array size automatically at compile-time, popping an error — with the correct count — if you happen to get it wrong.

Still, as a courtesy to your future self — and as an excuse to learn a few more tricks — let's automate it anyway.

Family and Species will want this too, so let's split the logic out into its own standalone macro:

/// # Count Items.
macro_rules! count {
    // Nothing.
    () =>                                   ( 0 );

    // One thing.
    ($odd:ident) =>                         ( 1 );

    // An odd number of things, with one or more pairs.
    //
    // Same as the next branch, but with a +1 for the $odd entry.
    ($odd:ident $( $a:ident $b:ident )+) => ( (count!($($a)+) * 2 + 1) );

    // An even number of things, with one or more pairs.
    //
    // Recursively count the number of _pairs_, then double it (since each
    // pair has two entries).
    ($( $a:ident $b:ident )+) =>            ( (count!($($a)+) * 2)     );
}

We've already covered everything happening on the pattern side, but the output side this time around recurses, which is definitely worth a closer look!

Macros can't actually "count", but they can generate expressions that are valid in e.g. [Self; N] type definitions, so we can fake it.

The key is to hardcode counts for 0 and 1 — an empty pattern has zero things; a pattern with one argument has one thing — and split the rest of infinity by odd or even pairings.

The latter employ a "strip and recurse" trick,8 recalling count! with fewer and fewer items until, eventually, there's one or none left, building up longer and longer expressions as they go.

This would be a mean thing to do at runtime, but macro magic is a purely compile-time experience, and on-the-fly at that. Whenever Rust stumbles across a macro! call, it immediately "expands" it into the matching output, just as if that's what had been there all along.

In this case, the result would look something like (((1 * 2) * 2) * 2), but much longer and uglier. The compiler, seeing that mess, will thankfully crunch all the numbers for us, leaving only the answer in its place, same as if we had hardcoded it ourselves.

Speaking of which, let's un-hardcode that value:

/// # Count Items.
macro_rules! count {
    () =>                          ( 0 );
    ($odd:tt) =>                   ( 1 );
    ($odd:tt $( $a:tt $b:tt )+) => ( (count!($($a)+) * 2 + 1) );
    ($( $a:tt $b:tt )+) =>         ( (count!($($a)+) * 2)     );
}

/// # Helper: Pterosaur Genera.
macro_rules! genera {
    ($( $k:ident $( $alias:literal )* ),+ $(,)?) => (
        …
        impl Genus {
            /// # All Genera.
            ///
            /// This array contains all genera, sorted and ready to fly.
            ///
            /// ```
            /// use pterosaur::Genus;
            ///
            /// assert!(Genus::ALL.is_sorted());
            /// ```
            pub const ALL: [Self; count!( $($k)+ )] = [ $( Self::$k, )+ ];
            //                    ^ Plop it down where the number should go.
        }
    );
}

Note that count! has to appear before genera!, spatially speaking, because Rust macros can only reference things that have already been "parsed" or are explicitly passed to them.

Fun with Arrays.

The Genus::ALL constant may have been created as a cheap and ready-made iterator, but now that we have it, we can use it for all sorts of other purposes too.

For example, if you wanted to have easy access to the first and last genera, alphabetically speaking, that'd be as easy as:

/// # Helper: Pterosaur Genera.
macro_rules! genera {
    ($( $k:ident $( $alias:literal )* ),+ $(,)?) => (
        …
        impl Genus {
            /// # First `Genus`.
            pub const MIN: Self = Self::ALL[0];

            /// # Last `Genus`.
            pub const MAX: Self = Self::ALL[Self::ALL.len() - 1];
        }
    );
}

Or perhaps you want to give pterosaurologists basic back/next-style navigation?

This'll be easy too, thanks to a characteristic of enums we haven't gotten around to discussing yet: "enumeration".

Rust hides most of the nittygritty, but enumerated types are called that because they're essentially just so many integer constants.

These constant values — called "discriminants" — can be assigned manually, but otherwise default to 0_isize... They can be accessed "for free"9 with simple as casting:

assert_eq!(Genus::Aerodactylus as isize, 0);
assert_eq!(Genus::Aerodraco as isize,    1);
assert_eq!(Genus::Aerotitan as isize,    2);
…

You probably see where this is going, but let's make it happen:

/// # Helper: Pterosaur Genera.
macro_rules! genera {
    ($( $k:ident $( $alias:literal )* ),+ $(,)?) => (
        …
        impl Genus {
            #[must_use]
            /// # Next `Genus`.
            ///
            /// Return the next genus, alphabetically, or `None` if none.
            ///
            /// ```
            /// use pterosaur::Genus;
            ///
            /// for pair in Genus::ALL.windows(2) {
            ///     assert_eq!(
            ///         pair[0].next(),
            ///         Some(pair[1]),
            ///     );
            /// }
            /// ```
            pub const fn next(self) -> Option<Self> {
                // The discriminants correspond to their index in ALL.
                let idx = self as usize + 1;

                if idx < Self::ALL.len() { Some(Self::ALL[idx]) }
                else { None } // We're at the end already.
            }

            #[must_use]
            /// # Previous `Genus`.
            ///
            /// Return the previous genus, alphabetically, or `None` if none.
            ///
            /// ```
            /// use pterosaur::Genus;
            ///
            /// for pair in Genus::ALL.windows(2) {
            ///     assert_eq!(
            ///         pair[1].next(),
            ///         Some(pair[0]),
            ///     );
            /// }
            /// ```
            pub const fn back(self) -> Option<Self> {
                // The discriminants correspond to their index in ALL.
                let idx = self as usize;

                if 0 != idx { Some(Self::ALL[idx - 1]) }
                else { None } // We're at the start already.
            }
        }
    );
}

K-Pg.

Genus is shaping up nicely!

Of course, we ignored a whole bunch of other data that would benefit from macroization — where and when each was first discovered, where and when each lived, etc. — but that's your problem now. Haha.

Arguments can be added to genera! until the cows come home.

We also largely ignored unit testing — aside from a few doctest examples — but thanks to Genus::ALL, you can automate those pretty easily, whether as part of the macro or another module entirely.

On the (declarative) macro side of things, we covered maybe 1% of all there is to know. An amacro-bouche, if you will. The full meal takes years to consume and digest, so good luck with that.

If this article piqued your interest in pterosaurs more than programming, check out David Unwin's The Pterosaurs: From Deep Time and Mark Witton's Pterosaurs: Natural History, Evolution, Anatomy.

---

1. You (the pterosaurologist) find this hilarious.

2. One can do a lot more with procedural macros, but they require extra dependencies, have to live in their own crate, and tend to noticeably slow down build times.

3. Declarative macro syntax is comparatively weird and confusing and comes with more constraints, but they're natively supported and can live alongside normal Rust code.

4. Pneumatic bones are great for flight, terrible for posterity.

5. Wikipedia's list of pterosaur genera is a good enough reference for our purposes.

6. Shift keys have a habit of breaking off in the field.

7. Rust exhaustively matches patterns so would warn us if we forgot a variant in as_str, but the righthand side has no such protections. For from_str, Rust only knows/cares that we're exhaustively matching all possible &str.

8. ($first:ident $( $rest:ident )+) => ( (1 + count!($($rest)+)) ); is a more obvious strip-and-recurse pattern, but we have too many genera for the default recursion limit. Our count! macro recurses through pairs instead, then doubles the result, requiring about half as many go-arounds. Even so, we'd likely have to bump that limit eventually to accommodate new finds.

9. Variant casting works just like it does for primitive integers, but only in one direction; 0_u8 as Genus is unsupported. For that, you either have to transmute or manually match/map the entire range.

Josh Stoik
6 September 2025
Previous Fixing Intel Meteor Lake: Less Is More