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