Skip to content

Instantly share code, notes, and snippets.

@Lokathor
Last active December 8, 2019 15:46
Show Gist options
  • Save Lokathor/0db94666baaf00cc8793068d60e8d552 to your computer and use it in GitHub Desktop.
Save Lokathor/0db94666baaf00cc8793068d60e8d552 to your computer and use it in GitHub Desktop.
Blog post about how to build `cfg_if` style macro.

Diving in to cfg_if!

How exactly does the cfg_if! macro do its thing?

First of all what is the cfg_if! macro? It's a macro to help you pick a set of code based on compile-time configutation. There is a sample usage on the docs page:

cfg_if::cfg_if! {
    if #[cfg(unix)] {
        fn foo() { /* unix specific functionality */ }
    } else if #[cfg(target_pointer_width = "32")] {
        fn foo() { /* non-unix, 32-bit functionality */ }
    } else {
        fn foo() { /* fallback implementation */ }
    }
}

This is showing a function being defined more than one way. In macro terms a function is an "item", but this actually work with any "tt". The term "tt" is short for "token tree". As you might guess, a token tree is very broad and can contain any series of Rust tokens. The main limit on a token tree is that the bracing has to match. You can't have ( without a matching ), same for { + } and [ + ].

So how does that work? Let's have a look.

If we go to the docs of the macro itself we can see a huge macro signature thing.

macro_rules! cfg_if {
    ($(
        if #[cfg($($meta:meta),*)] { $($tokens:tt)* }
    ) else * else {
        $($tokens2:tt)*
    }) => { ... };
    (
        if #[cfg($($i_met:meta),*)] { $($i_tokens:tt)* }
        $(
            else if #[cfg($($e_met:meta),*)] { $($e_tokens:tt)* }
        )*
    ) => { ... };
    (@__items ($($not:meta,)*) ; ) => { ... };
    (@__items ($($not:meta,)*) ; ( ($($m:meta),*) ($($tokens:tt)*) ), $($rest:tt)*) => { ... };
    (@__identity $($tokens:tt)*) => { ... };
}

This shows all the possible input cases that the macro accepts, but not what any of the cases do. It's a little compact, and has a lot of parens. To me that makes it hard to follow, so let's get a lot more space in there.

macro_rules! cfg_if {
  (
    $(if #[cfg($($meta:meta),*)] {
      $($tokens:tt)*
    })else* else {
      $($tokens2:tt)*
    }
  ) => { ... };
  
  (
    if #[cfg($($i_met:meta),*)] {
      $($i_tokens:tt)*
    } $(else if #[cfg($($e_met:meta),*)] {
      $($e_tokens:tt)*
    })*
  ) => { ... };
  
  (
    @__items ($($not:meta,)*) ;
  ) => { ... };
  
  (
    @__items ($($not:meta,)*) ;
    ( ($($m:meta),*) ($($tokens:tt)*) ), $($rest:tt)*
  ) => { ... };
  
  (
    @__identity $($tokens:tt)*
  ) => { ... };
}

Hmm, okay.

Still too many parens for my liking. I think those @__items cases can be made a little more clear by changeing them to use [] and {}. That way those groupings won't conflict with the $()* stuff (which has to use parens). Also, while we're at it let's call our version magic, give it a #[macro_export] attribute, and replace those "..." with line comments so that we can build it in a file.

#[macro_export]
macro_rules! magic {
  (
    $(if #[cfg($($meta:meta),*)] {
      $($tokens:tt)*
    })else* else {
      $($tokens2:tt)*
    }
  ) => {
    // ??
  };
  
  (
    if #[cfg($($i_met:meta),*)] {
      $($i_tokens:tt)*
    } $(else if #[cfg($($e_met:meta),*)] {
      $($e_tokens:tt)*
    })*
  ) => {
    // ??
  };
  
  (
    @__items [ $($not:meta,)* ] ;
  ) => {
    // ??
  };
  
  (
    @__items [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    // ??
  };
  
  (
    @__identity $($tokens:tt)*
  ) => {
    // ??
  };
}

we good?

