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