ideasman42
ideasman42

Reputation: 48058

Is it possible to prevent duplicate identical arguments to a macro in Rust?

There are certain rare cases where it may be useful to prevent duplicate arguments to a macro. One example is this elem(value, ...) macro to check if value is either A, B or C:

if (elem(value, A, B, C)) { .... }

Someone could accidentally pass in the same argument multiple times, e.g.:

if (elem(value, A, B, B)) { .... }

While this is valid Rust, it is almost certainly an accident and highly unlikely to be what the developer intended. This is a trivial example, actual error cases would be more complicated.

Is there a way to have the compiler warn/error when passing in duplicate arguments?

Upvotes: 6

Views: 994

Answers (1)

antoyo
antoyo

Reputation: 11923

One way to achieve what you want is the following:

macro_rules! unique_args {
    ($($idents:ident),*) => {
        {
            #[allow(dead_code, non_camel_case_types)]
            enum Idents { $($idents,)* __CountIdentsLast }
        }
    };
}

macro_rules! _my_elem {
    ($val:expr, $($var:expr),*) => {{
        $($val == $var)||*
    }};
}

macro_rules! my_elem {
    ($($tt:tt)*) => {{
        unique_args!($($tt)*);
        _my_elem!($($tt)*)
    }};
}

The idea is that having the same identifier twice will cause a compiler error because an enum cannot have duplicate variant names.

You can use this as such:

if my_elem!(w, x, y, z) {
    println!("{}", w);
}

Here is an example with an error:

// error[E0428]: a value named `y` has already been defined in this enum
if my_elem!(w, x, y, y) {
    println!("{}", w);
}

However, this will only work with identifiers.

If you want to use literals as well, you will need a macro with a different syntax to be able to differentiate between a literal and an identifier:

macro_rules! unique_idents {
    () => {
    };
    ($tt:tt) => {
    };
    ($ident1:ident, $ident2:ident) => {
        {
            #[allow(dead_code, non_camel_case_types)]
            enum Idents {
                $ident1,
                $ident2,
            }
        }
    };
    ($ident:ident, lit $expr:expr) => {
    };
    ($ident1:ident, $ident2:ident, $($tt:tt)*) => {
        {
            #[allow(dead_code, non_camel_case_types)]
            enum Idents {
                $ident1,
                $ident2,
            }
            unique_idents!($ident1, $($tt)*);
            unique_idents!($ident2, $($tt)*);
        }
    };
    ($ident:ident, lit $expr:expr, $($tt:tt)*) => {
        unique_idents!($ident, $($tt)*);
    };
    (lit $expr:expr, $($tt:tt)*) => {
        unique_idents!($($tt)*);
    };
}

macro_rules! unique_literals {
    () => {
    };
    ($tt:tt) => {
    };
    (lit $lit1:expr, lit $lit2:expr) => {{
            type ArrayForStaticAssert_ = [i8; 0 - (($lit1 == $lit2) as usize)];
    }};
    (lit $lit:expr, $ident:ident) => {
    };
    (lit $lit1:expr, lit $lit2:ident, $($tt:tt)*) => {{
            unique_literals!(lit $lit1, lit $lit2);
            unique_literals!(lit $lit1, $($tt)*);
            unique_literals!(lit $lit2, $($tt)*);
    }};
    (lit $lit:expr, $ident:ident, $($tt:tt)*) => {
        unique_literals!(lit $lit, $($tt)*);
    };
    ($ident:ident, $($tt:tt)*) => {
        unique_literals!($($tt)*);
    };
}

macro_rules! unique_args2 {
    ($($tt:tt)*) => {{
        unique_idents!($($tt)*);
        unique_literals!($($tt)*);
    }};
}

macro_rules! _elem {
    () => {
        false
    };
    ($val:expr) => {
        false
    };
    ($val1:expr, $val2:expr) => {{
        $val1 == $val2
    }};
    ($val1:expr, lit $val2:expr) => {{
        $val1 == $val2
    }};
    ($val1:expr, $val2:expr, $($tt:tt)*) => {{
        $val1 == $val2 || _elem!($val1, $($tt)*)
    }};
    ($val1:expr, lit $val2:expr, $($tt:tt)*) => {{
        $val1 == $val2 || _elem!($val1, $($tt)*)
    }};
}

macro_rules! elem {
    ($($tt:tt)*) => {{
        unique_args2!($($tt)*);
        _elem!($($tt)*)
    }};
}

The uniq_idents! macro uses the same trick as above.

The unique_literals! macro will cause a subtract with overflow error that is caught at compile time.

With these macros, you will need to prefix each literal by lit:

if elem!(w, x, lit 1, z) {
    println!("{}", w);
}

Here are some examples of errors:

// error[E0428]: a value named `y` has already been defined in this enum
if elem!(w, x, y, y) {
    println!("{}", w);
}

// error[E0080]: constant evaluation error
if elem!(w, x, lit 1, z, lit 1) {
    println!("{}", w);
}

I think it is the best we can do without using a compiler plugin.

It is possible to improve these macros, but you get the idea.

Even though there is a stringify! macro that can be use to convert any expression to a string, I don't think we currently have a way to compare these strings at compile time (without a compiler plugin), at least until we have const fn.

Upvotes: 5

Related Questions