pi@raspberrypi:~/testing $ cargo check
    Checking testing v0.1.0 (/home/pi/testing)
    Finished dev [unoptimized + debuginfo] target(s) in 0.24s

we good! Let's try to fill in the cases.

How will we test it? Obviously we use that example usage from the docs. What will we have for foo? We'll just make a #[test] with a name for each case. Each test itself will always pass. Then we run cargo test and we'll see which tests got in.

magic! {
  if #[cfg(unix)] {
    #[test] fn test_is_unix() { }
  } else if #[cfg(target_pointer_width = "32")] {
    #[test] fn test_is_non_unix_32_bit() { }
  } else {
    #[test] fn test_is_non_unix_non_32_bit() { }
  }
}

Okay and...

pi@raspberrypi:~/testing $ cargo test
   Compiling testing v0.1.0 (/home/pi/testing)
    Finished test [unoptimized + debuginfo] target(s) in 1.40s
     Running target/debug/deps/testing-da2a4f8b11b8d93e

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests testing

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Exactly what we expected. Right now it just eats everything and emits nothing. We end up with no tests running. What we want is something that compiles, but also that does exactly one test. If we get more than one test in the output that's also wrong.

Now we have to actually fill some stuff in.

@__identity

Looking at our cases, it seems obvious enough that the @__identity is an identity function. We'll try that.

  (
    @__identity $($tokens:tt)*
  ) => {
    $($tokens)*
  };

Why does it do that? The other cases for @__items actually process token trees, not items. And the demo code also uses items. And if we look at the closed pull requests we can see that the crate was converted to token trees. So maybe it's something you need for items but you don't need for token trees? Or maybe we'll really need it? Let's keep moving and we might find out as we go.

@__items

Okay, so, now that we know the history here we can see that the name @__items is no longer correct. It would technically be a breaking change for cfg-if to rename that entry point, but we don't care about breaking changes so we'll rename it. What name to pick?

Each of these works on 0 or more token trees. The $( and ),* parts around $foo:tt mean that a token tree will get matched 0 or more times, and each time it'll have a , between the trees if there's another match. Since we're working with groups of trees, we'll call it a "forest".

  (
    @__forests [ $($not:meta,)* ] ;
  ) => {
    // ??
  };

  (
    @__forests [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    // ??
  };

What's going on anyway? Well, our hint is that there's two cases here, and one of the cases is just like the other "plus more". This is an example of the Incremental TT Muncher pattern.

All the inputs are going to be piled up into a long list of elements like this:

[ { $($m:meta),* } { $($tokens:tt)* } ],

And then it'll turn into one huge token tree thing. Then these two cases will pull off one at a time, do something, and recurse. If we recurse with more in our list of work, we'll match to the second case. If we recurse with an empty list of work, we'll match to the first case.

So, if we have no more work, we can stop, right?

  (
    @__forests [ $($not:meta,)* ] ;
  ) => {
    /* halt expansion */
  };

Sounds fine.

What do we do if we do have work to do?

  (
    @__forests [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    // ??
  };

Well, uh, we want to emit the current item obviously.

  (
    @__forests [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    #[cfg($($m),*)] $($tokens)*
  };

Ok? Sure, maybe. We're not calling this yet so we can't tell. We'll fill in the if and else stuff, which will recurse to here, then we'll see.

if with else

So we have to at least fill in the "with a final else" branch. That's what our current test setup is testing against.

  (
    $(if #[cfg($($meta:meta),*)] {
      $($tokens:tt)*
    })else* else {
      $($tokens2:tt)*
    }
  ) => {
    // ??
  };

Naming the :meta to be $meta is certainly not the best I think. More renames!

  (
    $(if #[cfg($($test:meta),*)] {
      $($if_tokens:tt)*
    })else* else {
      $($else_tokens:tt)*
    }
  ) => {
    // ??
  };

Okay, and now we've got something else a little funky. We've seen $( and ),*, but now there's an else in there? Yeah, you can use any single token to break up the list in a list repetition. So this will work with "if {} else if {} else if {}" on the inside of the repetition. Then at the end, outside the repetition, there's a single "else {}". And it all works out.

We'll need one output per "if {}" repetition, and then one output for the "else". We also need to recurse to one of the @__forests cases.

  • What's our not stuff? Dunno, leave it blank and see what goes wrong I guess.
  • What's our [ stuff ], gonna be? Obviously one "if {}" repetition.
  • What's our rest? Oh, well we'll put the "else" on the end.
  (
    $(if #[cfg($($test:meta),*)] {
      $($if_tokens:tt)*
    })else* else {
      $($else_tokens:tt)*
    }
  ) => {
    $crate::magic!(
      @__forests [ ] ;
      $( [ {$test} {$if_tokens} ], )*
      $else_tokens
    )
  };

