Tuple indexing in macro

1.6k Views Asked by At

I'm trying to index data tuple in macro that generate signature for trait implementation, but have some errors. Can I index tuple or need another solution? Hack with tuple_index I found in google but it not works for me.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=7d4bc0f56c643cf4693279a9bd9db973

macro_rules! expr { ($x:expr) => ($x) } // HACK
macro_rules! tuple_index {
    ($tuple:expr, $idx:tt) => { expr!($tuple.$idx) }
}
macro_rules! gen_packer {
    (@step $data: expr, $_idx:expr,) => {};

    (@step $data: expr, $idx:expr, $T:ident, $($tail:ident,)*) => {
        // out.append(&mut $T::pack(tuple_index!(data,$idx)));
        tuple_index!($data, $idx);

        gen_packer!(@step $data, $idx + 1, $($tail,)*);
    };

    ($($T:ident),*) => {
        impl<$($T,)+> Packer<($($T,)+)> for Iproto
        where $($T: Pack<$T>,)+
        {
            fn pack(self, data: ($($T,)+)) -> Vec<u8> {
                let mut out = vec![];
                gen_packer!(@step data, 0, $($T,)*);
                return out
            }
        }
    }
}

gen_packer!(A);

Errors on compilation:

error: unexpected token: `0`
--> src/lib/iproto.rs:70:29
   |
70 |         tuple_index!($data, $idx);
   |                             ^^^^
...
99 | gen_packer!(A);
   | --------------- in this macro invocation

error: unexpected token: `,`
  --> src/lib/iproto.rs:63:46
   |
63 |     ($tuple:expr, $idx:tt) => { expr!($tuple.$idx) }
   |                                              ^
...
99 | gen_packer!(A);
   | --------------- in this macro invocation

error: no rules expected the token `0`
  --> src/lib/iproto.rs:70:29
   |
61 | macro_rules! expr { ($x:expr) => ($x) } // HACK
   | ----------------- when calling this macro
...
70 |         tuple_index!($data, $idx);
   |                             ^^^^ no rules expected this token in macro call
...
99 | gen_packer!(A);
   | --------------- in this macro invocation

error: aborting due to 3 previous errors
4

There are 4 best solutions below

0
On

I don't think it's possible with declarative macros. However, that's an easy job for procedural macros.

Here's a library to help with this specific use case: https://github.com/bkchr/impl-trait-for-tuples

0
On

Can I index tuple or need another solution?

No. The expression $idx + 1 will produce distinct tokens, for example 0, +, 1, and there is no way to evaluate that to a single literal token within a declarative macro.

0
On

The answer by @PeterHall correctly explained why your approach can't work. But there is a way to do that with declarative macros.

The trick is to use destructuring. There are two ways to access tuple elements: one is field access, like you tried. But then you face the problem that you cannot generate the indices. The only way to solve that with only declarative macros is to pass the indices, just like you pass the generic parameter names.

But there is another way: destructuring. And here we just need names to destructure into, not numbers. And we already have names: the generic parameters. We can just reuse them.

macro_rules! gen_packer {
    ($($T:ident),+) => {
        impl<$($T,)+> Packer<($($T,)+)> for Iproto
        where $(Iproto: Packer<$T>,)+
        {
            fn pack(&mut self, data: ($($T,)+)) -> Vec<u8> {
                #[allow(non_snake_case)]
                let ( $($T,)+ ) = data;
                let mut out = vec![];
                $(
                    out.append(&mut self.pack($T));
                )*
                out
            }
        }
    }
}


pub struct Iproto { }
pub trait Packer<T> {
    fn pack(&mut self, arg: T) -> Vec<u8>;
}

gen_packer!(A);
gen_packer!(A, B);
// ...

We can use another neat trick to save us the need to have multiple calls to our macros, by calling it from itself recursively with less parameters:

macro_rules! gen_packer {
    () => {
        impl Packer<()> for Iproto {
            fn pack(&mut self, _data: ()) -> Vec<u8> {
                vec![]
            }
        }
    };
    
    ($first:ident $(, $rest:ident)*) => {
        impl<$first $(, $rest)*> Packer<($first, $($rest,)*)> for Iproto
        where
            Iproto: Packer<$first>,
            $(Iproto: Packer<$rest>,)*
        {
            fn pack(&mut self, data: ($first, $($rest,)*)) -> Vec<u8> {
                #[allow(non_snake_case)]
                let ( $first, $($rest,)* ) = data;
                let mut out = vec![];
                out.append(&mut self.pack($first));
                $(
                    out.append(&mut self.pack($rest));
                )*
                out
            }
        }
        
        gen_packer!($($rest),*);
    };
}


pub struct Iproto { }
pub trait Packer<T> {
    fn pack(&mut self, arg: T) -> Vec<u8>;
}

gen_packer!(A, B, C, D, E, F, G, H, I, J, K);
0
On

You can use the typle crate:

#[typle(Tuple for 0..=12)]
impl<T> Packer<T> for Iproto
where
    T: Tuple,
    typle_bound!(i in .. => T<{i}>): Pack<T<{i}>>,
{
    fn pack(self, data: T) -> Vec<u8> {
        let mut out = Vec::new();
        for typle_index!(i) in 0..T::LEN {
            out.append(&mut T::<{i}>::pack(data[[i]]));
        }
        out
    }
}

This generates code equivalent to the following for 0..12 components:

impl<T0, T1> Packer<(T0, T1)> for Iproto
where
    T0: Pack<T0>,
    T1: Pack<T1>,
{
    fn pack(self, data: (T0, T1)) -> Vec<u8> {
        let mut out = Vec::new();
        out.append(&mut <T0>::pack(data.0));
        out.append(&mut <T1>::pack(data.1));
        out
    }
}