Skip to main content

iota_proc_macros/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use proc_macro::TokenStream;
6use quote::{ToTokens, format_ident, quote, quote_spanned};
7use syn::{
8    Attribute, BinOp, Data, DataEnum, DeriveInput, Expr, ExprBinary, ExprMacro, Item, ItemMacro,
9    Stmt, StmtMacro, Token, UnOp,
10    fold::{Fold, fold_expr, fold_item_macro, fold_stmt},
11    parse::Parser,
12    parse_macro_input, parse2,
13    punctuated::Punctuated,
14    spanned::Spanned,
15};
16
17#[proc_macro_attribute]
18pub fn init_static_initializers(_args: TokenStream, item: TokenStream) -> TokenStream {
19    let mut input = parse_macro_input!(item as syn::ItemFn);
20
21    let body = &input.block;
22    input.block = syn::parse2(quote! {
23        {
24            // We have some lazily-initialized static state in the program. The initializers
25            // alter the thread-local hash container state any time they create a new hash
26            // container. Therefore, we need to ensure that these initializers are run in a
27            // separate thread before the first test thread is launched. Otherwise, they would
28            // run inside of the first test thread, but not subsequent ones.
29            //
30            // Note that none of this has any effect on process-level determinism. Without this
31            // code, we can still get the same test results from two processes started with the
32            // same seed.
33            //
34            // However, when using sim_test(check_determinism) or MSIM_TEST_CHECK_DETERMINISM=1,
35            // we want the same test invocation to be deterministic when run twice
36            // _in the same process_, so we need to take care of this. This will also
37            // be very important for being able to reproduce a failure that occurs in the Nth
38            // iteration of a multi-iteration test run.
39            std::thread::spawn(|| {
40                use iota_protocol_config::ProtocolConfig;
41                ::iota_simulator::telemetry_subscribers::init_for_testing();
42                ::iota_simulator::iota_types::execution::get_denied_certificates();
43                ::iota_simulator::iota_framework::BuiltInFramework::all_package_ids();
44                ::iota_simulator::iota_types::gas::IotaGasStatus::new_unmetered();
45
46                // For reasons I can't understand, LruCache causes divergent behavior the second
47                // time one is constructed and inserted into, so construct one before the first
48                // test run for determinism.
49                let mut cache = ::iota_simulator::lru::LruCache::new(1.try_into().unwrap());
50                cache.put(1, 1);
51
52                {
53                    // Initialize the static initializers here:
54                    // https://github.com/move-language/move/blob/652badf6fd67e1d4cc2aa6dc69d63ad14083b673/language/tools/move-package/src/package_lock.rs#L12
55                    use std::path::PathBuf;
56                    use iota_simulator::iota_move_build::{BuildConfig, IotaPackageHooks};
57                    use iota_simulator::tempfile::TempDir;
58                    use iota_simulator::move_package::package_hooks::register_package_hooks;
59
60                    register_package_hooks(Box::new(IotaPackageHooks {}));
61                    let mut path = PathBuf::from(env!("SIMTEST_STATIC_INIT_MOVE"));
62                    let mut build_config = BuildConfig::new_for_testing();
63
64                    // Resolve the system packages (Iota framework, MoveStdlib, …) from the local
65                    // iota checkout rather than fetching them over the network. This lets the
66                    // static-init package omit an explicit `Iota` dependency; for packages that do
67                    // declare one, this injection is skipped and has no effect.
68                    build_config.config.implicit_dependencies =
69                        iota_simulator::iota_move_build::local_implicit_deps_latest();
70                    build_config.config.install_dir = Some(TempDir::new().unwrap().keep());
71                    let _all_module_bytes = build_config
72                        .build(&path)
73                        .unwrap()
74                        .get_package_bytes(/* with_unpublished_deps */ false);
75                }
76
77
78                use ::iota_simulator::anemo_tower::callback::CallbackLayer;
79                use ::iota_simulator::anemo_tower::trace::DefaultMakeSpan;
80                use ::iota_simulator::anemo_tower::trace::DefaultOnFailure;
81                use ::iota_simulator::anemo_tower::trace::TraceLayer;
82                use ::iota_metrics::metrics_network::{NetworkMetrics, MetricsMakeCallbackHandler};
83
84                use std::sync::Arc;
85                use ::iota_simulator::fastcrypto::traits::KeyPair;
86                use ::iota_simulator::rand_crate::rngs::{StdRng, OsRng};
87                use ::iota_simulator::rand::SeedableRng;
88                use ::iota_simulator::tower::ServiceBuilder;
89
90                // anemo uses x509-parser, which has many lazy static variables. start a network to
91                // initialize all that static state before the first test.
92                let rt = ::iota_simulator::runtime::Runtime::new();
93                rt.block_on(async move {
94                    use ::iota_simulator::anemo::{Network, Request};
95
96                    let make_network = |port: u16| {
97                        let registry = prometheus_filtered::Registry::new();
98                        let inbound_network_metrics =
99                            NetworkMetrics::new("iota", "inbound", &registry);
100                        let outbound_network_metrics =
101                            NetworkMetrics::new("iota", "outbound", &registry);
102
103                        let service = ServiceBuilder::new()
104                            .layer(
105                                TraceLayer::new_for_server_errors()
106                                    .make_span_with(DefaultMakeSpan::new().level(tracing::Level::INFO))
107                                    .on_failure(DefaultOnFailure::new().level(tracing::Level::WARN)),
108                            )
109                            .layer(CallbackLayer::new(MetricsMakeCallbackHandler::new(
110                                Arc::new(inbound_network_metrics),
111                                usize::MAX,
112                            )))
113                            .service(::iota_simulator::anemo::Router::new());
114
115                        let outbound_layer = ServiceBuilder::new()
116                            .layer(
117                                TraceLayer::new_for_client_and_server_errors()
118                                    .make_span_with(DefaultMakeSpan::new().level(tracing::Level::INFO))
119                                    .on_failure(DefaultOnFailure::new().level(tracing::Level::WARN)),
120                            )
121                            .layer(CallbackLayer::new(MetricsMakeCallbackHandler::new(
122                                Arc::new(outbound_network_metrics),
123                                usize::MAX,
124                            )))
125                            .into_inner();
126
127
128                        Network::bind(format!("127.0.0.1:{}", port))
129                            .server_name("static-init-network")
130                            .private_key(
131                                ::iota_simulator::fastcrypto::ed25519::Ed25519KeyPair::generate(&mut StdRng::from_rng(OsRng).unwrap())
132                                    .private()
133                                    .0
134                                    .to_bytes(),
135                            )
136                            .start(service)
137                            .unwrap()
138                    };
139                    let n1 = make_network(80);
140                    let n2 = make_network(81);
141
142                    let _peer = n1.connect(n2.local_addr()).await.unwrap();
143                });
144            }).join().unwrap();
145
146            #body
147        }
148    })
149    .expect("Parsing failure");
150
151    let result = quote! {
152        #input
153    };
154
155    result.into()
156}
157
158/// The iota_test macro will invoke either `#[msim::test]` or `#[tokio::test]`,
159/// depending on whether the simulator config var is enabled.
160///
161/// This should be used for tests that can meaningfully run in either
162/// environment.
163#[proc_macro_attribute]
164pub fn iota_test(args: TokenStream, item: TokenStream) -> TokenStream {
165    let input = parse_macro_input!(item as syn::ItemFn);
166    let arg_parser = Punctuated::<syn::Meta, Token![,]>::parse_terminated;
167    let args = arg_parser.parse(args).unwrap().into_iter();
168
169    let header = if cfg!(msim) {
170        quote! {
171            #[::iota_simulator::sim_test(crate = "iota_simulator", #(#args)* )]
172        }
173    } else {
174        quote! {
175            #[::tokio::test(#(#args)*)]
176        }
177    };
178
179    let result = quote! {
180        #header
181        #[::iota_macros::init_static_initializers]
182        #input
183    };
184
185    result.into()
186}
187
188/// The `sim_test` macro will invoke `#[msim::test]` if the simulator config var
189/// (`msim`) is enabled.
190///
191/// On this premise, this macro can be used in order to pass any
192/// simulator-specific arguments, such as `check_determinism`,
193/// which is not understood by tokio.
194///
195/// If the simulator config var is disabled, tests will run via
196/// `#[tokio::test]`, unless disabled by setting the environment variable
197/// `IOTA_SKIP_SIMTESTS`.
198#[proc_macro_attribute]
199pub fn sim_test(args: TokenStream, item: TokenStream) -> TokenStream {
200    let input = parse_macro_input!(item as syn::ItemFn);
201    let arg_parser = Punctuated::<syn::Meta, Token![,]>::parse_terminated;
202    let args = arg_parser.parse(args).unwrap().into_iter();
203
204    let ignore = input
205        .attrs
206        .iter()
207        .find(|attr| attr.path().is_ident("ignore"))
208        .map_or(quote! {}, |_| quote! { #[ignore] });
209
210    let result = if cfg!(msim) {
211        let sig = &input.sig;
212        let return_type = &sig.output;
213        let body = &input.block;
214        quote! {
215            #[::iota_simulator::sim_test(crate = "iota_simulator", #(#args),*)]
216            #[::iota_macros::init_static_initializers]
217            #ignore
218            #sig {
219                async fn body_fn() #return_type { #body }
220
221                let ret = body_fn().await;
222
223                ::iota_simulator::task::shutdown_all_nodes();
224
225                // all node handles should have been dropped after the above block exits, but task
226                // shutdown is asynchronous, so we need a brief delay before checking for leaks.
227                tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
228
229                assert_eq!(
230                    iota_simulator::NodeLeakDetector::get_current_node_count(),
231                    0,
232                    "IotaNode leak detected"
233                );
234
235                ret
236            }
237        }
238    } else {
239        let fn_name = &input.sig.ident;
240        let sig = &input.sig;
241        let body = &input.block;
242        quote! {
243            #[expect(clippy::needless_return)]
244            #[tokio::test]
245            #ignore
246            #sig {
247                if std::env::var("IOTA_SKIP_SIMTESTS").is_ok() {
248                    println!("not running test {} in `cargo test`: IOTA_SKIP_SIMTESTS is set", stringify!(#fn_name));
249
250                    struct Ret;
251
252                    impl From<Ret> for () {
253                        fn from(_ret: Ret) -> Self {
254                        }
255                    }
256
257                    impl<E> From<Ret> for Result<(), E> {
258                        fn from(_ret: Ret) -> Self {
259                            Ok(())
260                        }
261                    }
262
263                    return Ret.into();
264                }
265
266                #body
267            }
268        }
269    };
270
271    result.into()
272}
273
274#[proc_macro]
275pub fn checked_arithmetic(input: TokenStream) -> TokenStream {
276    let input_file = CheckArithmetic.fold_file(parse_macro_input!(input));
277
278    let output_items = input_file.items;
279
280    let output = quote! {
281        #(#output_items)*
282    };
283
284    TokenStream::from(output)
285}
286
287#[proc_macro_attribute]
288pub fn with_checked_arithmetic(_attr: TokenStream, item: TokenStream) -> TokenStream {
289    let input_item = parse_macro_input!(item as Item);
290    match input_item {
291        Item::Fn(input_fn) => {
292            let transformed_fn = CheckArithmetic.fold_item_fn(input_fn);
293            TokenStream::from(quote! { #transformed_fn })
294        }
295        Item::Impl(input_impl) => {
296            let transformed_impl = CheckArithmetic.fold_item_impl(input_impl);
297            TokenStream::from(quote! { #transformed_impl })
298        }
299        item => {
300            let transformed_impl = CheckArithmetic.fold_item(item);
301            TokenStream::from(quote! { #transformed_impl })
302        }
303    }
304}
305
306struct CheckArithmetic;
307
308impl CheckArithmetic {
309    fn maybe_skip_macro(&self, attrs: &mut Vec<Attribute>) -> bool {
310        if let Some(idx) = attrs
311            .iter()
312            .position(|attr| attr.path().is_ident("skip_checked_arithmetic"))
313        {
314            // Skip processing macro because it is annotated with
315            // #[skip_checked_arithmetic]
316            attrs.remove(idx);
317            true
318        } else {
319            false
320        }
321    }
322
323    fn process_macro_contents(
324        &mut self,
325        tokens: proc_macro2::TokenStream,
326    ) -> syn::Result<proc_macro2::TokenStream> {
327        // Parse the macro's contents as a comma-separated list of expressions.
328        let parser = Punctuated::<Expr, Token![,]>::parse_terminated;
329        let Ok(exprs) = parser.parse(tokens.clone().into()) else {
330            return Err(syn::Error::new_spanned(
331                tokens,
332                "could not process macro contents - use #[skip_checked_arithmetic] to skip this macro",
333            ));
334        };
335
336        // Fold each sub expression.
337        let folded_exprs = exprs
338            .into_iter()
339            .map(|expr| self.fold_expr(expr))
340            .collect::<Vec<_>>();
341
342        // Convert the folded expressions back into tokens and reconstruct the macro.
343        let mut folded_tokens = proc_macro2::TokenStream::new();
344        for (i, folded_expr) in folded_exprs.into_iter().enumerate() {
345            if i > 0 {
346                folded_tokens.extend(std::iter::once::<proc_macro2::TokenTree>(
347                    proc_macro2::Punct::new(',', proc_macro2::Spacing::Alone).into(),
348                ));
349            }
350            folded_expr.to_tokens(&mut folded_tokens);
351        }
352
353        Ok(folded_tokens)
354    }
355}
356
357impl Fold for CheckArithmetic {
358    fn fold_stmt(&mut self, stmt: Stmt) -> Stmt {
359        let stmt = fold_stmt(self, stmt);
360        if let Stmt::Macro(stmt_macro) = stmt {
361            let StmtMacro {
362                mut attrs,
363                mut mac,
364                semi_token,
365            } = stmt_macro;
366
367            if self.maybe_skip_macro(&mut attrs) {
368                Stmt::Macro(StmtMacro {
369                    attrs,
370                    mac,
371                    semi_token,
372                })
373            } else {
374                match self.process_macro_contents(mac.tokens.clone()) {
375                    Ok(folded_tokens) => {
376                        mac.tokens = folded_tokens;
377                        Stmt::Macro(StmtMacro {
378                            attrs,
379                            mac,
380                            semi_token,
381                        })
382                    }
383                    Err(error) => parse2(error.to_compile_error()).unwrap(),
384                }
385            }
386        } else {
387            stmt
388        }
389    }
390
391    fn fold_item_macro(&mut self, mut item_macro: ItemMacro) -> ItemMacro {
392        if !self.maybe_skip_macro(&mut item_macro.attrs) {
393            let err = syn::Error::new_spanned(
394                item_macro.to_token_stream(),
395                "cannot process macros - use #[skip_checked_arithmetic] to skip \
396                    processing this macro",
397            );
398
399            return parse2(err.to_compile_error()).unwrap();
400        }
401        fold_item_macro(self, item_macro)
402    }
403
404    fn fold_expr(&mut self, expr: Expr) -> Expr {
405        let span = expr.span();
406        let expr = fold_expr(self, expr);
407        let expr = match expr {
408            Expr::Macro(expr_macro) => {
409                let ExprMacro { mut attrs, mut mac } = expr_macro;
410
411                if self.maybe_skip_macro(&mut attrs) {
412                    return Expr::Macro(ExprMacro { attrs, mac });
413                } else {
414                    match self.process_macro_contents(mac.tokens.clone()) {
415                        Ok(folded_tokens) => {
416                            mac.tokens = folded_tokens;
417                            let expr_macro = Expr::Macro(ExprMacro { attrs, mac });
418                            quote!(#expr_macro)
419                        }
420                        Err(error) => {
421                            return Expr::Verbatim(error.to_compile_error());
422                        }
423                    }
424                }
425            }
426
427            Expr::Binary(expr_binary) => {
428                let ExprBinary {
429                    attrs,
430                    mut left,
431                    op,
432                    mut right,
433                } = expr_binary;
434
435                fn remove_parens(expr: &mut Expr) {
436                    if let Expr::Paren(paren) = expr {
437                        // i don't even think rust allows this, but just in case
438                        assert!(paren.attrs.is_empty(), "TODO: attrs on parenthesized");
439                        *expr = *paren.expr.clone();
440                    }
441                }
442
443                macro_rules! wrap_op {
444                    ($left: expr, $right: expr, $method: ident, $span: expr) => {{
445                        // Remove parens from exprs since both sides get assigned to tmp variables.
446                        // otherwise we get lint errors
447                        remove_parens(&mut $left);
448                        remove_parens(&mut $right);
449
450                        quote_spanned!($span => {
451                            // assign in one stmt in case either #left or #right contains
452                            // references to `left` or `right` symbols.
453                            let (left, right) = (#left, #right);
454                            left.$method(right)
455                                .unwrap_or_else(||
456                                    panic!(
457                                        "Overflow or underflow in {} {} + {}",
458                                        stringify!($method),
459                                        left,
460                                        right,
461                                    )
462                                )
463                        })
464                    }};
465                }
466
467                macro_rules! wrap_op_assign {
468                    ($left: expr, $right: expr, $method: ident, $span: expr) => {{
469                        // Remove parens from exprs since both sides get assigned to tmp variables.
470                        // otherwise we get lint errors
471                        remove_parens(&mut $left);
472                        remove_parens(&mut $right);
473
474                        quote_spanned!($span => {
475                            // assign in one stmt in case either #left or #right contains
476                            // references to `left` or `right` symbols.
477                            let (left, right) = (&mut #left, #right);
478                            *left = (*left).$method(right)
479                                .unwrap_or_else(||
480                                    panic!(
481                                        "Overflow or underflow in {} {} + {}",
482                                        stringify!($method),
483                                        *left,
484                                        right
485                                    )
486                                )
487                        })
488                    }};
489                }
490
491                match op {
492                    BinOp::Add(_) => {
493                        wrap_op!(left, right, checked_add, span)
494                    }
495                    BinOp::Sub(_) => {
496                        wrap_op!(left, right, checked_sub, span)
497                    }
498                    BinOp::Mul(_) => {
499                        wrap_op!(left, right, checked_mul, span)
500                    }
501                    BinOp::Div(_) => {
502                        wrap_op!(left, right, checked_div, span)
503                    }
504                    BinOp::Rem(_) => {
505                        wrap_op!(left, right, checked_rem, span)
506                    }
507                    BinOp::AddAssign(_) => {
508                        wrap_op_assign!(left, right, checked_add, span)
509                    }
510                    BinOp::SubAssign(_) => {
511                        wrap_op_assign!(left, right, checked_sub, span)
512                    }
513                    BinOp::MulAssign(_) => {
514                        wrap_op_assign!(left, right, checked_mul, span)
515                    }
516                    BinOp::DivAssign(_) => {
517                        wrap_op_assign!(left, right, checked_div, span)
518                    }
519                    BinOp::RemAssign(_) => {
520                        wrap_op_assign!(left, right, checked_rem, span)
521                    }
522                    _ => {
523                        let expr_binary = ExprBinary {
524                            attrs,
525                            left,
526                            op,
527                            right,
528                        };
529                        quote_spanned!(span => #expr_binary)
530                    }
531                }
532            }
533            Expr::Unary(expr_unary) => {
534                let op = &expr_unary.op;
535                let operand = &expr_unary.expr;
536                match op {
537                    UnOp::Neg(_) => {
538                        quote_spanned!(span => #operand.checked_neg().expect("Overflow or underflow in negation"))
539                    }
540                    _ => quote_spanned!(span => #expr_unary),
541                }
542            }
543            _ => quote_spanned!(span => #expr),
544        };
545
546        parse2(expr).unwrap()
547    }
548}
549
550/// This proc macro generates a function `order_to_variant_map` which returns a
551/// map of the position of each variant to the name of the variant.
552/// It is intended to catch changes in enum order when backward compat is
553/// required.
554/// ```rust,ignore
555///    /// Example for this enum
556///    #[derive(EnumVariantOrder)]
557///    pub enum MyEnum {
558///         A,
559///         B(u64),
560///         C{x: bool, y: i8},
561///     }
562///     let order_map = MyEnum::order_to_variant_map();
563///     assert!(order_map.get(0).unwrap() == "A");
564///     assert!(order_map.get(1).unwrap() == "B");
565///     assert!(order_map.get(2).unwrap() == "C");
566/// ```
567#[proc_macro_derive(EnumVariantOrder)]
568pub fn enum_variant_order_derive(input: TokenStream) -> TokenStream {
569    let ast = parse_macro_input!(input as DeriveInput);
570    let name = &ast.ident;
571
572    if let Data::Enum(DataEnum { variants, .. }) = ast.data {
573        let variant_entries = variants
574            .iter()
575            .enumerate()
576            .map(|(index, variant)| {
577                let variant_name = variant.ident.to_string();
578                quote! {
579                    map.insert( #index as u64, (#variant_name).to_string());
580                }
581            })
582            .collect::<Vec<_>>();
583
584        let deriv = quote! {
585            impl iota_enum_compat_util::EnumOrderMap for #name {
586                fn order_to_variant_map() -> std::collections::BTreeMap<u64, String > {
587                    let mut map = std::collections::BTreeMap::new();
588                    #(#variant_entries)*
589                    map
590                }
591            }
592        };
593
594        deriv.into()
595    } else {
596        panic!("EnumVariantOrder can only be used with enums.");
597    }
598}
599
600/// Wraps an item in a module with `#[allow(deprecated)]` and re-exports it.
601///
602/// Some proc-macro derives (e.g. strum, enum_dispatch) generate code that
603/// references deprecated variants, and `#[allow(deprecated)]` on the item
604/// itself does not propagate to their output. This macro works around that
605/// limitation by placing the item inside a private module where the
606/// `deprecated` lint is suppressed, then re-exporting the type.
607///
608/// If the item itself carries `#[deprecated]`, the attribute is moved onto
609/// the `pub use` re-export so that external callers still see the
610/// deprecation warning while derive-generated code inside the module does
611/// not.
612///
613/// Place this attribute **above** all `#[derive(...)]` and proc-macro
614/// attributes on the item:
615///
616/// ```ignore
617/// #[allow_deprecated_for_derives]
618/// #[derive(Debug, EnumString, strum_macros::Display)]
619/// pub enum MyEnum {
620///     Active,
621///     #[deprecated(note = "no longer used")]
622///     Legacy,
623/// }
624/// ```
625#[proc_macro_attribute]
626pub fn allow_deprecated_for_derives(_attr: TokenStream, item: TokenStream) -> TokenStream {
627    let mut input = parse_macro_input!(item as DeriveInput);
628    let vis = &input.vis;
629    let ident = &input.ident;
630    let mod_name = format_ident!("__allow_deprecated_{}", ident.to_string().to_lowercase());
631
632    // If the item itself is `#[deprecated]`, move that attribute onto the
633    // re-export so derive macros inside the module never see it.
634    let mut deprecated_attrs: Vec<Attribute> = Vec::new();
635    input.attrs.retain(|attr| {
636        if attr.path().is_ident("deprecated") {
637            deprecated_attrs.push(attr.clone());
638            false
639        } else {
640            true
641        }
642    });
643
644    quote! {
645        #[allow(deprecated)]
646        mod #mod_name {
647            use super::*;
648            #input
649        }
650        #(#deprecated_attrs)*
651        #vis use #mod_name::#ident;
652    }
653    .into()
654}