And we give it a try...

pi@raspberrypi:~/testing $ cargo test
   Compiling testing v0.1.0 (/home/pi/testing)
error: variable 'test' is still repeating at this depth
  --> src/lib.rs:12:13
   |
12 |       $( [ {$test} {$if_tokens} ], )*
   |             ^^^^^

error: aborting due to previous error

error: could not compile `testing`.

Oh no what did we do?

Our hint is the word "depth" in the error message. If we look at the $if_tokens we see that it has its own repetition inside of the "if {}" repetition. When you use one repetitoin in the output it uses the deepest one first, and then goes up. So what it's trying to tell us is that for every J $test outputs we get J*K $if_tokens. And in what we wrote we're using each the same number of times. It's like we're trying to shove [u32; N] into a u32 variable. Okay, okay, so we got this. We just repeat the $if_tokens repeater, then it'll use the deepest repeat. Then our next repeater gets used by the "if {}" repeater. Then we're all good.

  (
    $(if #[cfg($($test:meta),*)] {
      $($if_tokens:tt)*
    })else* else {
      $($else_tokens:tt)*
    }
  ) => {
    $crate::magic!(
      @__forests [ ] ;
      $( [ {$test} { $($if_tokens)* } ], )*
      $else_tokens
    )
  };

And we get

pi@raspberrypi:~/testing $ cargo test
   Compiling testing v0.1.0 (/home/pi/testing)
error: variable 'test' is still repeating at this depth
  --> src/lib.rs:12:14
   |
12 |       $( [ { $test } { $($if_tokens)* } ], )*
   |              ^^^^^

error: aborting due to previous error

Right, of course, because just like I said the entire time, we have to repeat both the $if_tokens and the $test, of course. Like I said the whole time. Hek, the $else_tokens has a repeater too. Repeaters all around I guess.

  (
    $(if #[cfg($($test:meta),*)] {
      $($if_tokens:tt)*
    })else* else {
      $($else_tokens:tt)*
    }
  ) => {
    $crate::magic!(
      @__forests [ ] ;
      $( [ {$($test),*} {$($if_tokens)*} ], )*
      $($else_tokens)*
    )
  };

So now it'll work, 100%

pi@raspberrypi:~/testing $ cargo test
   Compiling testing v0.1.0 (/home/pi/testing)
error: macros that expand to items must be delimited with braces or followed by a semicolon
  --> src/lib.rs:10:19
   |
10 |       $crate::magic!(
   |  ___________________^
11 | |       @__forests [ ] ;
12 | |       $( [ {$($test),*} {$($if_tokens)*} ], )*
13 | |       $($else_tokens)*
14 | |     )
   | |_____^
...
48 | / magic! {
49 | |   if #[cfg(unix)] {
50 | |     #[test] fn test_is_unix() { }
51 | |   } else if #[cfg(target_pointer_width = "32")] {
...  |
55 | |   }
56 | | }
   | |_- in this macro invocation
   |
help: change the delimiters to curly braces
   |
10 |     $crate::magic!{
11 |       @__forests [ ] ;
12 |       $( [ {$($test),*} {$($if_tokens)*} ], )*
13 |       $($else_tokens)*
14 |     }
   |
help: add a semicolon
   |
14 |     );
   |      ^

Whaaaaa? Okay, sure, sure, we can just use braces, no big deal.

pi@raspberrypi:~/testing $ cargo test
   Compiling testing v0.1.0 (/home/pi/testing)
    Finished test [unoptimized + debuginfo] target(s) in 1.57s
     Running target/debug/deps/testing-da2a4f8b11b8d93e

