Current macro expansion order

⚓ Rust    📅 2025-12-31    👤 surdeus    👁️ 1      

surdeus

I haven't been able to find a good, one place resource to find the actual macro expansion order, so I'll try to summarize what I think I have found the order is, alongside the questions I have found no answer to. Any mistakes on my part please do correct me, though.

  1. As per this PR, the derive macro is treated like any other attribute macro, and so should be the case for doc and the prior inert macros, but that I'm not sure about.
  2. Macro order among different items is NOT determined, consistent or reliable (since they are executed separately and multithreaded-ly).
    • e.g. Order between different structs of
      #[derive(Debug)]
      
    • There are tricks to work around it, though, which I believe linkme or inventory use. I don't think they're guaranteed to always work, but they currently work, afaik.
  3. All macros are, in general, resolved from outwards in. That is, the outer macro is fed the entirety of the token stream inside it, finishes, then the next proceeds.
    • This applies to all macros, with exceptions 4 and 5, respectively.
    • e.g.
      #[attr1]#[derive(Foo)]
      #[attr2] struct Bar {
          field1: i64,
          #[attr3]
          field2: bool,
      }
      
      (Order, if none start stripping other attributes: attr1, derive, attr2, attr3)
      In this example, attr1 is passed the entirety of the rest of the token stream (even the attribute on the field). Then, attr1 resolves and outputs a token stream (if it doesn't fail). Suppose it doesn't, and the output stream is the same as the input (exactly the example without attr1), then it is reevaluated and the outer macro is now #[derive(Trait)], which is given the rest of the token stream, is evaluated, and so on.
    • e.g.
      macro1!(macro2!(macro3!()))
      
      (Order, if no macro removes other macros: macro1, 2 then 3)
      Again, macro1 is given the entirety of the rest of the token stream, is evaluated and returns a token stream, which is then evaluated, and so on.
  4. derive is special: Since derive macros can have helpers, cfg and cfg_attr on fields are expanded before derive evaluation and stripped afterwards.
    • I do not know when exactly they are stripped, and if the following attribute macros still receive the stripped attributes.
    • Or if the cfgs are then properly evaluated for the next attribute macros.
    • Or if the cfgs are all expanded and not only the needed for the helpers.
  5. format_args is special: And any macro that uses it under the hood, and probably others in the standard library which I do not have a list for, evaluate the inner macros before it. It does make sense from a user standpoint, but it does break the ordering.
  6. cfg and cfg_attr aren't stripped at all before non derive macros. That is, attribute macros don't see fake inner attribute macros expanded, they see it fully. It's not the cfg and cfg_attr which are special (I argue they should), it's the derive.

(edit 1: Added workaround to 6)
(edit 2: Changed category to help since I do need help making sure this is correct but I also wished for a place with all this info condensed to begin with)

1 post - 1 participant

Read full topic

🏷️ Rust_feed