Any way to cleanly set a default value for a pseudo-named parameter in a macro?

I have a function that takes a truckload of parameters (29 in total, of whom 13*2 are "pre"/"post"):

fn assert_cpu_execute(
    cpu: &mut Cpu,
    instruction_bytes: &[u8],
    pre_A: u8,
    pre_zf: bool,
    // ...
    post_A: u8,
    post_zf: bool,
    // ...
)

I've therefore created a macro, which servers two purposes:

  1. make parameters optional,
  2. provide a readable way to specify the "pre" and "post" values as a single expression.

The macro has this structure:

macro_rules! assert_cpu_execute {
    (
        $cpu:ident,
        $instruction_bytes:ident,
        $( A: $A_pre_value:literal => $A_post_value:literal , )?
        $( zf: $zf_pre_value:literal => $zf_post_value:literal , )?
        // ...
    ) => {
        let pre_A = 0x21 $( - 0x21 + $A_pre_value )?;
        let post_A = 0x21 $( - 0x21 + $A_post_value )?;
        let pre_zf = false $( | $zf_pre_value )?;
        let post_zf = false $( | $zf_post_value )?;
        // ...
    }

However, this is a rather ugly workaround; this is because I couldn't find any way to express, via declarative macros, the concept "if a repetition exists, transcribe as this, if not transcribe as that".

Any suggestions?

And also, bonus question: it'd be great to generalize all the parameters as the single repetition, which I guess would be something around the lines of $( $store:ident: $pre_value:literal => $post_value:literal , )*; due to the different data type and defaults (u8, bool), I can't trivially do this. Is there any simple way to do this?

Thanks!

This is possible, but not particularly straightforward, with a tt muncher pattern:

macro_rules! assert_cpu_execute {
    (@arg A A { $pre:literal => $post:literal } $($tail:tt)*) => { ($pre, $post) };
    (@default A) => { (0x21, 0x21) };

    (@arg zf zf { $pre:literal => $post:literal } $($tail:tt)*) => { ($pre, $post) };
    (@default zf) => { (false, false) };

    // No match, strip the first token and try again
    ( @ arg $needle:ident $head:tt $($haystack:tt)* ) => {
        assert_cpu_execute!(@arg $needle $($haystack)*)
    };

    // Argument list gone, use default
    (@arg $needle:ident) => { assert_cpu_execute!(@default $needle) };
    
    // Main implementation
    ( $cpu:ident, $instr:ident $($args:tt)*) => {
        {
            let A:(u8,u8)      = assert_cpu_execute!(@arg A  $($args)*);
            let zf:(bool,bool) = assert_cpu_execute!(@arg zf $($args)*);
            
            assert_cpu_execute($cpu, $instr, A.0, zf.0, /* ... */ A.1, zf.1, /* ... */)
        }
    };
}

pub fn f() {
    assert_cpu_execute!(x86, nop,
        zf { true => false },
        A { 42 => 42 }
    );

    assert_cpu_execute!(arm, nop);
}

(Playground— Use “Expand Macros”)

Perhaps you could get away with creating a type, let's call it World, containing all those params, then writing a Default impl for World.

If you can do that you can cut down on the boilerplate with something like this:


fn foo() {
    let world = World {
        // params you're interested in
       .. Default::default() 
    };
} 

Oh and the best part, no macros necessary.

1 Like

Wow, very interesting. Thanks!

Thanks! I considered this solution, however, this trades readability for simplicity, as invocations would need to specify post_ and pre_. Example:

assert_cpu_execute(
  Values::new {
    pre_A: 0xCA, post_A: 0xFE,
    .. Values::default()
  }
)

assert_cpu_execute!(
  A: 0xCA => 0xFE,
)

In the context of a test suite with more than a hundred invocations, I (personally) think readability has more weight.

After considering both suggestions, I think that an interesting solution is to have a mixed approach, with a macro that has the signature and definition as in the question, however, instead the lets, it instantiates a struct that (additionally) uses a default().

Thanks both :slight_smile:

That’s probably the right choice. The trick to using macros effectively usually lies in being clever about what they produce, instead of trying to make them expand to a specific, predefined form.

You could also use 2-tuples to represent pre- and post-values:

assert_cpu_execute(
  Values::new {
    A: (0xCA, 0xFE),
    .. Values::default()
  }
)

Another option is something like a builder pattern:

AssertCPUExecute::new(cpu, instr)
    .A(0xCA, 0xFE)
    .zf(false, false)
    .check();

For clarity I would us one of @2e71828‘s approaches but add a struct (with a better name)

struct PrePost <T> {
    pre: T,
    post: T
} 

The struct name can hide behind a method call but it’s nicer at the use site to call the fields using .pre and .post

1 Like