running 1 test
test test_is_unix ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests testing

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Okay, so we're done. Ship it.

Wait we need to do the part without else! Drat.

Okay we add another test. This time we'll also add windows as the first branch of the if. For all we know it's accepting whatever the first input is.

magic! {
  if #[cfg(unix)] {
    #[test] fn with_else_is_unix() { }
  } else if #[cfg(target_pointer_width = "32")] {
    #[test] fn with_else_is_non_unix_32_bit() { }
  } else {
    #[test] fn with_else_is_non_unix_non_32_bit() { }
  }
}

magic! {
  if #[cfg(windows)] {
    #[test] fn no_else_is_windows() { }
  } else if #[cfg(unix)] {
    #[test] fn no_else_is_unix() { }
  } else if #[cfg(target_pointer_width = "32")] {
    #[test] fn no_else_is_non_unix_32_bit() { }
  }
}

So we want to see exactly one "with_else" and exactly one "no_else"

Let's run the tests again...

pi@raspberrypi:~/testing $ cargo test
   Compiling testing v0.1.0 (/home/pi/testing)
    Finished test [unoptimized + debuginfo] target(s) in 1.56s
     Running target/debug/deps/testing-da2a4f8b11b8d93e

running 1 test
test with_else_is_unix ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests testing

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Okay, like we were expecting. The "with_else" works, and "no_else" doens't since it expands to nothing right now.

if without else

  (
    if #[cfg($($i_met:meta),*)] {
      $($i_tokens:tt)*
    } $(else if #[cfg($($e_met:meta),*)] {
      $($e_tokens:tt)*
    })*
  ) => {
    // ??
  };

Okay, better names please.

  (
    if #[cfg($($if_meta:meta),*)] {
      $($if_tokens:tt)*
    } $(else if #[cfg($($else_meta:meta),*)] {
      $($else_tokens:tt)*
    })*
  ) => {
    // ??
  };

Alright so, again, we want to just recurse to the @__forest case.

This time, instead of having "if {}" repeat and "else {}" once, we have it reversed. The "if {}" is once and then "else if {}" will repeat. We can handle this.

  (
    if #[cfg($($if_meta:meta),*)] {
      $($if_tokens:tt)*
    } $(else if #[cfg($($else_meta:meta),*)] {
      $($else_tokens:tt)*
    })*
  ) => {
    $crate::magic!{
      @__forests [ ] ;
      [ {$($if_meta),*} {$($if_tokens)*} ],
      $( [ {$($else_meta),*} {$($else_tokens:tt)*} ] )*
    }
  };

Every expansion seems to repeat the correct number of times.

pi@raspberrypi:~/testing $ cargo test
   Compiling testing v0.1.0 (/home/pi/testing)
    Finished test [unoptimized + debuginfo] target(s) in 1.54s
     Running target/debug/deps/testing-da2a4f8b11b8d93e

running 1 test
test with_else_is_unix ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests testing

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Oh no. What went wrong? Well, like I said, it works if the very first "if" is selected. However, when we were processing in the @__forest case we didn't recurse to the next item. So it never processed anything past the first "if".

Fixing @__forest

