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