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 ident_case::RenameRule;
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!(
186 "{}_ffi",
187 RenameRule::SnakeCase.apply_to_variant(trait_name.to_string())
188 );
189 let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_impl.span())?;
190 let method_fn_pointers_const = {
191 let methods = contract_details
192 .methods
193 .iter()
194 .map(|method| &method.original_ident);
195
196 quote! {
197 #[doc(hidden)]
198 const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
199 #( #ffi_mod_ident::#methods::fn_pointer::METHOD_FN_POINTER, )*
200 ];
201 }
202 };
203
204 Ok(quote! {
205 #[cfg(feature = "guest")]
213 #[used]
214 #[unsafe(no_mangle)]
215 #[unsafe(link_section = "ab-contract-metadata")]
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 #[unsafe(link_section = "ab-contract-metadata")]
458 static #static_name: [
459 ::core::primitive::u8;
460 <#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
461 .len()
462 ] = unsafe {
463 *<#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
464 .as_ptr()
465 .cast()
466 };
467
468 impl ::ab_contracts_macros::__private::Contract for #struct_name {
469 #metadata_const
470 #method_fn_pointers_const
471 #[doc(hidden)]
472 const CODE: &::core::primitive::str = ::ab_contracts_macros::__private::concatcp!(
473 #struct_name_str,
474 '[',
475 ::core::env!("CARGO_PKG_NAME"),
476 '/',
477 ::core::file!(),
478 ':',
479 ::core::line!(),
480 ':',
481 ::core::column!(),
482 ']',
483 );
484 #[cfg(feature = "guest")]
486 #[doc(hidden)]
487 const GUEST_FEATURE_ENABLED: () = ();
488 type Slot = #slot_type;
489 type Tmp = #tmp_type;
490
491 fn code() -> impl ::core::ops::Deref<
492 Target = ::ab_contracts_macros::__private::VariableBytes<
493 { ::ab_contracts_macros::__private::MAX_CODE_SIZE },
494 >,
495 > {
496 const fn code_bytes() -> &'static [::core::primitive::u8] {
497 <#struct_name as ::ab_contracts_macros::__private::Contract>::CODE.as_bytes()
498 }
499
500 const fn code_size() -> ::core::primitive::u32 {
501 code_bytes().len() as ::core::primitive::u32
502 }
503
504 static CODE_SIZE: ::core::primitive::u32 = code_size();
505
506 ::ab_contracts_macros::__private::VariableBytes::from_buffer(
507 code_bytes(),
508 &CODE_SIZE
509 )
510 }
511 }
512
513 #item_impl
514
515 #ext_trait
516
517 pub mod ffi {
519 use super::*;
520
521 #( #guest_ffis )*
522 }
523 })
524}
525
526fn process_fn_definition(
527 trait_name: &Ident,
528 trait_item_fn: &mut TraitItemFn,
529 contract_details: &mut ContractDetails,
530) -> Result<MethodOutput, Error> {
531 let supported_attrs = HashMap::<_, fn(_, _, _, _) -> _>::from_iter([
532 (format_ident!("update"), process_update_fn_definition as _),
533 (format_ident!("view"), process_view_fn_definition as _),
534 ]);
535 let mut attrs = trait_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
536 Meta::Path(path) => {
537 path.leading_colon.is_none()
538 && path.segments.len() == 1
539 && supported_attrs.contains_key(&path.segments[0].ident)
540 }
541 Meta::List(_meta_list) => false,
542 Meta::NameValue(_meta_name_value) => false,
543 });
544
545 let Some(attr) = attrs.next() else {
546 drop(attrs);
547
548 return Ok(MethodOutput::default());
550 };
551
552 if let Some(next_attr) = attrs.take(1).next() {
553 return Err(Error::new(
554 next_attr.span(),
555 format!(
556 "The method `{}` can only have one of `#[update]` or `#[view]` attributes specified",
557 trait_item_fn.sig.ident
558 ),
559 ));
560 }
561
562 if let Some(abi) = &trait_item_fn.sig.abi {
564 return Err(Error::new(
565 abi.span(),
566 format!(
567 "The method `{}` with `#[{}]` attribute must have default ABI",
568 trait_item_fn.sig.ident,
569 attr.meta.path().segments[0].ident
570 ),
571 ));
572 }
573
574 if trait_item_fn.default.is_some() {
575 return Err(Error::new(
576 trait_item_fn.span(),
577 "`#[contract]` does not support `#[update]` or `#[view]` methods with default implementation \
578 in trait definition",
579 ));
580 }
581
582 let processor = supported_attrs
583 .get(&attr.path().segments[0].ident)
584 .expect("Matched above to be one of the supported attributes; qed");
585 processor(
586 trait_name,
587 &mut trait_item_fn.sig,
588 trait_item_fn.attrs.as_slice(),
589 contract_details,
590 )
591}
592
593fn process_fn(
594 struct_name: Type,
595 trait_name: Option<&Ident>,
596 impl_item_fn: &mut ImplItemFn,
597 contract_details: &mut ContractDetails,
598) -> Result<MethodOutput, Error> {
599 let supported_attrs = HashMap::<_, fn(_, _, _, _, _) -> _>::from_iter([
600 (format_ident!("init"), process_init_fn as _),
601 (format_ident!("update"), process_update_fn as _),
602 (format_ident!("view"), process_view_fn as _),
603 ]);
604 let mut attrs = impl_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
605 Meta::Path(path) => {
606 path.leading_colon.is_none()
607 && path.segments.len() == 1
608 && supported_attrs.contains_key(&path.segments[0].ident)
609 }
610 Meta::List(_meta_list) => false,
611 Meta::NameValue(_meta_name_value) => false,
612 });
613
614 let Some(attr) = attrs.next() else {
615 drop(attrs);
616
617 return Ok(MethodOutput::default());
619 };
620
621 if let Some(next_attr) = attrs.take(1).next() {
622 return Err(Error::new(
623 next_attr.span(),
624 format!(
625 "The method `{}` can only have one of `#[init]`, `#[update]` or `#[view]` attributes specified",
626 impl_item_fn.sig.ident
627 ),
628 ));
629 }
630
631 if !(matches!(impl_item_fn.vis, Visibility::Public(_)) || trait_name.is_some()) {
633 return Err(Error::new(
634 impl_item_fn.sig.span(),
635 format!(
636 "The method `{}` with `#[{}]` attribute must be public",
637 impl_item_fn.sig.ident,
638 attr.meta.path().segments[0].ident
639 ),
640 ));
641 }
642
643 if let Some(abi) = &impl_item_fn.sig.abi {
645 return Err(Error::new(
646 abi.span(),
647 format!(
648 "The method `{}` with `#[{}]` attribute must have default ABI",
649 impl_item_fn.sig.ident,
650 attr.meta.path().segments[0].ident
651 ),
652 ));
653 }
654
655 let processor = supported_attrs
656 .get(&attr.path().segments[0].ident)
657 .expect("Matched above to be one of the supported attributes; qed");
658 processor(
659 struct_name,
660 trait_name,
661 &mut impl_item_fn.sig,
662 impl_item_fn.attrs.as_slice(),
663 contract_details,
664 )
665}
666
667fn generate_extension_trait(
668 ident: &Ident,
669 trait_ext_components: &[ExtTraitComponents],
670) -> TokenStream {
671 let trait_name = format_ident!("{ident}Ext");
672 let trait_doc = format!(
673 "Extension trait that provides helper methods for calling [`{ident}`]'s methods on \
674 [`Env`](::ab_contracts_macros::__private::Env) for convenience purposes"
675 );
676 let definitions = trait_ext_components
677 .iter()
678 .map(|components| &components.definition);
679 let impls = trait_ext_components
680 .iter()
681 .map(|components| &components.r#impl);
682
683 quote! {
684 use ffi::*;
685
686 #[doc = #trait_doc]
687 pub trait #trait_name {
688 #( #definitions )*
689 }
690
691 impl #trait_name for ::ab_contracts_macros::__private::Env<'_> {
692 #( #impls )*
693 }
694 }
695}