An easy fix, right?

  (
    @__forests [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    #[cfg($($m),*)] $($tokens)*
    
    $crate::magic!{
      @__forests [ ] ;
      $($rest)*
    }
  };
pi@raspberrypi:~/testing $ cargo test
   Compiling testing v0.1.0 (/home/pi/testing)
error: no rules expected the token `#`
  --> src/lib.rs:63:5
   |
2  | macro_rules! magic {
   | ------------------ when calling this macro
...
63 |     #[test] fn with_else_is_non_unix_non_32_bit() { }
   |     ^ no rules expected this token in macro call

Say what? Actually there's more errors but we'll take them one at a time. Let's have another look at that with-else branch:

  (
    $(if #[cfg($($test:meta),*)] {
      $($if_tokens:tt)*
    })else* else {
      $($else_tokens:tt)*
    }
  ) => {
    $crate::magic!{
      @__forests [ ] ;
      $( [ {$($test),*} {$($if_tokens)*} ], )*
      $($else_tokens)*
    }
  };

Oh, we didn't put the little [ ], thing on the else! Of course. And then we can tell obviously that it'll need { } too. And a condition. What's the condition? Again, we'll leave it empty for now. That's worked out great so far, right?

   Compiling testing v0.1.0 (/home/pi/testing)
error: `cfg` predicate is not specified
  --> src/lib.rs:42:5
   |
42 |       #[cfg($($m),*)] $($tokens)*
   |       ^^^^^^^^^^^^^^^
...
57 | / magic! {
58 | |   if #[cfg(unix)] {
59 | |     #[test] fn with_else_is_unix() { }
60 | |   } else if #[cfg(target_pointer_width = "32")] {
...  |
64 | |   }
65 | | }
   | |_- in this macro invocation

This time what rust is trying to tell us is that our expansion will have that , in it, so we need to put an all( ) around our condition so that it can accept a list of things. I hope. It clears that error and we can move on to the next one at least.

   Compiling testing v0.1.0 (/home/pi/testing)
error: no rules expected the token `__forests`
  --> src/lib.rs:45:8
   |
2  |   macro_rules! magic {
   |   ------------------ when calling this macro
...
27 |         $( [ {$($else_meta),*} {$($else_tokens:tt)*} ] )*
   |                                                       - help: missing comma here
...
45 |         @__forests [ ] ;
   |          ^^^^^^^^^ no rules expected this token in macro call
...
67 | / magic! {
68 | |   if #[cfg(windows)] {
69 | |     #[test] fn no_else_is_windows() { }
70 | |   } else if #[cfg(unix)] {
...  |
74 | |   }
75 | | }
   | |_- in this macro invocation

Right, totally our fault, can't miss that ,

   Compiling testing v0.1.0 (/home/pi/testing)
error: expected `[`, found `:`
  --> src/lib.rs:27:45
   |
27 |         $( [ {$($else_meta),*} {$($else_tokens:tt)*} ], )*
   |                                               ^ unexpected token
...
67 | / magic! {
68 | |   if #[cfg(windows)] {
69 | |     #[test] fn no_else_is_windows() { }
70 | |   } else if #[cfg(unix)] {
71 | |     #[test] fn no_else_is_unix() { }
   | |      - expected `[`
...  |
74 | |   }
75 | | }
   | |_- in this macro invocation

error: aborting due to previous error

Right, agian, because I wouldn't write :tt in the output, don't be silly.

   Compiling testing v0.1.0 (/home/pi/testing)
    Finished test [unoptimized + debuginfo] target(s) in 1.54s
     Running target/debug/deps/testing-da2a4f8b11b8d93e

running 5 tests
test no_else_is_non_unix_32_bit ... ok
test no_else_is_unix ... ok
test with_else_is_non_unix_32_bit ... ok
test with_else_is_non_unix_non_32_bit ... ok
test with_else_is_unix ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests testing

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Okay, so... Hey at least there's no compilation errors.

We're getting way too much. We're just outputting everything. How do we fix this? Obviously with all that $not stuff we've been ignoring.

Every time we process a test, we put it into the $not pile on the next recursion. We better carry along the last pile of not stuff too.

  (
    @__forests [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    #[cfg(all($($m),*))] $($tokens)*
    
    $crate::magic!{
      @__forests [ $($not,)* $($m),* ] ;
      $($rest)*
    }
  };

Seems okay...

   Compiling testing v0.1.0 (/home/pi/testing)
error: no rules expected the token `]`
  --> src/lib.rs:45:38
   |
2  |   macro_rules! magic {
   |   ------------------ when calling this macro
...
45 |         @__forests [ $($not,)* $($m),* ] ;
   |                                        ^ no rules expected this token in macro call
...
57 | / magic! {
58 | |   if #[cfg(unix)] {
59 | |     #[test] fn with_else_is_unix() { }
60 | |   } else if #[cfg(target_pointer_width = "32")] {
...  |
64 | |   }
65 | | }
   | |_- in this macro invocation

