An idea for breaking full macro hygiene

One of the blockers of full hygiene is breaking it. While hygiene is nice, it is sometimes desired to define unhygienic identifiers, like in this example:

macro helper() {
    struct HygieneOfUpper;
}

macro foo() {
    helper!();
    impl HygieneOfUpper {}
}

There are many different ways in which this would be used, so it needs to be quite flexible. In the example above, the HygieneOfUpper would just be lift up one level to the hygiene of foo. I have seen some ideas, like a lift!() macro that can be wrapped around macro invocations to lift the hygiene of all identifiers defined in the nested macro.

But I have come up with an idea that would solve this problem in a much better way in my opinion.

with_hygiene!(hygiene, ident)

This solution would introduce a new compiler intrinsic macro (that could also be implemented as a proc macro in userland I think?) that defines a new identifier ident with the hygiene of hygiene.

It's defined like this

macro with_hygiene($hy:ident, $idents:tt) {
    /* expands to $idents but with all idents getting the hygiene of $hy */
}

The above example would be rewritten as:

macro helper($hy:ident) {
    with_hygiene!($hy, struct HygieneOfUpper);
}

macro foo() {
    helper!(hy);
    impl HygieneOfUpper {}
}

Advantages:

  • quite elegant in my opinion
  • extremely flexible
  • implementable as a proc macro, (I think?) so not much magic
  • the user of the macro must opt-in to hygiene breaking

Disadvantages:

  • needing to introduce all these unused $hy idents as params
  • a macro, so a bit awkward to use

I quite like this idea though, but I think it could be improved still.

What is your opinion on it, do you think it would be feasible?

If you have to pass in the $stuct_name:ident anyway, why not just plumb it through?

#![feature(decl_macro)]
macro helper($name:ident) {
    struct $name;
}

macro foo() {
    helper!(HygieneOfUpper);
    impl HygieneOfUpper {}
}

foo!();

Yes, in my example, hygiene breaking is not required. But looking at the comments on the tracking issue of decl_macro, breaking hygiene is a requested feature for more complex macros.

So, I get the idea of this, but I'd prefer it go in a slightly different direction using a macro similar to paste.

format_ident!($main, extra)

Eg to name a builder struct and expose it,

format_ident!($ident, Builder) would output an ident like FooBuilder.

This ident would have the hygiene support of this ident and also the span for diagnostics

The problem with this why concat_idents! is too weak of a macro and paste! has its own [<special syntax>]: you can't use a macro in "ident position".

Or as an example, this cannot work:

struct format_ident!($ident, Builder) {
}

This is a fundamental limitation of hygienic macros which expand to expressions/statements/items rather than being pure token substitution. Name resolution hygiene gets a lot of discussion, but expression hygiene is just as if not more important (as macro_rules! shows with its partial name resolution hygiene but full expression hygiene).


Is it a matter of being able to define new identifiers (ones not passed in) with an arbitrary name resolution hygiene, or is it enough to switch to call site hygiene for "unhygienic" identifiers? (Being able to define items with call site hygiene makes it possible to implement macro_rules!'s mixed site hygiene perfectly.)

If call site hygiene is enough/desirable, then providing call site hygiene with e.g. #ident or \ident or whatever is much more usable than a general solution.

(Fun fact! \ and ~ are the only two printable ASCII characters not currently used in the Rust grammar. \ gets an asterisk on that claim due to its semantic use for escape characters in string/character literals.)

If a more general "use this metavariable's span" is desired (and it's probably useful, even if just for controlling where error messages point e.g. as used in proc macros!), then it should probably use the syntax used by ${ignore}.

With #95860, you can write ${ignore(var)} to semantically expand $var but discard the expansion, to e.g. control macro repetition without introducing the bound token tree. Applying the same syntax space to this problem gives either the super explicit ${with_span_of(hy, HygieneOfUpper)} or perhaps a shorter sugar like ${hy:HygieneOfUpper}.

I personally think that a dedicated call site hygiene operator is useful (possible explicit versions: ${call_site(ident)} (${ignore(ident)} uses metavar $ident, so this is probably a bad choice), ${with_span_of($, ident)}, ${$:ident} (love me some symbol soup :yum:)) even with "use span of" syntax, but both are useful.

There are other implementation blockers on def site hygiene beyond just opting back into call site, though, so this is a small bit of bikeshed paint for the moment.

1 Like

I didn't think backtick (`) was used either (except for docs).

(and ~ is currently used on nightly for ~const Trait)