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