Oh come on! What does it want this time? Well, it's a matching error. In other words, it's not outputting bad things that the compiler rejects afterwords. The problem is that we're calling our own macro wrong somehow. The only thing we did was edit inside of that [ ], so that must have the problem. (Probably.) Well, there's a , diffence in the $not and $m repetitions. The $not needs the commas on the inside, meaning that they're really present in the output. The $m puts them on the outside, which makes them get processed by the repeater itself and they're not in the output. So we just move that , with the $m over a little bit to match how $not wants it.

  (
    @__forests [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    #[cfg(all($($m),*))] $($tokens)*
    
    $crate::magic!{
      @__forests [ $($not,)* $($m,)* ] ;
      $($rest)*
    }
  };

And at least it compile again:

   Compiling testing v0.1.0 (/home/pi/testing)
    Finished test [unoptimized + debuginfo] target(s) in 1.49s
     Running target/debug/deps/testing-da2a4f8b11b8d93e

running 5 tests
test no_else_is_non_unix_32_bit ... ok
test no_else_is_unix ... ok
test with_else_is_non_unix_non_32_bit ... ok
test with_else_is_unix ... ok
test with_else_is_non_unix_32_bit ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests testing

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Well, I guess that's fair. We're still skipping the $not in the cfg that we emit.

Well not is a cfg operation, so we'll just do that.

  (
    @__forests [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    #[cfg(all($($m),*) not($($not),*))] $($tokens)*
    
    $crate::magic!{
      @__forests [ $($not,)* $($m,)* ] ;
      $($rest)*
    }
  };

fixed?

   Compiling testing v0.1.0 (/home/pi/testing)
error: expected one of `)` or `,`, found `not`
  --> src/lib.rs:42:24
   |
42 |       #[cfg(all($($m),*) not($($not),*))] $($tokens)*
   |                          ^^^ expected one of `)` or `,`
...
57 | / magic! {
58 | |   if #[cfg(unix)] {
59 | |     #[test] fn with_else_is_unix() { }
60 | |   } else if #[cfg(target_pointer_width = "32")] {
...  |
64 | |   }
65 | | }
   | |_- in this macro invocation

Okay, right, my bad. This is why I said that too many parens was gonna kill us later... It needs to be like cfg(STUFF). But what we're writing, when you take out the repeaters is cfg(all(YES) not(NO)). So it's expecting that after the all(YES) we either put a comma or close the cfg. We wanted the not(NO) to be inside that all(YES). We want it to be all of the current if and none of any previous if. Oh so obviously we need an any in there too.

  (
    @__forests [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    #[cfg(all( $($m),* not(any($($not),*)) ))] $($tokens)*
    
    $crate::magic!{
      @__forests [ $($not,)* $($m,)* ] ;
      $($rest)*
    }
  };

cargo like?

   Compiling testing v0.1.0 (/home/pi/testing)
error: expected one of `)` or `,`, found `not`
  --> src/lib.rs:42:24
   |
42 |       #[cfg(all( $($m),* not(any($($not),*)) ))] $($tokens)*
   |                          ^^^ expected one of `)` or `,`
...
57 | / magic! {
58 | |   if #[cfg(unix)] {
59 | |     #[test] fn with_else_is_unix() { }
60 | |   } else if #[cfg(target_pointer_width = "32")] {
...  |
64 | |   }
65 | | }
   | |_- in this macro invocation

They told me they fixed it~! I trusted them to fix it! It's not my fault!

It is my fault, what did I do? It still wants a comma. Well the comma is right... right... in the repeater. Okay, so this is the same bug as before with the $not thing. We just move the , into the output instead of being in the repeater. Why do they even let you put the repeater token into the repeater output? It's seemignly never correct. I'm gonna get a "well actually" email about this I'm sure.

   Compiling testing v0.1.0 (/home/pi/testing)
    Finished test [unoptimized + debuginfo] target(s) in 1.55s
     Running target/debug/deps/testing-da2a4f8b11b8d93e

running 2 tests
test no_else_is_unix ... ok
test with_else_is_unix ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests testing

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Did... did we do it? I guess we did it. Horay!

