Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

macros

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?

  • Arguments are not necessarily all constants, they could be mixed with variables too.

  • This is an actual bug I found in some code. While there's a limit macros/compilers can go to prevent mistakes, this could have been detected early if the macro didn't allow it. These kinds of mistakes should be found in code review but mistakes happen.

  • One way to do this (which isn't fool proof), could be to convert the identifiers to strings, then static-assert if any of the identifiers are exact matches. This has the obvious drawback that different identifiers may represent the same constant value. The same identifier could also be written so as to not compare, e.g.: A[0] vs A[ 0 ].

  • If the preprocessor/compiler can't do this easily, a fall-back solution may be some basic static checking tool.

  • I managed to do this with the C preprocessor.

like image 264
ideasman42 Avatar asked Aug 13 '16 09:08

ideasman42


1 Answers

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.

like image 126
antoyo Avatar answered Oct 20 '22 03:10

antoyo