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 ident_case::RenameRule;
13use proc_macro2::{Ident, Literal, Span, TokenStream};
14use quote::{format_ident, quote};
15use std::collections::HashMap;
16use syn::spanned::Spanned;
17use syn::{
18 Error, ImplItem, ImplItemFn, ItemImpl, ItemTrait, Meta, TraitItem, TraitItemConst, TraitItemFn,
19 Type, Visibility, parse_quote, parse2,
20};
21
22#[derive(Default)]
23struct MethodOutput {
24 guest_ffi: TokenStream,
25 trait_ext_components: ExtTraitComponents,
26}
27
28struct Method {
29 original_ident: Ident,
31 methods_details: MethodDetails,
32}
33
34#[derive(Default)]
35struct ContractDetails {
36 methods: Vec<Method>,
37}
38
39pub(super) fn contract(item: TokenStream) -> Result<TokenStream, Error> {
40 if let Ok(item_trait) = parse2::<ItemTrait>(item.clone()) {
41 return process_trait_definition(item_trait);
43 }
44
45 let error_message = "`#[contract]` must be applied to struct implementation, trait definition or trait \
46 implementation";
47
48 let item_impl =
49 parse2::<ItemImpl>(item).map_err(|error| Error::new(error.span(), error_message))?;
50
51 if let Some((_not, path, _for)) = &item_impl.trait_ {
52 let trait_name = path
53 .get_ident()
54 .ok_or_else(|| Error::new(path.span(), error_message))?
55 .clone();
56 process_trait_impl(item_impl, &trait_name)
58 } else {
59 process_struct_impl(item_impl)
61 }
62}
63
64fn process_trait_definition(mut item_trait: ItemTrait) -> Result<TokenStream, Error> {
65 let trait_name = &item_trait.ident;
66
67 if !item_trait.generics.params.is_empty() {
68 return Err(Error::new(
69 item_trait.generics.span(),
70 "`#[contract]` does not support generics",
71 ));
72 }
73
74 let mut guest_ffis = Vec::with_capacity(item_trait.items.len());
75 let mut trait_ext_components = Vec::with_capacity(item_trait.items.len());
76 let mut contract_details = ContractDetails::default();
77
78 for item in &mut item_trait.items {
79 if let TraitItem::Fn(trait_item_fn) = item {
80 let method_output =
81 process_fn_definition(trait_name, trait_item_fn, &mut contract_details)?;
82 guest_ffis.push(method_output.guest_ffi);
83 trait_ext_components.push(method_output.trait_ext_components);
84
85 if let Some(where_clause) = &mut trait_item_fn.sig.generics.where_clause {
88 where_clause.predicates.push(parse_quote! {
89 Self: ::core::marker::Sized
90 });
91 } else {
92 trait_item_fn
93 .sig
94 .generics
95 .where_clause
96 .replace(parse_quote! {
97 where
98 Self: ::core::marker::Sized
99 });
100 }
101 }
102 }
103
104 let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_trait.span())?;
105 let ext_trait = generate_extension_trait(trait_name, &trait_ext_components);
106
107 Ok(quote! {
108 #item_trait
109
110 impl ::ab_contracts_macros::__private::ContractTraitDefinition for dyn #trait_name {
115 #[cfg(feature = "guest")]
116 #[doc(hidden)]
117 const GUEST_FEATURE_ENABLED: () = ();
118 #metadata_const
119 }
120
121 #ext_trait
122
123 pub mod ffi {
125 use super::*;
126
127 #( #guest_ffis )*
128 }
129 })
130}
131
132fn process_trait_impl(mut item_impl: ItemImpl, trait_name: &Ident) -> Result<TokenStream, Error> {
133 let struct_name = item_impl.self_ty.as_ref();
134
135 if !item_impl.generics.params.is_empty() {
136 return Err(Error::new(
137 item_impl.generics.span(),
138 "`#[contract]` does not support generics",
139 ));
140 }
141
142 let mut guest_ffis = Vec::with_capacity(item_impl.items.len());
143 let mut contract_details = ContractDetails::default();
144
145 for item in &mut item_impl.items {
146 match item {
147 ImplItem::Fn(impl_item_fn) => {
148 let method_output = process_fn(
149 struct_name.clone(),
150 Some(trait_name),
151 impl_item_fn,
152 &mut contract_details,
153 )?;
154 guest_ffis.push(method_output.guest_ffi);
155
156 if let Some(where_clause) = &mut impl_item_fn.sig.generics.where_clause {
157 where_clause.predicates.push(parse_quote! {
158 Self: ::core::marker::Sized
159 });
160 } else {
161 impl_item_fn
162 .sig
163 .generics
164 .where_clause
165 .replace(parse_quote! {
166 where
167 Self: ::core::marker::Sized
168 });
169 }
170 }
171 ImplItem::Const(impl_item_const) => {
172 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 _ => {
180 }
182 }
183 }
184
185 let static_name = format_ident!(
186 "{}_METADATA",
187 RenameRule::ScreamingSnakeCase.apply_to_variant(trait_name.to_string())
188 );
189 let ffi_mod_ident = format_ident!(
190 "{}_ffi",
191 RenameRule::SnakeCase.apply_to_variant(trait_name.to_string())
192 );
193 let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_impl.span())?;
194 let method_fn_pointers_const = {
195 let methods = contract_details
196 .methods
197 .iter()
198 .map(|method| &method.original_ident);
199
200 quote! {
201 #[doc(hidden)]
202 const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
203 #( #ffi_mod_ident::#methods::fn_pointer::METHOD_FN_POINTER, )*
204 ];
205 }
206 };
207
208 Ok(quote! {
209 #[cfg(feature = "guest")]
217 #[used]
218 #[unsafe(no_mangle)]
219 #[unsafe(link_section = "ab-contract-metadata")]
220 static #static_name: [::core::primitive::u8; <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA.len()] = unsafe {
221 *<dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA.as_ptr().cast()
222 };
223
224 const _: () = {
226 use #ffi_mod_ident as ffi;
228 #metadata_const
229
230 let (impl_compact_metadata, impl_compact_metadata_size) =
234 ::ab_contracts_macros::__private::ContractMetadataKind::compact(METADATA)
235 .expect("Generated metadata is correct; qed");
236 let (def_compact_metadata, def_compact_metadata_size) =
237 ::ab_contracts_macros::__private::ContractMetadataKind::compact(
238 <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA,
239 )
240 .expect("Generated metadata is correct; qed");
241 assert!(
242 impl_compact_metadata_size == def_compact_metadata_size,
243 "Trait implementation must match trait definition exactly"
244 );
245 let mut i = 0;
246 while impl_compact_metadata_size > i {
247 assert!(
248 impl_compact_metadata[i] == def_compact_metadata[i],
249 "Trait implementation must match trait definition exactly"
250 );
251 i += 1;
252 }
253 };
254
255 #[cfg(feature = "guest")]
257 const _: () = <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::GUEST_FEATURE_ENABLED;
258
259 #item_impl
260
261 impl ::ab_contracts_macros::__private::ContractTrait<dyn #trait_name> for #struct_name {
264 #method_fn_pointers_const
265 }
266
267 pub mod #ffi_mod_ident {
269 use super::*;
270
271
272 #( #guest_ffis )*
273 }
274 })
275}
276
277fn generate_trait_metadata(
278 contract_details: &ContractDetails,
279 trait_name: &Ident,
280 span: Span,
281) -> Result<TraitItemConst, Error> {
282 let num_methods = u8::try_from(contract_details.methods.len()).map_err(|_error| {
283 Error::new(
284 span,
285 format!("Trait can't have more than {} methods", u8::MAX),
286 )
287 })?;
288 let num_methods = Literal::u8_unsuffixed(num_methods);
289 let methods = contract_details
290 .methods
291 .iter()
292 .map(|method| &method.original_ident);
293 let trait_name_metadata = derive_ident_metadata(trait_name)?;
294
295 Ok(parse_quote! {
302 const METADATA: &[::core::primitive::u8] = {
306 const fn metadata()
307 -> ([::core::primitive::u8; ::ab_contracts_macros::__private::MAX_METADATA_CAPACITY], usize)
308 {
309 ::ab_contracts_macros::__private::concat_metadata_sources(&[
310 &[::ab_contracts_macros::__private::ContractMetadataKind::Trait as ::core::primitive::u8],
311 #trait_name_metadata,
312 &[#num_methods],
313 #( ffi::#methods::METADATA, )*
314 ])
315 }
316
317 metadata()
320 .0
321 .split_at(metadata().1)
322 .0
323 };
324 })
325}
326
327fn process_struct_impl(mut item_impl: ItemImpl) -> Result<TokenStream, Error> {
328 let struct_name = item_impl.self_ty.as_ref();
329
330 if !item_impl.generics.params.is_empty() {
331 return Err(Error::new(
332 item_impl.generics.span(),
333 "`#[contract]` does not support generics",
334 ));
335 }
336
337 let mut guest_ffis = Vec::with_capacity(item_impl.items.len());
338 let mut trait_ext_components = Vec::with_capacity(item_impl.items.len());
339 let mut contract_details = ContractDetails::default();
340
341 for item in &mut item_impl.items {
342 if let ImplItem::Fn(impl_item_fn) = item {
343 let method_output = process_fn(
344 struct_name.clone(),
345 None,
346 impl_item_fn,
347 &mut contract_details,
348 )?;
349 guest_ffis.push(method_output.guest_ffi);
350 trait_ext_components.push(method_output.trait_ext_components);
351 }
352 }
353
354 let maybe_slot_type = MethodDetails::slot_type(
355 contract_details
356 .methods
357 .iter()
358 .map(|method| &method.methods_details),
359 );
360 let Some(slot_type) = maybe_slot_type else {
361 return Err(Error::new(
362 item_impl.span(),
363 "All `#[slot]` arguments must be of the same type in all methods of a contract",
364 ));
365 };
366
367 let maybe_tmp_type = MethodDetails::tmp_type(
368 contract_details
369 .methods
370 .iter()
371 .map(|method| &method.methods_details),
372 );
373
374 let Some(tmp_type) = maybe_tmp_type else {
375 return Err(Error::new(
376 item_impl.span(),
377 "All `#[tmp]` arguments must be of the same type in all methods of a contract",
378 ));
379 };
380
381 let metadata_const = {
382 let num_methods = u8::try_from(contract_details.methods.len()).map_err(|_error| {
383 Error::new(
384 item_impl.span(),
385 format!("Struct can't have more than {} methods", u8::MAX),
386 )
387 })?;
388 let num_methods = Literal::u8_unsuffixed(num_methods);
389 let methods = contract_details
390 .methods
391 .iter()
392 .map(|method| &method.original_ident);
393
394 quote! {
400 const MAIN_CONTRACT_METADATA: &[::core::primitive::u8] = {
401 const fn metadata()
402 -> ([::core::primitive::u8; ::ab_contracts_macros::__private::MAX_METADATA_CAPACITY], usize)
403 {
404 ::ab_contracts_macros::__private::concat_metadata_sources(&[
405 &[::ab_contracts_macros::__private::ContractMetadataKind::Contract as ::core::primitive::u8],
406 <#struct_name as ::ab_contracts_macros::__private::IoType>::METADATA,
407 <#slot_type as ::ab_contracts_macros::__private::IoType>::METADATA,
408 <#tmp_type as ::ab_contracts_macros::__private::IoType>::METADATA,
409 &[#num_methods],
410 #( ffi::#methods::METADATA, )*
411 ])
412 }
413
414 metadata()
417 .0
418 .split_at(metadata().1)
419 .0
420 };
421 }
422 };
423 let method_fn_pointers_const = {
424 let methods = contract_details
425 .methods
426 .iter()
427 .map(|method| &method.original_ident);
428
429 quote! {
430 #[doc(hidden)]
431 const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
432 #( ffi::#methods::fn_pointer::METHOD_FN_POINTER, )*
433 ];
434 }
435 };
436
437 let struct_name_ident = extract_ident_from_type(struct_name).ok_or_else(|| {
438 Error::new(
439 struct_name.span(),
440 "`#[contract]` must be applied to simple struct implementation",
441 )
442 })?;
443
444 let ext_trait = generate_extension_trait(struct_name_ident, &trait_ext_components);
445
446 let struct_name_str = struct_name_ident.to_string();
447 let static_name = format_ident!(
448 "{}_METADATA",
449 RenameRule::ScreamingSnakeCase.apply_to_variant(&struct_name_str)
450 );
451 Ok(quote! {
452 #[cfg(all(feature = "guest", not(any(unix, windows))))]
453 #[panic_handler]
454 fn panic(_info: &::core::panic::PanicInfo<'_>) -> ! {
455 loop {}
457 }
458
459 #[cfg(feature = "guest")]
469 #[used]
470 #[unsafe(no_mangle)]
471 #[unsafe(link_section = "ab-contract-metadata")]
472 static #static_name: [
473 ::core::primitive::u8;
474 <#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
475 .len()
476 ] = unsafe {
477 *<#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
478 .as_ptr()
479 .cast()
480 };
481
482 impl ::ab_contracts_macros::__private::Contract for #struct_name {
483 #metadata_const
484 #method_fn_pointers_const
485 #[doc(hidden)]
486 const CODE: &::core::primitive::str = ::ab_contracts_macros::__private::concatcp!(
487 #struct_name_str,
488 '[',
489 ::core::env!("CARGO_PKG_NAME"),
490 '/',
491 ::core::file!(),
492 ':',
493 ::core::line!(),
494 ':',
495 ::core::column!(),
496 ']',
497 );
498 #[cfg(feature = "guest")]
500 #[doc(hidden)]
501 const GUEST_FEATURE_ENABLED: () = ();
502 type Slot = #slot_type;
503 type Tmp = #tmp_type;
504
505 fn code() -> impl ::core::ops::Deref<
506 Target = ::ab_contracts_macros::__private::VariableBytes<
507 { ::ab_contracts_macros::__private::MAX_CODE_SIZE },
508 >,
509 > {
510 const fn code_bytes() -> &'static [::core::primitive::u8] {
511 <#struct_name as ::ab_contracts_macros::__private::Contract>::CODE.as_bytes()
512 }
513
514 const fn code_size() -> ::core::primitive::u32 {
515 code_bytes().len() as ::core::primitive::u32
516 }
517
518 static CODE_SIZE: ::core::primitive::u32 = code_size();
519
520 ::ab_contracts_macros::__private::VariableBytes::from_buffer(
521 code_bytes(),
522 &CODE_SIZE
523 )
524 }
525 }
526
527 #item_impl
528
529 #ext_trait
530
531 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 "Function can only have one of `#[update]` or `#[view]` attributes specified",
570 ));
571 }
572
573 if let Some(abi) = &trait_item_fn.sig.abi {
575 return Err(Error::new(
576 abi.span(),
577 format!(
578 "Function with `#[{}]` attribute must have default ABI",
579 attr.meta.path().segments[0].ident
580 ),
581 ));
582 }
583
584 if trait_item_fn.default.is_some() {
585 return Err(Error::new(
586 trait_item_fn.span(),
587 "`#[contract]` does not support `#[update]` or `#[view]` methods with default implementation \
588 in trait definition",
589 ));
590 }
591
592 let processor = supported_attrs
593 .get(&attr.path().segments[0].ident)
594 .expect("Matched above to be one of the supported attributes; qed");
595 processor(
596 trait_name,
597 &mut trait_item_fn.sig,
598 trait_item_fn.attrs.as_slice(),
599 contract_details,
600 )
601}
602
603fn process_fn(
604 struct_name: Type,
605 trait_name: Option<&Ident>,
606 impl_item_fn: &mut ImplItemFn,
607 contract_details: &mut ContractDetails,
608) -> Result<MethodOutput, Error> {
609 let supported_attrs = HashMap::<_, fn(_, _, _, _, _) -> _>::from_iter([
610 (format_ident!("init"), process_init_fn as _),
611 (format_ident!("update"), process_update_fn as _),
612 (format_ident!("view"), process_view_fn as _),
613 ]);
614 let mut attrs = impl_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
615 Meta::Path(path) => {
616 path.leading_colon.is_none()
617 && path.segments.len() == 1
618 && supported_attrs.contains_key(&path.segments[0].ident)
619 }
620 Meta::List(_meta_list) => false,
621 Meta::NameValue(_meta_name_value) => false,
622 });
623
624 let Some(attr) = attrs.next() else {
625 drop(attrs);
626
627 return Ok(MethodOutput::default());
629 };
630
631 if let Some(next_attr) = attrs.take(1).next() {
632 return Err(Error::new(
633 next_attr.span(),
634 "Function can only have one of `#[init]`, `#[update]` or `#[view]` attributes specified",
635 ));
636 }
637
638 if !(matches!(impl_item_fn.vis, Visibility::Public(_)) || trait_name.is_some()) {
640 return Err(Error::new(
641 impl_item_fn.sig.span(),
642 format!(
643 "Function with `#[{}]` attribute must be public",
644 attr.meta.path().segments[0].ident
645 ),
646 ));
647 }
648
649 if let Some(abi) = &impl_item_fn.sig.abi {
651 return Err(Error::new(
652 abi.span(),
653 format!(
654 "Function with `#[{}]` attribute must have default ABI",
655 attr.meta.path().segments[0].ident
656 ),
657 ));
658 }
659
660 let processor = supported_attrs
661 .get(&attr.path().segments[0].ident)
662 .expect("Matched above to be one of the supported attributes; qed");
663 processor(
664 struct_name,
665 trait_name,
666 &mut impl_item_fn.sig,
667 impl_item_fn.attrs.as_slice(),
668 contract_details,
669 )
670}
671
672fn generate_extension_trait(
673 ident: &Ident,
674 trait_ext_components: &[ExtTraitComponents],
675) -> TokenStream {
676 let trait_name = format_ident!("{ident}Ext");
677 let trait_doc = format!(
678 "Extension trait that provides helper methods for calling [`{ident}`]'s methods on \
679 [`Env`](::ab_contracts_macros::__private::Env) for convenience purposes"
680 );
681 let definitions = trait_ext_components
682 .iter()
683 .map(|components| &components.definition);
684 let impls = trait_ext_components
685 .iter()
686 .map(|components| &components.r#impl);
687
688 quote! {
689 use ffi::*;
690
691 #[doc = #trait_doc]
692 pub trait #trait_name {
693 #( #definitions )*
694 }
695
696 impl #trait_name for ::ab_contracts_macros::__private::Env<'_> {
697 #( #impls )*
698 }
699 }
700}