Narrator: Actually, He Was Not Done

But we didn't even use the @__identity. I'm suspicious.

Let's add another test:

#[test]
fn test_expr(){
  let mut x = 1;
  
  magic! {
    if #[cfg(unix)] {
      x += 1;
    } else {
      x += 5;
    }
  }
  
  
  magic! {
    if #[cfg(unix)] {
      assert_eq!(x, 2);
    } else {
      assert_eq!(x, 6);
    }
  }
}

So then we see

   Compiling testing v0.1.0 (/home/pi/testing)
error[E0658]: attributes on expressions are experimental
  --> src/lib.rs:42:5
   |
42 |       #[cfg(all( $($m,)* not(any($($not),*)) ))] $($tokens)*
   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
80 | /   magic! {
81 | |     if #[cfg(unix)] {
82 | |       x += 1;
83 | |     } else {
84 | |       x += 5;
85 | |     }
86 | |   }
   | |___- in this macro invocation
   |
   = note: for more information, see https://github.com/rust-lang/rust/issues/15701
   = help: add `#![feature(stmt_expr_attributes)]` to the crate attributes to enable

Interesting.

And, what happens if we use that identity call each time we output tokens?

  (
    @__forests [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    #[cfg(all( $($m,)* not(any($($not),*)) ))]
    $crate::magic!{ @__identity $($tokens)* }
    
    $crate::magic!{
      @__forests [ $($not,)* $($m,)* ] ;
      $($rest)*
    }
  };

Well it's still applying a cfg to an expression, so it'll still not work right?

   Compiling testing v0.1.0 (/home/pi/testing)
    Finished test [unoptimized + debuginfo] target(s) in 1.63s
     Running target/debug/deps/testing-da2a4f8b11b8d93e

running 3 tests
test no_else_is_unix ... ok
test with_else_is_unix ... ok
test test_expr ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests testing

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Macros! How do they work? No one knows.

Actually Yandros on the Rust Community Discord knows: you can't apply cfg to an expression, but the macro usage here counts as being an item, so it can be configured in or out, and then if it's in it will keep expanding into the inner token tree.

I guess this is some weird macro edge case. Like how self is an identifier and also not an identifier.

Well, now we can configure all the things we need to configure.


The Full Thing so you can look at it all at once.

(Note: that if you put this directly into a crate you must remove $crate. It only works right when a macro is called from another crate. Eg: one lib calling another lib, bin calling a lib, a test running a lib. However, if you put it into a lib and then try to call it in that same lib you'll get errors. So in that case you can just remove the $crate and it'll work)

Final Code

#[macro_export]
macro_rules! magic {
  (
    $(if #[cfg($($test:meta),*)] {
      $($if_tokens:tt)*
    })else* else {
      $($else_tokens:tt)*
    }
  ) => {
    $crate::magic!{
      @__forests [ ] ;
      $( [ {$($test),*} {$($if_tokens)*} ], )*
      [ { } {$($else_tokens)*} ],
    }
  };

  (
    if #[cfg($($if_meta:meta),*)] {
      $($if_tokens:tt)*
    } $(else if #[cfg($($else_meta:meta),*)] {
      $($else_tokens:tt)*
    })*
  ) => {
    $crate::magic!{
      @__forests [ ] ;
      [ {$($if_meta),*} {$($if_tokens)*} ],
      $( [ {$($else_meta),*} {$($else_tokens)*} ], )*
    }
  };

  (
    @__forests [ $($not:meta,)* ] ;
  ) => {
    /* halt expansion */
  };

  (
    @__forests [ $($not:meta,)* ] ;
    [ { $($m:meta),* } { $($tokens:tt)* } ],
    $($rest:tt)*
  ) => {
    #[cfg(all( $($m,)* not(any($($not),*)) ))]
    $crate::magic!{ @__identity $($tokens)* }
    
    $crate::magic!{
      @__forests [ $($not,)* $($m,)* ] ;
      $($rest)*
    }
  };

  (
    @__identity $($tokens:tt)*
  ) => {
    $($tokens)*
  };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment