1use proc_macro::TokenStream;
4use proc_macro_crate::{FoundCrate, crate_name};
5use proc_macro2::{Span, TokenStream as TokenStream2};
6use quote::{format_ident, quote};
7use syn::parse::Parser;
8use syn::punctuated::Punctuated;
9use syn::{Error, Expr, ExprLit, ItemFn, Lit, Meta, Token, parse2, spanned::Spanned};
10
11#[derive(Clone, Copy)]
12enum AttributeKind {
13 Main,
14 Test,
15}
16
17impl AttributeKind {
18 fn is_test(self) -> bool {
19 matches!(self, Self::Test)
20 }
21}
22
23#[derive(Clone, Copy, Default)]
24enum Executor {
25 #[default]
26 Global,
27 Tokio,
28 Smol,
29 Compio,
30 ThreadPool,
31 Lite,
32}
33
34impl Executor {
35 fn parse(value: &str, span: Span) -> syn::Result<Self> {
36 match value {
37 "global" => Ok(Self::Global),
38 "tokio" => Ok(Self::Tokio),
39 "smol" => Ok(Self::Smol),
40 "compio" => Ok(Self::Compio),
41 "threadpool" | "thread_pool" => Ok(Self::ThreadPool),
42 "lite" => Ok(Self::Lite),
43 _ => Err(Error::new(
44 span,
45 "unknown executor. expected `global`, `tokio`, `smol`, `compio`, `threadpool`, or `lite`",
46 )),
47 }
48 }
49
50 fn drive(
51 self,
52 kind: AttributeKind,
53 runtime_crate: &TokenStream2,
54 body: &syn::Block,
55 ) -> TokenStream2 {
56 let builtin_executor = match self {
57 Self::Tokio => quote!(#runtime_crate::global::BuiltinExecutor::Tokio),
58 Self::Smol => quote!(#runtime_crate::global::BuiltinExecutor::Smol),
59 Self::Compio => quote!(#runtime_crate::global::BuiltinExecutor::Compio),
60 Self::ThreadPool => quote!(#runtime_crate::global::BuiltinExecutor::ThreadPool),
61 Self::Lite => quote!(#runtime_crate::global::BuiltinExecutor::Lite),
62 Self::Global => unreachable!("the global executor must be resolved before expansion"),
63 };
64 let create_executor = match self {
65 Self::Tokio if kind.is_test() => quote! {
66 #runtime_crate::rt::tokio::TokioRuntimeExecutor::with_single_thread()
67 .expect("async-rt failed to create the Tokio test runtime")
68 },
69 Self::Tokio => quote! {
70 #runtime_crate::rt::tokio::TokioRuntimeExecutor::with_multi_thread()
71 .expect("async-rt failed to create the Tokio runtime")
72 },
73 Self::Smol => quote! {
74 #runtime_crate::rt::smol::SmolRuntimeExecutor::new()
75 },
76 Self::Compio => quote! {
77 #runtime_crate::rt::compio::CompioRuntimeExecutor::new()
78 .expect("async-rt failed to create the Compio runtime")
79 },
80 Self::ThreadPool => quote! {
81 #runtime_crate::rt::threadpool::ThreadPoolExecutor
82 },
83 Self::Lite => quote! {
84 #runtime_crate::rt::lite::LiteExecutor
85 },
86 Self::Global => unreachable!("the global executor must be resolved before expansion"),
87 };
88
89 quote! {
90 let __async_rt_body = async move #body;
91 #[allow(
92 clippy::diverging_sub_expression,
93 clippy::expect_used,
94 clippy::needless_return,
95 clippy::unwrap_in_result
96 )]
97 {
98 let __async_rt_executor = #runtime_crate::global::ConfiguredExecutor::with_task_executor(
99 #create_executor,
100 #builtin_executor,
101 );
102 return #runtime_crate::ExecutorBlockOn::block_on(
103 &__async_rt_executor,
104 __async_rt_body,
105 );
106 }
107 }
108 }
109}
110
111#[derive(Default)]
112struct Arguments {
113 executor: Executor,
114 executor_set: bool,
115 driver: Option<Expr>,
116}
117
118impl Arguments {
119 fn parse(tokens: TokenStream2) -> syn::Result<Self> {
120 let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
121 let arguments = parser.parse2(tokens)?;
122 let mut result = Self::default();
123
124 for argument in arguments {
125 let Meta::NameValue(argument) = argument else {
126 return Err(Error::new(
127 argument.span(),
128 "expected `executor = \"...\"` or `driver = ...`",
129 ));
130 };
131
132 if argument.path.is_ident("driver") {
133 if result.driver.is_some() {
134 return Err(Error::new(
135 argument.path.span(),
136 "duplicate `driver` option",
137 ));
138 }
139 if result.executor_set {
140 return Err(Error::new(
141 argument.path.span(),
142 "`driver` and `executor` cannot be used together",
143 ));
144 }
145
146 result.driver = Some(argument.value);
147 continue;
148 }
149
150 if !argument.path.is_ident("executor") {
151 return Err(Error::new(
152 argument.path.span(),
153 "unknown option. expected `executor` or `driver`",
154 ));
155 }
156 if result.executor_set {
157 return Err(Error::new(
158 argument.path.span(),
159 "duplicate `executor` option",
160 ));
161 }
162 if result.driver.is_some() {
163 return Err(Error::new(
164 argument.path.span(),
165 "`driver` and `executor` cannot be used together",
166 ));
167 }
168
169 let (value, span) = match argument.value {
170 Expr::Lit(ExprLit {
171 lit: Lit::Str(value),
172 ..
173 }) => (value.value(), value.span()),
174 Expr::Path(value) if value.path.get_ident().is_some() => {
175 let ident = value.path.get_ident().expect("checked above");
176 (ident.to_string(), ident.span())
177 }
178 value => {
179 return Err(Error::new(
180 value.span(),
181 "executor must be a string or identifier",
182 ));
183 }
184 };
185
186 result.executor = Executor::parse(&value, span)?;
187 result.executor_set = true;
188 }
189
190 Ok(result)
191 }
192}
193
194macro_rules! entry_points {
195 ($main:ident, $test:ident, $executor:ident) => {
196 #[proc_macro_attribute]
208 pub fn $main(arguments: TokenStream, item: TokenStream) -> TokenStream {
209 expand(
210 arguments,
211 item,
212 AttributeKind::Main,
213 Some(Executor::$executor),
214 )
215 }
216
217 #[proc_macro_attribute]
230 pub fn $test(arguments: TokenStream, item: TokenStream) -> TokenStream {
231 expand(
232 arguments,
233 item,
234 AttributeKind::Test,
235 Some(Executor::$executor),
236 )
237 }
238 };
239}
240
241entry_points!(main_tokio, test_tokio, Tokio);
242entry_points!(main_smol, test_smol, Smol);
243entry_points!(main_compio, test_compio, Compio);
244entry_points!(main_threadpool, test_threadpool, ThreadPool);
245entry_points!(main_lite, test_lite, Lite);
246
247#[proc_macro_attribute]
249pub fn main_fail(arguments: TokenStream, item: TokenStream) -> TokenStream {
250 expand(arguments, item, AttributeKind::Main, None)
251}
252
253#[proc_macro_attribute]
255pub fn test_fail(arguments: TokenStream, item: TokenStream) -> TokenStream {
256 expand(arguments, item, AttributeKind::Test, None)
257}
258
259macro_rules! failure_entry_points {
260 ($main:ident, $test:ident, $message:literal) => {
261 #[proc_macro_attribute]
263 pub fn $main(_arguments: TokenStream, _item: TokenStream) -> TokenStream {
264 Error::new(Span::call_site(), $message)
265 .into_compile_error()
266 .into()
267 }
268
269 #[proc_macro_attribute]
271 pub fn $test(_arguments: TokenStream, _item: TokenStream) -> TokenStream {
272 Error::new(Span::call_site(), $message)
273 .into_compile_error()
274 .into()
275 }
276 };
277}
278
279failure_entry_points!(
280 main_wasm_fail,
281 test_wasm_fail,
282 "async-rt's `main` and `test` macros do not yet support wasm32"
283);
284
285fn expand(
286 arguments: TokenStream,
287 item: TokenStream,
288 kind: AttributeKind,
289 default_executor: Option<Executor>,
290) -> TokenStream {
291 expand_inner(arguments.into(), item.into(), kind, default_executor)
292 .unwrap_or_else(Error::into_compile_error)
293 .into()
294}
295
296fn expand_inner(
297 arguments: TokenStream2,
298 item: TokenStream2,
299 kind: AttributeKind,
300 default_executor: Option<Executor>,
301) -> syn::Result<TokenStream2> {
302 let arguments = Arguments::parse(arguments)?;
303 let mut function: ItemFn = parse2(item)?;
304
305 if function.sig.asyncness.take().is_none() {
306 return Err(Error::new(
307 function.sig.fn_token.span,
308 "the `async` keyword is required",
309 ));
310 }
311
312 let runtime_crate = runtime_crate()?;
313 let test_attribute = match kind {
314 AttributeKind::Main => TokenStream2::new(),
315 AttributeKind::Test => quote!(#[::core::prelude::v1::test]),
316 };
317 let attributes = function.attrs;
318 let visibility = function.vis;
319 let signature = function.sig;
320 let body = function.block;
321 let drive = match arguments.driver {
322 Some(driver) => drive_custom(&runtime_crate, &body, driver),
323 None => {
324 let executor = match arguments.executor {
325 Executor::Global => default_executor.ok_or_else(|| {
326 Error::new(
327 Span::call_site(),
328 "async-rt's `main` and `test` macros require a default runtime feature, an explicit executor, or a custom `driver`",
329 )
330 })?,
331 executor => executor,
332 };
333 executor.drive(kind, &runtime_crate, &body)
334 }
335 };
336
337 Ok(quote! {
338 #(#attributes)*
339 #test_attribute
340 #visibility #signature {
341 #drive
342 }
343 })
344}
345
346fn drive_custom(runtime_crate: &TokenStream2, body: &syn::Block, driver: Expr) -> TokenStream2 {
347 quote! {
348 let __async_rt_body = async move #body;
349 let __async_rt_executor = #runtime_crate::global::ConfiguredExecutor::new(#driver);
350 return #runtime_crate::ExecutorBlockOn::block_on(
351 &__async_rt_executor,
352 __async_rt_body,
353 );
354 }
355}
356
357fn runtime_crate() -> syn::Result<TokenStream2> {
358 match crate_name("async-rt") {
359 Ok(FoundCrate::Itself) => Ok(quote!(::async_rt)),
360 Ok(FoundCrate::Name(name)) => {
361 let name = format_ident!("{}", name.replace('-', "_"));
362 Ok(quote!(::#name))
363 }
364 Err(error) => Err(Error::new(
365 Span::call_site(),
366 format!("could not find the `async-rt` crate: {error}"),
367 )),
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::{Arguments, Executor};
374 use quote::quote;
375 use syn::Expr;
376
377 #[test]
378 fn parses_string_and_identifier_executors() {
379 assert!(matches!(
380 Arguments::parse(quote!(executor = "compio"))
381 .unwrap()
382 .executor,
383 Executor::Compio
384 ));
385 assert!(matches!(
386 Arguments::parse(quote!(executor = tokio)).unwrap().executor,
387 Executor::Tokio
388 ));
389 assert!(matches!(
390 Arguments::parse(quote!(executor = "lite"))
391 .unwrap()
392 .executor,
393 Executor::Lite
394 ));
395 }
396
397 #[test]
398 fn rejects_unknown_options_and_executors() {
399 assert!(Arguments::parse(quote!(flavor = "current_thread")).is_err());
400 assert!(Arguments::parse(quote!(executor = "unknown")).is_err());
401 }
402
403 #[test]
404 fn parses_custom_driver_expression() {
405 let arguments = Arguments::parse(quote!(driver = custom::executor())).unwrap();
406 assert!(matches!(arguments.driver, Some(Expr::Call(_))));
407 }
408
409 #[test]
410 fn rejects_duplicate_or_conflicting_driver_options() {
411 assert!(Arguments::parse(quote!(driver = first(), driver = second())).is_err());
412 assert!(Arguments::parse(quote!(executor = "tokio", driver = custom())).is_err());
413 assert!(Arguments::parse(quote!(driver = custom(), executor = "tokio")).is_err());
414 }
415}