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