Local Ambiguity When Calling Macro?

Until lifetimes of temporary variables are somehow fixed, I propose the following macro, which extends lifetimes by passing the value to a closure:

macro_rules! let_workaround {
    (let $name:ident = $val:expr; $($rest:tt)+) => {
        let f = |$name| { // naming closure avoids #[allow(clippy::redundant_closure_call)]
            let_workaround! { $($rest)+ }
        };
        f($val)
    };
    ($($rest:tt)+) => { $($rest)+ }
}

let_workaround! {
    let a = format_args!(…);
    let b = format_args!(…);
    let c = format_args!(…);
    // use a, b and c
}

Works like a charm! However if I optimize away the macro recursion, by passing all values to the same closure:

macro_rules! let_workaround {
    ($(let $name:ident = $val:expr;)+ $($rest:tt)+) => {
        let f = |$($name),+| { // naming closure avoids #[allow(clippy::redundant_closure_call)]
            $($rest)+
        };
        f($($val),+)
    }
}

I get

error: local ambiguity when calling macro `let_workaround`: multiple parsing options: built-in NTs tt ('rest') or 1 other option.

What's an NT (other than Microsoft’s old technology?) But more than the jargon, what's going on? Why doesn't the macro greedily gobble let statements, before drifting off into tt land?

Btw. not sure I actually want that in this concrete case, as it would prevent b from referencing a. But I stumbled over this while playing around, and it doesn't seem right.

NT presumably means "non-terminal" but yeah, I also just recently encountered this mysterious error – is it even meant to be seen by the user? It’s certainly vastly more opaque than your usual rustc error messages…

2 Likes

Yeah, that must be referring to the CS jargon “nonterminal symbol”.

Seems meant to be seen by users, there’s even a UI test featuring this kind of error message

fn main() {}

macro_rules! ambiguity {
    ($($i:ident)* $j:ident) => {};
}

ambiguity!(error); //~ ERROR local ambiguity
ambiguity!(error); //~ ERROR local ambiguity
error: local ambiguity when calling macro `ambiguity`: multiple parsing options: built-in NTs ident ('i') or ident ('j').
  --> $DIR/local-ambiguity-multiple-parsing-options.rs:7:12
   |
LL | ambiguity!(error);
   |            ^^^^^

error: local ambiguity when calling macro `ambiguity`: multiple parsing options: built-in NTs ident ('i') or ident ('j').
  --> $DIR/local-ambiguity-multiple-parsing-options.rs:8:12
   |
LL | ambiguity!(error);
   |            ^^^^^

error: aborting due to 2 previous errors

I don’t see any way to remove the recursion that doesn’t run into this error.

However, I want to propose an improvement to your macro/workaround approach: Instead of an immediately invoked closure, you can also use a match statement in order to extend temporary lifetimes.

Just turn let PATTERN = EXPR; REST into match EXPR { PATTERN => { REST } }, then temporaries from EXPR stay alive throughout REST.

macro_rules! let_workaround {
    (let $name:pat = $val:expr; $($rest:tt)+) => {{
        match $val {
            $name => let_workaround!($($rest)*)
        }
    }};
    ($($rest:tt)+) => {{ $($rest)+ }}
}

The UI test is far more ambiguous than my literal let on the 1st path, vs. vague tt later. I guess I'm too much used to regex, where greedy match would dominate (and even backtrack when the submatch fails.)

I like your alternative though! Cleaner and no overzealous clippy to deal with. And the pat (on the back) makes this more useful than ident.

(Not really relevant, but I can't help pointing out that all "modern" Windowses are still NT – the last non-NT desktop Windows was the ME!)

1 Like

I thought it was a reference to the fact that NT stands for "New Technology", despite it not being new at all at this point.

1 Like