rust

Increment constants inside macro


I try to define macro that will be used to define a bunch of constants and one mapping. I suppose that the usage will be like this:

options_filter_consts!(
  MEAL_TYPE => "meal_type" // filter_id
  options { // filter options (const_name => const_value)
     MEAL_NO   => "meal_no"
     BREAKFAST => "breakfast"
     FULLBOARD => "fullboard"
     HALFBOARD => "halfboard"
  }
);

and then it will produce the following code:

// Filter ID
const MEAL_TYPE:&str = "meal_type";

// Filter Options
const NO_MEAL  :&str = "no_meal";
const BREAKFAST:&str = "breakfast";
const FULLBOARD:&str = "fullboard";
const HALFBOARD:&str = "halfboard";

// Option indexes
const NO_MEAL_IDX  :usize = 0;
const BREAKFAST_IDX:usize = 1;
const FULLBOARD_IDX:usize = 2;
const HALFBOARD_IDX:usize = 3;

// Mapping indexes -> options
// Naming: <filter_id> + _MAPPING
const MEAL_TYPE_MAPPING:[&str; 4] = [
  /* NO_MEAL_IDX:   */ NO_MEAL,
  /* BREAKFAST_IDX: */ BREAKFAST,
  /* FULLBOARD_IDX: */ FULLBOARD,
  /* HALFBOARD_IDX: */ HALFBOARD,
];

These constants will be used in main function, e.g.:

fn main() {
  println!("filter_id: {}", MEAL_TYPE);
  println!("mapping_len: {}", MEAL_TYPE_MAPPING.len());
  println!("mapping_item_value: {}", MEAL_TYPE_MAPPING[MEAL_NO_IDX]);
  println!("item_value: {}", BREAKFAST);
}

I started writing macro:

macro_rules! options_filter_consts {
  ($filt_const:ident => $filt_value:expr, options { $($name:ident => $value:expr)+ }) => {
    const $filt_const: &str = $filt_value;

    $(
        const $name : &str = $value;
    )+

    const IOTA:usize = 0; // how to increment inside marco ?

    $(
      paste! {
        const [<$name _IDX>]:usize = IOTA;
      }
    )+
  };
}

But I have errors such as I cannot increment IOTA constant values and get stuck on it.


