Limit macro_rules! literal to certain integers

104 Views Asked by At

Problem

How can I create a Rust macro that only accepts certain number literals in a scalable manor?

Ideally, such a solution meets the following requirements:

  • Scales to hundreds of allowed numbers
  • Raises compile time errors where the macro is called when the number literal is invalid

Non-solutions

Multiple branches

You can create a macro where each arm accepts a specific number as shown below. However I also want to perform computation on the allowed numbers, leading to lots of code duplication since I'm dealing with hundreds of allowed numbers.

macro_rules! my_macro {
    (1) => {
        1
    };
    (2) => {
        2
    };
    (3) => {
        3
    };
}

Using compile_error!

The compile_error! macro reports the error inside my_macro, not on the arguments to my_macro!. This can be very unintuitive.

macro_rules! my_macro {
    ($num: literal) => {
        // if number is invalid
        compile_error!("number is invalid")
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // error is reported here

        // else
        $num
    };
}

// Let's say 34 is an invalid number
my_macro!(34)

Context

I'm trying to emulate a behavior with TypeScript's type system where you can create a type that limits the specific numbers a function accepts.

type AllowedNumbers = 1 | 2 | 3

const myFunc = (num: AllowedNumbers) => {
    // ...
}
1

There are 1 best solutions below

3
On BEST ANSWER

You can panic in a const to create a compile-time error dependent on a literal.

macro_rules! one_to_ten {
    ($n:literal) => {
        const _: () = if $n < 1 || $n > 10 {
            panic!("was not between 1 and 10")
        };
    };
}

This produces the following error.

error[E0080]: evaluation of constant value failed
  --> src/lib.rs:10:5
   |
10 |     one_to_ten!(20);
   |     ^^^^^^^^^^^^^^^ the evaluated program panicked at 'was not between 1 and 10', src/lib.rs:10:5
   |
   = note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `one_to_ten` (in Nightly builds, run with -Z macro-backtrace for more info)

Your idea to use compile_error doesn't work anyway since there's no way to conditionally compile it based on the value of a literal besides using multiple branches.

I think this is as good as you'll get with declarative macros. You could create better error messages if you write a procedural macro.