Skip to main content

sui_proc_macros/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use proc_macro::TokenStream;
5use quote::{ToTokens, quote, quote_spanned};
6use syn::{
7    Attribute, BinOp, Data, DataEnum, DeriveInput, Expr, ExprBinary, ExprMacro, Item, ItemMacro,
8    Stmt, StmtMacro, Token, UnOp,
9    fold::{Fold, fold_expr, fold_item_macro, fold_stmt},
10    parse::Parser,
11    parse_macro_input, parse2,
12    punctuated::Punctuated,
13    spanned::Spanned,
14};
15
16/// The sui_test macro will invoke either `#[msim::test]` or `#[tokio::test]`,
17/// depending on whether the simulator config var is enabled.
18///
19/// This should be used for tests that can meaningfully run in either environment.
20#[proc_macro_attribute]
21pub fn sui_test(args: TokenStream, item: TokenStream) -> TokenStream {
22    let input = parse_macro_input!(item as syn::ItemFn);
23    let arg_parser = Punctuated::<syn::Meta, Token![,]>::parse_terminated;
24    let args = arg_parser.parse(args).unwrap().into_iter();
25
26    let header = if cfg!(msim) {
27        quote! {
28            #[::sui_simulator::sim_test(crate = "sui_simulator", #(#args)* )]
29        }
30    } else {
31        quote! {
32            #[::tokio::test(#(#args)*)]
33        }
34    };
35
36    let result = quote! {
37        #header
38        #input
39    };
40
41    result.into()
42}
43
44/// The sim_test macro will invoke `#[msim::test]` if the simulator config var is enabled.
45///
46/// Otherwise, it will emit an ignored test - if forcibly run, the ignored test will panic.
47///
48/// This macro must be used in order to pass any simulator-specific arguments (e.g. a
49/// custom `config`), which are not understood by tokio.
50#[proc_macro_attribute]
51pub fn sim_test(args: TokenStream, item: TokenStream) -> TokenStream {
52    let input = parse_macro_input!(item as syn::ItemFn);
53    let arg_parser = Punctuated::<syn::Meta, Token![,]>::parse_terminated;
54    let args = arg_parser.parse(args).unwrap().into_iter();
55
56    let ignore = input
57        .attrs
58        .iter()
59        .find(|attr| attr.path().is_ident("ignore"))
60        .map_or(quote! {}, |_| quote! { #[ignore] });
61
62    let result = if cfg!(msim) {
63        let sig = &input.sig;
64        let return_type = &sig.output;
65        let body = &input.block;
66        quote! {
67            #[::sui_simulator::sim_test(crate = "sui_simulator", #(#args),*)]
68            #ignore
69            #sig {
70                ::sui_simulator::telemetry_subscribers::init_for_testing();
71
72                async fn body_fn() #return_type { #body }
73
74                let timeout_secs: u64 = std::env::var("SUI_SIM_TEST_TIMEOUT_SECS")
75                    .ok()
76                    .and_then(|s| s.parse().ok())
77                    .unwrap_or(1000);
78                let timeout_duration = tokio::time::Duration::from_secs(timeout_secs);
79
80                let ret = tokio::time::timeout(timeout_duration, body_fn())
81                    .await
82                    .expect("sim_test timed out");
83
84                ::sui_simulator::task::shutdown_all_nodes();
85
86                // all node handles should have been dropped after the above block exits, but task
87                // shutdown is asynchronous, so we need a brief delay before checking for leaks.
88                tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
89
90                assert_eq!(
91                    sui_simulator::NodeLeakDetector::get_current_node_count(),
92                    0,
93                    "SuiNode leak detected"
94                );
95
96                ret
97            }
98        }
99    } else {
100        let fn_name = &input.sig.ident;
101        let sig = &input.sig;
102        let body = &input.block;
103        quote! {
104            #[allow(clippy::needless_return)]
105            #[tokio::test]
106            #ignore
107            #sig {
108                if std::env::var("SUI_SKIP_SIMTESTS").is_ok() {
109                    println!("not running test {} in `cargo test`: SUI_SKIP_SIMTESTS is set", stringify!(#fn_name));
110
111                    struct Ret;
112
113                    impl From<Ret> for () {
114                        fn from(_ret: Ret) -> Self {
115                        }
116                    }
117
118                    impl<E> From<Ret> for Result<(), E> {
119                        fn from(_ret: Ret) -> Self {
120                            Ok(())
121                        }
122                    }
123
124                    return Ret.into();
125                }
126
127                #body
128            }
129        }
130    };
131
132    result.into()
133}
134
135#[proc_macro]
136pub fn checked_arithmetic(input: TokenStream) -> TokenStream {
137    let input_file = CheckArithmetic.fold_file(parse_macro_input!(input));
138
139    let output_items = input_file.items;
140
141    let output = quote! {
142        #(#output_items)*
143    };
144
145    TokenStream::from(output)
146}
147
148#[proc_macro_attribute]
149pub fn with_checked_arithmetic(_attr: TokenStream, item: TokenStream) -> TokenStream {
150    let input_item = parse_macro_input!(item as Item);
151    match input_item {
152        Item::Fn(input_fn) => {
153            let transformed_fn = CheckArithmetic.fold_item_fn(input_fn);
154            TokenStream::from(quote! { #transformed_fn })
155        }
156        Item::Impl(input_impl) => {
157            let transformed_impl = CheckArithmetic.fold_item_impl(input_impl);
158            TokenStream::from(quote! { #transformed_impl })
159        }
160        item => {
161            let transformed_impl = CheckArithmetic.fold_item(item);
162            TokenStream::from(quote! { #transformed_impl })
163        }
164    }
165}
166
167struct CheckArithmetic;
168
169impl CheckArithmetic {
170    fn maybe_skip_macro(&self, attrs: &mut Vec<Attribute>) -> bool {
171        if let Some(idx) = attrs
172            .iter()
173            .position(|attr| attr.path().is_ident("skip_checked_arithmetic"))
174        {
175            // Skip processing macro because it is annotated with
176            // #[skip_checked_arithmetic]
177            attrs.remove(idx);
178            true
179        } else {
180            false
181        }
182    }
183
184    fn process_macro_contents(
185        &mut self,
186        tokens: proc_macro2::TokenStream,
187    ) -> syn::Result<proc_macro2::TokenStream> {
188        // Parse the macro's contents as a comma-separated list of expressions.
189        let parser = Punctuated::<Expr, Token![,]>::parse_terminated;
190        let Ok(exprs) = parser.parse(tokens.clone().into()) else {
191            return Err(syn::Error::new_spanned(
192                tokens,
193                "could not process macro contents - use #[skip_checked_arithmetic] to skip this macro",
194            ));
195        };
196
197        // Fold each sub expression.
198        let folded_exprs = exprs
199            .into_iter()
200            .map(|expr| self.fold_expr(expr))
201            .collect::<Vec<_>>();
202
203        // Convert the folded expressions back into tokens and reconstruct the macro.
204        let mut folded_tokens = proc_macro2::TokenStream::new();
205        for (i, folded_expr) in folded_exprs.into_iter().enumerate() {
206            if i > 0 {
207                folded_tokens.extend(std::iter::once::<proc_macro2::TokenTree>(
208                    proc_macro2::Punct::new(',', proc_macro2::Spacing::Alone).into(),
209                ));
210            }
211            folded_expr.to_tokens(&mut folded_tokens);
212        }
213
214        Ok(folded_tokens)
215    }
216}
217
218impl Fold for CheckArithmetic {
219    fn fold_stmt(&mut self, stmt: Stmt) -> Stmt {
220        let stmt = fold_stmt(self, stmt);
221        if let Stmt::Macro(stmt_macro) = stmt {
222            let StmtMacro {
223                mut attrs,
224                mut mac,
225                semi_token,
226            } = stmt_macro;
227
228            if self.maybe_skip_macro(&mut attrs) {
229                Stmt::Macro(StmtMacro {
230                    attrs,
231                    mac,
232                    semi_token,
233                })
234            } else {
235                match self.process_macro_contents(mac.tokens.clone()) {
236                    Ok(folded_tokens) => {
237                        mac.tokens = folded_tokens;
238                        Stmt::Macro(StmtMacro {
239                            attrs,
240                            mac,
241                            semi_token,
242                        })
243                    }
244                    Err(error) => parse2(error.to_compile_error()).unwrap(),
245                }
246            }
247        } else {
248            stmt
249        }
250    }
251
252    fn fold_item_macro(&mut self, mut item_macro: ItemMacro) -> ItemMacro {
253        if !self.maybe_skip_macro(&mut item_macro.attrs) {
254            let err = syn::Error::new_spanned(
255                item_macro.to_token_stream(),
256                "cannot process macros - use #[skip_checked_arithmetic] to skip \
257                    processing this macro",
258            );
259
260            return parse2(err.to_compile_error()).unwrap();
261        }
262        fold_item_macro(self, item_macro)
263    }
264
265    fn fold_expr(&mut self, expr: Expr) -> Expr {
266        let span = expr.span();
267        let expr = fold_expr(self, expr);
268        let expr = match expr {
269            Expr::Macro(expr_macro) => {
270                let ExprMacro { mut attrs, mut mac } = expr_macro;
271
272                if self.maybe_skip_macro(&mut attrs) {
273                    return Expr::Macro(ExprMacro { attrs, mac });
274                } else {
275                    match self.process_macro_contents(mac.tokens.clone()) {
276                        Ok(folded_tokens) => {
277                            mac.tokens = folded_tokens;
278                            let expr_macro = Expr::Macro(ExprMacro { attrs, mac });
279                            quote!(#expr_macro)
280                        }
281                        Err(error) => {
282                            return Expr::Verbatim(error.to_compile_error());
283                        }
284                    }
285                }
286            }
287
288            Expr::Binary(expr_binary) => {
289                let ExprBinary {
290                    attrs,
291                    mut left,
292                    op,
293                    mut right,
294                } = expr_binary;
295
296                fn remove_parens(expr: &mut Expr) {
297                    if let Expr::Paren(paren) = expr {
298                        // i don't even think rust allows this, but just in case
299                        assert!(paren.attrs.is_empty(), "TODO: attrs on parenthesized");
300                        *expr = *paren.expr.clone();
301                    }
302                }
303
304                macro_rules! wrap_op {
305                    ($left: expr, $right: expr, $method: ident, $span: expr) => {{
306                        // Remove parens from exprs since both sides get assigned to tmp variables.
307                        // otherwise we get lint errors
308                        remove_parens(&mut $left);
309                        remove_parens(&mut $right);
310
311                        quote_spanned!($span => {
312                            // assign in one stmt in case either #left or #right contains
313                            // references to `left` or `right` symbols.
314                            let (left, right) = (#left, #right);
315                            left.$method(right)
316                                .unwrap_or_else(||
317                                    panic!(
318                                        "Overflow or underflow in {} {} + {}",
319                                        stringify!($method),
320                                        left,
321                                        right,
322                                    )
323                                )
324                        })
325                    }};
326                }
327
328                macro_rules! wrap_op_assign {
329                    ($left: expr, $right: expr, $method: ident, $span: expr) => {{
330                        // Remove parens from exprs since both sides get assigned to tmp variables.
331                        // otherwise we get lint errors
332                        remove_parens(&mut $left);
333                        remove_parens(&mut $right);
334
335                        quote_spanned!($span => {
336                            // assign in one stmt in case either #left or #right contains
337                            // references to `left` or `right` symbols.
338                            let (left, right) = (&mut #left, #right);
339                            *left = (*left).$method(right)
340                                .unwrap_or_else(||
341                                    panic!(
342                                        "Overflow or underflow in {} {} + {}",
343                                        stringify!($method),
344                                        *left,
345                                        right
346                                    )
347                                )
348                        })
349                    }};
350                }
351
352                match op {
353                    BinOp::Add(_) => {
354                        wrap_op!(left, right, checked_add, span)
355                    }
356                    BinOp::Sub(_) => {
357                        wrap_op!(left, right, checked_sub, span)
358                    }
359                    BinOp::Mul(_) => {
360                        wrap_op!(left, right, checked_mul, span)
361                    }
362                    BinOp::Div(_) => {
363                        wrap_op!(left, right, checked_div, span)
364                    }
365                    BinOp::Rem(_) => {
366                        wrap_op!(left, right, checked_rem, span)
367                    }
368                    BinOp::AddAssign(_) => {
369                        wrap_op_assign!(left, right, checked_add, span)
370                    }
371                    BinOp::SubAssign(_) => {
372                        wrap_op_assign!(left, right, checked_sub, span)
373                    }
374                    BinOp::MulAssign(_) => {
375                        wrap_op_assign!(left, right, checked_mul, span)
376                    }
377                    BinOp::DivAssign(_) => {
378                        wrap_op_assign!(left, right, checked_div, span)
379                    }
380                    BinOp::RemAssign(_) => {
381                        wrap_op_assign!(left, right, checked_rem, span)
382                    }
383                    _ => {
384                        let expr_binary = ExprBinary {
385                            attrs,
386                            left,
387                            op,
388                            right,
389                        };
390                        quote_spanned!(span => #expr_binary)
391                    }
392                }
393            }
394            Expr::Unary(expr_unary) => {
395                let op = &expr_unary.op;
396                let operand = &expr_unary.expr;
397                match op {
398                    UnOp::Neg(_) => {
399                        quote_spanned!(span => #operand.checked_neg().expect("Overflow or underflow in negation"))
400                    }
401                    _ => quote_spanned!(span => #expr_unary),
402                }
403            }
404            _ => quote_spanned!(span => #expr),
405        };
406
407        parse2(expr).unwrap()
408    }
409}
410
411/// This proc macro generates a function `order_to_variant_map` which returns a map
412/// of the position of each variant to the name of the variant.
413/// It is intended to catch changes in enum order when backward compat is required.
414/// ```rust,ignore
415///    /// Example for this enum
416///    #[derive(EnumVariantOrder)]
417///    pub enum MyEnum {
418///         A,
419///         B(u64),
420///         C{x: bool, y: i8},
421///     }
422///     let order_map = MyEnum::order_to_variant_map();
423///     assert!(order_map.get(0).unwrap() == "A");
424///     assert!(order_map.get(1).unwrap() == "B");
425///     assert!(order_map.get(2).unwrap() == "C");
426/// ```
427#[proc_macro_derive(EnumVariantOrder)]
428pub fn enum_variant_order_derive(input: TokenStream) -> TokenStream {
429    let ast = parse_macro_input!(input as DeriveInput);
430    let name = &ast.ident;
431
432    if let Data::Enum(DataEnum { variants, .. }) = ast.data {
433        let variant_entries = variants
434            .iter()
435            .enumerate()
436            .map(|(index, variant)| {
437                let variant_name = variant.ident.to_string();
438                quote! {
439                    map.insert( #index as u64, (#variant_name).to_string());
440                }
441            })
442            .collect::<Vec<_>>();
443
444        let deriv = quote! {
445            impl sui_enum_compat_util::EnumOrderMap for #name {
446                fn order_to_variant_map() -> std::collections::BTreeMap<u64, String > {
447                    let mut map = std::collections::BTreeMap::new();
448                    #(#variant_entries)*
449                    map
450                }
451            }
452        };
453
454        deriv.into()
455    } else {
456        panic!("EnumVariantOrder can only be used with enums.");
457    }
458}