Skip to main content

ab_contracts_macros_impl/
contract.rs

1mod common;
2mod init;
3mod method;
4mod update;
5mod view;
6
7use crate::contract::common::{derive_ident_metadata, extract_ident_from_type};
8use crate::contract::init::process_init_fn;
9use crate::contract::method::{ExtTraitComponents, MethodDetails};
10use crate::contract::update::{process_update_fn, process_update_fn_definition};
11use crate::contract::view::{process_view_fn, process_view_fn_definition};
12use ab_contracts_common::METADATA_STATIC_NAME_PREFIX;
13use ident_case::RenameRule;
14use proc_macro2::{Ident, Literal, Span, TokenStream};
15use quote::{format_ident, quote};
16use std::collections::HashMap;
17use syn::spanned::Spanned;
18use syn::{
19    Error, ImplItem, ImplItemFn, ItemImpl, ItemTrait, Meta, TraitItem, TraitItemConst, TraitItemFn,
20    Type, Visibility, parse_quote, parse2,
21};
22
23#[derive(Default)]
24struct MethodOutput {
25    guest_ffi: TokenStream,
26    trait_ext_components: ExtTraitComponents,
27}
28
29struct Method {
30    /// As authored in source code
31    original_ident: Ident,
32    methods_details: MethodDetails,
33}
34
35#[derive(Default)]
36struct ContractDetails {
37    methods: Vec<Method>,
38}
39
40pub(super) fn contract(item: TokenStream) -> Result<TokenStream, Error> {
41    if let Ok(item_trait) = parse2::<ItemTrait>(item.clone()) {
42        // Trait definition
43        return process_trait_definition(item_trait);
44    }
45
46    let error_message = "`#[contract]` must be applied to struct implementation, trait definition or trait \
47        implementation";
48
49    let item_impl =
50        parse2::<ItemImpl>(item).map_err(|error| Error::new(error.span(), error_message))?;
51
52    if let Some((_not, path, _for)) = &item_impl.trait_ {
53        let trait_name = path
54            .get_ident()
55            .ok_or_else(|| Error::new(path.span(), error_message))?
56            .clone();
57        // Trait implementation
58        process_trait_impl(item_impl, &trait_name)
59    } else {
60        // Implementation of a struct
61        process_struct_impl(item_impl)
62    }
63}
64
65fn process_trait_definition(mut item_trait: ItemTrait) -> Result<TokenStream, Error> {
66    let trait_name = &item_trait.ident;
67
68    if !item_trait.generics.params.is_empty() {
69        return Err(Error::new(
70            item_trait.generics.span(),
71            "`#[contract]` does not support generics",
72        ));
73    }
74
75    let mut guest_ffis = Vec::with_capacity(item_trait.items.len());
76    let mut trait_ext_components = Vec::with_capacity(item_trait.items.len());
77    let mut contract_details = ContractDetails::default();
78
79    for item in &mut item_trait.items {
80        if let TraitItem::Fn(trait_item_fn) = item {
81            let method_output =
82                process_fn_definition(trait_name, trait_item_fn, &mut contract_details)?;
83            guest_ffis.push(method_output.guest_ffi);
84            trait_ext_components.push(method_output.trait_ext_components);
85
86            // This is needed to make trait itself object safe, which is in turn used as a hack for
87            // some APIs
88            if let Some(where_clause) = &mut trait_item_fn.sig.generics.where_clause {
89                where_clause.predicates.push(parse_quote! {
90                    Self: ::core::marker::Sized
91                });
92            } else {
93                trait_item_fn
94                    .sig
95                    .generics
96                    .where_clause
97                    .replace(parse_quote! {
98                        where
99                            Self: ::core::marker::Sized
100                    });
101            }
102        }
103    }
104
105    let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_trait.span())?;
106    let ext_trait = generate_extension_trait(trait_name, &trait_ext_components);
107
108    Ok(quote! {
109        #item_trait
110
111        // `dyn ContractTrait` here is a bit of a hack that allows treating a trait as a type. These
112        // constants specifically can't be implemented on a trait itself because that'll make trait
113        // not object safe, which is needed for `ContractTrait` that uses a similar hack with
114        // `dyn ContractTrait`.
115        impl ::ab_contracts_macros::__private::ContractTraitDefinition for dyn #trait_name {
116            #[cfg(feature = "guest")]
117            #[doc(hidden)]
118            const GUEST_FEATURE_ENABLED: () = ();
119            #metadata_const
120        }
121
122        #ext_trait
123
124        /// FFI code generated by procedural macro
125        pub mod ffi {
126            use super::*;
127
128            #( #guest_ffis )*
129        }
130    })
131}
132
133fn process_trait_impl(mut item_impl: ItemImpl, trait_name: &Ident) -> Result<TokenStream, Error> {
134    let struct_name = item_impl.self_ty.as_ref();
135
136    if !item_impl.generics.params.is_empty() {
137        return Err(Error::new(
138            item_impl.generics.span(),
139            "`#[contract]` does not support generics",
140        ));
141    }
142
143    let mut guest_ffis = Vec::with_capacity(item_impl.items.len());
144    let mut contract_details = ContractDetails::default();
145
146    for item in &mut item_impl.items {
147        match item {
148            ImplItem::Fn(impl_item_fn) => {
149                let method_output = process_fn(
150                    struct_name.clone(),
151                    Some(trait_name),
152                    impl_item_fn,
153                    &mut contract_details,
154                )?;
155                guest_ffis.push(method_output.guest_ffi);
156
157                if let Some(where_clause) = &mut impl_item_fn.sig.generics.where_clause {
158                    where_clause.predicates.push(parse_quote! {
159                        Self: ::core::marker::Sized
160                    });
161                } else {
162                    impl_item_fn
163                        .sig
164                        .generics
165                        .where_clause
166                        .replace(parse_quote! {
167                            where
168                                Self: ::core::marker::Sized
169                        });
170                }
171            }
172            ImplItem::Const(impl_item_const) if impl_item_const.ident == "METADATA" => {
173                return Err(Error::new(
174                    impl_item_const.span(),
175                    "`#[contract]` doesn't allow overriding `METADATA` constant",
176                ));
177            }
178            _ => {
179                // Ignore
180            }
181        }
182    }
183
184    let static_name = format_ident!("{METADATA_STATIC_NAME_PREFIX}{}", trait_name);
185    let ffi_mod_ident = format_ident!(
186        "{}_ffi",
187        RenameRule::SnakeCase.apply_to_variant(trait_name.to_string())
188    );
189    let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_impl.span())?;
190    let method_fn_pointers_const = {
191        let methods = contract_details
192            .methods
193            .iter()
194            .map(|method| &method.original_ident);
195
196        quote! {
197            #[doc(hidden)]
198            const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
199                #( #ffi_mod_ident::#methods::fn_pointer::METHOD_FN_POINTER, )*
200            ];
201        }
202    };
203
204    Ok(quote! {
205        /// Contribute trait metadata to contract's metadata
206        ///
207        /// Enabled with `guest` feature to appear in the final binary.
208        ///
209        /// See [`Contract::MAIN_CONTRACT_METADATA`] for details.
210        ///
211        /// [`Contract::MAIN_CONTRACT_METADATA`]: ::ab_contracts_macros::__private::Contract::MAIN_CONTRACT_METADATA
212        #[cfg(feature = "guest")]
213        #[used]
214        #[unsafe(no_mangle)]
215        #[unsafe(link_section = "ab-contract-metadata")]
216        static #static_name: [::core::primitive::u8; <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA.len()] = unsafe {
217            *<dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA.as_ptr().cast()
218        };
219
220        // Sanity check that trait implementation fully matches trait definition
221        const _: () = {
222            // Import as `ffi` for generated metadata constant to pick up a correct version
223            use #ffi_mod_ident as ffi;
224            #metadata_const
225
226            // Comparing compact metadata to allow argument name differences and similar things
227            // TODO: This two-step awkwardness because simple comparison doesn't work in const
228            //  environment yet
229            let (impl_compact_metadata, impl_compact_metadata_size) =
230                ::ab_contracts_macros::__private::ContractMetadataKind::compact(METADATA)
231                    .expect("Generated metadata is correct; qed");
232            let (def_compact_metadata, def_compact_metadata_size) =
233                ::ab_contracts_macros::__private::ContractMetadataKind::compact(
234                    <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA,
235                )
236                    .expect("Generated metadata is correct; qed");
237            assert!(
238                impl_compact_metadata_size == def_compact_metadata_size,
239                "Trait implementation must match trait definition exactly"
240            );
241            let mut i = 0;
242            while impl_compact_metadata_size > i {
243                assert!(
244                    impl_compact_metadata[i] == def_compact_metadata[i],
245                    "Trait implementation must match trait definition exactly"
246                );
247                i += 1;
248            }
249        };
250
251        // Ensure `guest` feature is enabled for crate with trait definition
252        #[cfg(feature = "guest")]
253        const _: () = <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::GUEST_FEATURE_ENABLED;
254
255        #item_impl
256
257        // `dyn ContractTrait` here is a bit of a hack that allows treating a trait as a type for
258        // convenient API in native execution environment
259        impl ::ab_contracts_macros::__private::ContractTrait<dyn #trait_name> for #struct_name {
260            #method_fn_pointers_const
261        }
262
263        /// FFI code generated by procedural macro
264        pub mod #ffi_mod_ident {
265            use super::*;
266
267
268            #( #guest_ffis )*
269        }
270    })
271}
272
273fn generate_trait_metadata(
274    contract_details: &ContractDetails,
275    trait_name: &Ident,
276    span: Span,
277) -> Result<TraitItemConst, Error> {
278    let num_methods = u8::try_from(contract_details.methods.len()).map_err(|_error| {
279        Error::new(
280            span,
281            format!("Trait can't have more than {} methods", u8::MAX),
282        )
283    })?;
284    let num_methods = Literal::u8_unsuffixed(num_methods);
285    let methods = contract_details
286        .methods
287        .iter()
288        .map(|method| &method.original_ident);
289    let trait_name_metadata = derive_ident_metadata(trait_name)?;
290
291    // Encodes the following:
292    // * Type: trait definition
293    // * Length of trait name in bytes (u8)
294    // * Trait name as UTF-8 bytes
295    // * Number of methods
296    // * Metadata of methods
297    Ok(parse_quote! {
298        /// Trait metadata, see [`ContractMetadataKind`] for encoding details
299        ///
300        /// [`ContractMetadataKind`]: ::ab_contracts_macros::__private::ContractMetadataKind
301        const METADATA: &[::core::primitive::u8] = {
302            const fn metadata()
303                -> ([::core::primitive::u8; ::ab_contracts_macros::__private::MAX_METADATA_CAPACITY], usize)
304            {
305                ::ab_contracts_macros::__private::concat_metadata_sources(&[
306                    &[::ab_contracts_macros::__private::ContractMetadataKind::Trait as ::core::primitive::u8],
307                    #trait_name_metadata,
308                    &[#num_methods],
309                    #( ffi::#methods::METADATA, )*
310                ])
311            }
312
313            // Strange syntax to allow Rust to extend the lifetime of metadata scratch
314            // automatically
315            metadata()
316                .0
317                .split_at(metadata().1)
318                .0
319        };
320    })
321}
322
323fn process_struct_impl(mut item_impl: ItemImpl) -> Result<TokenStream, Error> {
324    let struct_name = item_impl.self_ty.as_ref();
325
326    if !item_impl.generics.params.is_empty() {
327        return Err(Error::new(
328            item_impl.generics.span(),
329            "`#[contract]` does not support generics",
330        ));
331    }
332
333    let mut guest_ffis = Vec::with_capacity(item_impl.items.len());
334    let mut trait_ext_components = Vec::with_capacity(item_impl.items.len());
335    let mut contract_details = ContractDetails::default();
336
337    for item in &mut item_impl.items {
338        if let ImplItem::Fn(impl_item_fn) = item {
339            let method_output = process_fn(
340                struct_name.clone(),
341                None,
342                impl_item_fn,
343                &mut contract_details,
344            )?;
345            guest_ffis.push(method_output.guest_ffi);
346            trait_ext_components.push(method_output.trait_ext_components);
347        }
348    }
349
350    let maybe_slot_type = MethodDetails::slot_type(
351        contract_details
352            .methods
353            .iter()
354            .map(|method| &method.methods_details),
355    );
356    let Some(slot_type) = maybe_slot_type else {
357        return Err(Error::new(
358            item_impl.span(),
359            "All `#[slot]` arguments must be of the same type in all methods of a contract",
360        ));
361    };
362
363    let maybe_tmp_type = MethodDetails::tmp_type(
364        contract_details
365            .methods
366            .iter()
367            .map(|method| &method.methods_details),
368    );
369
370    let Some(tmp_type) = maybe_tmp_type else {
371        return Err(Error::new(
372            item_impl.span(),
373            "All `#[tmp]` arguments must be of the same type in all methods of a contract",
374        ));
375    };
376
377    let metadata_const = {
378        let num_methods = u8::try_from(contract_details.methods.len()).map_err(|_error| {
379            Error::new(
380                item_impl.span(),
381                format!("Struct can't have more than {} methods", u8::MAX),
382            )
383        })?;
384        let num_methods = Literal::u8_unsuffixed(num_methods);
385        let methods = contract_details
386            .methods
387            .iter()
388            .map(|method| &method.original_ident);
389
390        // Encodes the following:
391        // * Type: contract
392        // * Metadata of the state type
393        // * Number of methods
394        // * Metadata of methods
395        quote! {
396            const MAIN_CONTRACT_METADATA: &[::core::primitive::u8] = {
397                const fn metadata()
398                    -> ([::core::primitive::u8; ::ab_contracts_macros::__private::MAX_METADATA_CAPACITY], usize)
399                {
400                    ::ab_contracts_macros::__private::concat_metadata_sources(&[
401                        &[::ab_contracts_macros::__private::ContractMetadataKind::Contract as ::core::primitive::u8],
402                        <#struct_name as ::ab_contracts_macros::__private::IoType>::METADATA,
403                        <#slot_type as ::ab_contracts_macros::__private::IoType>::METADATA,
404                        <#tmp_type as ::ab_contracts_macros::__private::IoType>::METADATA,
405                        &[#num_methods],
406                        #( ffi::#methods::METADATA, )*
407                    ])
408                }
409
410                // Strange syntax to allow Rust to extend the lifetime of metadata scratch
411                // automatically
412                metadata()
413                    .0
414                    .split_at(metadata().1)
415                    .0
416            };
417        }
418    };
419    let method_fn_pointers_const = {
420        let methods = contract_details
421            .methods
422            .iter()
423            .map(|method| &method.original_ident);
424
425        quote! {
426            #[doc(hidden)]
427            const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
428                #( ffi::#methods::fn_pointer::METHOD_FN_POINTER, )*
429            ];
430        }
431    };
432
433    let struct_name_ident = extract_ident_from_type(struct_name).ok_or_else(|| {
434        Error::new(
435            struct_name.span(),
436            "`#[contract]` must be applied to simple struct implementation",
437        )
438    })?;
439
440    let ext_trait = generate_extension_trait(struct_name_ident, &trait_ext_components);
441
442    let struct_name_str = struct_name_ident.to_string();
443    let static_name = format_ident!("{METADATA_STATIC_NAME_PREFIX}{}", struct_name_str);
444    Ok(quote! {
445        /// Main contract metadata
446        ///
447        /// Enabled with `guest` feature to appear in the final binary, also prevents from
448        /// `guest` feature being enabled in dependencies at the same time since that'll cause
449        /// duplicated symbols.
450        ///
451        /// See [`Contract::MAIN_CONTRACT_METADATA`] for details.
452        ///
453        /// [`Contract::MAIN_CONTRACT_METADATA`]: ::ab_contracts_macros::__private::Contract::MAIN_CONTRACT_METADATA
454        #[cfg(feature = "guest")]
455        #[used]
456        #[unsafe(no_mangle)]
457        #[unsafe(link_section = "ab-contract-metadata")]
458        static #static_name: [
459            ::core::primitive::u8;
460            <#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
461                .len()
462        ] = unsafe {
463            *<#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
464                .as_ptr()
465                .cast()
466        };
467
468        impl ::ab_contracts_macros::__private::Contract for #struct_name {
469            #metadata_const
470            #method_fn_pointers_const
471            #[doc(hidden)]
472            const CODE: &::core::primitive::str = ::ab_contracts_macros::__private::concatcp!(
473                #struct_name_str,
474                '[',
475                ::core::env!("CARGO_PKG_NAME"),
476                '/',
477                ::core::file!(),
478                ':',
479                ::core::line!(),
480                ':',
481                ::core::column!(),
482                ']',
483            );
484            // Ensure `guest` feature is enabled for `ab-contracts-common` crate
485            #[cfg(feature = "guest")]
486            #[doc(hidden)]
487            const GUEST_FEATURE_ENABLED: () = ();
488            type Slot = #slot_type;
489            type Tmp = #tmp_type;
490
491            fn code() -> impl ::core::ops::Deref<
492                Target = ::ab_contracts_macros::__private::VariableBytes<
493                    { ::ab_contracts_macros::__private::MAX_CODE_SIZE },
494                >,
495            > {
496                const fn code_bytes() -> &'static [::core::primitive::u8] {
497                    <#struct_name as ::ab_contracts_macros::__private::Contract>::CODE.as_bytes()
498                }
499
500                const fn code_size() -> ::core::primitive::u32 {
501                    code_bytes().len() as ::core::primitive::u32
502                }
503
504                static CODE_SIZE: ::core::primitive::u32 = code_size();
505
506                ::ab_contracts_macros::__private::VariableBytes::from_buffer(
507                    code_bytes(),
508                    &CODE_SIZE
509                )
510            }
511        }
512
513        #item_impl
514
515        #ext_trait
516
517        /// FFI code generated by procedural macro
518        pub mod ffi {
519            use super::*;
520
521            #( #guest_ffis )*
522        }
523    })
524}
525
526fn process_fn_definition(
527    trait_name: &Ident,
528    trait_item_fn: &mut TraitItemFn,
529    contract_details: &mut ContractDetails,
530) -> Result<MethodOutput, Error> {
531    let supported_attrs = HashMap::<_, fn(_, _, _, _) -> _>::from_iter([
532        (format_ident!("update"), process_update_fn_definition as _),
533        (format_ident!("view"), process_view_fn_definition as _),
534    ]);
535    let mut attrs = trait_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
536        Meta::Path(path) => {
537            path.leading_colon.is_none()
538                && path.segments.len() == 1
539                && supported_attrs.contains_key(&path.segments[0].ident)
540        }
541        Meta::List(_meta_list) => false,
542        Meta::NameValue(_meta_name_value) => false,
543    });
544
545    let Some(attr) = attrs.next() else {
546        drop(attrs);
547
548        // Return an unmodified original if no recognized arguments are present
549        return Ok(MethodOutput::default());
550    };
551
552    if let Some(next_attr) = attrs.take(1).next() {
553        return Err(Error::new(
554            next_attr.span(),
555            format!(
556                "The method `{}` can only have one of `#[update]` or `#[view]` attributes specified",
557                trait_item_fn.sig.ident
558            ),
559        ));
560    }
561
562    // Make sure the method doesn't have customized ABI
563    if let Some(abi) = &trait_item_fn.sig.abi {
564        return Err(Error::new(
565            abi.span(),
566            format!(
567                "The method `{}` with `#[{}]` attribute must have default ABI",
568                trait_item_fn.sig.ident,
569                attr.meta.path().segments[0].ident
570            ),
571        ));
572    }
573
574    if trait_item_fn.default.is_some() {
575        return Err(Error::new(
576            trait_item_fn.span(),
577            "`#[contract]` does not support `#[update]` or `#[view]` methods with default implementation \
578            in trait definition",
579        ));
580    }
581
582    let processor = supported_attrs
583        .get(&attr.path().segments[0].ident)
584        .expect("Matched above to be one of the supported attributes; qed");
585    processor(
586        trait_name,
587        &mut trait_item_fn.sig,
588        trait_item_fn.attrs.as_slice(),
589        contract_details,
590    )
591}
592
593fn process_fn(
594    struct_name: Type,
595    trait_name: Option<&Ident>,
596    impl_item_fn: &mut ImplItemFn,
597    contract_details: &mut ContractDetails,
598) -> Result<MethodOutput, Error> {
599    let supported_attrs = HashMap::<_, fn(_, _, _, _, _) -> _>::from_iter([
600        (format_ident!("init"), process_init_fn as _),
601        (format_ident!("update"), process_update_fn as _),
602        (format_ident!("view"), process_view_fn as _),
603    ]);
604    let mut attrs = impl_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
605        Meta::Path(path) => {
606            path.leading_colon.is_none()
607                && path.segments.len() == 1
608                && supported_attrs.contains_key(&path.segments[0].ident)
609        }
610        Meta::List(_meta_list) => false,
611        Meta::NameValue(_meta_name_value) => false,
612    });
613
614    let Some(attr) = attrs.next() else {
615        drop(attrs);
616
617        // Return an unmodified original if no recognized arguments are present
618        return Ok(MethodOutput::default());
619    };
620
621    if let Some(next_attr) = attrs.take(1).next() {
622        return Err(Error::new(
623            next_attr.span(),
624            format!(
625                "The method `{}` can only have one of `#[init]`, `#[update]` or `#[view]` attributes specified",
626                impl_item_fn.sig.ident
627            ),
628        ));
629    }
630
631    // Make sure the method is public if not a trait impl
632    if !(matches!(impl_item_fn.vis, Visibility::Public(_)) || trait_name.is_some()) {
633        return Err(Error::new(
634            impl_item_fn.sig.span(),
635            format!(
636                "The method `{}` with `#[{}]` attribute must be public",
637                impl_item_fn.sig.ident,
638                attr.meta.path().segments[0].ident
639            ),
640        ));
641    }
642
643    // Make sure the method doesn't have customized ABI
644    if let Some(abi) = &impl_item_fn.sig.abi {
645        return Err(Error::new(
646            abi.span(),
647            format!(
648                "The method `{}` with `#[{}]` attribute must have default ABI",
649                impl_item_fn.sig.ident,
650                attr.meta.path().segments[0].ident
651            ),
652        ));
653    }
654
655    let processor = supported_attrs
656        .get(&attr.path().segments[0].ident)
657        .expect("Matched above to be one of the supported attributes; qed");
658    processor(
659        struct_name,
660        trait_name,
661        &mut impl_item_fn.sig,
662        impl_item_fn.attrs.as_slice(),
663        contract_details,
664    )
665}
666
667fn generate_extension_trait(
668    ident: &Ident,
669    trait_ext_components: &[ExtTraitComponents],
670) -> TokenStream {
671    let trait_name = format_ident!("{ident}Ext");
672    let trait_doc = format!(
673        "Extension trait that provides helper methods for calling [`{ident}`]'s methods on \
674        [`Env`](::ab_contracts_macros::__private::Env) for convenience purposes"
675    );
676    let definitions = trait_ext_components
677        .iter()
678        .map(|components| &components.definition);
679    let impls = trait_ext_components
680        .iter()
681        .map(|components| &components.r#impl);
682
683    quote! {
684        use ffi::*;
685
686        #[doc = #trait_doc]
687        pub trait #trait_name {
688            #( #definitions )*
689        }
690
691        impl #trait_name for ::ab_contracts_macros::__private::Env<'_> {
692            #( #impls )*
693        }
694    }
695}