Why
I am writing a proc macro wrapping a user-provided closure. I want compilation to fail if the user-provided closure captures from its environment.
It's for reasons specific to my use-case: The closure is supposed to be a pure function of its input parameters, so capturing is almost always a serious logical error in context. I want a compile-time check.
In short: What code could I generate that does nothing if the user's closure is like the first line, but fails to compile if it's like the second?:
let user_closure = async |a, b| a + b;
let user_closure = async |a, b| a + b + captured_variable;
Solution for non-async closures
What I already do if the user passes a non-async
closure, is to attempt to cast its clone to an fn
-pointer. fn
s can't capture, so the cast succeeds if the closure captures nothing:
let user_closure = |a, b| a + b;
// Macro-generated check:
let _: fn(_, _) -> _ = user_closure.clone(); // This compiles.
And it fails if the closure captures anything:
let captured_variable = 1;
let user_closure = |a, b| a + b + captured_variable;
// Macro-generated check:
let _: fn(_, _) -> _ = user_closure.clone(); // Compile error!
This works great!
But async
closures are stable on nightly now, and I want to support them.
What I've tried
Collapsed for compactness:
Idea 1: Assign to a `fn` pointer anyway? (Doesn't work.)
async
closures can't be coerced to fn
pointers, even though this looks reasonable:
let user_closure = async |a, b| a + b;
// Macro-generated check:
let _: fn(_, _) -> _ = user_closure.clone();
// Compile error!
It would be neat if the compiler allowed this, but alas.
Idea 2: Programmatically rearrange the closure into `|| async {}`? (Works, but technically incorrect.)
-
Reorganise the
syn::ExprClosure
of the user's closure fromasync |$params| {$body}
to
|$params| async move {$body}
-
Assign that to an
fn
-pointer.
let user_closure = async |a, b| a + b;
// Macro-generated check:
let user_closure = |a, b| async move { a + b + something().await };
let _: fn(_, _) -> _ = user_closure;
// This compiles.
let user_closure = async |a, b| a + b + something().await;
// Macro-generated check:
let user_closure = |a, b| async move { a + b + something().await };
let _: fn(_, _) -> _ = user_closure;
// Compile error, because `something` is captured. (Good!)
This works!
However, async || {...}
is not equivalent to || async {...}
, so we're not actually supporting async
closures, just pretending to.
Can you think of a way to do this?