Declarative macro re-invocation using `self!`, and macro arm privacy

Sometimes a macro needs to use a helper macro to perform some operation (e.g. summing the number of items passed in). Declaring and exporting a secondary helper macro unfortunately allows a downstream crate to depend upon it (e.g. if they manually expand one level of the macro it'll contain a use of the hidden helper macro).

macro_rules! reflect_struct {
  // This delegates the field parsing to other arms so it doesn't have to
  // reconstruct the fields (which is impossible if it matches on an optional
  // keyword using multiple clauses or a match group)
  (struct $Name:ident { $($fields:tt)* }) => {
    impl Reflect for $Name {
      type Kind = StructKind;
      const FIELDS: &[(&str, usize, Layout); self!(@count_fields $($fields)*] =
        &self!(@gen_fields $($fields)*);
    }
  };
  // These macro arms are private. _External_ invocations of the macro ignore
  // these matchers, but internal (visible from the file the span is within
  // _before macro expansion_) is okay.
  priv (@count_fields <...stuff goes here>) => { <...stuff goes here> };
  priv (@gen_fields <...stuff goes here>) => { <...stuff goes here> };
}

I've used self! because the meaning is intuitive and macros cannot be named self!.

4 Likes

Are there problems with just marking the helper macros as #[doc(hidden)]? That's what I often do and see, and I've never seen a problem with that approach. Downstream users know that the items hidden in the docs are subject to change and depending on them can cause patch releases to break your build.

1 Like

It's possible to inline the first macro invocation with rust-analyzer, which then introduces uses of the hidden helper macros. I've done it once or twice to fix an issue with another library's macro, but of course that's very fragile (but I accept the breakage for my own projects). With private match arms, rust-analyzer would have to expand those otherwise inaccessible macro invocations.

Also regarding self!, not having to think about exporting helper macros and fully qualifying those invocations with $crate:: is nice, imo.

2 Likes

r-a can be taught to expand a #[doc(hidden)] macro, no?

r-a can expand #[doc(hidden)] macros just fine, but if they arms are private they won't be resolved. It could be taught to expand them (since the macro itself is resolved) but in this propsal this changes behavior by ignoring later non-private arms. It could only expand them when they're otherwise unresolved, it can still change behavior but should practically be enough.