rustrust-macros

Macro function indirection


I'm trying to write a macro for a formatting library of sorts. The idea is you can use a mix of string literals, regular functions, and (this is where I get stuck) inferred/scoped functions. I suspect I could get this working with proc macros, but that's more complicated, and I'd like to keep the code for the macro easier to read if possible.

Example

Here is a trimmed down version of the code that I'm writing.

enum TemplateEntry<T> {
    Text(String),
    Typed(T, String),
}

enum ColorType {
    Red(String),
    Blue(String),
}

impl ColorType {
    pub fn red(s: &str) -> Self {
        Self::Red(s.to_string())
    
    pub fn blue(s: &str) -> Self {
        Self::Blue(s.to_string())
    }
}

impl Into<TemplateEntry<ColorType>> for ColorType {
    fn into(&self) -> TemplateEntry<Self> {
        match self {
            Self::Blue(text) => TemplateEntry::Typed(self.clone(), text.clone()),
            Self::Red(text) => TemplateEntry::Typed(self.clone(), text.clone()),
        }
    }
}

macro_rules! markup_helper {
    ($temp_vec:ident,  $type_id:tt, @ $x:expr) => {{
        $temp_vec.push($type_id::$x);
    }};
    ( $temp_vec:ident, $type_id:tt, $x:expr) => {{
        $temp_vec.push($x.into());
    }};
}

#[macro_export]
macro_rules! markup {
    ( $type_id:tt, $( $($opt:literal)? $rest:expr ),* ) => {
        {
            let mut temp_vec: Vec<TemplateEntry<$type_id>> = Vec::new();
            $(
                markup_helper!(temp_vec, $type_id, $($opt)? $rest);
            )*
            temp_vec
        }
    };
}

fn example_call() {
    let _example = markup!(ColorType, "regular text", @blue("other text"), "more regular text", regular_func_returning_text());
    // What I want it to expand to
    let _example = {
        let mut temp_vec: Vec<TemplateEntry<ColorType>> = Vec::new();
        temp_vec.push("regular text".into());
        temp_vec.push(ColorType::blue("other text"));
        temp_vec.push("more regular text".into());
        temp_vec.push(regular_func_returning_text().into());
        temp_vec
    };
}

The @ lookup doesn't work properly because the compiler can't tell if string literals are the optional literal or the expression. Also, it doesn't seem to forward the @ to the helper if I try to parse it with a recursively defined macro.

Reframed Questions

Is there a way I can leverage use to try and look up associated functions on the base type maybe without the @ indicator?

Is there something obvious I'm missing for getting @ or really any other handy indicator symbol to work that would allow a macro like this to work?


Solution

  • Thanks to @eggyal, here is the final version I ended up with, which is almost entirely what they provided, just refactored for my own personal preferences.

    macro_rules! markup_internal {
        // Expand leading `@` into `$type_id::`
        (($temp_vec:ident:$type_id:ident): [@ $($rest:tt)*]) => {
            markup_internal!(($temp_vec:$type_id): [$type_id::$($rest)*])
        };
    
        // Handle all other entries
        (($temp_vec:ident:$type_id:ident): [$expr:expr$(, $($rest:tt)*)?]) => {
            // Actually append the entries
            $temp_vec.push($expr.into());
            // Recursively expand
            $(markup_internal!(($temp_vec:$type_id): [$($rest)*]))?
        };
        
        // Supports trailing commas
        (($temp_vec:ident:$type_id:ident): []) => {}
    }
    
    #[macro_export]
    macro_rules! markup {
        // set up the array and push each list item into it by recursively invoking this macro
        ($type_id:ident: [$($rest:tt)*]) => {{
            let mut temp_vec = Vec::<TemplateEntry<$type_id>>::new();
            markup_internal!((temp_vec:$type_id): [$($rest)*]);
            temp_vec
        }};
    }