Negation or "else"-like block for macro optionals

Hello,

While hygienic, Rust macro rules quickly become cumbersome to write due to the need of working around their limitations. While there is work ongoing to improve the situation, one question I did not find any information about is whether there is a way to generate code in case of the absence of an optional parameter (or an "else" following the code for its presence). Without that, I found myself needing to invent a weird syntax to let the macro parser accept two different no-ambiguous sub-trees (one with the optional, and one without, as the parser doesn't seem to do any look ahead to resolve ambiguity in case two optionals follow each others).

Note that in my case I cannot use a recursive macro because I need to generate code in case the parameter is absent within a match, so I cannot delegate the => to a sub-rule as I need code on both sides of the arrow.

Does anyone know any issue, work or document related to an "else" for optional parameters?

Thank you!

Actually answering the OP question: no, there's not really any work into if-else style productions in the decl macro metalanguage.


For input, the general expectation is to make separate macro arms for each input you expect to take. Yes, this means exponentially many input arms in the naive case. Each user-facing arm then reorganizes into a more structured format (typically prefixed with something like @internal, or a #[doc(hidden)] helper macro) that contains the actual expansion.

For output, the following helper macro can be invaluable:

macro_rules! __switch {
    {
        if { $($if:tt)+ }
        do { $($do:tt)* }
        else { $($else:tt)* }
    } => { $($do)* };
    {
        if { }
        do { $($do:tt)* }
        else { $($else:tt)* }
    } => { $($else)* };
}

This is actually quite simple in theory: rather than recursing in just the macro arm position, recurse on the entire input without any output yet, just transforming the input so far into a trivial to match format. This pattern is more generally known as a tt-muncher, but can be applied at a much higher level. For example, if you have a macro accepting $a, $b or $c, $d, $e or $f, and then $g, it could be written as roughly

macro_rules! m {

// done
( #{[$a] [$bc] [$d] [$ef] [$g] )
    => { ... };

// munch
(             #{[  ] [   ] [  ] [   ] [  ]} $a    $($tt:tt)*)
    => ( m! { #{[$a] [   ] [  ] [   ] [  ]}       $($tt)* } );
(             #{[$a] [   ] [  ] [   ] [  ]} $b $d $($tt:tt)* )
    => ( m! { #{[$a] [$b ] [$d] [   ] [  ]}       $($tt)* } );
(             #{[$a] [   ] [  ] [   ] [  ]} $c $d $($tt:tt)* )
    => ( m! { #{[$a] [$ c] [$d] [   ] [  ]}       $($tt)* } );
(             #{[$a] [$bc] [$d] [   ] [  ]} $e $g $($tt:tt)* )
    => ( m! { #{[$a] [$bc] [$d] [$e ] [$g]}       $($tt)* } );
(             #{[$a] [$bc] [$d] [   ] [  ]} $f $g $($tt:tt)* )
    => ( m! { #{[$a] [$bc] [$d] [$ f] [$g]}       $($tt)* } );

// entry
( $($tt:tt)* )
    => ( m! { #{[  ] [   ] [  ] [   ] [  ]}       $($tt)* } );

}

The part missing in this generic example is transforming the alternatives into a shared instruction that can be handled uniformly in a single arm.

1 Like

Thank you very much for the detailed explanation!

I am not sure whether I understand fully how the described pattern solves the match case. I'll try to give a small description of the more concrete problem I try to solve.

We have a programming language with blocks, some blocks have parameters, others not. Let's take the following trivial example:

enum Block {
    Init,
    Button(ButtonInner),
    ...
}

We want to do something to all blocks, in my cases, it is generating a Flatbuffers serialization, using schemas matching the Rust block structure. What we want is something like this (syntax simplified):

fn to_fb(&self, builder: &mut FlatBufferBuilder) -> SomeFbType {
    match self {
        Block::Init => fb::Init::create(builder, &(InitArgs {})),
        Block::Button(inner) => inner.to_fb(builder),
        ...
    }
}

I am currently having a macro of the following type (the > is the "guard" in my current weird syntax):

match_block_type_to_fb!{
    self, builder,
    Init => InitArgs,
    > Button,
}

In case there is no inner, there are two arguments because of constraints from Flatbuffers (I could probably have used the paste crate to avoid the second one but that is another topic), and where there is an inner, we delegate to it. The macro ends-up being implemented like this:

macro_rules! match_block_type_to_fb {
    ( $self:ident, $builder:ident, $( $($block0:ident => $fb_arg_ty:ident)? $(> $block1:ident)? ),* ) => {
        match $self {
            $(
                $(
                    Block::$block0 => fb::$block0::create($builder, &$fb_arg_ty {}),
                )?
                $(
                    Block::$block1(inner) => inner.to_fb($builder),
                )?
            )*
        }
    }
}

Inside the match I need to have rules with =>, but in case of inner, I need it on both sides. So I did not find a way to have a cleaner approach, am I missing something?

Ideally, it would be great to be able to write the macro like that:

macro_rules! match_block_type_to_fb {
    ( $self:ident, $builder:ident, $( $block $( => $fb_arg_ty:ident)? ),* ) => {
        match $self {
            $(
                $(
                    Block::$block => fb::$block::create($builder, &$fb_arg_ty {}),
                )else(
                    Block::$block(inner) => inner.to_fb($builder),
                )?
            )*
        }
    }
}

There might be a shorter way to do that specific goal, but tt munching could look something like

( $self:ident, $builder:ident, [$($done:tt)*] ) => {
    match $self { $($done)* }
}

( $self:ident, $builder:ident, [$($done:tt)*] $block:ident  $(, $($rest:tt)*)? ) => {
    m! {
        $self, $builder, [
            $($done)*
            Block::$block(inner) => inner.to_fb($builder),
        ] $($rest)*
    }
}

( $self:ident, $builder:ident, [$($done:tt)*] $block:ident => $arg:expr $(, $($rest:tt)*)? ) => {
    m! {
        $self, $builder, [
            $($done)*
            Block::$block => fb::$block::create($builder, $arg),
        ] $($rest)*
    }
}

Incrementally build the tokenstream you want in the macro args directly.

That's an interesting approach indeed, thanks for writing it up!

In term of ergonomy though, and for users who do not want to invest into tt-mulching magic :wink:, I do believe that "else"-ish clause for optional match could be an interesting feature, as it would allow to write things more naturally. I think the core argument is similar to the case for repetition counters, one can do without it by moving things from left to right and building unary sums in the process, but that's hardly practical, readable, and maintainable (and doesn't generate a literal in the end by instead a const expression leading to contrieved x if x == expr for usage in match statements).

In term of syntax, an alternate one I imagine could be:

$(
    // var is present
    $var
) ~ (
    // var is absent
)?

if else feels too much like a keyword.

If this is added, it'd probably use else as the repeater.

However, the existing unstable syntax for metavar expressions would more likely be how this is provided. With just eager macro expansion, it'd allow to write

${__switch! {
    if { $($arg)? }
    do { $(Block::$block => fb::$block::create($builder, $arg),)? }
    else { Block::$block(inner) => inner.to_fb($builder), }
}}

using my above __switch! macro.

A much more general declarative control flow option could be provided with

${match ($($arg)?) {
    () => { Block::$block(inner) => inner.to_fb($builder), };
    ($arg:ident) => { lock::$block => fb::$block::create($builder, $arg), }
}}

which is basically just defining an adhoc eager helper expansion with the same semantics as a usual macro pattern match, but not restricted to macro expansion position since it's eager.

...but if you already do have a $()? it's hard to beat the simplicity of $()else()?. This is also parsable with the standard LL(2) style that decl macro $() uses for its repetition specifier, so there's no technical reason against providing it. It could maybe even work for $()else()+ on a $()*-captured repetition... though that's a much harder sell.

AIUI the next step here would be to file a T-lang initiative for feature(decl_macro_kleene_else) and follow that process. Feel free to ping me as a potential implementor/mentor. (Though no promises!) @petrochenkov probably should get a ping as well as resident macro compiler wizard.