Passing computed constants to procmacro

I have a procmacro deriving whose behavior I'd like to customize based on a caller-provided constant. The standard way is to provide this constant as a literal attribute; for example I can provide the literal array [1, 2, 3, 4, 5]:

#[derive(MyTrait)]
#[my_trait([1, 2, 3, 4, 5])]
struct SomeStruct { .. }

I'd like to do better than this; I want to be able to use a computed const value to store this constant. For example, I'd like to write this:

const CONFIG: [u64; 5] = [1, 2, 3, 4, 5];

#[derive(MyMacro)]
#[my_macro(CONFIG)]
struct SomeStruct { .. }

Is it possible to do something like this? From what I understand, I think not, because of phase ordering problems: procmacros are evaluated during an early syntax stage, before this module's semantics are ready. So all the procmacro will get is the token CONFIG rather than being able to access the value referenced by the constant CONFIG.


Context of what I'm trying to do: the trait I'm trying to derive is for encoding types in dense bit-packed way. For enum types, it turns out that the desired encoding is a nontrivial function of the bit widths of all the fields (involves running a dynamic programming algorithm). I'd like to run that dynamic programming algorithm at compile time, during the evaluation of the procmacro, but in order to do so it needs access to the bit widths of all of the fields of the enum, which I don't think I have access to.

Indeed.

That being said, you can still interact with the given input as if it were some kind of "function parameter": the exact value may be opaque to you, but there are still some properties you can use out of it. For instance, you can make your macro generate the type:
[u8; (CONFIG[0] + CONFIG[1] + CONFIG[2] + CONFIG[3] + CONFIG[4]) / 8]
so as to let Rust "inject semantics" into your generated code.

More generally, you can kind of "lazily" compute some stuff with a proc-macro, by using traits with impls and expanding to something using it:
<SomeUserInput as MyHelperTrait>::AssociatedItem

For instance, once const_generics become stable, you could be able to define a mapping from a finite number of inputs to types, e.g.,

def_f! {
 // usize => ty,
    0 => Release,
    1 => Debug,
}

and then use something like <N<{input number}> as HelperTrait>::Ret to get the associated type.

Once const fn with the usual suspects imperative programming constructs (conditionals, loops) become stable, you will also be able to define some of these functions as a const fn, and then have your macro expand to such function calling (part of) the user input:

// Helper
const fn fibo (mut n: u64) -> u64
{
    if n < 1 { return 1; }
    let (mut a, mut b) = (1, 1);
    while n > 1 {
        let temp = a + b;
        a = b;
        b = temp;
        n -= 1;
    }
    b
}

// Input
const CONFIG: u64 = 5;

// Generated
const GENERATED: u64 = fibo(CONFIG);

Finally, if you happen to want your macro to accept a constant rather than a literal for ergonomics / readability, remember that macros can accept many syntaxes, so sometimes it's just about being imaginative with them. For instance, the ::bitwrap crate uses the following syntax:

#[derive(BitWrap)]
struct Packet {
    // Get/Set bit
    #[bits(1)]
    flag_1: u8,

    // Get/Set bit and convert into bool:
    // - 0 - false
    // - 1 - true
    #[bits(1)]
    flag_2: bool,

    // ...

This is an excellent response, thank you for that!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.