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 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 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 process_trait_impl(item_impl, &trait_name)
59 } else {
60 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 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 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 #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
126 pub mod ffi {
127 use super::*;
128
129 #( #guest_ffis )*
130 }
131 })
132}
133
134fn process_trait_impl(mut item_impl: ItemImpl, trait_name: &Ident) -> Result<TokenStream, Error> {
135 let struct_name = item_impl.self_ty.as_ref();
136
137 if !item_impl.generics.params.is_empty() {
138 return Err(Error::new(
139 item_impl.generics.span(),
140 "`#[contract]` does not support generics",
141 ));
142 }
143
144 let mut guest_ffis = Vec::with_capacity(item_impl.items.len());
145 let mut contract_details = ContractDetails::default();
146
147 for item in &mut item_impl.items {
148 match item {
149 ImplItem::Fn(impl_item_fn) => {
150 let method_output = process_fn(
151 struct_name.clone(),
152 Some(trait_name),
153 impl_item_fn,
154 &mut contract_details,
155 )?;
156 guest_ffis.push(method_output.guest_ffi);
157
158 if let Some(where_clause) = &mut impl_item_fn.sig.generics.where_clause {
159 where_clause.predicates.push(parse_quote! {
160 Self: ::core::marker::Sized
161 });
162 } else {
163 impl_item_fn
164 .sig
165 .generics
166 .where_clause
167 .replace(parse_quote! {
168 where
169 Self: ::core::marker::Sized
170 });
171 }
172 }
173 ImplItem::Const(impl_item_const) 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 }
182 }
183 }
184
185 let static_name = format_ident!("{METADATA_STATIC_NAME_PREFIX}{}", trait_name);
186 let ffi_mod_ident = format_ident!("{}_ffi", trait_name.to_string().to_snake_case());
187 let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_impl.span())?;
188 let method_fn_pointers_const = {
189 let methods = contract_details
190 .methods
191 .iter()
192 .map(|method| &method.original_ident);
193
194 quote! {
195 #[doc(hidden)]
196 const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
197 #( #ffi_mod_ident::#methods::fn_pointer::METHOD_FN_POINTER, )*
198 ];
199 }
200 };
201
202 Ok(quote! {
203 #[cfg(feature = "guest")]
211 #[used]
212 #[unsafe(no_mangle)]
213 #[cfg_attr(
214 target_env = "abundance",
215 unsafe(link_section = "ab-contract-metadata")
216 )]
217 static #static_name: [::core::primitive::u8; <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA.len()] = unsafe {
218 *<dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA.as_ptr().cast()
219 };
220
221 const _: () = {
223 use #ffi_mod_ident as ffi;
225 #metadata_const
226
227 let (impl_compact_metadata, impl_compact_metadata_size) =
231 ::ab_contracts_macros::__private::ContractMetadataKind::compact(METADATA)
232 .expect("Generated metadata is correct; qed");
233 let (def_compact_metadata, def_compact_metadata_size) =
234 ::ab_contracts_macros::__private::ContractMetadataKind::compact(
235 <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA,
236 )
237 .expect("Generated metadata is correct; qed");
238 assert!(
239 impl_compact_metadata_size == def_compact_metadata_size,
240 "Trait implementation must match trait definition exactly"
241 );
242 let mut i = 0;
243 while impl_compact_metadata_size > i {
244 assert!(
245 impl_compact_metadata[i] == def_compact_metadata[i],
246 "Trait implementation must match trait definition exactly"
247 );
248 i += 1;
249 }
250 };
251
252 #[cfg(feature = "guest")]
254 const _: () = <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::GUEST_FEATURE_ENABLED;
255
256 #item_impl
257
258 impl ::ab_contracts_macros::__private::ContractTrait<dyn #trait_name> for #struct_name {
261 #method_fn_pointers_const
262 }
263
264 #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
266 pub mod #ffi_mod_ident {
267 use super::*;
268
269 #( #guest_ffis )*
270 }
271 })
272}
273
274fn generate_trait_metadata(
275 contract_details: &ContractDetails,
276 trait_name: &Ident,
277 span: Span,
278) -> Result<TraitItemConst, Error> {
279 let num_methods = u8::try_from(contract_details.methods.len()).map_err(|_error| {
280 Error::new(
281 span,
282 format!("Trait can't have more than {} methods", u8::MAX),
283 )
284 })?;
285 let num_methods = Literal::u8_unsuffixed(num_methods);
286 let methods = contract_details
287 .methods
288 .iter()
289 .map(|method| &method.original_ident);
290 let trait_name_metadata = derive_ident_metadata(trait_name)?;
291
292 Ok(parse_quote! {
299 const METADATA: &[::core::primitive::u8] = {
303 const fn metadata()
304 -> ([::core::primitive::u8; ::ab_contracts_macros::__private::MAX_METADATA_CAPACITY], usize)
305 {
306 ::ab_contracts_macros::__private::concat_metadata_sources(&[
307 &[::ab_contracts_macros::__private::ContractMetadataKind::Trait as ::core::primitive::u8],
308 #trait_name_metadata,
309 &[#num_methods],
310 #( ffi::#methods::METADATA, )*
311 ])
312 }
313
314 metadata()
317 .0
318 .split_at(metadata().1)
319 .0
320 };
321 })
322}
323
324fn process_struct_impl(mut item_impl: ItemImpl) -> Result<TokenStream, Error> {
325 let struct_name = item_impl.self_ty.as_ref();
326
327 if !item_impl.generics.params.is_empty() {
328 return Err(Error::new(
329 item_impl.generics.span(),
330 "`#[contract]` does not support generics",
331 ));
332 }
333
334 item_impl.attrs.extend([
336 parse_quote! { #[expect(clippy::allow_attributes, reason = "Attribute below")] },
337 parse_quote! { #[allow(clippy::trivially_copy_pass_by_ref, reason = "API requirement")] },
338 ]);
339
340 let mut guest_ffis = Vec::with_capacity(item_impl.items.len());
341 let mut trait_ext_components = Vec::with_capacity(item_impl.items.len());
342 let mut contract_details = ContractDetails::default();
343
344 for item in &mut item_impl.items {
345 if let ImplItem::Fn(impl_item_fn) = item {
346 let method_output = process_fn(
347 struct_name.clone(),
348 None,
349 impl_item_fn,
350 &mut contract_details,
351 )?;
352 guest_ffis.push(method_output.guest_ffi);
353 trait_ext_components.push(method_output.trait_ext_components);
354 }
355 }
356
357 let maybe_slot_type = MethodDetails::slot_type(
358 contract_details
359 .methods
360 .iter()
361 .map(|method| &method.methods_details),
362 );
363 let Some(slot_type) = maybe_slot_type else {
364 return Err(Error::new(
365 item_impl.span(),
366 "All `#[slot]` arguments must be of the same type in all methods of a contract",
367 ));
368 };
369
370 let maybe_tmp_type = MethodDetails::tmp_type(
371 contract_details
372 .methods
373 .iter()
374 .map(|method| &method.methods_details),
375 );
376
377 let Some(tmp_type) = maybe_tmp_type else {
378 return Err(Error::new(
379 item_impl.span(),
380 "All `#[tmp]` arguments must be of the same type in all methods of a contract",
381 ));
382 };
383
384 let metadata_const = {
385 let num_methods = u8::try_from(contract_details.methods.len()).map_err(|_error| {
386 Error::new(
387 item_impl.span(),
388 format!("Struct can't have more than {} methods", u8::MAX),
389 )
390 })?;
391 let num_methods = Literal::u8_unsuffixed(num_methods);
392 let methods = contract_details
393 .methods
394 .iter()
395 .map(|method| &method.original_ident);
396
397 quote! {
403 const MAIN_CONTRACT_METADATA: &[::core::primitive::u8] = {
404 const fn metadata()
405 -> ([::core::primitive::u8; ::ab_contracts_macros::__private::MAX_METADATA_CAPACITY], usize)
406 {
407 ::ab_contracts_macros::__private::concat_metadata_sources(&[
408 &[::ab_contracts_macros::__private::ContractMetadataKind::Contract as ::core::primitive::u8],
409 <#struct_name as ::ab_contracts_macros::__private::IoType>::METADATA,
410 <#slot_type as ::ab_contracts_macros::__private::IoType>::METADATA,
411 <#tmp_type as ::ab_contracts_macros::__private::IoType>::METADATA,
412 &[#num_methods],
413 #( ffi::#methods::METADATA, )*
414 ])
415 }
416
417 metadata()
420 .0
421 .split_at(metadata().1)
422 .0
423 };
424 }
425 };
426 let method_fn_pointers_const = {
427 let methods = contract_details
428 .methods
429 .iter()
430 .map(|method| &method.original_ident);
431
432 quote! {
433 #[doc(hidden)]
434 const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
435 #( ffi::#methods::fn_pointer::METHOD_FN_POINTER, )*
436 ];
437 }
438 };
439
440 let struct_name_ident = extract_ident_from_type(struct_name).ok_or_else(|| {
441 Error::new(
442 struct_name.span(),
443 "`#[contract]` must be applied to simple struct implementation",
444 )
445 })?;
446
447 let ext_trait = generate_extension_trait(struct_name_ident, &trait_ext_components);
448
449 let struct_name_str = struct_name_ident.to_string();
450 let static_name = format_ident!("{METADATA_STATIC_NAME_PREFIX}{}", struct_name_str);
451 Ok(quote! {
452 #[cfg(feature = "guest")]
462 #[used]
463 #[unsafe(no_mangle)]
464 #[cfg_attr(
465 target_env = "abundance",
466 unsafe(link_section = "ab-contract-metadata")
467 )]
468 static #static_name: [
469 ::core::primitive::u8;
470 <#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
471 .len()
472 ] = unsafe {
473 *<#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
474 .as_ptr()
475 .cast()
476 };
477
478 impl ::ab_contracts_macros::__private::Contract for #struct_name {
479 #metadata_const
480 #method_fn_pointers_const
481 #[doc(hidden)]
482 const CODE: &::core::primitive::str = ::ab_contracts_macros::__private::concatcp!(
483 #struct_name_str,
484 '[',
485 ::core::env!("CARGO_PKG_NAME"),
486 '/',
487 ::core::file!(),
488 ':',
489 ::core::line!(),
490 ':',
491 ::core::column!(),
492 ']',
493 );
494 #[cfg(feature = "guest")]
496 #[doc(hidden)]
497 const GUEST_FEATURE_ENABLED: () = ();
498 type Slot = #slot_type;
499 type Tmp = #tmp_type;
500
501 fn code() -> impl ::core::ops::Deref<
502 Target = ::ab_contracts_macros::__private::VariableBytes<
503 { ::ab_contracts_macros::__private::MAX_CODE_SIZE },
504 >,
505 > {
506 const fn code_bytes() -> &'static [::core::primitive::u8] {
507 <#struct_name as ::ab_contracts_macros::__private::Contract>::CODE.as_bytes()
508 }
509
510 const fn code_size() -> ::core::primitive::u32 {
511 code_bytes().len() as ::core::primitive::u32
512 }
513
514 static CODE_SIZE: ::core::primitive::u32 = code_size();
515
516 ::ab_contracts_macros::__private::VariableBytes::from_buffer(
517 code_bytes(),
518 &CODE_SIZE
519 )
520 }
521 }
522
523 #item_impl
524
525 #ext_trait
526
527 #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
529 pub mod ffi {
530 use super::*;
531
532 #( #guest_ffis )*
533 }
534 })
535}
536
537fn process_fn_definition(
538 trait_name: &Ident,
539 trait_item_fn: &mut TraitItemFn,
540 contract_details: &mut ContractDetails,
541) -> Result<MethodOutput, Error> {
542 let supported_attrs = HashMap::<_, fn(_, _, _, _) -> _>::from_iter([
543 (format_ident!("update"), process_update_fn_definition as _),
544 (format_ident!("view"), process_view_fn_definition as _),
545 ]);
546 let mut attrs = trait_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
547 Meta::Path(path) => {
548 path.leading_colon.is_none()
549 && path.segments.len() == 1
550 && supported_attrs.contains_key(&path.segments[0].ident)
551 }
552 Meta::List(_meta_list) => false,
553 Meta::NameValue(_meta_name_value) => false,
554 });
555
556 let Some(attr) = attrs.next() else {
557 drop(attrs);
558
559 return Ok(MethodOutput::default());
561 };
562
563 if let Some(next_attr) = attrs.take(1).next() {
564 return Err(Error::new(
565 next_attr.span(),
566 format!(
567 "The method `{}` can only have one of `#[update]` or `#[view]` attributes specified",
568 trait_item_fn.sig.ident
569 ),
570 ));
571 }
572
573 if let Some(abi) = &trait_item_fn.sig.abi {
575 return Err(Error::new(
576 abi.span(),
577 format!(
578 "The method `{}` with `#[{}]` attribute must have default ABI",
579 trait_item_fn.sig.ident,
580 attr.meta.path().segments[0].ident
581 ),
582 ));
583 }
584
585 if trait_item_fn.default.is_some() {
586 return Err(Error::new(
587 trait_item_fn.span(),
588 "`#[contract]` does not support `#[update]` or `#[view]` methods with default implementation \
589 in trait definition",
590 ));
591 }
592
593 let processor = supported_attrs
594 .get(&attr.path().segments[0].ident)
595 .expect("Matched above to be one of the supported attributes; qed");
596 processor(
597 trait_name,
598 &mut trait_item_fn.sig,
599 trait_item_fn.attrs.as_slice(),
600 contract_details,
601 )
602}
603
604fn process_fn(
605 struct_name: Type,
606 trait_name: Option<&Ident>,
607 impl_item_fn: &mut ImplItemFn,
608 contract_details: &mut ContractDetails,
609) -> Result<MethodOutput, Error> {
610 let supported_attrs = HashMap::<_, fn(_, _, _, _, _) -> _>::from_iter([
611 (format_ident!("init"), process_init_fn as _),
612 (format_ident!("update"), process_update_fn as _),
613 (format_ident!("view"), process_view_fn as _),
614 ]);
615 let mut attrs = impl_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
616 Meta::Path(path) => {
617 path.leading_colon.is_none()
618 && path.segments.len() == 1
619 && supported_attrs.contains_key(&path.segments[0].ident)
620 }
621 Meta::List(_meta_list) => false,
622 Meta::NameValue(_meta_name_value) => false,
623 });
624
625 let Some(attr) = attrs.next() else {
626 drop(attrs);
627
628 return Ok(MethodOutput::default());
630 };
631
632 if let Some(next_attr) = attrs.take(1).next() {
633 return Err(Error::new(
634 next_attr.span(),
635 format!(
636 "The method `{}` can only have one of `#[init]`, `#[update]` or `#[view]` attributes specified",
637 impl_item_fn.sig.ident
638 ),
639 ));
640 }
641
642 if !(matches!(impl_item_fn.vis, Visibility::Public(_)) || trait_name.is_some()) {
644 return Err(Error::new(
645 impl_item_fn.sig.span(),
646 format!(
647 "The method `{}` with `#[{}]` attribute must be public",
648 impl_item_fn.sig.ident,
649 attr.meta.path().segments[0].ident
650 ),
651 ));
652 }
653
654 if let Some(abi) = &impl_item_fn.sig.abi {
656 return Err(Error::new(
657 abi.span(),
658 format!(
659 "The method `{}` with `#[{}]` attribute must have default ABI",
660 impl_item_fn.sig.ident,
661 attr.meta.path().segments[0].ident
662 ),
663 ));
664 }
665
666 let processor = supported_attrs
667 .get(&attr.path().segments[0].ident)
668 .expect("Matched above to be one of the supported attributes; qed");
669 processor(
670 struct_name,
671 trait_name,
672 &mut impl_item_fn.sig,
673 impl_item_fn.attrs.as_slice(),
674 contract_details,
675 )
676}
677
678fn generate_extension_trait(
679 ident: &Ident,
680 trait_ext_components: &[ExtTraitComponents],
681) -> TokenStream {
682 let trait_name = format_ident!("{ident}Ext");
683 let trait_doc = format!(
684 "Extension trait that provides helper methods for calling [`{ident}`]'s methods on \
685 [`Env`](::ab_contracts_macros::__private::Env) for convenience purposes"
686 );
687 let definitions = trait_ext_components
688 .iter()
689 .map(|components| &components.definition);
690 let impls = trait_ext_components
691 .iter()
692 .map(|components| &components.r#impl);
693
694 quote! {
695 #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
696 use ffi::*;
697
698 #[doc = #trait_doc]
699 #[automatically_derived]
700 #[expect(clippy::allow_attributes, reason = "Attribute below")]
701 #[allow(clippy::trivially_copy_pass_by_ref, reason = "API requirement")]
702 pub trait #trait_name {
703 #( #definitions )*
704 }
705
706 #[automatically_derived]
707 impl #trait_name for ::ab_contracts_macros::__private::Env<'_> {
708 #( #impls )*
709 }
710 }
711}