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 heck::ToSnakeCase;
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!("{}_ffi", trait_name.to_string().to_snake_case());
186    let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_impl.span())?;
187    let method_fn_pointers_const = {
188        let methods = contract_details
189            .methods
190            .iter()
191            .map(|method| &method.original_ident);
192
193        quote! {
194            #[doc(hidden)]
195            const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
196                #( #ffi_mod_ident::#methods::fn_pointer::METHOD_FN_POINTER, )*
197            ];
198        }
199    };
200
201    Ok(quote! {
202        /// Contribute trait metadata to contract's metadata
203        ///
204        /// Enabled with `guest` feature to appear in the final binary.
205        ///
206        /// See [`Contract::MAIN_CONTRACT_METADATA`] for details.
207        ///
208        /// [`Contract::MAIN_CONTRACT_METADATA`]: ::ab_contracts_macros::__private::Contract::MAIN_CONTRACT_METADATA
209        #[cfg(feature = "guest")]
210        #[used]
211        #[unsafe(no_mangle)]
212        #[cfg_attr(
213            target_env = "abundance",
214            unsafe(link_section = "ab-contract-metadata")
215        )]
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        #[cfg_attr(
458            target_env = "abundance",
459            unsafe(link_section = "ab-contract-metadata")
460        )]
461        static #static_name: [
462            ::core::primitive::u8;
463            <#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
464                .len()
465        ] = unsafe {
466            *<#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
467                .as_ptr()
468                .cast()
469        };
470
471        impl ::ab_contracts_macros::__private::Contract for #struct_name {
472            #metadata_const
473            #method_fn_pointers_const
474            #[doc(hidden)]
475            const CODE: &::core::primitive::str = ::ab_contracts_macros::__private::concatcp!(
476                #struct_name_str,
477                '[',
478                ::core::env!("CARGO_PKG_NAME"),
479                '/',
480                ::core::file!(),
481                ':',
482                ::core::line!(),
483                ':',
484                ::core::column!(),
485                ']',
486            );
487            // Ensure `guest` feature is enabled for `ab-contracts-common` crate
488            #[cfg(feature = "guest")]
489            #[doc(hidden)]
490            const GUEST_FEATURE_ENABLED: () = ();
491            type Slot = #slot_type;
492            type Tmp = #tmp_type;
493
494            fn code() -> impl ::core::ops::Deref<
495                Target = ::ab_contracts_macros::__private::VariableBytes<
496                    { ::ab_contracts_macros::__private::MAX_CODE_SIZE },
497                >,
498            > {
499                const fn code_bytes() -> &'static [::core::primitive::u8] {
500                    <#struct_name as ::ab_contracts_macros::__private::Contract>::CODE.as_bytes()
501                }
502
503                const fn code_size() -> ::core::primitive::u32 {
504                    code_bytes().len() as ::core::primitive::u32
505                }
506
507                static CODE_SIZE: ::core::primitive::u32 = code_size();
508
509                ::ab_contracts_macros::__private::VariableBytes::from_buffer(
510                    code_bytes(),
511                    &CODE_SIZE
512                )
513            }
514        }
515
516        #item_impl
517
518        #ext_trait
519
520        /// FFI code generated by procedural macro
521        pub mod ffi {
522            use super::*;
523
524            #( #guest_ffis )*
525        }
526    })
527}
528
529fn process_fn_definition(
530    trait_name: &Ident,
531    trait_item_fn: &mut TraitItemFn,
532    contract_details: &mut ContractDetails,
533) -> Result<MethodOutput, Error> {
534    let supported_attrs = HashMap::<_, fn(_, _, _, _) -> _>::from_iter([
535        (format_ident!("update"), process_update_fn_definition as _),
536        (format_ident!("view"), process_view_fn_definition as _),
537    ]);
538    let mut attrs = trait_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
539        Meta::Path(path) => {
540            path.leading_colon.is_none()
541                && path.segments.len() == 1
542                && supported_attrs.contains_key(&path.segments[0].ident)
543        }
544        Meta::List(_meta_list) => false,
545        Meta::NameValue(_meta_name_value) => false,
546    });
547
548    let Some(attr) = attrs.next() else {
549        drop(attrs);
550
551        // Return an unmodified original if no recognized arguments are present
552        return Ok(MethodOutput::default());
553    };
554
555    if let Some(next_attr) = attrs.take(1).next() {
556        return Err(Error::new(
557            next_attr.span(),
558            format!(
559                "The method `{}` can only have one of `#[update]` or `#[view]` attributes specified",
560                trait_item_fn.sig.ident
561            ),
562        ));
563    }
564
565    // Make sure the method doesn't have customized ABI
566    if let Some(abi) = &trait_item_fn.sig.abi {
567        return Err(Error::new(
568            abi.span(),
569            format!(
570                "The method `{}` with `#[{}]` attribute must have default ABI",
571                trait_item_fn.sig.ident,
572                attr.meta.path().segments[0].ident
573            ),
574        ));
575    }
576
577    if trait_item_fn.default.is_some() {
578        return Err(Error::new(
579            trait_item_fn.span(),
580            "`#[contract]` does not support `#[update]` or `#[view]` methods with default implementation \
581            in trait definition",
582        ));
583    }
584
585    let processor = supported_attrs
586        .get(&attr.path().segments[0].ident)
587        .expect("Matched above to be one of the supported attributes; qed");
588    processor(
589        trait_name,
590        &mut trait_item_fn.sig,
591        trait_item_fn.attrs.as_slice(),
592        contract_details,
593    )
594}
595
596fn process_fn(
597    struct_name: Type,
598    trait_name: Option<&Ident>,
599    impl_item_fn: &mut ImplItemFn,
600    contract_details: &mut ContractDetails,
601) -> Result<MethodOutput, Error> {
602    let supported_attrs = HashMap::<_, fn(_, _, _, _, _) -> _>::from_iter([
603        (format_ident!("init"), process_init_fn as _),
604        (format_ident!("update"), process_update_fn as _),
605        (format_ident!("view"), process_view_fn as _),
606    ]);
607    let mut attrs = impl_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
608        Meta::Path(path) => {
609            path.leading_colon.is_none()
610                && path.segments.len() == 1
611                && supported_attrs.contains_key(&path.segments[0].ident)
612        }
613        Meta::List(_meta_list) => false,
614        Meta::NameValue(_meta_name_value) => false,
615    });
616
617    let Some(attr) = attrs.next() else {
618        drop(attrs);
619
620        // Return an unmodified original if no recognized arguments are present
621        return Ok(MethodOutput::default());
622    };
623
624    if let Some(next_attr) = attrs.take(1).next() {
625        return Err(Error::new(
626            next_attr.span(),
627            format!(
628                "The method `{}` can only have one of `#[init]`, `#[update]` or `#[view]` attributes specified",
629                impl_item_fn.sig.ident
630            ),
631        ));
632    }
633
634    // Make sure the method is public if not a trait impl
635    if !(matches!(impl_item_fn.vis, Visibility::Public(_)) || trait_name.is_some()) {
636        return Err(Error::new(
637            impl_item_fn.sig.span(),
638            format!(
639                "The method `{}` with `#[{}]` attribute must be public",
640                impl_item_fn.sig.ident,
641                attr.meta.path().segments[0].ident
642            ),
643        ));
644    }
645
646    // Make sure the method doesn't have customized ABI
647    if let Some(abi) = &impl_item_fn.sig.abi {
648        return Err(Error::new(
649            abi.span(),
650            format!(
651                "The method `{}` with `#[{}]` attribute must have default ABI",
652                impl_item_fn.sig.ident,
653                attr.meta.path().segments[0].ident
654            ),
655        ));
656    }
657
658    let processor = supported_attrs
659        .get(&attr.path().segments[0].ident)
660        .expect("Matched above to be one of the supported attributes; qed");
661    processor(
662        struct_name,
663        trait_name,
664        &mut impl_item_fn.sig,
665        impl_item_fn.attrs.as_slice(),
666        contract_details,
667    )
668}
669
670fn generate_extension_trait(
671    ident: &Ident,
672    trait_ext_components: &[ExtTraitComponents],
673) -> TokenStream {
674    let trait_name = format_ident!("{ident}Ext");
675    let trait_doc = format!(
676        "Extension trait that provides helper methods for calling [`{ident}`]'s methods on \
677        [`Env`](::ab_contracts_macros::__private::Env) for convenience purposes"
678    );
679    let definitions = trait_ext_components
680        .iter()
681        .map(|components| &components.definition);
682    let impls = trait_ext_components
683        .iter()
684        .map(|components| &components.r#impl);
685
686    quote! {
687        use ffi::*;
688
689        #[doc = #trait_doc]
690        pub trait #trait_name {
691            #( #definitions )*
692        }
693
694        impl #trait_name for ::ab_contracts_macros::__private::Env<'_> {
695            #( #impls )*
696        }
697    }
698}