How to add conditional string to the #[doc] attr generated by macro_rules!

Hi Community !

I want to add conditional string to the #[doc] attr which generated in the macros:

#[macro_export]
macro_rules! fields {
    (
        $(#[$enum_attr:meta])*
        pub enum $enum_name:ident {
            $(
                $(#[$variant_attr:meta])*
                $field_type:ident $(( $($custom_types:ident),+ ))?
                $([$len:expr])? $variant:ident = $start_index:expr$(,)?
            )*
        }
    ) => {
        $(#[$enum_attr])*
        #[derive(Serialize, Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
        #[allow(dead_code)]
        pub enum $enum_name {
            $(
                $(#[$variant_attr])*
                #[doc = stringify!(FieldValue::$field_type)$(if $len >= 1 {stringify!(Array)})?]
                $variant,
            )*
        }
        // ... rest code

here I expect "Array" word will be added to the end if $len param exists. Otherwise there will be a string without "Array" in it. The usage of the macro:

fields! {
    pub enum ObjectField {
        Long Guid = 0,
        Integer Type = 2,
        Integer Entry = 3,
        Float ScaleX = 4,
        Long[2] ArrayField = 5,
    }
}

but if I try to add this string using this code:

#[doc = stringify!(FieldValue::$field_type)$(if $len >= 1 {stringify!(Array)})?]

I got an error:

error: expected one of `.`, `?`, `]`, or an operator, found keyword `if`
   --> src/primary/traits/src/types/update_fields.rs:25:62
    |
25  |                   #[doc = stringify!(FieldValue::$field_type)$(if $len >= 1 {stringify!(Array)})?]
    |                                                                ^^ expected one of `.`, `?`, `]`, or an operator

could somebody explain, how can I fix this ?

There’s no magic macro language with if statements or anything like that. The only way to express selection between two alternatives in a declarative macro is to use two separate patterns.

3 Likes

This is not valid syntax.

What you could do instead is use multiple rules inside your macro for the different possible values $len can take. Like one for [n] (where we want to add the "Array" suffix to the docs), one for [1] (no suffix) and one for non-existent $len (also no suffix), I presume. Here a minimal example of what I have in mind.

1 Like

yep, this could help, but except this I have also a lot of code which added by this macro. Full macro is:

#[macro_export]
macro_rules! fields {
    (
        $(#[$enum_attr:meta])*
        pub enum $enum_name:ident {
            $(
                $(#[$variant_attr:meta])*
                $field_type:ident $(( $($custom_types:ident),+ ))?
                $([$len:expr])? $variant:ident = $start_index:expr$(,)?
            )*
        }
    ) => {
        $(#[$enum_attr])*
        #[derive(Serialize, Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
        #[allow(dead_code)]
        pub enum $enum_name {
            $(
                $(#[$variant_attr])*
                #[doc = stringify!(FieldValue::$field_type)$(if $len >= 1 {stringify!(Array)})?]
                $variant,
            )*
        }

        #[allow(dead_code)]
        impl $enum_name {
            pub fn get_field_name(&self) -> String {
                match self {
                    $(
                        Self::$variant => format!("{}::{}",
                            stringify!($enum_name),
                            tentacli_utils::camel_to_upper_snake_case(
                                stringify!($variant)
                            ).to_uppercase()
                        ),
                    )*
                }
            }

            pub fn get_limit() -> u32 {
                let mut max_value: u32 = 0;
                $(
                    max_value = max_value.max($start_index);
                )*
                max_value.wrapping_add(1)
            }

            pub fn read_from(
                buffer: Vec<u32>,
                update_mask: &mut Vec<bool>,
            ) -> AnyResult<BTreeMap<Self, FieldValue>> {
                let mut fields = BTreeMap::new();

                let indices_to_update: Vec<u32> = update_mask.iter()
                    .enumerate()
                    .filter_map(|(index, &value)| if value { Some(index as u32) } else { None })
                    .collect();

                let mut buffer_iter = buffer.iter();

                $(
                    #[allow(unused_assignments)]
                    #[allow(unused_mut)]
                    let mut size: u32 = 1;
                    $(
                        size = $len;
                    )?

                    let field_type = stringify!($field_type);

                    let types = match field_type {
                        "Custom" => {
                            #[allow(unused_mut)]
                            let mut types = Vec::new();
                            $(
                                $(
                                    types.push(stringify!($custom_types).to_string());
                                )*
                            )?
                            types
                        }
                        _ => vec![field_type.to_string()],
                    };

                    // TODO: rename this
                    let offset = types.iter().fold(0, |sum, field_type| {
                        sum + match field_type.as_str() {
                            "Long" => 2,
                            _ => 1
                        }
                    });

                    let range: Range<u32> = $start_index..($start_index + size * offset);

                    let field_indices = indices_to_update.iter()
                        .filter(|&index| range.contains(index))
                        .cloned()
                        .collect::<Vec<u32>>();

                    if !field_indices.is_empty() {
                        let field_value = Self::read_value(field_type, types, &mut buffer_iter, field_indices, range)?;

                        if let Some(variant) = Self::get_variant_by_index($start_index) {
                            fields.insert(variant, field_value);
                        }
                    }
                )*

                Ok(fields)
            }

            fn read_value(
                field_type: &str,
                types: Vec<String>,
                buffer_iter: &mut Iter<u32>,
                field_indices: Vec<u32>,
                range: Range<u32>
            ) -> AnyResult<FieldValue> {
                let value = match field_type {
                    "Long" => {
                        let mut values: Vec<u32> = vec![];

                        for i in range {
                            if !field_indices.contains(&i) {
                                values.push(0);
                                continue;
                            }

                            values.push(*buffer_iter.next()
                                .ok_or(anyhow!("Cannot read item(u32 of u64)"))? as u32);
                        }

                        let mut values_u64 = vec![];
                        for i in (0..values.len()).step_by(2) {
                            let low = values[i] as u64;
                            let high = values[i + 1] as u64;
                            values_u64.push(low | (high << 32))
                        }

                        if values.len() > 2 {
                            FieldValue::LongArray(values_u64)
                        } else {
                            FieldValue::Long(values_u64[0])
                        }
                    }
                    "Integer" => {
                        let mut values = vec![];
                        for i in range {
                            if !field_indices.contains(&i) {
                                values.push(0);
                                continue;
                            }

                            values.push(*buffer_iter.next()
                                .ok_or(anyhow!("Cannot read item(i32)"))? as i32);
                        }

                        if values.len() > 1 {
                            FieldValue::IntegerArray(values)
                        } else {
                            FieldValue::Integer(values[0])
                        }
                    }
                    "Bytes" => {
                        let mut values = vec![];

                        for i in range {
                            if !field_indices.contains(&i) {
                                values.push(0);
                                continue;
                            }

                            values.push(*buffer_iter.next()
                                .ok_or(anyhow!("Cannot read item(bytes)"))?);
                        }

                        if values.len() > 1 {
                            FieldValue::BytesArray(values)
                        } else {
                            FieldValue::Bytes(values[0])
                        }
                    }
                    "Float" => {
                        let mut values = vec![];

                        for i in range {
                            if !field_indices.contains(&i) {
                                values.push(0.);
                                continue;
                            }

                            let value = *buffer_iter.next()
                                .ok_or(anyhow!("Cannot read item(f32)"))?;
                            values.push(f32::from_bits(value));
                        }

                        if values.len() > 1 {
                            FieldValue::FloatArray(values)
                        } else {
                            FieldValue::Float(values[0])
                        }
                    }
                    "TwoShorts" => {
                        let mut values = vec![];

                        for i in range {
                            if !field_indices.contains(&i) {
                                values.push((0, 0));
                                continue;
                            }

                            let value = *buffer_iter.next()
                                .ok_or(anyhow!("Cannot read item(two shorts)"))?;
                            let first = (value & 0xFFFF) as i16;
                            let second = (value >> 16) as i16;

                            values.push((first, second));
                        }

                        if values.len() > 1 {
                            FieldValue::TwoShortsArray(values)
                        } else {
                            FieldValue::TwoShorts(values[0])
                        }
                    }
                    "Custom" => {
                        let mut values = vec![];
                        let mut cycle_iter = types.iter().cycle();
                        let mut sub_values = vec![];

                        for i in range {
                            let field_type = cycle_iter.next()
                                .ok_or(anyhow!("Cannot read field type for custom"))?;

                            if !field_indices.contains(&i) {
                                sub_values.push(FieldValue::None);
                            } else {
                                let value = Self::read_value(
                                    field_type, vec![field_type.to_string()], buffer_iter, vec![i], i..i+1,
                                )?;
                                sub_values.push(value);
                            }

                            if sub_values.len() == types.len() {
                                values.push(sub_values.clone());
                                sub_values.clear();
                            }
                        }

                        if values.len() > 1 {
                            FieldValue::CustomArray(values)
                        } else {
                            FieldValue::Custom(values[0].clone())
                        }
                    },
                    _ => FieldValue::None
                };

                Ok(value)
            }

            fn get_name_by_index(index: u32) -> Option<String> {
                match index {
                    $(
                        $start_index => Some(stringify!($variant).to_string()),
                    )*
                    _ => None,
                }
            }

            fn get_variant_by_index(index: u32) -> Option<Self> {
                match index {
                    $(
                        $start_index => Some(Self::$variant),
                    )*
                    _ => None,
                }
            }

            pub fn get_index(variant: &$enum_name) -> u32 {
                match variant {
                    $(
                        Self::$variant => $start_index,
                    )*
                }
            }
        }
    };
}

if I add multiple rules, I will need to duplicate all this stuff, doesn't it ?

Sounds like you should be writing a procedural macro at this point.

1 Like

No, you can forward rules to each other by simply transforming the input into something the other rule understands and then calling the macro with the transformed input. So you could have one giant rule (or a separate macro) that does the code generation and pass it a literal "Array" if you want it in your docs or pass nothing if you don't. Here an example of what I mean.

2 Likes

My purpose to implement macro_rules! was to make the fields representation as compact as possible. So in case I add proc-macro, there will be attribute for each field. And this way there will be a lot of screen place taken.

For example, this is player fields:

fields! {
    pub enum PlayerField {
        Long DuelArbiter = 148,
        Integer Flags = 150,
        Integer GuildId = 151,
        Integer GuildRank = 152,
        Bytes[3] Bytes = 153,
        Integer DuelTeam = 156,
        Integer GuildTimestamp = 157,
        Custom (Integer, Integer, TwoShorts, TwoShorts, Integer)[25] QuestLog = 158,
        Custom (Integer, TwoShorts)[12] VisibleItems = 283,
        Integer ChosenTitle = 321,
        Integer FakeInebriation = 322,
        Integer Pad0 = 323,
        Long[23] InvSlot = 324,
        Long[16] PackSlot = 370,
        Long[28] BankSlot = 402,
        Long[7] BankBagSlot = 458,
        Long[12] VendorBuybackSlot = 472,
        Long[32] KeyringSlot = 496,
        Long[32] CurrencyTokenSlot = 560,
        Long Farsight = 624,
        Long[3] KnownTitles = 626,
        Long KnownCurrencies = 632,
        Integer Xp = 634,
        Integer NextLevelXp = 635,
        TwoShorts[384] SkillInfo = 636,
        Integer[2] CharacterPoints = 1020,
        Integer TrackCreatures = 1022,
        Integer TrackResources = 1023,
        Float BlockPercentage = 1024,
        Float DodgePercentage = 1025,
        Float ParryPercentage = 1026,
        Integer Expertise = 1027,
        Integer OffhandExpertise = 1028,
        Float CritPercentage = 1029,
        Float RangedCritPercentage = 1030,
        Float OffhandCritPercentage = 1031,
        Float[7] SpellCritPercentage = 1032,
        Integer ShieldBlock = 1039,
        Float ShieldBlockCritPercentage = 1040,
        Bytes[128] ExploredZones = 1041,
        Integer RestStateExperience = 1169,
        Integer Coinage = 1170,
        Integer[7] ModDamageDonePos = 1171,
        Integer[7] ModDamageDoneNeg = 1178,
        Integer[7] ModDamageDonePct = 1185,
        Integer ModHealingDonePos = 1192,
        Float ModHealingPct = 1193,
        Float ModHealingDonePct = 1194,
        Integer ModTargetResistance = 1195,
        Integer ModTargetPhysicalResistance = 1196,
        Bytes FieldBytes = 1197,
        Long AmmoId = 1198,
        Integer SelfResSpell = 1199,
        Integer PvpMedals = 1200,
        Integer[12] BuybackPrice = 1201,
        Integer[12] BuybackTimestamp = 1213,
        TwoShorts Kills = 1225,
        Integer TodayContribution = 1226,
        Integer YesterdayContribution = 1227,
        Integer LifetimeHonorableKills = 1228,
        Integer FieldBytes2 = 1229,
        Integer WatchedFactionIndex = 1230,
        Integer[25] CombatRating = 1231,
        Integer[21] ArenaTeamInfo1 = 1256,
        Integer HonorCurrency = 1277,
        Integer ArenaCurrency = 1278,
        Integer MaxLevel = 1279,
        Integer[25] DailyQuests = 1280,
        Float[4] RuneRegen = 1305,
        Integer[3] NoReagentCost = 1309,
        Integer[6] GlyphSlots = 1312,
        Integer[6] Glyphs = 1318,
        Integer GlyphsEnabled = 1324,
        Integer PetSpellPower = 1325,
    }
}

Imagine how much place will be taken in case I will use proc-macro. It will be pretty hard to read.

Sorry but I have zero idea what you are trying to say. A proc-macro can parse the same input just as well. If you somehow think that you would necessarily need to provide more verbose input to a proc-macro, that's not the case.

1 Like

I mean, instead of current input:

fields! {
    pub enum ObjectField {
        Long Guid = 0,
        Integer Type = 2,
        Integer Entry = 3,
        Float ScaleX = 4
    }
}

to use proc-macro I will need smth like:

#[derive(MyProcMacro)]
pub enum ObjectField {
    #[field(Long)]
    Guid = 0,
    #[field(Integer)]
    Type = 2,
    #[field(Integer)]
    Entry = 3,
    #[field(Float)]
    ScaleX = 4,
}

which is more verbose

On nightly (soon on stable, I believe):

$( ${ignore($len)}
    /* code to emit if `$len` was provided */
    "Array"
)?

And then, to have an optional string appended / concatenated to the previous docstring, the trick is to have it all wrapped within a concat!() invocation:

#[doc = concat!(
    ::core::stringify!( FieldValue :: $field_type ),
    $( ${ignore($len)}
        "Array",
    )?
)]

Stable Rust polyfill

Otherwise you could have a helper macro to do the ignore-ing part on stable rust:

macro_rules! ignore {( $($ignored:expr)? ) => ( "" )}

so as to write:

#[doc = concat!(
    ::core::stringify!( FieldValue :: $field_type ),
    $(
        ignore!($len),
        "Array",
    )?
)]
  • (I am taking advantage of concat!( …, "", … ) being the same as concat!(…, …))
3 Likes

btw, this is my mistake. Idk why I always thought proc attr should be only under the field, but actually this part also works:

pub enum ObjectField {
    #[field(Long)] Guid = 0,
    #[field(Integer)] Type = 2,
    #[field(Integer)] Entry = 3,
    #[field(Float)] ScaleX = 4,
}

most likely proc-macro will be the best solution for this issue. Macro_rules adds some mess.

Thanks everyone for the answers and for your time ! I marked the best answer and I think I should refactor my code to use proc-macro instead.

Why would you need that? You don't have to write a derive macro. You can write a function-like proc-macro that accepts exactly the same input as the current declarative macro.

1 Like

ah, I see, thank you for clarification. I just never implemented such macro before. Need to investigate this topic a bit.

It's off topic, but I gotta ask: what are the semantics of this? Or perhaps the better question, could you provide me with a link to e.g. the RFC?

Macro meta expressions have gone through a lot of churn and development since the RFC, unfortunately​. I wouldn't call them "soon on stable" either; there was an attempt to stabilize previously that got backed out for various concerns, which I don't recall having been addressed. (But it's been a while, so I'm not sure.)

As for semantics, ${} delimits a metavar expression, of which ignore is the most trivial, taking a metavar (thus participating as a controlling usage for macro repetition) but expanding to nothing. The additional ability this unlocks is that ${} can exist anywhere in the token stream (since it's processed as part of the containing macro's expansion), whereas calling into a helper macro is limited to specific syntactic usage.

But also in full fairness, ignore is mostly just along for the ride with other metavar functionality, notably including a way to directly get the repetition count.

2 Likes

In my most ambitious macro_rules! abuse, I have a helper macro that looks something like the following:

macro_rules! tt_cond {
    {
        if { $($cond:tt)+ }
        => { $($then:tt)* }
        else { $($else:tt)* }
    } => {
        $($then)*
    };
    {
        if { /* empty */ }
        => { $($then:tt)* }
        else { $($else:tt)* }
    } => {
        $($else)*
    };
}

This doesn't fully eliminate the need to have a way to discard a "dummy value" like is produced when using ignore! (and in fact I still use a similar macro since it's often useful), but it's sometimes useful in some of the places that using ignore! isn't straightforward.

... and also #[doc] has some quirks of its own on what works and what doesn't that I can never fully remember.

I found better solution !

#[doc = concat!(
    stringify!(FieldValue::$field_type ),
    $(
        "Array(", stringify!($len), ")"
    )?
)]
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.