1mod common;
2mod init;
3mod method;
4mod update;
5mod view;
6
7use crate::contract::common::{
8 derive_ident_metadata, extract_ident_from_type, max_depth_assertion,
9};
10use crate::contract::init::process_init_fn;
11use crate::contract::method::{ExtTraitComponents, MethodDetails};
12use crate::contract::update::{process_update_fn, process_update_fn_definition};
13use crate::contract::view::{process_view_fn, process_view_fn_definition};
14use ab_contracts_common::METADATA_STATIC_NAME_PREFIX;
15use heck::ToSnakeCase;
16use proc_macro2::{Ident, Literal, Span, TokenStream};
17use quote::{format_ident, quote};
18use std::collections::HashMap;
19use syn::spanned::Spanned;
20use syn::{
21 Error, ImplItem, ImplItemFn, ItemImpl, ItemTrait, Meta, TraitItem, TraitItemConst, TraitItemFn,
22 Type, Visibility, parse_quote, parse2,
23};
24
25#[derive(Default)]
26struct MethodOutput {
27 guest_ffi: TokenStream,
28 trait_ext_components: ExtTraitComponents,
29}
30
31struct Method {
32 original_ident: Ident,
34 methods_details: MethodDetails,
35}
36
37#[derive(Default)]
38struct ContractDetails {
39 methods: Vec<Method>,
40}
41
42pub(super) fn contract(item: TokenStream) -> Result<TokenStream, Error> {
43 if let Ok(item_trait) = parse2::<ItemTrait>(item.clone()) {
44 return process_trait_definition(item_trait);
46 }
47
48 let error_message = "`#[contract]` must be applied to struct implementation, trait definition or trait \
49 implementation";
50
51 let item_impl =
52 parse2::<ItemImpl>(item).map_err(|error| Error::new(error.span(), error_message))?;
53
54 if let Some((path, _for)) = &item_impl.trait_ {
55 let trait_name = path
56 .get_ident()
57 .ok_or_else(|| Error::new(path.span(), error_message))?
58 .clone();
59 process_trait_impl(item_impl, &trait_name)
61 } else {
62 process_struct_impl(item_impl)
64 }
65}
66
67fn process_trait_definition(mut item_trait: ItemTrait) -> Result<TokenStream, Error> {
68 let trait_name = &item_trait.ident;
69
70 if !item_trait.generics.params.is_empty() {
71 return Err(Error::new(
72 item_trait.generics.span(),
73 "`#[contract]` does not support generics",
74 ));
75 }
76
77 let mut guest_ffis = Vec::with_capacity(item_trait.items.len());
78 let mut trait_ext_components = Vec::with_capacity(item_trait.items.len());
79 let mut contract_details = ContractDetails::default();
80
81 for item in &mut item_trait.items {
82 if let TraitItem::Fn(trait_item_fn) = item {
83 let method_output =
84 process_fn_definition(trait_name, trait_item_fn, &mut contract_details)?;
85 guest_ffis.push(method_output.guest_ffi);
86 trait_ext_components.push(method_output.trait_ext_components);
87
88 if let Some(where_clause) = &mut trait_item_fn.sig.generics.where_clause {
91 where_clause.predicates.push(parse_quote! {
92 Self: ::core::marker::Sized
93 });
94 } else {
95 trait_item_fn
96 .sig
97 .generics
98 .where_clause
99 .replace(parse_quote! {
100 where
101 Self: ::core::marker::Sized
102 });
103 }
104 }
105 }
106
107 let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_trait.span())?;
108 let ext_trait = generate_extension_trait(trait_name, &trait_ext_components);
109
110 Ok(quote! {
111 #item_trait
112
113 impl ::ab_contracts_macros::__private::ContractTraitDefinition for dyn #trait_name {
118 #[cfg(feature = "guest")]
119 #[doc(hidden)]
120 const GUEST_FEATURE_ENABLED: () = ();
121 #metadata_const
122 }
123
124 #ext_trait
125
126 #[expect(clippy::inline_modules, reason = "Macro-generated")]
128 #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
129 pub mod ffi {
130 use super::*;
131
132 #( #guest_ffis )*
133 }
134 })
135}
136
137fn process_trait_impl(mut item_impl: ItemImpl, trait_name: &Ident) -> Result<TokenStream, Error> {
138 let struct_name = item_impl.self_ty.as_ref();
139
140 if !item_impl.generics.params.is_empty() {
141 return Err(Error::new(
142 item_impl.generics.span(),
143 "`#[contract]` does not support generics",
144 ));
145 }
146
147 let mut guest_ffis = Vec::with_capacity(item_impl.items.len());
148 let mut contract_details = ContractDetails::default();
149
150 for item in &mut item_impl.items {
151 match item {
152 ImplItem::Fn(impl_item_fn) => {
153 let method_output = process_fn(
154 struct_name.clone(),
155 Some(trait_name),
156 impl_item_fn,
157 &mut contract_details,
158 )?;
159 guest_ffis.push(method_output.guest_ffi);
160
161 if let Some(where_clause) = &mut impl_item_fn.sig.generics.where_clause {
162 where_clause.predicates.push(parse_quote! {
163 Self: ::core::marker::Sized
164 });
165 } else {
166 impl_item_fn
167 .sig
168 .generics
169 .where_clause
170 .replace(parse_quote! {
171 where
172 Self: ::core::marker::Sized
173 });
174 }
175 }
176 ImplItem::Const(impl_item_const) if impl_item_const.ident == "METADATA" => {
177 return Err(Error::new(
178 impl_item_const.span(),
179 "`#[contract]` doesn't allow overriding `METADATA` constant",
180 ));
181 }
182 _ => {
183 }
185 }
186 }
187
188 let static_name = format_ident!("{METADATA_STATIC_NAME_PREFIX}{}", trait_name);
189 let ffi_mod_ident = format_ident!("{}_ffi", trait_name.to_string().to_snake_case());
190 let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_impl.span())?;
191 let method_fn_pointers_const = {
192 let methods = contract_details
193 .methods
194 .iter()
195 .map(|method| &method.original_ident);
196
197 quote! {
198 #[doc(hidden)]
199 const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
200 #( #ffi_mod_ident::#methods::fn_pointer::METHOD_FN_POINTER, )*
201 ];
202 }
203 };
204
205 Ok(quote! {
206 #[cfg(feature = "guest")]
208 #[allow(
209 unexpected_cfgs,
210 reason = "`abundance` is only a known `target_env` value when compiling for contracts"
211 )]
212 const _: () = {
213 #[used]
221 #[unsafe(no_mangle)]
222 #[cfg_attr(
223 target_env = "abundance",
224 unsafe(link_section = "ab-contract-metadata")
225 )]
226 static #static_name: [::core::primitive::u8; <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA.len()] = unsafe {
227 *<dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA.as_ptr().cast()
228 };
229 };
230
231 const _: () = {
233 use #ffi_mod_ident as ffi;
235 #metadata_const
236
237 let (impl_compact_metadata, impl_compact_metadata_size) =
241 ::ab_contracts_macros::__private::ContractMetadataKind::compact(METADATA)
242 .expect(
243 "Generated metadata is correct and its types are not nested deeper than \
244 `MAX_METADATA_DEPTH`; qed"
245 );
246 let (def_compact_metadata, def_compact_metadata_size) =
247 ::ab_contracts_macros::__private::ContractMetadataKind::compact(
248 <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA,
249 )
250 .expect(
251 "Generated metadata is correct and its types are not nested deeper than \
252 `MAX_METADATA_DEPTH`; qed"
253 );
254 assert!(
255 impl_compact_metadata_size == def_compact_metadata_size,
256 "Trait implementation must match trait definition exactly"
257 );
258 let mut i = 0;
259 while impl_compact_metadata_size > i {
260 assert!(
261 impl_compact_metadata[i] == def_compact_metadata[i],
262 "Trait implementation must match trait definition exactly"
263 );
264 i += 1;
265 }
266 };
267
268 #[cfg(feature = "guest")]
270 const _: () = <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::GUEST_FEATURE_ENABLED;
271
272 #item_impl
273
274 unsafe impl ::ab_contracts_macros::__private::ContractTrait<dyn #trait_name> for #struct_name {
277 #method_fn_pointers_const
278 }
279
280 #[expect(clippy::inline_modules, reason = "Macro-generated")]
282 #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
283 pub mod #ffi_mod_ident {
284 use super::*;
285
286 #( #guest_ffis )*
287 }
288 })
289}
290
291fn generate_trait_metadata(
292 contract_details: &ContractDetails,
293 trait_name: &Ident,
294 span: Span,
295) -> Result<TraitItemConst, Error> {
296 let num_methods = u8::try_from(contract_details.methods.len()).map_err(|_error| {
297 Error::new(
298 span,
299 format!("Trait can't have more than {} methods", u8::MAX),
300 )
301 })?;
302 let num_methods = Literal::u8_unsuffixed(num_methods);
303 let methods = contract_details
304 .methods
305 .iter()
306 .map(|method| &method.original_ident);
307 let trait_name_metadata = derive_ident_metadata(trait_name)?;
308
309 Ok(parse_quote! {
316 const METADATA: &[::core::primitive::u8] = {
320 const fn metadata()
321 -> ([::core::primitive::u8; ::ab_contracts_macros::__private::MAX_METADATA_CAPACITY], usize)
322 {
323 ::ab_contracts_macros::__private::concat_metadata_sources(&[
324 &[::ab_contracts_macros::__private::ContractMetadataKind::Trait as ::core::primitive::u8],
325 #trait_name_metadata,
326 &[#num_methods],
327 #( ffi::#methods::METADATA, )*
328 ])
329 }
330
331 let (metadata_scratch, metadata_size) = &metadata();
333 <[_]>::split_at(metadata_scratch, *metadata_size).0
334 };
335 })
336}
337
338fn process_struct_impl(mut item_impl: ItemImpl) -> Result<TokenStream, Error> {
339 let struct_name = item_impl.self_ty.as_ref();
340
341 if !item_impl.generics.params.is_empty() {
342 return Err(Error::new(
343 item_impl.generics.span(),
344 "`#[contract]` does not support generics",
345 ));
346 }
347
348 item_impl.attrs.extend([
350 parse_quote! { #[expect(clippy::allow_attributes, reason = "Attribute below")] },
351 parse_quote! { #[allow(clippy::trivially_copy_pass_by_ref, reason = "API requirement")] },
352 ]);
353
354 let mut guest_ffis = Vec::with_capacity(item_impl.items.len());
355 let mut trait_ext_components = Vec::with_capacity(item_impl.items.len());
356 let mut contract_details = ContractDetails::default();
357
358 for item in &mut item_impl.items {
359 if let ImplItem::Fn(impl_item_fn) = item {
360 let method_output = process_fn(
361 struct_name.clone(),
362 None,
363 impl_item_fn,
364 &mut contract_details,
365 )?;
366 guest_ffis.push(method_output.guest_ffi);
367 trait_ext_components.push(method_output.trait_ext_components);
368 }
369 }
370
371 let maybe_slot_type = MethodDetails::slot_type(
372 contract_details
373 .methods
374 .iter()
375 .map(|method| &method.methods_details),
376 );
377 let Some(slot_type) = maybe_slot_type else {
378 return Err(Error::new(
379 item_impl.span(),
380 "All `#[slot]` arguments must be of the same type in all methods of a contract",
381 ));
382 };
383
384 let maybe_tmp_type = MethodDetails::tmp_type(
385 contract_details
386 .methods
387 .iter()
388 .map(|method| &method.methods_details),
389 );
390
391 let Some(tmp_type) = maybe_tmp_type else {
392 return Err(Error::new(
393 item_impl.span(),
394 "All `#[tmp]` arguments must be of the same type in all methods of a contract",
395 ));
396 };
397
398 let max_depth_assertions = [
399 max_depth_assertion(
400 struct_name,
401 "State type must not be nested deeper than `MAX_METADATA_DEPTH`",
402 ),
403 max_depth_assertion(
404 &slot_type,
405 "`#[slot]` type must not be nested deeper than `MAX_METADATA_DEPTH`",
406 ),
407 max_depth_assertion(
408 &tmp_type,
409 "`#[tmp]` type must not be nested deeper than `MAX_METADATA_DEPTH`",
410 ),
411 ];
412
413 let metadata_const = {
414 let num_methods = u8::try_from(contract_details.methods.len()).map_err(|_error| {
415 Error::new(
416 item_impl.span(),
417 format!("Struct can't have more than {} methods", u8::MAX),
418 )
419 })?;
420 let num_methods = Literal::u8_unsuffixed(num_methods);
421 let methods = contract_details
422 .methods
423 .iter()
424 .map(|method| &method.original_ident);
425
426 quote! {
432 const MAIN_CONTRACT_METADATA: &[::core::primitive::u8] = {
433 const fn metadata()
434 -> ([::core::primitive::u8; ::ab_contracts_macros::__private::MAX_METADATA_CAPACITY], usize)
435 {
436 ::ab_contracts_macros::__private::concat_metadata_sources(&[
437 &[::ab_contracts_macros::__private::ContractMetadataKind::Contract as ::core::primitive::u8],
438 <#struct_name as ::ab_contracts_macros::__private::IoType>::METADATA,
439 <#slot_type as ::ab_contracts_macros::__private::IoType>::METADATA,
440 <#tmp_type as ::ab_contracts_macros::__private::IoType>::METADATA,
441 &[#num_methods],
442 #( ffi::#methods::METADATA, )*
443 ])
444 }
445
446 let (metadata_scratch, metadata_size) = &metadata();
448 <[_]>::split_at(metadata_scratch, *metadata_size).0
449 };
450 }
451 };
452 let method_fn_pointers_const = {
453 let methods = contract_details
454 .methods
455 .iter()
456 .map(|method| &method.original_ident);
457
458 quote! {
459 #[doc(hidden)]
460 const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
461 #( ffi::#methods::fn_pointer::METHOD_FN_POINTER, )*
462 ];
463 }
464 };
465
466 let struct_name_ident = extract_ident_from_type(struct_name).ok_or_else(|| {
467 Error::new(
468 struct_name.span(),
469 "`#[contract]` must be applied to simple struct implementation",
470 )
471 })?;
472
473 let ext_trait = generate_extension_trait(struct_name_ident, &trait_ext_components);
474
475 let struct_name_str = struct_name_ident.to_string();
476 let static_name = format_ident!("{METADATA_STATIC_NAME_PREFIX}{}", struct_name_str);
477 Ok(quote! {
478 #[cfg(feature = "guest")]
480 #[allow(
481 unexpected_cfgs,
482 reason = "`abundance` is only a known `target_env` value when compiling for contracts"
483 )]
484 const _: () = {
485 #[used]
495 #[unsafe(no_mangle)]
496 #[cfg_attr(
497 target_env = "abundance",
498 unsafe(link_section = "ab-contract-metadata")
499 )]
500 static #static_name: [
501 ::core::primitive::u8;
502 <#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
503 .len()
504 ] = unsafe {
505 *<#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
506 .as_ptr()
507 .cast()
508 };
509 };
510
511 const _: () = {
514 #( #max_depth_assertions )*
515 };
516
517 unsafe impl ::ab_contracts_macros::__private::Contract for #struct_name {
518 #metadata_const
519 #method_fn_pointers_const
520 #[doc(hidden)]
521 const CODE: &::core::primitive::str = ::ab_contracts_macros::__private::concatcp!(
522 #struct_name_str,
523 '[',
524 ::core::env!("CARGO_PKG_NAME"),
525 '/',
526 ::core::file!(),
527 ':',
528 ::core::line!(),
529 ':',
530 ::core::column!(),
531 ']',
532 );
533 #[cfg(feature = "guest")]
535 #[doc(hidden)]
536 const GUEST_FEATURE_ENABLED: () = ();
537 type Slot = #slot_type;
538 type Tmp = #tmp_type;
539
540 fn code() -> impl ::core::ops::Deref<
541 Target = ::ab_contracts_macros::__private::VariableBytes<
542 'static,
543 { ::ab_contracts_macros::__private::MAX_CODE_SIZE },
544 >,
545 > {
546 const fn code_bytes() -> &'static [::core::primitive::u8] {
547 <#struct_name as ::ab_contracts_macros::__private::Contract>::CODE.as_bytes()
548 }
549
550 const fn code_size() -> ::core::primitive::u32 {
551 code_bytes().len() as ::core::primitive::u32
552 }
553
554 static CODE_SIZE: ::core::primitive::u32 = code_size();
555
556 ::ab_contracts_macros::__private::VariableBytes::from_buffer(
557 code_bytes(),
558 &CODE_SIZE
559 )
560 .expect("Size is the length of the code, which is a short string; qed")
561 }
562 }
563
564 #item_impl
565
566 #ext_trait
567
568 #[expect(clippy::inline_modules, reason = "Macro-generated")]
570 #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
571 pub mod ffi {
572 use super::*;
573
574 #( #guest_ffis )*
575 }
576 })
577}
578
579fn process_fn_definition(
580 trait_name: &Ident,
581 trait_item_fn: &mut TraitItemFn,
582 contract_details: &mut ContractDetails,
583) -> Result<MethodOutput, Error> {
584 let supported_attrs = HashMap::<_, fn(_, _, _, _) -> _>::from_iter([
585 (format_ident!("update"), process_update_fn_definition as _),
586 (format_ident!("view"), process_view_fn_definition as _),
587 ]);
588 let mut attrs = trait_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
589 Meta::Path(path) => {
590 path.leading_colon.is_none()
591 && path.segments.len() == 1
592 && supported_attrs.contains_key(&path.segments[0].ident)
593 }
594 Meta::List(_meta_list) => false,
595 Meta::NameValue(_meta_name_value) => false,
596 });
597
598 let Some(attr) = attrs.next() else {
599 drop(attrs);
600
601 return Ok(MethodOutput::default());
603 };
604
605 if let Some(next_attr) = attrs.take(1).next() {
606 return Err(Error::new(
607 next_attr.span(),
608 format!(
609 "The method `{}` can only have one of `#[update]` or `#[view]` attributes specified",
610 trait_item_fn.sig.ident
611 ),
612 ));
613 }
614
615 if let Some(abi) = &trait_item_fn.sig.abi {
617 return Err(Error::new(
618 abi.span(),
619 format!(
620 "The method `{}` with `#[{}]` attribute must have default ABI",
621 trait_item_fn.sig.ident,
622 attr.meta.path().segments[0].ident
623 ),
624 ));
625 }
626
627 if trait_item_fn.default.is_some() {
628 return Err(Error::new(
629 trait_item_fn.span(),
630 "`#[contract]` does not support `#[update]` or `#[view]` methods with default implementation \
631 in trait definition",
632 ));
633 }
634
635 let processor = supported_attrs
636 .get(&attr.path().segments[0].ident)
637 .expect("Matched above to be one of the supported attributes; qed");
638 processor(
639 trait_name,
640 &mut trait_item_fn.sig,
641 trait_item_fn.attrs.as_slice(),
642 contract_details,
643 )
644}
645
646fn process_fn(
647 struct_name: Type,
648 trait_name: Option<&Ident>,
649 impl_item_fn: &mut ImplItemFn,
650 contract_details: &mut ContractDetails,
651) -> Result<MethodOutput, Error> {
652 let supported_attrs = HashMap::<_, fn(_, _, _, _, _) -> _>::from_iter([
653 (format_ident!("init"), process_init_fn as _),
654 (format_ident!("update"), process_update_fn as _),
655 (format_ident!("view"), process_view_fn as _),
656 ]);
657 let mut attrs = impl_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
658 Meta::Path(path) => {
659 path.leading_colon.is_none()
660 && path.segments.len() == 1
661 && supported_attrs.contains_key(&path.segments[0].ident)
662 }
663 Meta::List(_meta_list) => false,
664 Meta::NameValue(_meta_name_value) => false,
665 });
666
667 let Some(attr) = attrs.next() else {
668 drop(attrs);
669
670 return Ok(MethodOutput::default());
672 };
673
674 if let Some(next_attr) = attrs.take(1).next() {
675 return Err(Error::new(
676 next_attr.span(),
677 format!(
678 "The method `{}` can only have one of `#[init]`, `#[update]` or `#[view]` attributes specified",
679 impl_item_fn.sig.ident
680 ),
681 ));
682 }
683
684 if !(matches!(impl_item_fn.vis, Visibility::Public(_)) || trait_name.is_some()) {
686 return Err(Error::new(
687 impl_item_fn.sig.span(),
688 format!(
689 "The method `{}` with `#[{}]` attribute must be public",
690 impl_item_fn.sig.ident,
691 attr.meta.path().segments[0].ident
692 ),
693 ));
694 }
695
696 if let Some(abi) = &impl_item_fn.sig.abi {
698 return Err(Error::new(
699 abi.span(),
700 format!(
701 "The method `{}` with `#[{}]` attribute must have default ABI",
702 impl_item_fn.sig.ident,
703 attr.meta.path().segments[0].ident
704 ),
705 ));
706 }
707
708 let processor = supported_attrs
709 .get(&attr.path().segments[0].ident)
710 .expect("Matched above to be one of the supported attributes; qed");
711 processor(
712 struct_name,
713 trait_name,
714 &mut impl_item_fn.sig,
715 impl_item_fn.attrs.as_slice(),
716 contract_details,
717 )
718}
719
720fn generate_extension_trait(
721 ident: &Ident,
722 trait_ext_components: &[ExtTraitComponents],
723) -> TokenStream {
724 let trait_name = format_ident!("{ident}Ext");
725 let trait_doc = format!(
726 "Extension trait that provides helper methods for calling [`{ident}`]'s methods on \
727 [`Env`](::ab_contracts_macros::__private::Env) for convenience purposes"
728 );
729 let definitions = trait_ext_components
730 .iter()
731 .map(|components| &components.definition);
732 let impls = trait_ext_components
733 .iter()
734 .map(|components| &components.r#impl);
735
736 quote! {
737 #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
738 use ffi::*;
739
740 #[doc = #trait_doc]
741 #[automatically_derived]
742 #[expect(clippy::allow_attributes, reason = "Attribute below")]
743 #[allow(clippy::trivially_copy_pass_by_ref, reason = "API requirement")]
744 #[allow(rustdoc::redundant_explicit_links, reason = "Macro-generated")]
745 pub trait #trait_name {
746 #( #definitions )*
747 }
748
749 #[automatically_derived]
750 impl #trait_name for ::ab_contracts_macros::__private::Env<'_> {
751 #( #impls )*
752 }
753 }
754}