0
votes

Is it possible for a Rust macro to match against the enclosing function's return type?

An example is something like a logging and assert macro which also returns Err in functions returning Result and panics in functions not returning Result. To implement this the macro should somehow know about the enclosing function's return type.

I suppose this is not possible with declarative macros (macro_rules!) because they have a limited set of matching types (as described in The Rust Reference, chapter Macros By Example): items, blocks, statements, patterns, expressions, types, identifiers, and so on, but not the enclosing function's return type.

But perhaps with procedural macros?

1

1 Answers

1
votes

Summary: No, it's not easily possible, even with procedural macros. And I actually think you shouldn't write such a thing, even if it's possible. Just let your macro evaluate to a Result and let the user deal with it.


Function-like macro

Rust macros, procedural and declarative, only have access to their input stream: just a list of tokens. For function-like macros (the ones you invoke via foo!(...)), the input is just what you pass to them. So you could manually pass the return type:

macro_rules! foo {
    (Result $($stuff:tt)*) => { return Err(()); };  
    ($($stuff:tt)*) => { panic!(); };
}

fn returns_result() -> Result<String, ()> {
    foo!(Result<(), ()>);   // will return `Err`
    Ok("hi".into())
}

fn returns_string() -> String {
    foo!(String);           // will panic
    "hi".into()
}

But I guess that is not what you want: the user would have to manually specify the return type for each macro invocation.

The same goes for procedural macros that are invoked this way.


Procedural macro attribute

Can we define a procedural macro where the return type of the function is in the input token stream? Yes, preferably via a proc-macro attribute. If you define such an attribute bar, you could write this:

#[bar]
fn returns_result() -> Result<String, ()> { ... }

And your procedural macro would receive the whole function definition as input, including the return type. But what are you going to do with that information?

You can change the whole function as you like, so one idea would be to search for all foo!() macro invocations in the function and replace them with return Err or panic!() depending on the return type. That is: do the macro invocation step for your own macros via a procedural macro.

But I think this is a bad idea for several reasons. Most importantly, I don't think it's well defined when the compiler calls the procedural macro. So the compiler could attempt to invoke your foo!() macros before calling the procedural macro.

So it could work via procedural macro, but not in a way that is typical. So it's rather hacky.


What I think is the best solution

Lastly, how would I do it? Let your macro evaluate to a Result. Then the user can easily decide themselves what to do with it. If they return a Result, they just need to add ?. If they don't, they have the freedom to choose between .unwrap(), expect() and other ways to panic.

I understand why you are trying to do what you want to do (it's easier for the user and comfortable), but I think it's not a good idea. It probably comes down to "spooky action at a distance": what a macro in your function does suddenly depends on the return type of that function. That means when you change that, the whole semantics of the function change. This sounds like something you could shoot yourself in the foot with very easily. That's also probably the reason why it's not easy in Rust.