Solution

  • Currently, Rust macros do not support any code execution inside the macro. The macro language is based on pattern matching and variable substitution and only evaluates macros.

    In such a situation, covering some cases with macros recursion could be possible. For example counting.

    In your case, you could also use a similar approach.

    /// A macro that generates a sequence of constants with incrementing index values.
    macro_rules! increment_consts {
        ($name:ident, $value:expr) => {
            paste! {
                const [<$name _IDX>] : usize = $value;
            }
        };
        ($name:ident, $value: expr, $($rest:ident, $new_value:expr),*) => {
            paste! {
                const [<$name _IDX>] : usize = $value;
            }
            increment_consts!($($rest, $value + 1),*);
        };
    }
    
    /// A macro that generates a sequence of constants with incrementing index values.
    /// The `create_consts!` macro takes a list of identifiers as input and generates
    /// a sequence of constants with incrementing index values for each identifier.
    macro_rules! create_consts {
        ($($rest:ident),* $(,)?) => {
            increment_consts!($($rest, 0),*);
        };
    }
    

    Then in main.rs let's use a new macro.

    fn main() {
        create_consts!(A, B, C, D);
    }
    

    Let's expand the macro with:

    cargo +nightly rustc --profile=check -- -Zunpretty=expanded
    

    It’s working; we have exactly what we need.

    ...
    fn main() {
        const A_IDX: usize = 0;
        const B_IDX: usize = 0 + 1;
        const C_IDX: usize = 0 + 1 + 1;
        const D_IDX: usize = 0 + 1 + 1 + 1;
    }
    

    Let's add it to your solution.

    use paste::paste;
    
    /// A macro that generates a sequence of constants with incrementing index values.
    macro_rules! increment_consts {
        ($name:ident, $value:expr) => {
            paste! {
                const [<$name _IDX>] : usize = $value;
            }
            // small fine-tune to know the final mapping array size
            const _ARR_LEN: usize = $value;
        };
        ($name:ident, $value: expr, $($rest:ident, $new_value:expr),*) => {
            paste! {
                const [<$name _IDX>] : usize = $value;
            }
            increment_consts!($($rest, $value + 1),*);
        };
    }
    
    /// A macro that generates a sequence of constants with incrementing index values.
    /// The `create_consts!` macro takes a list of identifiers as input and generates
    /// a sequence of constants with incrementing index values for each identifier.
    macro_rules! create_consts {
        ($($rest:ident),* $(,)?) => {
            increment_consts!($($rest, 0),*);
        };
    }
    
    macro_rules! options_filter_consts {
        ($filt_const:ident => $filt_value:expr, options { $($name:ident => $value:expr)+ }) => {
            const $filt_const: &str = $filt_value;
    
            $(
                const $name : &str = $value;
            )+
    
            create_consts!($($name),+);
    
            paste! {
                const [<$filt_const _MAPPING>] : [&str; _ARR_LEN + 1] = [$($name,)+];
            }
        };
    }
    
    fn main() {
        options_filter_consts!(
            MEAL_TYPE => "meal_type", // filter_id
            options { // filter options (const_name => const_value)
               MEAL_NO   => "meal_no"
               BREAKFAST => "breakfast"
               FULLBOARD => "fullboard"
               HALFBOARD => "halfboard"
            }
        );
    
        println!("{}", HALFBOARD_IDX);
        println!("{}", MEAL_NO_IDX);
        println!("{}", FULLBOARD_IDX);
        println!("{:?}", MEAL_TYPE_MAPPING);
    }
    

    Expand macro one more time to see what we have this time:

    ...
    fn main() {
        const MEAL_TYPE: &str = "meal_type";
        const MEAL_NO: &str = "meal_no";
        const BREAKFAST: &str = "breakfast";
        const FULLBOARD: &str = "fullboard";
        const HALFBOARD: &str = "halfboard";
        const MEAL_NO_IDX: usize = 0;
        const BREAKFAST_IDX: usize = 0 + 1;
        const FULLBOARD_IDX: usize = 0 + 1 + 1;
        const HALFBOARD_IDX: usize = 0 + 1 + 1 + 1;
        const _ARR_LEN: usize = 0 + 1 + 1 + 1;
        const MEAL_TYPE_MAPPING: [&str; _ARR_LEN + 1] =
            [MEAL_NO, BREAKFAST, FULLBOARD, HALFBOARD];
    
        { ::std::io::_print(format_args!("{0}\n", HALFBOARD_IDX)); };
        { ::std::io::_print(format_args!("{0}\n", MEAL_NO_IDX)); };
        { ::std::io::_print(format_args!("{0}\n", FULLBOARD_IDX)); };
        { ::std::io::_print(format_args!("{0:?}\n", MEAL_TYPE_MAPPING)); };
    }
    

    Update:

    If you want to avoid having _ARR_LEN leftover, consider using the count macro.

    macro_rules! count {
        () => { 0 };
        ($odd:tt $($a:tt $b:tt)*) => { (count!($($a)*) << 1) | 1 };
        ($($a:tt $even:tt)*) => { count!($($a)*) << 1 };
    }
    
    /// A macro that generates a sequence of constants with incrementing index values.
    macro_rules! increment_consts {
        ($name:ident, $value:expr) => {
            paste! {
                const [<$name _IDX>] : usize = $value;
            }
            // const _ARR_LEN: usize = $value; -- remove this line
        };
        ($name:ident, $value: expr, $($rest:ident, $new_value:expr),*) => {
            paste! {
                const [<$name _IDX>] : usize = $value;
            }
            increment_consts!($($rest, $value + 1),*);
        };
    }
    
    /// A macro that generates a sequence of constants with incrementing index values.
    /// The `create_consts!` macro takes a list of identifiers as input and generates
    /// a sequence of constants with incrementing index values for each identifier.
    macro_rules! create_consts {
        ($($rest:ident),* $(,)?) => {
            increment_consts!($($rest, 0),*);
        };
    }
    
    macro_rules! options_filter_consts {
        ($filt_const:ident => $filt_value:expr, options { $($name:ident => $value:expr)+ }) => {
            const $filt_const: &str = $filt_value;
    
            $(
                const $name : &str = $value;
            )+
    
            create_consts!($($name),+);
    
            paste! {
                const [<$filt_const _MAPPING>] : [&str; count!($($name)+)] = [$($name,)+];
            }
        };
    }