diff --git a/AGENTS.md b/AGENTS.md index dfdc6821f..36bc6b825 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,7 +135,10 @@ inward (core/macros) when changing fundamentals, outward (main) only to adjust t **`cgp-macro-core`** — this is where the real parsing, AST types, and codegen live (see `cgp-macro-core/src/{types,functions,visitors,macros}/`). When a macro misbehaves, the logic to fix is almost always in `cgp-macro-core`, not the entrypoint crate. `cgp-async-macro` provides - `#[async_trait]`; `cgp-extra-macro{,-lib}` host the extra-feature macros. + `#[async_trait]`. The extra-feature macros (`#[cgp_computer]`, `#[cgp_producer]`, + `#[cgp_auto_dispatch]`) mirror the same split: `cgp-macro-extra` forwards to + `cgp-macro-extra-lib`, which builds on **`cgp-macro-extra-core`**, which in turn builds on + `cgp-macro-core`, lowering through its AST types. - **`crates/core/`** — the foundational runtime traits the macros expand into: - `cgp-component` — the wiring machinery: `DelegateComponent`, `IsProviderFor`, diff --git a/Cargo.lock b/Cargo.lock index 01c19d792..8eabc49e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,32 +137,14 @@ dependencies = [ "cgp-core", "cgp-dispatch", "cgp-error-extra", - "cgp-extra-macro", "cgp-field-extra", "cgp-handler", + "cgp-macro-extra", "cgp-monad", "cgp-run", "cgp-runtime", ] -[[package]] -name = "cgp-extra-macro" -version = "0.8.0-alpha" -dependencies = [ - "cgp-extra-macro-lib", - "syn", -] - -[[package]] -name = "cgp-extra-macro-lib" -version = "0.8.0-alpha" -dependencies = [ - "cgp-macro-core", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "cgp-field" version = "0.8.0-alpha" @@ -204,6 +186,35 @@ dependencies = [ "syn", ] +[[package]] +name = "cgp-macro-extra" +version = "0.8.0-alpha" +dependencies = [ + "cgp-macro-extra-lib", + "syn", +] + +[[package]] +name = "cgp-macro-extra-core" +version = "0.8.0-alpha" +dependencies = [ + "cgp-macro-core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cgp-macro-extra-lib" +version = "0.8.0-alpha" +dependencies = [ + "cgp-macro-core", + "cgp-macro-extra-core", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cgp-macro-lib" version = "0.8.0-alpha" @@ -232,6 +243,7 @@ name = "cgp-macro-test-util-lib" version = "0.8.0-alpha" dependencies = [ "cgp-macro-core", + "cgp-macro-extra-lib", "cgp-macro-lib", "prettyplease", "proc-macro2", @@ -244,8 +256,8 @@ name = "cgp-macro-tests" version = "0.8.0-alpha" dependencies = [ "cgp", - "cgp-extra-macro-lib", "cgp-macro-core", + "cgp-macro-extra-lib", "cgp-macro-lib", "cgp-macro-test-util", "cgp-macro-test-util-lib", diff --git a/Cargo.toml b/Cargo.toml index 0d9a92d38..90399d73b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,10 +27,11 @@ members = [ "crates/macros/cgp-macro", "crates/macros/cgp-macro-core", "crates/macros/cgp-macro-lib", + "crates/macros/cgp-macro-extra", + "crates/macros/cgp-macro-extra-core", + "crates/macros/cgp-macro-extra-lib", "crates/macros/cgp-macro-test-util", "crates/macros/cgp-macro-test-util-lib", - "crates/macros/cgp-extra-macro", - "crates/macros/cgp-extra-macro-lib", "crates/standalone/error/cgp-error-anyhow", "crates/standalone/error/cgp-error-eyre", @@ -80,8 +81,9 @@ cgp-async-macro = { version = "0.8.0-alpha", path = "./crates/macros cgp-macro = { version = "0.8.0-alpha", path = "./crates/macros/cgp-macro" } cgp-macro-core = { version = "0.8.0-alpha", path = "./crates/macros/cgp-macro-core" } cgp-macro-lib = { version = "0.8.0-alpha", path = "./crates/macros/cgp-macro-lib" } +cgp-macro-extra = { version = "0.8.0-alpha", path = "./crates/macros/cgp-macro-extra" } +cgp-macro-extra-core = { version = "0.8.0-alpha", path = "./crates/macros/cgp-macro-extra-core" } +cgp-macro-extra-lib = { version = "0.8.0-alpha", path = "./crates/macros/cgp-macro-extra-lib" } cgp-macro-test-util = { version = "0.8.0-alpha", path = "./crates/macros/cgp-macro-test-util" } cgp-macro-test-util-lib = { version = "0.8.0-alpha", path = "./crates/macros/cgp-macro-test-util-lib" } -cgp-extra-macro = { version = "0.8.0-alpha", path = "./crates/macros/cgp-extra-macro" } -cgp-extra-macro-lib = { version = "0.8.0-alpha", path = "./crates/macros/cgp-extra-macro-lib" } diff --git a/crates/macros/cgp-extra-macro-lib/src/entrypoints/cgp_auto_dispatch.rs b/crates/macros/cgp-extra-macro-lib/src/entrypoints/cgp_auto_dispatch.rs deleted file mode 100644 index 1fef7a776..000000000 --- a/crates/macros/cgp-extra-macro-lib/src/entrypoints/cgp_auto_dispatch.rs +++ /dev/null @@ -1,448 +0,0 @@ -use std::collections::BTreeSet; - -use cgp_macro_core::functions::to_camel_case_str; -use proc_macro2::{Span, TokenStream}; -use quote::quote; -use syn::ext::IdentExt; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::token::Comma; -use syn::{ - FnArg, GenericParam, Ident, ImplItem, ImplItemFn, ItemTrait, Lifetime, Pat, PatIdent, - ReturnType, TraitItemFn, Type, Visibility, parse2, -}; - -pub fn cgp_auto_dispatch(_attr: TokenStream, mut out: TokenStream) -> syn::Result { - let item_trait: ItemTrait = parse2(out.clone())?; - - let blanket_impl = derive_blanket_impl(&item_trait)?; - out.extend(blanket_impl); - - for item in item_trait.items.iter() { - match item { - syn::TraitItem::Fn(fn_item) => { - let method_computer = derive_method_computer(&item_trait, fn_item)?; - out.extend(method_computer); - } - _ => { - return Err(syn::Error::new( - item.span(), - "Only function items are allowed in a dispatch trait", - )); - } - } - } - - Ok(out) -} - -fn derive_blanket_impl(item_trait: &ItemTrait) -> syn::Result { - let trait_ident = &item_trait.ident; - let context_ident = quote! { __Variants__ }; - - let mut generics = item_trait.generics.clone(); - generics - .params - .insert(0, parse2(quote! { #context_ident })?); - - let where_clause = generics.make_where_clause(); - - let extra_life: Lifetime = parse2(quote! { '__a__ })?; - - let mut impl_items: Vec = Vec::new(); - - for trait_item in item_trait.items.iter() { - let method = if let syn::TraitItem::Fn(method) = trait_item { - method - } else { - return Err(syn::Error::new( - trait_item.span(), - "Only function items are allowed in a dispatch trait", - )); - }; - - let mut signature = method.sig.clone(); - let method_ident = &signature.ident; - let mut hrtbs: BTreeSet = BTreeSet::new(); - - let computer_ident = derive_computer_ident(method_ident); - - for generic_param in signature.generics.params.iter() { - match generic_param { - GenericParam::Lifetime(_) => {} - _ => { - return Err(syn::Error::new( - generic_param.span(), - "Dispatch trait methods cannot contain non-lifetime generic parameters due to the lack of quantified constraints in Rust", - )); - } - } - } - - let mut args = signature.inputs.iter_mut(); - - let receiver = if let Some(FnArg::Receiver(receiver)) = args.next() { - receiver - } else { - return Err(syn::Error::new( - signature.span(), - "Dispatcher method must have a self argument", - )); - }; - - let mut arg_idents = Punctuated::<_, Comma>::new(); - let mut arg_types = Punctuated::<_, Comma>::new(); - - for (i, arg) in args.enumerate() { - if let FnArg::Typed(pat_type) = arg { - let arg_ident = Ident::new(&format!("arg_{}", i), pat_type.span()); - arg_idents.push(arg_ident.clone()); - *pat_type.pat = Pat::Ident(PatIdent { - ident: arg_ident, - attrs: Default::default(), - by_ref: Default::default(), - mutability: Default::default(), - subpat: Default::default(), - }); - - let mut arg_type = pat_type.ty.as_ref().clone(); - if let Type::Reference(arg_type) = &mut arg_type { - match &arg_type.lifetime { - Some(lifetime) => { - hrtbs.insert(lifetime.ident.clone()); - } - None => { - hrtbs.insert(extra_life.ident.clone()); - arg_type.lifetime = Some(extra_life.clone()); - } - } - } - - arg_types.push(arg_type); - } else { - return Err(syn::Error::new( - arg.span(), - "Dispatcher method arguments must be typed", - )); - } - } - - let output_type = match &signature.output { - ReturnType::Default => { - quote! { () } - } - ReturnType::Type(_, output) => { - let mut output = output.as_ref().clone(); - if let Type::Reference(output_type) = &mut output { - match &output_type.lifetime { - Some(lifetime) => { - hrtbs.insert(lifetime.ident.clone()); - } - None => { - hrtbs.insert(extra_life.ident.clone()); - output_type.lifetime = Some(extra_life.clone()); - } - } - } - quote! { #output } - } - }; - - let (context_type, matcher) = if let Some((_, life)) = &receiver.reference { - let life = life.as_ref().unwrap_or_else(|| { - hrtbs.insert(extra_life.ident.clone()); - &extra_life - }); - - let mutability = &receiver.mutability; - let context_type = quote! { & #life #mutability #context_ident }; - let matcher = if mutability.is_some() { - if arg_types.is_empty() { - quote! { MatchWithValueHandlersMut } - } else { - quote! { MatchFirstWithValueHandlersMut } - } - } else if arg_types.is_empty() { - quote! { MatchWithValueHandlersRef } - } else { - quote! { MatchFirstWithValueHandlersRef } - }; - - (context_type, matcher) - } else { - let context_type = quote! { #context_ident }; - let matcher = if arg_types.is_empty() { - quote! { MatchWithValueHandlers } - } else { - quote! { MatchFirstWithValueHandlers } - }; - - (context_type, matcher) - }; - - let mut hrtb = TokenStream::new(); - - for ident in hrtbs { - if ident != "static" { - let lifetime = Lifetime { - apostrophe: Span::call_site(), - ident, - }; - hrtb = quote! { for<#lifetime> } - } - } - - let input_type = if arg_types.is_empty() { - quote! { #context_type } - } else { - quote! { (#context_type, (#arg_types)) } - }; - - if signature.asyncness.is_some() { - where_clause.predicates.push(parse2(quote! { - #matcher<#computer_ident>: #hrtb - AsyncComputer<(), (), #input_type, Output = #output_type> - })?); - } else { - where_clause.predicates.push(parse2(quote! { - #matcher<#computer_ident>: #hrtb - Computer<(), (), #input_type, Output = #output_type> - })?); - } - - let args = if arg_idents.is_empty() { - quote! { self } - } else { - quote! { (self, (#arg_idents)) } - }; - - let method_body = if signature.asyncness.is_some() { - quote! { - // Name the provider trait so the call stays unambiguous when the - // consumer trait `CanComputeAsync` is also in scope. The `_` arguments - // are inferred, which avoids naming the HRTB-only `'__a__` lifetime. - <#matcher<#computer_ident> as AsyncComputer<_, _, _>>::compute_async( - &(), - ::core::marker::PhantomData::<()>, - #args, - ).await - } - } else { - quote! { - // As above, qualified so that an imported `CanCompute` does not make - // the call ambiguous. - <#matcher<#computer_ident> as Computer<_, _, _>>::compute( - &(), - ::core::marker::PhantomData::<()>, - #args, - ) - } - }; - - let impl_item = ImplItem::Fn(ImplItemFn { - attrs: Default::default(), - vis: Visibility::Inherited, - defaultness: None, - sig: signature, - block: parse2(quote! { - { #method_body } - })?, - }); - - impl_items.push(impl_item); - } - - where_clause.predicates.push(parse2(quote! { - #context_ident: HasExtractor - })?); - - let ty_generics = item_trait.generics.split_for_impl().1; - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let item_impl = quote! { - impl #impl_generics #trait_ident #ty_generics for #context_ident - #where_clause - { - #(#impl_items)* - } - }; - - Ok(item_impl) -} - -fn derive_method_computer( - item_trait: &ItemTrait, - method: &TraitItemFn, -) -> syn::Result { - let mut signature = method.sig.clone(); - let method_ident = &signature.ident; - let async_token = signature.asyncness; - - let context_ident = quote! { __Variants__ }; - - let mut generics = { - let mut generics = item_trait.generics.clone(); - - generics - .params - .extend(signature.generics.params.iter().cloned()); - - if let Some(method_where_clause) = &signature.generics.where_clause { - generics - .make_where_clause() - .predicates - .extend(method_where_clause.predicates.iter().cloned()); - } - - let trait_ident = &item_trait.ident; - - let type_generics = item_trait.generics.split_for_impl().1; - - generics.params.insert( - 0, - parse2(quote! { - #context_ident: #trait_ident #type_generics - })?, - ); - - generics - }; - - let mut args = signature.inputs.iter_mut(); - - let receiver = if let Some(FnArg::Receiver(receiver)) = args.next() { - receiver - } else { - return Err(syn::Error::new( - signature.span(), - "Dispatcher method must have a self argument", - )); - }; - - let extra_life: Lifetime = parse2(quote! { '__a__ })?; - let mut use_extra_life = false; - - let context_type = match (&receiver.reference, &receiver.mutability) { - (Some((_, life)), Some(_)) => { - let life = life.as_ref().unwrap_or_else(|| { - use_extra_life = true; - &extra_life - }); - - quote! { &#life mut #context_ident } - } - (Some((_, life)), None) => { - let life = life.as_ref().unwrap_or_else(|| { - use_extra_life = true; - &extra_life - }); - - quote! { & #life #context_ident } - } - _ => quote! { #context_ident }, - }; - - let mut arg_idents = Punctuated::<_, Comma>::new(); - let mut arg_types = Punctuated::<_, Comma>::new(); - - for (i, arg) in args.enumerate() { - if let FnArg::Typed(pat_type) = arg { - arg_idents.push(Ident::new(&format!("arg_{}", i), pat_type.span())); - - let arg_type = pat_type.ty.as_mut(); - if let Type::Reference(arg_type) = arg_type - && arg_type.lifetime.is_none() - { - use_extra_life = true; - arg_type.lifetime = Some(extra_life.clone()); - } - - arg_types.push(arg_type); - } else { - return Err(syn::Error::new( - arg.span(), - "Dispatcher method arguments must be typed", - )); - } - } - - let return_type = &mut signature.output; - - if let ReturnType::Type(_, return_type) = return_type - && let Type::Reference(return_type) = return_type.as_mut() - && return_type.lifetime.is_none() - { - use_extra_life = true; - return_type.lifetime = Some(extra_life.clone()); - } - - if use_extra_life { - generics.params.insert(0, parse2(quote! { #extra_life })?); - } - - let arg_params = if arg_idents.is_empty() { - TokenStream::new() - } else { - quote! { - (#arg_idents): (#arg_types) - } - }; - - let dot_await = if async_token.is_some() { - quote! { .await } - } else { - TokenStream::new() - }; - - let computer_ident = derive_computer_ident(method_ident); - - let method_generics = { - let method_generics = method - .sig - .generics - .params - .iter() - .filter(|param| !matches!(param, syn::GenericParam::Lifetime(_))) - .collect::>(); - - if method_generics.is_empty() { - TokenStream::new() - } else { - quote! { ::< #method_generics > } - } - }; - - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - // The helper function takes a reserved name rather than the method's own, so it - // cannot collide with an item of the same name already in the caller's module. - let helper_ident = Ident::new( - &format!("__compute_{}__", method_ident.unraw()), - method_ident.span(), - ); - - Ok(quote! { - #[cgp_computer( #computer_ident )] - #async_token fn #helper_ident #impl_generics ( - #context_ident: #context_type, - #arg_params - ) #return_type - #where_clause - { - #context_ident. #method_ident #method_generics ( #arg_idents ) #dot_await - } - }) -} - -/// The per-variant computer's name, `Compute` plus the method name in PascalCase. The -/// method name is unrawed first, so `r#type` yields `ComputeType` rather than an -/// invalid identifier. -fn derive_computer_ident(method_ident: &Ident) -> Ident { - Ident::new( - &format!( - "Compute{}", - to_camel_case_str(&method_ident.unraw().to_string()) - ), - method_ident.span(), - ) -} diff --git a/crates/macros/cgp-extra-macro-lib/src/entrypoints/cgp_computer.rs b/crates/macros/cgp-extra-macro-lib/src/entrypoints/cgp_computer.rs deleted file mode 100644 index 7c7daea6f..000000000 --- a/crates/macros/cgp-extra-macro-lib/src/entrypoints/cgp_computer.rs +++ /dev/null @@ -1,176 +0,0 @@ -use cgp_macro_core::functions::to_camel_case_str; -use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::token::Comma; -use syn::{FnArg, Ident, ItemFn, ItemImpl, ReturnType, Type, parse2}; - -use crate::parse::MaybeResultType; - -pub fn cgp_computer(attr: TokenStream, body: TokenStream) -> syn::Result { - let item_fn: ItemFn = parse2(body)?; - - let fn_sig = &item_fn.sig; - let fn_ident = &fn_sig.ident; - let fn_inputs = &fn_sig.inputs; - - let computer_ident = if attr.is_empty() { - Ident::new(&to_camel_case_str(&fn_ident.to_string()), fn_ident.span()) - } else { - parse2(attr)? - }; - - let mut input_types = Punctuated::::new(); - let mut input_idents = Punctuated::::new(); - - for (i, input) in fn_inputs.iter().enumerate() { - match input { - FnArg::Receiver(_) => { - return Err(syn::Error::new( - input.span(), - "Computer functions cannot have a receiver", - )); - } - FnArg::Typed(pat) => { - input_types.push(pat.ty.as_ref().clone()); - input_idents.push(Ident::new(&format!("arg_{i}"), pat.span())); - } - } - } - - let fn_output = match &fn_sig.output { - ReturnType::Type(_, ty) => ty.as_ref().clone(), - ReturnType::Default => syn::parse_quote!(()), - }; - - let maybe_result_type = parse2::(fn_output.to_token_stream())?; - - if fn_sig.asyncness.is_none() { - let mut generics = fn_sig.generics.clone(); - generics.params.push(parse2(quote! { __Context__ })?); - generics.params.push(parse2(quote! { __Code__ })?); - - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let computer: ItemImpl = parse2(quote! { - #[cgp_new_provider] - impl #impl_generics - Computer<__Context__, __Code__, ( #input_types )> - for #computer_ident - #where_clause - { - type Output = #fn_output; - - fn compute(_context: &__Context__, _code: PhantomData<__Code__>, ( #input_idents ): ( #input_types )) -> Self::Output { - #fn_ident( #input_idents ) - } - } - })?; - - let delegate = if maybe_result_type.error_type.is_some() { - quote! { - delegate_components! { - #computer_ident { - [ - ComputerRefComponent, - TryComputerComponent, - TryComputerRefComponent, - AsyncComputerComponent, - AsyncComputerRefComponent, - HandlerComponent, - HandlerRefComponent, - ] -> - PromoteTryComputer, - } - } - } - } else { - quote! { - delegate_components! { - #computer_ident { - [ - ComputerRefComponent, - TryComputerComponent, - TryComputerRefComponent, - AsyncComputerComponent, - AsyncComputerRefComponent, - HandlerComponent, - HandlerRefComponent, - ] -> - PromoteComputer, - } - } - } - }; - - Ok(quote! { - #item_fn - - #computer - - #delegate - }) - } else { - let mut generics = fn_sig.generics.clone(); - generics.params.push(parse2(quote! { __Context__ })?); - generics.params.push(parse2(quote! { __Code__ })?); - - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let computer: ItemImpl = parse2(quote! { - #[cgp_new_provider] - impl #impl_generics - AsyncComputer<__Context__, __Code__, ( #input_types )> - for #computer_ident - #where_clause - { - type Output = #fn_output; - - async fn compute_async( - _context: &__Context__, - _code: PhantomData<__Code__>, - ( #input_idents ): ( #input_types ) - ) -> Self::Output { - #fn_ident( #input_idents ).await - } - } - })?; - - let delegate_ref = if maybe_result_type.error_type.is_some() { - quote! { - delegate_components! { - #computer_ident { - [ - AsyncComputerRefComponent, - HandlerComponent, - HandlerRefComponent, - ] -> - PromoteHandler, - } - } - } - } else { - quote! { - delegate_components! { - #computer_ident { - [ - AsyncComputerRefComponent, - HandlerComponent, - HandlerRefComponent, - ] -> - PromoteAsyncComputer, - } - } - } - }; - - Ok(quote! { - #item_fn - - #computer - - #delegate_ref - }) - } -} diff --git a/crates/macros/cgp-extra-macro-lib/src/entrypoints/cgp_producer.rs b/crates/macros/cgp-extra-macro-lib/src/entrypoints/cgp_producer.rs deleted file mode 100644 index 7f3489140..000000000 --- a/crates/macros/cgp-extra-macro-lib/src/entrypoints/cgp_producer.rs +++ /dev/null @@ -1,84 +0,0 @@ -use cgp_macro_core::functions::to_camel_case_str; -use proc_macro2::TokenStream; -use quote::quote; -use syn::spanned::Spanned; -use syn::{Ident, ItemFn, ItemImpl, ReturnType, parse2}; - -pub fn cgp_producer(attr: TokenStream, body: TokenStream) -> syn::Result { - let item_fn: ItemFn = parse2(body)?; - - let fn_sig = &item_fn.sig; - let fn_ident = &fn_sig.ident; - - let producer_ident = if attr.is_empty() { - Ident::new(&to_camel_case_str(&fn_ident.to_string()), fn_ident.span()) - } else { - parse2(attr)? - }; - - if !fn_sig.inputs.is_empty() { - return Err(syn::Error::new( - fn_sig.inputs.span(), - "Producer functions cannot have parameters", - )); - } - - if fn_sig.asyncness.is_some() { - return Err(syn::Error::new( - fn_sig.asyncness.span(), - "Producer functions cannot be async", - )); - } - - if !fn_sig.generics.params.is_empty() { - return Err(syn::Error::new( - fn_sig.generics.params.span(), - "Producer functions must have empty generic parameters", - )); - } - - let fn_output = match &fn_sig.output { - ReturnType::Type(_, ty) => ty.as_ref().clone(), - ReturnType::Default => syn::parse_quote!(()), - }; - - let producer: ItemImpl = parse2(quote! { - #[cgp_new_provider] - impl<__Context__, __Code__> - Producer<__Context__, __Code__> - for #producer_ident - { - type Output = #fn_output; - - fn produce(_context: &__Context__, _code: PhantomData<__Code__>) -> Self::Output { - #fn_ident() - } - } - })?; - - let delegate = quote! { - delegate_components! { - #producer_ident { - [ - ComputerComponent, - ComputerRefComponent, - TryComputerComponent, - TryComputerRefComponent, - AsyncComputerComponent, - AsyncComputerRefComponent, - HandlerComponent, - HandlerRefComponent, - ]: - PromoteProducer, - } - } - }; - - Ok(quote! { - #item_fn - - #producer - - #delegate - }) -} diff --git a/crates/macros/cgp-extra-macro-lib/src/lib.rs b/crates/macros/cgp-extra-macro-lib/src/lib.rs deleted file mode 100644 index 9d4782ca9..000000000 --- a/crates/macros/cgp-extra-macro-lib/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod entrypoints; -pub(crate) mod parse; - -pub use entrypoints::*; diff --git a/crates/macros/cgp-extra-macro-lib/src/parse/maybe_result.rs b/crates/macros/cgp-extra-macro-lib/src/parse/maybe_result.rs deleted file mode 100644 index 1c8fa27b1..000000000 --- a/crates/macros/cgp-extra-macro-lib/src/parse/maybe_result.rs +++ /dev/null @@ -1,36 +0,0 @@ -use proc_macro2::Span; -use syn::parse::discouraged::Speculative; -use syn::parse::{Parse, ParseStream}; -use syn::token::{Comma, Gt, Lt}; -use syn::{Ident, Type}; - -pub struct MaybeResultType { - pub error_type: Option, -} - -impl Parse for MaybeResultType { - fn parse(input: ParseStream) -> syn::Result { - let fork = input.fork(); - if fork.parse::().ok() == Some(Ident::new("Result", Span::call_site())) { - input.advance_to(&fork); - - let _: Lt = input.parse()?; - - input.parse::()?; - - let _: Comma = input.parse()?; - - let error_type = input.parse()?; - - let _: Gt = input.parse()?; - - Ok(Self { - error_type: Some(error_type), - }) - } else { - input.parse::()?; - - Ok(Self { error_type: None }) - } - } -} diff --git a/crates/macros/cgp-extra-macro-lib/src/parse/mod.rs b/crates/macros/cgp-extra-macro-lib/src/parse/mod.rs deleted file mode 100644 index 0f7467f68..000000000 --- a/crates/macros/cgp-extra-macro-lib/src/parse/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod maybe_result; - -pub use maybe_result::*; diff --git a/crates/macros/cgp-extra-macro/src/lib.rs b/crates/macros/cgp-extra-macro/src/lib.rs deleted file mode 100644 index 5950da51a..000000000 --- a/crates/macros/cgp-extra-macro/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -use proc_macro::TokenStream; - -#[proc_macro_attribute] -pub fn cgp_producer(attr: TokenStream, body: TokenStream) -> TokenStream { - cgp_extra_macro_lib::cgp_producer(attr.into(), body.into()) - .unwrap_or_else(syn::Error::into_compile_error) - .into() -} - -#[proc_macro_attribute] -pub fn cgp_computer(attr: TokenStream, body: TokenStream) -> TokenStream { - cgp_extra_macro_lib::cgp_computer(attr.into(), body.into()) - .unwrap_or_else(syn::Error::into_compile_error) - .into() -} - -#[proc_macro_attribute] -pub fn cgp_auto_dispatch(attr: TokenStream, body: TokenStream) -> TokenStream { - cgp_extra_macro_lib::cgp_auto_dispatch(attr.into(), body.into()) - .unwrap_or_else(syn::Error::into_compile_error) - .into() -} diff --git a/crates/macros/cgp-macro-core/AGENTS.md b/crates/macros/cgp-macro-core/AGENTS.md index 7b62abeef..a88d05e35 100644 --- a/crates/macros/cgp-macro-core/AGENTS.md +++ b/crates/macros/cgp-macro-core/AGENTS.md @@ -19,6 +19,9 @@ and the crate hierarchy. `cgp-macro-core` (this crate, all the logic)**. A `cgp-macro-lib` function is thin: `syn::parse2` the attr/item, build a `cgp_macro_core::types::::Item*`, run its transform pipeline, and wrap the result in `quote!`. See `cgp-macro-lib/src/cgp_component.rs` for the canonical shape. + The extra-feature macros follow the same split one level up, in `cgp-macro-extra-core`, which + depends on this crate and lowers through its AST types (see that crate's `AGENTS.md`), so a + change to a type it reuses, such as `ItemCgpProvider` or `DelegateTable`, reaches them too. - Because it is `proc-macro`-free, its parsers and codegen are **unit-testable as plain functions**. Tests live in the sibling `crates/tests/cgp-macro-tests` crate (parser corner cases like `IdentWithTypeArgs`, plus expansion **snapshots** via the `snapshot_*` macros). Prefer adding diff --git a/crates/macros/cgp-macro-extra-core/AGENTS.md b/crates/macros/cgp-macro-extra-core/AGENTS.md new file mode 100644 index 000000000..9258a2eb0 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/AGENTS.md @@ -0,0 +1,51 @@ +# AGENTS.md + +`cgp-macro-extra-core` is the pure-logic core of CGP's extra-feature macros: +`#[cgp_computer]`, `#[cgp_producer]`, and `#[cgp_auto_dispatch]`. It is to +`cgp-macro-extra-lib` what [`cgp-macro-core`](../cgp-macro-core) is to +`cgp-macro-lib`, and it depends on `cgp-macro-core` so it can reuse that crate's +AST types and helpers rather than duplicating them. **Read +[cgp-macro-core/AGENTS.md](../cgp-macro-core/AGENTS.md) first**: its conventions +(`Parse`/`ToTokens` construct types, `parse_internal!`, `exports` markers, staged +pipelines, `define_keyword!`, `VisitMut` visitors, and brief inline docs) all +apply here unchanged. This file adds only what is specific to this crate. + +## The pipeline is `crate → cgp-macro-core` + +The proc-macro pipeline is **`cgp-macro-extra` (entrypoints) → +`cgp-macro-extra-lib` (per-macro glue) → `cgp-macro-extra-core` (this crate) → +`cgp-macro-core`**. Like `cgp-macro-core`, this crate has no `#[proc_macro]` +entrypoints and does not depend on `proc-macro`, so its stages are unit-testable +as plain functions. + +## Lower through another macro's IR + +These macros build on CGP's core macros: a computer is a `#[cgp_new_provider]` +impl plus a `delegate_components!` table, and a dispatch trait adds a +`#[cgp_computer]` per method. Rather than emitting those invocations for the +compiler to expand again, each macro's `eval()` stage returns the **AST nodes +those macros parse into**, its intermediate representation (IR): +`cgp_macro_core::types::cgp_provider::ItemCgpProvider`, +`cgp_macro_core::types::delegate_component::DelegateTable`, or this crate's own +`ItemCgpComputer`. The entrypoint then lowers that IR with the owning type's own +method (`ItemCgpProvider::lower`, `DelegateTable::eval`), so the expansion is the +final code in one step and every CGP name in it is qualified through an `exports` +marker. `#[cgp_impl]` lowers through `ItemCgpProvider` the same way. Build each IR +node with `parse_internal!` from quoted tokens, as any other AST node. + +## Module map + +- **[src/types/](src/types/)**: one submodule per macro, each a staged pipeline + of `Item…` (raw input) → `.preprocess()` → `Preprocessed…` → `.eval()` → the + IR, plus `handler_fn`, the `EvaluatedHandlerFn` IR the computer and producer + macros share. +- **[src/exports.rs](src/exports.rs)**: the `exports` markers for the handler and + dispatch items the expansions name, declared with `cgp-macro-core`'s + `export_constructs!`. Reuse `cgp_macro_core::exports` for any item already + declared there, such as `HasExtractor`. +- **[src/functions/](src/functions/)**: free helpers the stages share, such as + the default provider and computer names (`derive_provider_ident`, + `derive_computer_ident`, which unraw a raw identifier) and `return_type`. +- **[src/visitors/](src/visitors/)**: the `syn` passes over types: + `ElaborateElidedLifetimes` (a `VisitMut` naming a dispatch method's elided + lifetimes by the elision rules), `collect_lifetimes`, and `find_impl_trait`. diff --git a/crates/macros/cgp-extra-macro-lib/Cargo.toml b/crates/macros/cgp-macro-extra-core/Cargo.toml similarity index 75% rename from crates/macros/cgp-extra-macro-lib/Cargo.toml rename to crates/macros/cgp-macro-extra-core/Cargo.toml index 412857a24..def7fb7cc 100644 --- a/crates/macros/cgp-extra-macro-lib/Cargo.toml +++ b/crates/macros/cgp-macro-extra-core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cgp-extra-macro-lib" +name = "cgp-macro-extra-core" version = "0.8.0-alpha" edition = { workspace = true } license = { workspace = true } @@ -11,11 +11,9 @@ keywords = { workspace = true } categories = { workspace = true } description = { workspace = true } -[features] -default = [] - [dependencies] -syn = { version = "2.0.95", features = [ "full", "extra-traits" ] } -quote = "1.0.38" -proc-macro2 = "1.0.92" cgp-macro-core = { workspace = true } + +syn = { version = "2.0.95", features = [ "full", "extra-traits", "visit", "visit-mut" ] } +quote = { version = "1.0.38" } +proc-macro2 = { version = "1.0.92" } diff --git a/crates/macros/cgp-macro-extra-core/src/exports.rs b/crates/macros/cgp-macro-extra-core/src/exports.rs new file mode 100644 index 000000000..da030f554 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/exports.rs @@ -0,0 +1,30 @@ +use cgp_macro_core::export_constructs; + +export_constructs! { + Computer, + AsyncComputer, + Producer, + + ComputerComponent, + ComputerRefComponent, + TryComputerComponent, + TryComputerRefComponent, + AsyncComputerComponent, + AsyncComputerRefComponent, + HandlerComponent, + HandlerRefComponent, + ProducerComponent, + + PromoteComputer, + PromoteTryComputer, + PromoteAsyncComputer, + PromoteHandler, + PromoteProducer, + + MatchWithValueHandlers, + MatchWithValueHandlersRef, + MatchWithValueHandlersMut, + MatchFirstWithValueHandlers, + MatchFirstWithValueHandlersRef, + MatchFirstWithValueHandlersMut, +} diff --git a/crates/macros/cgp-macro-extra-core/src/functions/mod.rs b/crates/macros/cgp-macro-extra-core/src/functions/mod.rs new file mode 100644 index 000000000..7b9916734 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/functions/mod.rs @@ -0,0 +1,5 @@ +mod provider_ident; +mod return_type; + +pub use provider_ident::*; +pub use return_type::*; diff --git a/crates/macros/cgp-macro-extra-core/src/functions/provider_ident.rs b/crates/macros/cgp-macro-extra-core/src/functions/provider_ident.rs new file mode 100644 index 000000000..6cb74f82f --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/functions/provider_ident.rs @@ -0,0 +1,26 @@ +use cgp_macro_core::functions::to_camel_case_str; +use syn::Ident; +use syn::ext::IdentExt; + +/// The default name of a provider defined from a function: the function name in +/// PascalCase, so `magic_number` names `MagicNumber`. The name is spanned on the +/// function identifier it derives from, so an error on the generated struct, and +/// an editor's go-to-definition, both land on that identifier. +pub fn derive_provider_ident(fn_ident: &Ident) -> Ident { + Ident::new(&to_pascal_case(fn_ident), fn_ident.span()) +} + +/// The name of a dispatch method's per-variant computer: `Compute` plus the +/// method name in PascalCase, spanned on the method identifier. +pub fn derive_computer_ident(method_ident: &Ident) -> Ident { + Ident::new( + &format!("Compute{}", to_pascal_case(method_ident)), + method_ident.span(), + ) +} + +/// `ident` in PascalCase. A raw identifier is unrawed first, since `r#` cannot +/// begin a longer identifier: `r#type` yields `Type`. +fn to_pascal_case(ident: &Ident) -> String { + to_camel_case_str(&ident.unraw().to_string()) +} diff --git a/crates/macros/cgp-macro-extra-core/src/functions/return_type.rs b/crates/macros/cgp-macro-extra-core/src/functions/return_type.rs new file mode 100644 index 000000000..5972ecf68 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/functions/return_type.rs @@ -0,0 +1,10 @@ +use cgp_macro_core::parse_internal; +use syn::{ReturnType, Type}; + +/// The type a function returns, reading an omitted return type as `()`. +pub fn return_type(output: &ReturnType) -> syn::Result { + match output { + ReturnType::Type(_, ty) => Ok(ty.as_ref().clone()), + ReturnType::Default => Ok(parse_internal!(())), + } +} diff --git a/crates/macros/cgp-macro-extra-core/src/lib.rs b/crates/macros/cgp-macro-extra-core/src/lib.rs new file mode 100644 index 000000000..59e1bcb29 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/lib.rs @@ -0,0 +1,4 @@ +pub mod exports; +pub mod functions; +pub mod types; +pub mod visitors; diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/evaluated.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/evaluated.rs new file mode 100644 index 000000000..8bd0057ae --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/evaluated.rs @@ -0,0 +1,16 @@ +use syn::{ItemImpl, ItemTrait}; + +use crate::types::cgp_computer::ItemCgpComputer; + +/// The intermediate representation `#[cgp_auto_dispatch]` evaluates a trait +/// into. Each entry of `computers` is what the macro would otherwise emit as a +/// `#[cgp_computer(Compute{Method})]` helper function; the entrypoint runs it +/// through the `#[cgp_computer]` pipeline instead. +pub struct EvaluatedCgpAutoDispatch { + /// The annotated trait, emitted unchanged. + pub item_trait: ItemTrait, + /// The impl of the trait for any extensible enum. + pub blanket_impl: ItemImpl, + /// One per-variant computer per method, in method order. + pub computers: Vec, +} diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/item.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/item.rs new file mode 100644 index 000000000..4f6d9d97e --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/item.rs @@ -0,0 +1,36 @@ +use syn::{Error, ItemTrait, TraitItem}; + +use crate::types::cgp_auto_dispatch::{DispatchMethod, PreprocessedCgpAutoDispatch}; + +/// Raw input stage: the annotated trait. First stage of the +/// `#[cgp_auto_dispatch]` pipeline. +pub struct ItemCgpAutoDispatch { + pub item_trait: ItemTrait, +} + +impl ItemCgpAutoDispatch { + /// Check that every trait item is a method the macro can dispatch, yielding + /// the next stage with one [`DispatchMethod`] per method. + pub fn preprocess(&self) -> syn::Result { + let mut methods = Vec::new(); + + for item in self.item_trait.items.iter() { + match item { + TraitItem::Fn(method) => { + methods.push(DispatchMethod::new(method)?); + } + _ => { + return Err(Error::new_spanned( + item, + "Only function items are allowed in a dispatch trait", + )); + } + } + } + + Ok(PreprocessedCgpAutoDispatch { + item_trait: self.item_trait.clone(), + methods, + }) + } +} diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/method.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/method.rs new file mode 100644 index 000000000..c7d74625b --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/method.rs @@ -0,0 +1,385 @@ +use cgp_macro_core::functions::merge_generics; +use cgp_macro_core::parse_internal; +use proc_macro2::TokenStream; +use quote::quote; +use syn::ext::IdentExt; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::token::Comma; +use syn::visit_mut::VisitMut; +use syn::{ + Error, FnArg, GenericParam, Ident, ImplItem, ImplItemFn, ItemTrait, Lifetime, Pat, PatIdent, + ReturnType, TraitItemFn, Type, Visibility, WherePredicate, +}; + +use crate::exports::{ + AsyncComputer, Computer, MatchFirstWithValueHandlers, MatchFirstWithValueHandlersMut, + MatchFirstWithValueHandlersRef, MatchWithValueHandlers, MatchWithValueHandlersMut, + MatchWithValueHandlersRef, +}; +use crate::functions::derive_computer_ident; +use crate::types::cgp_computer::ItemCgpComputer; +use crate::visitors::{ElaborateElidedLifetimes, collect_lifetimes}; + +/// How a dispatch method takes `self`, with the lifetime a borrowed receiver +/// carries: the one the method names, or the reserved `'__a__` when elided. +pub enum DispatchReceiver { + Owned, + Ref(Lifetime), + Mut(Lifetime), +} + +/// One method of a dispatch trait, checked to have a shape the macro can +/// dispatch (a `self` receiver, typed arguments, and lifetime generics only), +/// with every elided lifetime in its signature given a name. +/// +/// The names follow the compiler's elision rules, so the generated bound means +/// what the method's signature means: each elided input lifetime is distinct, +/// and an elided output lifetime is the receiver's, or, for a by-value `self`, +/// the single lifetime the arguments use. +pub struct DispatchMethod { + pub method: TraitItemFn, + /// The per-variant computer's name: `Compute` plus the method name in + /// PascalCase. + pub computer_ident: Ident, + pub receiver: DispatchReceiver, + /// The argument types after the receiver, with elided lifetimes named. + pub arg_types: Vec, + /// The return type with elided lifetimes named, or `None` for `()`. + pub output: Option, + /// The lifetimes the elaboration introduced, which the trait and the method + /// do not declare. + pub introduced_lifetimes: Vec, +} + +impl DispatchMethod { + /// Check the method's shape and name its elided lifetimes. Rejects a type or + /// const generic parameter, a missing or typed receiver, and an untyped + /// argument, each with a spanned error. + pub fn new(method: &TraitItemFn) -> syn::Result { + let sig = &method.sig; + + for generic_param in sig.generics.params.iter() { + if !matches!(generic_param, GenericParam::Lifetime(_)) { + return Err(Error::new_spanned( + generic_param, + "Dispatch trait methods cannot contain non-lifetime generic parameters due to the lack of quantified constraints in Rust", + )); + } + } + + let mut inputs = sig.inputs.iter(); + + let receiver = match inputs.next() { + Some(FnArg::Receiver(receiver)) => receiver, + _ => { + return Err(Error::new_spanned( + sig, + "Dispatcher method must have a self argument", + )); + } + }; + + // A typed receiver such as `self: Box` is not the enum or a borrow of + // it, so no value-handler matcher can take it. + if receiver.colon_token.is_some() { + return Err(Error::new_spanned( + receiver, + "Dispatcher method receiver must be `self`, `&self`, or `&mut self`", + )); + } + + let mut introduced_lifetimes = Vec::new(); + + let receiver = match &receiver.reference { + None => DispatchReceiver::Owned, + Some((_, lifetime)) => { + let lifetime = match lifetime { + Some(lifetime) => lifetime.clone(), + None => { + let lifetime: Lifetime = parse_internal!('__a__); + introduced_lifetimes.push(lifetime.clone()); + lifetime + } + }; + + if receiver.mutability.is_some() { + DispatchReceiver::Mut(lifetime) + } else { + DispatchReceiver::Ref(lifetime) + } + } + }; + + let mut elaborate_inputs = ElaborateElidedLifetimes::fresh(1); + let mut arg_types = Vec::new(); + + for input in inputs { + match input { + FnArg::Typed(pat_type) => { + let mut arg_type = pat_type.ty.as_ref().clone(); + elaborate_inputs.visit_type_mut(&mut arg_type); + arg_types.push(arg_type); + } + FnArg::Receiver(_) => { + return Err(Error::new_spanned( + input, + "Dispatcher method arguments must be typed", + )); + } + } + } + + introduced_lifetimes.extend(elaborate_inputs.introduced); + + let output = match &sig.output { + ReturnType::Default => None, + ReturnType::Type(_, output) => { + let mut output = output.as_ref().clone(); + + let output_lifetime = match &receiver { + DispatchReceiver::Ref(lifetime) | DispatchReceiver::Mut(lifetime) => { + Some(lifetime.clone()) + } + DispatchReceiver::Owned => { + let input_lifetimes = collect_lifetimes(&arg_types); + if input_lifetimes.len() == 1 { + input_lifetimes.into_iter().next() + } else { + // The compiler rejects the trait itself here, since the + // elided output lifetime has no lifetime to take. + None + } + } + }; + + if let Some(lifetime) = output_lifetime { + ElaborateElidedLifetimes::fixed(lifetime).visit_type_mut(&mut output); + } + + Some(output) + } + }; + + Ok(Self { + method: method.clone(), + computer_ident: derive_computer_ident(&sig.ident), + receiver, + arg_types, + output, + introduced_lifetimes, + }) + } + + /// The type the matcher receives as the payload: the enum itself, or a + /// borrow of it. + fn context_type(&self) -> TokenStream { + match &self.receiver { + DispatchReceiver::Owned => quote! { __Variants__ }, + DispatchReceiver::Ref(lifetime) => quote! { & #lifetime __Variants__ }, + DispatchReceiver::Mut(lifetime) => quote! { & #lifetime mut __Variants__ }, + } + } + + fn output_type(&self) -> TokenStream { + match &self.output { + Some(output) => quote! { #output }, + None => quote! { () }, + } + } + + /// The blanket impl's method, which runs the matcher selected for this + /// method's shape, and the `where` predicate bounding that matcher. + pub fn to_blanket_impl_item(&self) -> syn::Result<(ImplItem, WherePredicate)> { + let mut signature = self.method.sig.clone(); + let computer_ident = &self.computer_ident; + + // Rebind the arguments positionally, so the body can forward them + // whatever patterns the trait declares them with. + let mut arg_idents = Punctuated::::new(); + + for (i, arg) in signature.inputs.iter_mut().skip(1).enumerate() { + if let FnArg::Typed(pat_type) = arg { + let arg_ident = Ident::new(&format!("arg_{i}"), pat_type.span()); + arg_idents.push(arg_ident.clone()); + *pat_type.pat = Pat::Ident(PatIdent { + ident: arg_ident, + attrs: Default::default(), + by_ref: Default::default(), + mutability: Default::default(), + subpat: Default::default(), + }); + } + } + + let has_args = !self.arg_types.is_empty(); + + let matcher = match (&self.receiver, has_args) { + (DispatchReceiver::Owned, false) => quote! { #MatchWithValueHandlers }, + (DispatchReceiver::Owned, true) => quote! { #MatchFirstWithValueHandlers }, + (DispatchReceiver::Ref(_), false) => quote! { #MatchWithValueHandlersRef }, + (DispatchReceiver::Ref(_), true) => quote! { #MatchFirstWithValueHandlersRef }, + (DispatchReceiver::Mut(_), false) => quote! { #MatchWithValueHandlersMut }, + (DispatchReceiver::Mut(_), true) => quote! { #MatchFirstWithValueHandlersMut }, + }; + + let context_type = self.context_type(); + let output_type = self.output_type(); + let arg_types = &self.arg_types; + + let input_type = if has_args { + quote! { (#context_type, (#(#arg_types),*)) } + } else { + quote! { #context_type } + }; + + // Quantify the method's own lifetimes and the ones the elaboration named, + // but never the trait's, which the impl already declares. + let quantified: Vec<&Lifetime> = signature + .generics + .lifetimes() + .map(|param| ¶m.lifetime) + .chain(self.introduced_lifetimes.iter()) + .collect(); + + let hrtb = if quantified.is_empty() { + TokenStream::new() + } else { + quote! { for< #(#quantified),* > } + }; + + let args = if has_args { + quote! { (self, (#arg_idents)) } + } else { + quote! { self } + }; + + let (predicate, method_body): (WherePredicate, TokenStream) = + if signature.asyncness.is_some() { + ( + parse_internal! { + #matcher<#computer_ident>: #hrtb + #AsyncComputer<(), (), #input_type, Output = #output_type> + }, + // Name the provider trait so the call stays unambiguous when the + // consumer trait `CanComputeAsync` is also in scope. The `_` + // arguments are inferred, which avoids naming the lifetimes only + // the bound's `for<..>` declares. + quote! { + <#matcher<#computer_ident> as #AsyncComputer<_, _, _>>::compute_async( + &(), + ::core::marker::PhantomData::<()>, + #args, + ).await + }, + ) + } else { + ( + parse_internal! { + #matcher<#computer_ident>: #hrtb + #Computer<(), (), #input_type, Output = #output_type> + }, + // As above, qualified so that an imported `CanCompute` does not + // make the call ambiguous. + quote! { + <#matcher<#computer_ident> as #Computer<_, _, _>>::compute( + &(), + ::core::marker::PhantomData::<()>, + #args, + ) + }, + ) + }; + + let impl_item = ImplItem::Fn(ImplItemFn { + attrs: Default::default(), + vis: Visibility::Inherited, + defaultness: None, + sig: signature, + block: parse_internal!({ #method_body }), + }); + + Ok((impl_item, predicate)) + } + + /// The `#[cgp_computer]` input for this method's per-variant computer: a + /// private helper function, generic over any payload implementing the + /// trait, that calls the method on the payload. + pub fn to_computer(&self, item_trait: &ItemTrait) -> syn::Result { + let method_ident = &self.method.sig.ident; + let async_token = &self.method.sig.asyncness; + let trait_ident = &item_trait.ident; + let type_generics = item_trait.generics.split_for_impl().1; + + let mut generics = merge_generics(&item_trait.generics, &self.method.sig.generics); + // Insert the payload parameter, then the introduced lifetimes, as leading + // generics. Position 0 is safe with a lifetime present because + // `syn::Generics::to_tokens` emits lifetimes first. See + // cgp-knowledge-base/cgp/implementation/README.md, "Generic-parameter + // insertion and lifetime ordering". + generics.params.insert( + 0, + parse_internal!(__Variants__: #trait_ident #type_generics), + ); + + for lifetime in self.introduced_lifetimes.iter().rev() { + generics.params.insert(0, parse_internal!(#lifetime)); + } + + let context_type = self.context_type(); + let arg_types = &self.arg_types; + + let arg_idents: Punctuated = self + .method + .sig + .inputs + .iter() + .skip(1) + .enumerate() + .map(|(i, arg)| Ident::new(&format!("arg_{i}"), arg.span())) + .collect(); + + let arg_params = if arg_idents.is_empty() { + TokenStream::new() + } else { + quote! { + (#arg_idents): (#(#arg_types),*) + } + }; + + let return_type = match &self.output { + Some(output) => quote! { -> #output }, + None => TokenStream::new(), + }; + + let dot_await = if async_token.is_some() { + quote! { .await } + } else { + TokenStream::new() + }; + + let (impl_generics, _, where_clause) = generics.split_for_impl(); + + // The helper function takes a reserved name rather than the method's own, + // so it cannot collide with an item of the same name already in the + // caller's module. + let helper_ident = Ident::new( + &format!("__compute_{}__", method_ident.unraw()), + method_ident.span(), + ); + + Ok(ItemCgpComputer { + ident: Some(self.computer_ident.clone()), + item_fn: parse_internal! { + #async_token fn #helper_ident #impl_generics ( + __Variants__: #context_type, + #arg_params + ) #return_type + #where_clause + { + __Variants__. #method_ident ( #arg_idents ) #dot_await + } + }, + }) + } +} diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/mod.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/mod.rs new file mode 100644 index 000000000..9e3bf4247 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/mod.rs @@ -0,0 +1,9 @@ +mod evaluated; +mod item; +mod method; +mod preprocessed; + +pub use evaluated::*; +pub use item::*; +pub use method::*; +pub use preprocessed::*; diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/preprocessed.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/preprocessed.rs new file mode 100644 index 000000000..92c02ea33 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_auto_dispatch/preprocessed.rs @@ -0,0 +1,85 @@ +use cgp_macro_core::exports::HasExtractor; +use cgp_macro_core::functions::override_item_span; +use cgp_macro_core::parse_internal; +use syn::{ItemImpl, ItemTrait}; + +use crate::types::cgp_auto_dispatch::{DispatchMethod, EvaluatedCgpAutoDispatch}; + +/// The checked `#[cgp_auto_dispatch]` input: the trait and one +/// [`DispatchMethod`] per method. +pub struct PreprocessedCgpAutoDispatch { + pub item_trait: ItemTrait, + pub methods: Vec, +} + +impl PreprocessedCgpAutoDispatch { + /// Evaluate into the dispatch IR: the enum-level blanket impl, and one + /// `#[cgp_computer]` input per method, left for the entrypoint to lower. + pub fn eval(&self) -> syn::Result { + Ok(EvaluatedCgpAutoDispatch { + item_trait: self.item_trait.clone(), + blanket_impl: self.to_blanket_impl()?, + computers: self + .methods + .iter() + .map(|method| method.to_computer(&self.item_trait)) + .collect::>()?, + }) + } + + /// The impl of the trait for any `__Variants__`, each method running a + /// value-handler matcher over that method's per-variant computer. + pub fn to_blanket_impl(&self) -> syn::Result { + let item_trait = &self.item_trait; + let trait_ident = &item_trait.ident; + + let mut generics = item_trait.generics.clone(); + // Insert the enum parameter as the leading impl generic. Position 0 is safe + // with a lifetime present because `syn::Generics::to_tokens` emits + // lifetimes first. See cgp-knowledge-base/cgp/implementation/README.md, + // "Generic-parameter insertion and lifetime ordering". + generics.params.insert(0, parse_internal!(__Variants__)); + + let mut impl_items = Vec::new(); + + for method in self.methods.iter() { + let (impl_item, predicate) = method.to_blanket_impl_item()?; + + impl_items.push(impl_item); + generics.make_where_clause().predicates.push(predicate); + } + + generics + .make_where_clause() + .predicates + .push(parse_internal!(__Variants__: #HasExtractor)); + + // The impl must meet the trait's supertraits, which it can only require of + // the enum: implementing the trait for every `__Variants__` would otherwise + // need each supertrait to hold for every type. + let supertraits = &item_trait.supertraits; + if !supertraits.is_empty() { + generics + .make_where_clause() + .predicates + .push(parse_internal!(__Variants__: #supertraits)); + } + + let ty_generics = item_trait.generics.split_for_impl().1; + let (impl_generics, _, where_clause) = generics.split_for_impl(); + + let blanket_impl: ItemImpl = parse_internal! { + impl #impl_generics #trait_ident #ty_generics for __Variants__ + #where_clause + { + #(#impl_items)* + } + }; + + // Re-span the impl's boundary tokens onto the trait name, so an error on + // the blanket impl (a conflict with a hand-written impl of the trait, say) + // points at the trait rather than the whole attribute. See + // cgp-knowledge-base/cgp/implementation/README.md, "Spans". + override_item_span(trait_ident.span(), &blanket_impl) + } +} diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/item.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/item.rs new file mode 100644 index 000000000..24488dc2b --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/item.rs @@ -0,0 +1,78 @@ +use quote::ToTokens; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::{Error, FnArg, Ident, ItemFn, parse2}; + +use crate::functions::{derive_provider_ident, return_type}; +use crate::types::cgp_computer::{MaybeResultType, PreprocessedCgpComputer}; +use crate::visitors::find_impl_trait; + +/// Raw input stage: the optional provider name from the attribute and the +/// annotated function. First stage of the `#[cgp_computer]` pipeline. +pub struct ItemCgpComputer { + pub ident: Option, + pub item_fn: ItemFn, +} + +impl ItemCgpComputer { + /// Read the function's inputs, output, and whether it is async or returns + /// `Result`, yielding the next stage. + pub fn preprocess(&self) -> syn::Result { + let sig = &self.item_fn.sig; + + let provider_ident = match &self.ident { + Some(ident) => ident.clone(), + None => derive_provider_ident(&sig.ident), + }; + + let mut input_idents = Punctuated::new(); + let mut input_types = Punctuated::new(); + + for (i, input) in sig.inputs.iter().enumerate() { + match input { + FnArg::Receiver(receiver) => { + return Err(Error::new_spanned( + receiver, + "Computer functions cannot have a receiver", + )); + } + FnArg::Typed(pat_type) => { + if let Some(impl_trait) = find_impl_trait(&pat_type.ty) { + return Err(Error::new_spanned( + impl_trait, + "Computer function parameters cannot use `impl Trait`; declare a generic parameter instead", + )); + } + + // Each input is rebound positionally, so the function's own + // pattern (a `mut` binding, a destructuring) is not repeated. + input_idents.push(Ident::new(&format!("arg_{i}"), pat_type.span())); + input_types.push(pat_type.ty.as_ref().clone()); + } + } + } + + let output = return_type(&sig.output)?; + + if let Some(impl_trait) = find_impl_trait(&output) { + return Err(Error::new_spanned( + impl_trait, + "Computer functions cannot return `impl Trait`", + )); + } + + // A re-parse of the user's own return type, so its tokens keep their spans + // and an error lands on the offending token. + let maybe_result: MaybeResultType = parse2(output.to_token_stream())?; + + Ok(PreprocessedCgpComputer { + provider_ident, + item_fn: self.item_fn.clone(), + input_idents, + input_types, + output, + is_async: sig.asyncness.is_some(), + is_fallible: maybe_result.error_type.is_some(), + }) + } +} diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/maybe_result.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/maybe_result.rs new file mode 100644 index 000000000..e141e582d --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/maybe_result.rs @@ -0,0 +1,53 @@ +use syn::parse::discouraged::Speculative; +use syn::parse::{Parse, ParseStream}; +use syn::token::{Comma, Gt, Lt}; +use syn::{Error, Ident, Type}; + +/// The return type of a `#[cgp_computer]` function, read for whether it is +/// fallible. The check is purely syntactic: only a bare two-argument +/// `Result` counts, and any other type (a qualified +/// `core::result::Result` included) is a plain value. A type that starts +/// with `Result` but is not that exact form, such as a one-argument alias +/// `Result`, is rejected, since the macro cannot tell whether it is fallible. +pub struct MaybeResultType { + /// The `E` of `Result`, or `None` for a plain value. + pub error_type: Option, +} + +impl Parse for MaybeResultType { + fn parse(input: ParseStream) -> syn::Result { + let fork = input.fork(); + + if let Ok(ident) = fork.parse::() + && ident == "Result" + { + input.advance_to(&fork); + + let error_type = parse_result_arguments(input).map_err(|_| { + Error::new( + ident.span(), + "A `Result` return type must be written as `Result`, naming its error type", + ) + })?; + + Ok(Self { + error_type: Some(error_type), + }) + } else { + input.parse::()?; + + Ok(Self { error_type: None }) + } + } +} + +/// Parse the `` after `Result`, returning `E`. +fn parse_result_arguments(input: ParseStream) -> syn::Result { + let _: Lt = input.parse()?; + let _: Type = input.parse()?; + let _: Comma = input.parse()?; + let error_type = input.parse()?; + let _: Gt = input.parse()?; + + Ok(error_type) +} diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/mod.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/mod.rs new file mode 100644 index 000000000..371786e24 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/mod.rs @@ -0,0 +1,7 @@ +mod item; +mod maybe_result; +mod preprocessed; + +pub use item::*; +pub use maybe_result::*; +pub use preprocessed::*; diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/preprocessed.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/preprocessed.rs new file mode 100644 index 000000000..89abc7bf8 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_computer/preprocessed.rs @@ -0,0 +1,181 @@ +use cgp_macro_core::functions::override_item_span; +use cgp_macro_core::parse_internal; +use cgp_macro_core::types::cgp_provider::{ItemCgpProvider, ProviderArgs}; +use cgp_macro_core::types::delegate_component::DelegateTable; +use cgp_macro_core::types::keyword::Keyword; +use syn::punctuated::Punctuated; +use syn::token::Comma; +use syn::{Ident, ItemFn, ItemImpl, Type}; + +use crate::exports::{ + AsyncComputer, AsyncComputerComponent, AsyncComputerRefComponent, Computer, ComputerComponent, + ComputerRefComponent, HandlerComponent, HandlerRefComponent, PromoteAsyncComputer, + PromoteComputer, PromoteHandler, PromoteTryComputer, TryComputerComponent, + TryComputerRefComponent, +}; +use crate::types::handler_fn::EvaluatedHandlerFn; + +/// The read `#[cgp_computer]` input: the resolved provider name, the function, +/// its inputs rebound as `arg_0, arg_1, …`, its output type, and the two facts +/// that choose the expansion. +pub struct PreprocessedCgpComputer { + pub provider_ident: Ident, + pub item_fn: ItemFn, + pub input_idents: Punctuated, + pub input_types: Punctuated, + pub output: Type, + /// An `async` function implements `AsyncComputer` rather than `Computer`. + pub is_async: bool, + /// A `Result` return selects the fallible promotion bundle. + pub is_fallible: bool, +} + +impl PreprocessedCgpComputer { + /// Evaluate into the shared handler-function IR: a `Computer` (or + /// `AsyncComputer`) impl that calls the function, and a table routing the + /// rest of the handler family to the promotion bundle the two facts select. + pub fn eval(&self) -> syn::Result { + let provider_ident = &self.provider_ident; + let fn_ident = &self.item_fn.sig.ident; + let input_idents = &self.input_idents; + let input_types = &self.input_types; + let output = &self.output; + + // The function's generics carry over, and the reserved context and code + // parameters are appended after them. + let mut generics = self.item_fn.sig.generics.clone(); + generics.params.push(parse_internal!(__Context__)); + generics.params.push(parse_internal!(__Code__)); + + let (impl_generics, _, where_clause) = generics.split_for_impl(); + + let (item_impl, component_type, delegate_table): (ItemImpl, Type, DelegateTable) = + if self.is_async { + let item_impl = parse_internal! { + impl #impl_generics + #AsyncComputer<__Context__, __Code__, ( #input_types )> + for #provider_ident + #where_clause + { + type Output = #output; + + async fn compute_async( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + ( #input_idents ): ( #input_types ), + ) -> Self::Output { + #fn_ident( #input_idents ).await + } + } + }; + + // The synchronous members of the family cannot be derived from an + // async base, so only the async ones are promoted. + let delegate_table = if self.is_fallible { + parse_internal! { + #provider_ident { + [ + #AsyncComputerRefComponent, + #HandlerComponent, + #HandlerRefComponent, + ] -> + #PromoteHandler, + } + } + } else { + parse_internal! { + #provider_ident { + [ + #AsyncComputerRefComponent, + #HandlerComponent, + #HandlerRefComponent, + ] -> + #PromoteAsyncComputer, + } + } + }; + + ( + item_impl, + parse_internal!(#AsyncComputerComponent), + delegate_table, + ) + } else { + let item_impl = parse_internal! { + impl #impl_generics + #Computer<__Context__, __Code__, ( #input_types )> + for #provider_ident + #where_clause + { + type Output = #output; + + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + ( #input_idents ): ( #input_types ), + ) -> Self::Output { + #fn_ident( #input_idents ) + } + } + }; + + let delegate_table = if self.is_fallible { + parse_internal! { + #provider_ident { + [ + #ComputerRefComponent, + #TryComputerComponent, + #TryComputerRefComponent, + #AsyncComputerComponent, + #AsyncComputerRefComponent, + #HandlerComponent, + #HandlerRefComponent, + ] -> + #PromoteTryComputer, + } + } + } else { + parse_internal! { + #provider_ident { + [ + #ComputerRefComponent, + #TryComputerComponent, + #TryComputerRefComponent, + #AsyncComputerComponent, + #AsyncComputerRefComponent, + #HandlerComponent, + #HandlerRefComponent, + ] -> + #PromoteComputer, + } + } + }; + + ( + item_impl, + parse_internal!(#ComputerComponent), + delegate_table, + ) + }; + + // Re-span the impl's boundary tokens onto the function name, so an error on + // the generated impl (a conflict with another provider of the same name, + // say) points at the function rather than the whole attribute. See + // cgp-knowledge-base/cgp/implementation/README.md, "Spans". + let item_impl = override_item_span(fn_ident.span(), &item_impl)?; + + let provider = ItemCgpProvider { + args: ProviderArgs { + new: Some(Keyword::default()), + component_type: Some(component_type), + }, + item_impl, + }; + + Ok(EvaluatedHandlerFn { + item_fn: self.item_fn.clone(), + provider, + delegate_table, + }) + } +} diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_producer/item.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_producer/item.rs new file mode 100644 index 000000000..eef691f4f --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_producer/item.rs @@ -0,0 +1,87 @@ +use syn::{Error, Ident, ItemFn}; + +use crate::functions::{derive_provider_ident, return_type}; +use crate::types::cgp_producer::PreprocessedCgpProducer; +use crate::visitors::find_impl_trait; + +/// Raw input stage: the optional provider name from the attribute and the +/// annotated function. First stage of the `#[cgp_producer]` pipeline. +pub struct ItemCgpProducer { + pub ident: Option, + pub item_fn: ItemFn, +} + +impl ItemCgpProducer { + /// Check the function has a producer's shape and resolve the provider name + /// and output type, yielding the next stage. + pub fn preprocess(&self) -> syn::Result { + self.check_no_inputs()?; + self.check_not_async()?; + self.check_no_generics()?; + + let sig = &self.item_fn.sig; + + let provider_ident = match &self.ident { + Some(ident) => ident.clone(), + None => derive_provider_ident(&sig.ident), + }; + + let output = return_type(&sig.output)?; + + if let Some(impl_trait) = find_impl_trait(&output) { + return Err(Error::new_spanned( + impl_trait, + "Producer functions cannot return `impl Trait`", + )); + } + + Ok(PreprocessedCgpProducer { + provider_ident, + item_fn: self.item_fn.clone(), + output, + }) + } + + /// Reject any parameter, a receiver included: `Producer::produce` receives + /// only the context and the code tag, so there is no input to pass on. + fn check_no_inputs(&self) -> syn::Result<()> { + let inputs = &self.item_fn.sig.inputs; + + if inputs.is_empty() { + Ok(()) + } else { + Err(Error::new_spanned( + inputs, + "Producer functions cannot have parameters", + )) + } + } + + /// Reject `async`: the `Producer` trait is synchronous. + fn check_not_async(&self) -> syn::Result<()> { + match &self.item_fn.sig.asyncness { + None => Ok(()), + Some(asyncness) => Err(Error::new_spanned( + asyncness, + "Producer functions cannot be async", + )), + } + } + + /// Reject generic parameters, lifetimes included. The generated impl's only + /// parameters are the reserved context and code, and the function's own + /// parameters would appear in neither its trait arguments nor its self type, + /// so the compiler would reject them as unconstrained (`E0207`). + fn check_no_generics(&self) -> syn::Result<()> { + let params = &self.item_fn.sig.generics.params; + + if params.is_empty() { + Ok(()) + } else { + Err(Error::new_spanned( + params, + "Producer functions must have empty generic parameters", + )) + } + } +} diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_producer/mod.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_producer/mod.rs new file mode 100644 index 000000000..70721b5eb --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_producer/mod.rs @@ -0,0 +1,5 @@ +mod item; +mod preprocessed; + +pub use item::*; +pub use preprocessed::*; diff --git a/crates/macros/cgp-macro-extra-core/src/types/cgp_producer/preprocessed.rs b/crates/macros/cgp-macro-extra-core/src/types/cgp_producer/preprocessed.rs new file mode 100644 index 000000000..264ed8f54 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/cgp_producer/preprocessed.rs @@ -0,0 +1,82 @@ +use cgp_macro_core::functions::override_item_span; +use cgp_macro_core::parse_internal; +use cgp_macro_core::types::cgp_provider::{ItemCgpProvider, ProviderArgs}; +use cgp_macro_core::types::delegate_component::DelegateTable; +use cgp_macro_core::types::keyword::Keyword; +use syn::{Ident, ItemFn, ItemImpl, Type}; + +use crate::exports::{ + AsyncComputerComponent, AsyncComputerRefComponent, ComputerComponent, ComputerRefComponent, + HandlerComponent, HandlerRefComponent, Producer, ProducerComponent, PromoteProducer, + TryComputerComponent, TryComputerRefComponent, +}; +use crate::types::handler_fn::EvaluatedHandlerFn; + +/// The validated `#[cgp_producer]` input: the resolved provider name, the +/// function, and its output type. +pub struct PreprocessedCgpProducer { + pub provider_ident: Ident, + pub item_fn: ItemFn, + pub output: Type, +} + +impl PreprocessedCgpProducer { + /// Evaluate into the shared handler-function IR: a `Producer` impl that calls + /// the function, and a table routing every handler component to + /// `PromoteProducer`, which lets each handler shape yield the produced + /// value whatever input it is given. + pub fn eval(&self) -> syn::Result { + let provider_ident = &self.provider_ident; + let fn_ident = &self.item_fn.sig.ident; + let output = &self.output; + + let item_impl: ItemImpl = parse_internal! { + impl<__Context__, __Code__> #Producer<__Context__, __Code__> for #provider_ident { + type Output = #output; + + fn produce( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + ) -> Self::Output { + #fn_ident() + } + } + }; + + // Re-span the impl's boundary tokens onto the function name, so an error on + // the generated impl (a conflict with another provider of the same name, + // say) points at the function rather than the whole attribute. See + // cgp-knowledge-base/cgp/implementation/README.md, "Spans". + let item_impl = override_item_span(fn_ident.span(), &item_impl)?; + + let provider = ItemCgpProvider { + args: ProviderArgs { + new: Some(Keyword::default()), + component_type: Some(parse_internal!(#ProducerComponent)), + }, + item_impl, + }; + + let delegate_table: DelegateTable = parse_internal! { + #provider_ident { + [ + #ComputerComponent, + #ComputerRefComponent, + #TryComputerComponent, + #TryComputerRefComponent, + #AsyncComputerComponent, + #AsyncComputerRefComponent, + #HandlerComponent, + #HandlerRefComponent, + ]: + #PromoteProducer, + } + }; + + Ok(EvaluatedHandlerFn { + item_fn: self.item_fn.clone(), + provider, + delegate_table, + }) + } +} diff --git a/crates/macros/cgp-macro-extra-core/src/types/handler_fn/evaluated.rs b/crates/macros/cgp-macro-extra-core/src/types/handler_fn/evaluated.rs new file mode 100644 index 000000000..d78aab74f --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/handler_fn/evaluated.rs @@ -0,0 +1,22 @@ +use cgp_macro_core::types::cgp_provider::ItemCgpProvider; +use cgp_macro_core::types::delegate_component::DelegateTable; +use syn::ItemFn; + +/// The intermediate representation `#[cgp_computer]` and `#[cgp_producer]` +/// evaluate a function into, built from `cgp-macro-core` AST nodes rather than +/// emitted as nested macro invocations. +/// +/// Each field is what the macro would otherwise write out for another macro to +/// expand: `provider` is the `#[cgp_new_provider] impl …` block, and +/// `delegate_table` is the `delegate_components! { … }` body that promotes the +/// provider across the handler family. The entrypoint lowers both with +/// [`ItemCgpProvider::lower`] and [`DelegateTable::eval`]. +pub struct EvaluatedHandlerFn { + /// The annotated function, emitted unchanged. + pub item_fn: ItemFn, + /// The base provider impl, which declares the provider struct. + pub provider: ItemCgpProvider, + /// The wiring that routes the rest of the handler family to a promotion + /// bundle. + pub delegate_table: DelegateTable, +} diff --git a/crates/macros/cgp-macro-extra-core/src/types/handler_fn/mod.rs b/crates/macros/cgp-macro-extra-core/src/types/handler_fn/mod.rs new file mode 100644 index 000000000..7970adc40 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/handler_fn/mod.rs @@ -0,0 +1,3 @@ +mod evaluated; + +pub use evaluated::*; diff --git a/crates/macros/cgp-macro-extra-core/src/types/mod.rs b/crates/macros/cgp-macro-extra-core/src/types/mod.rs new file mode 100644 index 000000000..ea9668e8d --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/types/mod.rs @@ -0,0 +1,4 @@ +pub mod cgp_auto_dispatch; +pub mod cgp_computer; +pub mod cgp_producer; +pub mod handler_fn; diff --git a/crates/macros/cgp-macro-extra-core/src/visitors/elaborate_lifetimes.rs b/crates/macros/cgp-macro-extra-core/src/visitors/elaborate_lifetimes.rs new file mode 100644 index 000000000..e15824dae --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/visitors/elaborate_lifetimes.rs @@ -0,0 +1,116 @@ +use proc_macro2::Span; +use syn::visit::Visit; +use syn::visit_mut::{self, VisitMut}; +use syn::{Lifetime, ParenthesizedGenericArguments, Type, TypeBareFn, TypeReference}; + +/// Name every elided lifetime in a type: a reference written without one +/// (`&T`) and the placeholder `'_`, at any depth. +/// +/// A dispatch method's argument and return types are copied into a `where` +/// bound and a helper function's signature, where an elided lifetime is either +/// rejected (`E0637`) or means something else, so each one must be given a name. +/// In [`fresh`](Self::fresh) mode every elided lifetime gets its own new name, +/// matching how the compiler treats elided input lifetimes, and the names are +/// recorded in `introduced`; in [`fixed`](Self::fixed) mode each one gets the +/// same given lifetime, matching how elided output lifetimes take the lifetime +/// of `&self`. +/// +/// A function-pointer type (`fn(&T)`) and the `Fn(&T)` sugar bind their own +/// elided lifetimes, so the visitor leaves them alone. +pub struct ElaborateElidedLifetimes { + fixed: Option, + next_index: usize, + /// The lifetimes [`fresh`](Self::fresh) mode introduced, in order. + pub introduced: Vec, +} + +impl ElaborateElidedLifetimes { + /// Give each elided lifetime a new name, `'__a{n}__` counting from + /// `first_index`. + pub fn fresh(first_index: usize) -> Self { + Self { + fixed: None, + next_index: first_index, + introduced: Vec::new(), + } + } + + /// Give each elided lifetime the name `lifetime`. + pub fn fixed(lifetime: Lifetime) -> Self { + Self { + fixed: Some(lifetime), + next_index: 0, + introduced: Vec::new(), + } + } + + fn next_lifetime(&mut self) -> Lifetime { + match &self.fixed { + Some(lifetime) => lifetime.clone(), + None => { + let lifetime = + Lifetime::new(&format!("'__a{}__", self.next_index), Span::call_site()); + self.next_index += 1; + self.introduced.push(lifetime.clone()); + lifetime + } + } + } +} + +impl VisitMut for ElaborateElidedLifetimes { + fn visit_type_reference_mut(&mut self, node: &mut TypeReference) { + if node.lifetime.is_none() { + node.lifetime = Some(self.next_lifetime()); + } + + visit_mut::visit_type_reference_mut(self, node); + } + + fn visit_lifetime_mut(&mut self, node: &mut Lifetime) { + if node.ident == "_" { + *node = self.next_lifetime(); + } + } + + fn visit_type_bare_fn_mut(&mut self, _node: &mut TypeBareFn) {} + + fn visit_parenthesized_generic_arguments_mut( + &mut self, + _node: &mut ParenthesizedGenericArguments, + ) { + } +} + +/// The distinct lifetimes named in `types`, in order of first appearance, +/// skipping the ones a function-pointer type or `Fn(..)` sugar binds itself. +pub fn collect_lifetimes<'a>(types: impl IntoIterator) -> Vec { + let mut collector = CollectLifetimes::default(); + + for ty in types { + collector.visit_type(ty); + } + + collector.lifetimes +} + +#[derive(Default)] +struct CollectLifetimes { + lifetimes: Vec, +} + +impl<'ast> Visit<'ast> for CollectLifetimes { + fn visit_lifetime(&mut self, node: &'ast Lifetime) { + if !self.lifetimes.contains(node) { + self.lifetimes.push(node.clone()); + } + } + + fn visit_type_bare_fn(&mut self, _node: &'ast TypeBareFn) {} + + fn visit_parenthesized_generic_arguments( + &mut self, + _node: &'ast ParenthesizedGenericArguments, + ) { + } +} diff --git a/crates/macros/cgp-macro-extra-core/src/visitors/find_impl_trait.rs b/crates/macros/cgp-macro-extra-core/src/visitors/find_impl_trait.rs new file mode 100644 index 000000000..0e82d20e2 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/visitors/find_impl_trait.rs @@ -0,0 +1,31 @@ +use syn::visit::{self, Visit}; +use syn::{Type, TypeImplTrait}; + +/// The first `impl Trait` type inside `ty`, at any depth. +/// +/// A handler function's parameter and return types become a provider impl's +/// trait arguments and `Output` associated type, where `impl Trait` is not +/// allowed (`E0562`), so the function macros reject it up front. +pub fn find_impl_trait(ty: &Type) -> Option<&TypeImplTrait> { + let mut finder = FindImplTrait { found: None }; + finder.visit_type(ty); + finder.found +} + +struct FindImplTrait<'ast> { + found: Option<&'ast TypeImplTrait>, +} + +impl<'ast> Visit<'ast> for FindImplTrait<'ast> { + fn visit_type_impl_trait(&mut self, node: &'ast TypeImplTrait) { + if self.found.is_none() { + self.found = Some(node); + } + } + + fn visit_type(&mut self, node: &'ast Type) { + if self.found.is_none() { + visit::visit_type(self, node); + } + } +} diff --git a/crates/macros/cgp-macro-extra-core/src/visitors/mod.rs b/crates/macros/cgp-macro-extra-core/src/visitors/mod.rs new file mode 100644 index 000000000..3e6bb4ee7 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/visitors/mod.rs @@ -0,0 +1,5 @@ +mod elaborate_lifetimes; +mod find_impl_trait; + +pub use elaborate_lifetimes::*; +pub use find_impl_trait::*; diff --git a/crates/macros/cgp-macro-extra-lib/Cargo.toml b/crates/macros/cgp-macro-extra-lib/Cargo.toml new file mode 100644 index 000000000..a463b64ae --- /dev/null +++ b/crates/macros/cgp-macro-extra-lib/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cgp-macro-extra-lib" +version = "0.8.0-alpha" +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +keywords = { workspace = true } +categories = { workspace = true } +description = { workspace = true } + +[dependencies] +cgp-macro-core = { workspace = true } +cgp-macro-extra-core = { workspace = true } + +syn = { version = "2.0.95", features = [ "full", "extra-traits" ] } +quote = { version = "1.0.38" } +proc-macro2 = { version = "1.0.92" } diff --git a/crates/macros/cgp-macro-extra-lib/src/cgp_auto_dispatch.rs b/crates/macros/cgp-macro-extra-lib/src/cgp_auto_dispatch.rs new file mode 100644 index 000000000..d5c27cd13 --- /dev/null +++ b/crates/macros/cgp-macro-extra-lib/src/cgp_auto_dispatch.rs @@ -0,0 +1,33 @@ +use cgp_macro_extra_core::types::cgp_auto_dispatch::ItemCgpAutoDispatch; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Error, ItemTrait, parse2}; + +use crate::handler_fn::lower_handler_fn; + +pub fn cgp_auto_dispatch(attr: TokenStream, body: TokenStream) -> syn::Result { + if !attr.is_empty() { + return Err(Error::new_spanned( + attr, + "`#[cgp_auto_dispatch]` takes no arguments", + )); + } + + let item_trait: ItemTrait = parse2(body)?; + + let evaluated = ItemCgpAutoDispatch { item_trait }.preprocess()?.eval()?; + + let item_trait = &evaluated.item_trait; + let blanket_impl = &evaluated.blanket_impl; + + let mut output = quote! { + #item_trait + #blanket_impl + }; + + for computer in evaluated.computers.iter() { + output.extend(lower_handler_fn(&computer.preprocess()?.eval()?)?); + } + + Ok(output) +} diff --git a/crates/macros/cgp-macro-extra-lib/src/cgp_computer.rs b/crates/macros/cgp-macro-extra-lib/src/cgp_computer.rs new file mode 100644 index 000000000..8233c5f52 --- /dev/null +++ b/crates/macros/cgp-macro-extra-lib/src/cgp_computer.rs @@ -0,0 +1,15 @@ +use cgp_macro_extra_core::types::cgp_computer::ItemCgpComputer; +use proc_macro2::TokenStream; +use syn::{Ident, ItemFn, parse2}; + +use crate::handler_fn::lower_handler_fn; + +pub fn cgp_computer(attr: TokenStream, body: TokenStream) -> syn::Result { + let item_fn: ItemFn = parse2(body)?; + + let ident: Option = parse2(attr)?; + + let evaluated = ItemCgpComputer { ident, item_fn }.preprocess()?.eval()?; + + lower_handler_fn(&evaluated) +} diff --git a/crates/macros/cgp-macro-extra-lib/src/cgp_producer.rs b/crates/macros/cgp-macro-extra-lib/src/cgp_producer.rs new file mode 100644 index 000000000..713cbb273 --- /dev/null +++ b/crates/macros/cgp-macro-extra-lib/src/cgp_producer.rs @@ -0,0 +1,15 @@ +use cgp_macro_extra_core::types::cgp_producer::ItemCgpProducer; +use proc_macro2::TokenStream; +use syn::{Ident, ItemFn, parse2}; + +use crate::handler_fn::lower_handler_fn; + +pub fn cgp_producer(attr: TokenStream, body: TokenStream) -> syn::Result { + let item_fn: ItemFn = parse2(body)?; + + let ident: Option = parse2(attr)?; + + let evaluated = ItemCgpProducer { ident, item_fn }.preprocess()?.eval()?; + + lower_handler_fn(&evaluated) +} diff --git a/crates/macros/cgp-macro-extra-lib/src/handler_fn.rs b/crates/macros/cgp-macro-extra-lib/src/handler_fn.rs new file mode 100644 index 000000000..75d615246 --- /dev/null +++ b/crates/macros/cgp-macro-extra-lib/src/handler_fn.rs @@ -0,0 +1,18 @@ +use cgp_macro_extra_core::types::handler_fn::EvaluatedHandlerFn; +use proc_macro2::TokenStream; +use quote::quote; + +/// Lower the handler-function IR to its final form: the function unchanged, the +/// provider impl with its `IsProviderFor` impl and struct, and the promotion +/// wiring's `DelegateComponent` impls. +pub fn lower_handler_fn(evaluated: &EvaluatedHandlerFn) -> syn::Result { + let item_fn = &evaluated.item_fn; + let provider = evaluated.provider.lower()?; + let delegate_table = evaluated.delegate_table.eval()?; + + Ok(quote! { + #item_fn + #provider + #delegate_table + }) +} diff --git a/crates/macros/cgp-extra-macro-lib/src/entrypoints/mod.rs b/crates/macros/cgp-macro-extra-lib/src/lib.rs similarity index 89% rename from crates/macros/cgp-extra-macro-lib/src/entrypoints/mod.rs rename to crates/macros/cgp-macro-extra-lib/src/lib.rs index 3d643a6fd..5bc532c3a 100644 --- a/crates/macros/cgp-extra-macro-lib/src/entrypoints/mod.rs +++ b/crates/macros/cgp-macro-extra-lib/src/lib.rs @@ -1,6 +1,7 @@ mod cgp_auto_dispatch; mod cgp_computer; mod cgp_producer; +mod handler_fn; pub use cgp_auto_dispatch::*; pub use cgp_computer::*; diff --git a/crates/macros/cgp-extra-macro/Cargo.toml b/crates/macros/cgp-macro-extra/Cargo.toml similarity index 86% rename from crates/macros/cgp-extra-macro/Cargo.toml rename to crates/macros/cgp-macro-extra/Cargo.toml index d72ee9e6c..6d49f7b7f 100644 --- a/crates/macros/cgp-extra-macro/Cargo.toml +++ b/crates/macros/cgp-macro-extra/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cgp-extra-macro" +name = "cgp-macro-extra" version = "0.8.0-alpha" edition = { workspace = true } license = { workspace = true } @@ -19,4 +19,4 @@ default = [] [dependencies] syn = { version = "2.0.95", features = [ "full", "extra-traits" ] } -cgp-extra-macro-lib = { workspace = true } +cgp-macro-extra-lib = { workspace = true } diff --git a/crates/macros/cgp-macro-extra/src/lib.rs b/crates/macros/cgp-macro-extra/src/lib.rs new file mode 100644 index 000000000..72adb4c87 --- /dev/null +++ b/crates/macros/cgp-macro-extra/src/lib.rs @@ -0,0 +1,109 @@ +#![no_std] + +/*! + This crate provides the proc macros that define handler providers from + functions, `#[cgp_computer]` and `#[cgp_producer]`, and the + `#[cgp_auto_dispatch]` macro that extends a per-type trait to extensible + enums. Each one forwards to `cgp-macro-extra-lib`. +*/ + +use proc_macro::TokenStream; + +/** + `#[cgp_producer]` defines a `Producer` provider from a function that takes + no input. + + The provider is named after the function in PascalCase, or after the + identifier given as the attribute's argument. It is wired so the same + function answers every member of the handler family, each returning the + produced value whatever input it is given. The function must be + synchronous, with no parameters, no generic parameters, and no + `impl Trait` return type. + + ## Example + + ```rust,ignore + #[cgp_producer] + fn magic_number() -> u64 { + 42 + } + ``` + + defines the provider `MagicNumber`, so `MagicNumber::produce(&context, code)` + returns `42`. The full reference is at + . +*/ +#[proc_macro_attribute] +pub fn cgp_producer(attr: TokenStream, body: TokenStream) -> TokenStream { + cgp_macro_extra_lib::cgp_producer(attr.into(), body.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/** + `#[cgp_computer]` defines a `Computer` provider from a function. + + The function's parameters become the provider's input and its return type + its output. A synchronous function implements `Computer` and an `async` one + `AsyncComputer`, and a return type written `Result` makes the fallible + members of the handler family propagate the error. The provider is wired so + the same function answers every member of the family it can reach. The + provider is named after the function in PascalCase, or after the identifier + given as the attribute's argument. + + ## Example + + ```rust,ignore + #[cgp_computer] + fn add(a: u64, b: u64) -> u64 { + a + b + } + ``` + + defines the provider `Add`, so `Add::compute(&context, code, (1, 2))` + returns `3`. The full reference is at + . +*/ +#[proc_macro_attribute] +pub fn cgp_computer(attr: TokenStream, body: TokenStream) -> TokenStream { + cgp_macro_extra_lib::cgp_computer(attr.into(), body.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/** + `#[cgp_auto_dispatch]` implements a trait for every extensible enum whose + variants' payloads implement it. + + The trait is kept unchanged. The macro adds a blanket impl that matches the + enum's current variant and calls the same method on its payload, plus one + per-variant `Computer` provider per method, named `Compute` followed by the + method name in PascalCase. Every method must take `self`, `&self`, or + `&mut self`, and must not have type or const generic parameters; the + attribute takes no arguments. + + ## Example + + ```rust,ignore + #[cgp_auto_dispatch] + pub trait HasArea { + fn area(&self) -> f64; + } + + #[derive(CgpData)] + pub enum Shape { + Circle(Circle), + Rectangle(Rectangle), + } + ``` + + With `HasArea` implemented for `Circle` and `Rectangle`, `Shape` implements + it too. The full reference is at + . +*/ +#[proc_macro_attribute] +pub fn cgp_auto_dispatch(attr: TokenStream, body: TokenStream) -> TokenStream { + cgp_macro_extra_lib::cgp_auto_dispatch(attr.into(), body.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} diff --git a/crates/macros/cgp-macro-test-util-lib/Cargo.toml b/crates/macros/cgp-macro-test-util-lib/Cargo.toml index ded4573b9..71d9e1b36 100644 --- a/crates/macros/cgp-macro-test-util-lib/Cargo.toml +++ b/crates/macros/cgp-macro-test-util-lib/Cargo.toml @@ -12,10 +12,11 @@ categories = { workspace = true } description = { workspace = true } [dependencies] -cgp-macro-lib = { workspace = true } -cgp-macro-core = { workspace = true } +cgp-macro-lib = { workspace = true } +cgp-macro-core = { workspace = true } +cgp-macro-extra-lib = { workspace = true } -syn = { version = "2.0.95" } -quote = { version = "1.0.38" } -proc-macro2 = { version = "1.0.92" } -prettyplease = { version = "0.2.37" } +syn = { version = "2.0.95" } +quote = { version = "1.0.38" } +proc-macro2 = { version = "1.0.92" } +prettyplease = { version = "0.2.37" } diff --git a/crates/macros/cgp-macro-test-util-lib/src/entrypoints/mod.rs b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/mod.rs index aef0db18d..f0312e55d 100644 --- a/crates/macros/cgp-macro-test-util-lib/src/entrypoints/mod.rs +++ b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/mod.rs @@ -1,11 +1,14 @@ mod snapshot_blanket_trait; +mod snapshot_cgp_auto_dispatch; mod snapshot_cgp_auto_getter; mod snapshot_cgp_component; +mod snapshot_cgp_computer; mod snapshot_cgp_fn; mod snapshot_cgp_getter; mod snapshot_cgp_impl; mod snapshot_cgp_namespace; mod snapshot_cgp_new_provider; +mod snapshot_cgp_producer; mod snapshot_cgp_provider; mod snapshot_cgp_type; mod snapshot_check_components; @@ -19,13 +22,16 @@ mod snapshot_derive_has_field; mod snapshot_derive_has_fields; pub use snapshot_blanket_trait::*; +pub use snapshot_cgp_auto_dispatch::*; pub use snapshot_cgp_auto_getter::*; pub use snapshot_cgp_component::*; +pub use snapshot_cgp_computer::*; pub use snapshot_cgp_fn::*; pub use snapshot_cgp_getter::*; pub use snapshot_cgp_impl::*; pub use snapshot_cgp_namespace::*; pub use snapshot_cgp_new_provider::*; +pub use snapshot_cgp_producer::*; pub use snapshot_cgp_provider::*; pub use snapshot_cgp_type::*; pub use snapshot_check_components::*; diff --git a/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_cgp_auto_dispatch.rs b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_cgp_auto_dispatch.rs new file mode 100644 index 000000000..4ed02c09c --- /dev/null +++ b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_cgp_auto_dispatch.rs @@ -0,0 +1,14 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{ItemTrait, parse2}; + +use crate::keywords::CgpAutoDispatch; +use crate::types::AttributeMacroSnapshot; + +pub fn snapshot_cgp_auto_dispatch(body: TokenStream) -> syn::Result { + let item: AttributeMacroSnapshot = parse2(body)?; + + let output = cgp_macro_extra_lib::cgp_auto_dispatch(item.attr, item.body.to_token_stream())?; + + item.snapshot.wrap_output(output) +} diff --git a/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_cgp_computer.rs b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_cgp_computer.rs new file mode 100644 index 000000000..c50931fc0 --- /dev/null +++ b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_cgp_computer.rs @@ -0,0 +1,14 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{ItemFn, parse2}; + +use crate::keywords::CgpComputer; +use crate::types::AttributeMacroSnapshot; + +pub fn snapshot_cgp_computer(body: TokenStream) -> syn::Result { + let item: AttributeMacroSnapshot = parse2(body)?; + + let output = cgp_macro_extra_lib::cgp_computer(item.attr, item.body.to_token_stream())?; + + item.snapshot.wrap_output(output) +} diff --git a/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_cgp_producer.rs b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_cgp_producer.rs new file mode 100644 index 000000000..78a030fe3 --- /dev/null +++ b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_cgp_producer.rs @@ -0,0 +1,14 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{ItemFn, parse2}; + +use crate::keywords::CgpProducer; +use crate::types::AttributeMacroSnapshot; + +pub fn snapshot_cgp_producer(body: TokenStream) -> syn::Result { + let item: AttributeMacroSnapshot = parse2(body)?; + + let output = cgp_macro_extra_lib::cgp_producer(item.attr, item.body.to_token_stream())?; + + item.snapshot.wrap_output(output) +} diff --git a/crates/macros/cgp-macro-test-util-lib/src/keywords.rs b/crates/macros/cgp-macro-test-util-lib/src/keywords.rs index 104f52480..c656f6f05 100644 --- a/crates/macros/cgp-macro-test-util-lib/src/keywords.rs +++ b/crates/macros/cgp-macro-test-util-lib/src/keywords.rs @@ -39,3 +39,9 @@ define_keyword!(BuildField, "BuildField"); define_keyword!(ExtractField, "ExtractField"); define_keyword!(FromVariant, "FromVariant"); + +define_keyword!(CgpComputer, "cgp_computer"); + +define_keyword!(CgpProducer, "cgp_producer"); + +define_keyword!(CgpAutoDispatch, "cgp_auto_dispatch"); diff --git a/crates/macros/cgp-macro-test-util/README.md b/crates/macros/cgp-macro-test-util/README.md index f3db5726a..1c436069d 100644 --- a/crates/macros/cgp-macro-test-util/README.md +++ b/crates/macros/cgp-macro-test-util/README.md @@ -32,7 +32,8 @@ cgp-macro-test-util/ # proc-macro entry points (#[proc_macro] fns) └── functions/ # parse_attribute, pretty_format ``` -The implementation calls the real macro logic in `cgp-macro-lib`, not a copy of it, +The implementation calls the real macro logic in `cgp-macro-lib` and +`cgp-macro-extra-lib`, not a copy of it, so a snapshot is guaranteed to show what the production macros generate. ## Available macros @@ -54,6 +55,13 @@ so a snapshot is guaranteed to show what the production macros generate. | `snapshot_check_components!` | `check_components!` | | `snapshot_delegate_and_check_components!` | `delegate_and_check_components!` | | `snapshot_cgp_namespace!` | `cgp_namespace!` | +| `snapshot_blanket_trait!` | `#[blanket_trait]` | +| `snapshot_derive_build_field!` | `#[derive(BuildField)]` | +| `snapshot_derive_extract_field!` | `#[derive(ExtractField)]` | +| `snapshot_derive_from_variant!` | `#[derive(FromVariant)]` | +| `snapshot_cgp_computer!` | `#[cgp_computer]` | +| `snapshot_cgp_producer!` | `#[cgp_producer]` | +| `snapshot_cgp_auto_dispatch!` | `#[cgp_auto_dispatch]` | Each accepts the same argument forms as the macro it wraps — `snapshot_cgp_component!` takes both `#[cgp_component(Greeter)]` and the brace form, for instance — precisely diff --git a/crates/macros/cgp-macro-test-util/src/lib.rs b/crates/macros/cgp-macro-test-util/src/lib.rs index 6d6c25104..0f037e9bd 100644 --- a/crates/macros/cgp-macro-test-util/src/lib.rs +++ b/crates/macros/cgp-macro-test-util/src/lib.rs @@ -133,3 +133,24 @@ pub fn snapshot_derive_from_variant(body: TokenStream) -> TokenStream { .unwrap_or_else(syn::Error::into_compile_error) .into() } + +#[proc_macro] +pub fn snapshot_cgp_computer(body: TokenStream) -> TokenStream { + entrypoints::snapshot_cgp_computer(body.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro] +pub fn snapshot_cgp_producer(body: TokenStream) -> TokenStream { + entrypoints::snapshot_cgp_producer(body.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro] +pub fn snapshot_cgp_auto_dispatch(body: TokenStream) -> TokenStream { + entrypoints::snapshot_cgp_auto_dispatch(body.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} diff --git a/crates/main/cgp-extra/Cargo.toml b/crates/main/cgp-extra/Cargo.toml index 911520391..71c5c6d58 100644 --- a/crates/main/cgp-extra/Cargo.toml +++ b/crates/main/cgp-extra/Cargo.toml @@ -17,7 +17,7 @@ full = [ "cgp-error-extra/alloc" ] [dependencies] cgp-core = { workspace = true } -cgp-extra-macro = { workspace = true } +cgp-macro-extra = { workspace = true } cgp-error-extra = { workspace = true } cgp-field-extra = { workspace = true } cgp-run = { workspace = true } diff --git a/crates/main/cgp-extra/src/prelude.rs b/crates/main/cgp-extra/src/prelude.rs index 531388653..20674cce4 100644 --- a/crates/main/cgp-extra/src/prelude.rs +++ b/crates/main/cgp-extra/src/prelude.rs @@ -2,7 +2,6 @@ pub use cgp_dispatch::{ MatchFirstWithValueHandlers, MatchFirstWithValueHandlersMut, MatchFirstWithValueHandlersRef, MatchWithValueHandlers, MatchWithValueHandlersMut, MatchWithValueHandlersRef, }; -pub use cgp_extra_macro::{cgp_auto_dispatch, cgp_computer, cgp_producer}; pub use cgp_handler::{ AsyncComputer, AsyncComputerComponent, AsyncComputerRef, AsyncComputerRefComponent, Computer, ComputerComponent, ComputerRefComponent, Handler, HandlerComponent, HandlerRefComponent, @@ -10,3 +9,4 @@ pub use cgp_handler::{ PromoteProducer, PromoteTryComputer, TryComputer, TryComputerComponent, TryComputerRefComponent, }; +pub use cgp_macro_extra::{cgp_auto_dispatch, cgp_computer, cgp_producer}; diff --git a/crates/tests/AGENTS.md b/crates/tests/AGENTS.md index 2ffaf438a..48d385fae 100644 --- a/crates/tests/AGENTS.md +++ b/crates/tests/AGENTS.md @@ -109,6 +109,8 @@ genuinely distinct variants) in its owning target, and nowhere else: | `cgp_namespace!` | `namespaces` | | `#[blanket_trait]` | `blanket_traits` | | `#[derive(HasField)]` / `HasFields` / `CgpData` | `field_access` / `extensible_records` / `extensible_variants` | +| `#[cgp_computer]` / `#[cgp_producer]` | `handlers` | +| `#[cgp_auto_dispatch]` | `auto_dispatch` | When a file uses one of these macros as **incidental scaffolding** — a `#[cgp_component]` needed to set up a `delegate_components!` test, say — write the diff --git a/crates/tests/cgp-macro-tests/Cargo.toml b/crates/tests/cgp-macro-tests/Cargo.toml index 53d72a8ea..b3fe5823e 100644 --- a/crates/tests/cgp-macro-tests/Cargo.toml +++ b/crates/tests/cgp-macro-tests/Cargo.toml @@ -18,7 +18,7 @@ cgp-macro-test-util = { workspace = true } insta = { version = "1.48.0" } [dev-dependencies] -cgp-extra-macro-lib = { workspace = true } +cgp-macro-extra-lib = { workspace = true } cgp-macro-core = { workspace = true } cgp-macro-lib = { workspace = true } cgp-macro-test-util-lib = { workspace = true } diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_auto_dispatch.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_auto_dispatch.rs index 81e18c399..59af3eef4 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_auto_dispatch.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_auto_dispatch.rs @@ -1,7 +1,10 @@ //! `#[cgp_auto_dispatch]` rejects dispatch traits it cannot lower: a non-method //! trait item, a method without a `self` receiver, and a method with a type //! generic parameter, which the blanket impl could only bound with a quantified -//! constraint Rust does not have. +//! constraint Rust does not have. Associated consts and const generic methods are +//! rejected the same way as associated types and type generic methods. A typed +//! receiver such as `self: Box`, which no matcher can take, and any +//! attribute argument are rejected too. Each case pins the rejection's message. //! //! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md (Tests) for //! these failure cases, and cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md for @@ -9,48 +12,134 @@ use quote::quote; -use super::assert_macro_rejects; +use super::assert_macro_rejects_with; #[test] fn rejects_associated_type() { - assert_macro_rejects("cgp_auto_dispatch with an associated type", || { - cgp_extra_macro_lib::cgp_auto_dispatch( - quote!(), - quote!( - pub trait HasArea { - type Area; - - fn area(&self) -> f64; - } - ), - ) - }); + assert_macro_rejects_with( + "cgp_auto_dispatch with an associated type", + "Only function items are allowed in a dispatch trait", + || { + cgp_macro_extra_lib::cgp_auto_dispatch( + quote!(), + quote!( + pub trait HasArea { + type Area; + + fn area(&self) -> f64; + } + ), + ) + }, + ); } #[test] fn rejects_method_without_receiver() { - assert_macro_rejects("cgp_auto_dispatch on a method without self", || { - cgp_extra_macro_lib::cgp_auto_dispatch( - quote!(), - quote!( - pub trait HasArea { - fn unit_area() -> f64; - } - ), - ) - }); + assert_macro_rejects_with( + "cgp_auto_dispatch on a method without self", + "Dispatcher method must have a self argument", + || { + cgp_macro_extra_lib::cgp_auto_dispatch( + quote!(), + quote!( + pub trait HasArea { + fn unit_area() -> f64; + } + ), + ) + }, + ); } #[test] fn rejects_generic_method() { - assert_macro_rejects("cgp_auto_dispatch on a generic method", || { - cgp_extra_macro_lib::cgp_auto_dispatch( - quote!(), - quote!( - pub trait CanScale { - fn scale>(&mut self, factor: T); - } - ), - ) - }); + assert_macro_rejects_with( + "cgp_auto_dispatch on a generic method", + "Dispatch trait methods cannot contain non-lifetime generic parameters due to the lack of quantified constraints in Rust", + || { + cgp_macro_extra_lib::cgp_auto_dispatch( + quote!(), + quote!( + pub trait CanScale { + fn scale>(&mut self, factor: T); + } + ), + ) + }, + ); +} + +#[test] +fn rejects_associated_const() { + assert_macro_rejects_with( + "cgp_auto_dispatch with an associated const", + "Only function items are allowed in a dispatch trait", + || { + cgp_macro_extra_lib::cgp_auto_dispatch( + quote!(), + quote!( + pub trait HasArea { + const SIDES: u8; + + fn area(&self) -> f64; + } + ), + ) + }, + ); +} + +#[test] +fn rejects_const_generic_method() { + assert_macro_rejects_with( + "cgp_auto_dispatch on a const-generic method", + "Dispatch trait methods cannot contain non-lifetime generic parameters due to the lack of quantified constraints in Rust", + || { + cgp_macro_extra_lib::cgp_auto_dispatch( + quote!(), + quote!( + pub trait CanScale { + fn scale(&mut self); + } + ), + ) + }, + ); +} + +#[test] +fn rejects_typed_receiver() { + assert_macro_rejects_with( + "cgp_auto_dispatch on a method with a typed receiver", + "Dispatcher method receiver must be `self`, `&self`, or `&mut self`", + || { + cgp_macro_extra_lib::cgp_auto_dispatch( + quote!(), + quote!( + pub trait HasArea { + fn area(self: Box) -> f64; + } + ), + ) + }, + ); +} + +#[test] +fn rejects_attribute_arguments() { + assert_macro_rejects_with( + "cgp_auto_dispatch with an attribute argument", + "`#[cgp_auto_dispatch]` takes no arguments", + || { + cgp_macro_extra_lib::cgp_auto_dispatch( + quote!(AreaDispatcher), + quote!( + pub trait HasArea { + fn area(&self) -> f64; + } + ), + ) + }, + ); } diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_computer.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_computer.rs index 8b641a279..a63b54c3b 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_computer.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_computer.rs @@ -1,6 +1,8 @@ //! `#[cgp_computer]` rejects a function with a `self` receiver, since a handler -//! provider has no receiver, and a one-argument `Result` alias, which its -//! syntactic `Result` detection cannot parse. +//! provider has no receiver, a one-argument `Result` alias, which its +//! syntactic `Result` detection cannot read, `impl Trait` in a parameter or the +//! return type, which a provider impl cannot name, and a provider name that is a +//! path rather than an identifier. Each case pins the rejection's message. //! //! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md (Tests) for these //! failure cases, and cgp-knowledge-base/cgp/reference/macros/cgp_computer.md for the @@ -8,34 +10,98 @@ use quote::quote; -use super::assert_macro_rejects; +use super::assert_macro_rejects_with; #[test] fn rejects_self_receiver() { - assert_macro_rejects("cgp_computer on a method with a receiver", || { - cgp_extra_macro_lib::cgp_computer( - quote!(), - quote!( - fn add(&self, b: u64) -> u64 { - b - } - ), - ) - }); + assert_macro_rejects_with( + "cgp_computer on a method with a receiver", + "Computer functions cannot have a receiver", + || { + cgp_macro_extra_lib::cgp_computer( + quote!(), + quote!( + fn add(&self, b: u64) -> u64 { + b + } + ), + ) + }, + ); } #[test] fn rejects_one_argument_result_alias() { - // Recorded as a Known issue: `Result` starts with `Result`, so the parser - // demands a second argument and fails with "expected `,`". - assert_macro_rejects("cgp_computer returning a one-argument Result alias", || { - cgp_extra_macro_lib::cgp_computer( - quote!(), - quote!( - fn parse(value: String) -> Result { - todo!() - } - ), - ) - }); + // `Result` starts with `Result`, so the macro cannot tell whether it is + // fallible without its error type, and asks for the two-argument form. + assert_macro_rejects_with( + "cgp_computer returning a one-argument Result alias", + "A `Result` return type must be written as `Result`, naming its error type", + || { + cgp_macro_extra_lib::cgp_computer( + quote!(), + quote!( + fn parse(value: String) -> Result { + todo!() + } + ), + ) + }, + ); +} + +#[test] +fn rejects_path_provider_name() { + // The provider name is a single identifier; a path cannot name the struct + // the macro declares. + assert_macro_rejects_with( + "cgp_computer with a path as the provider name", + "unexpected token", + || { + cgp_macro_extra_lib::cgp_computer( + quote!(providers::Add), + quote!( + fn add(a: u64, b: u64) -> u64 { + a + b + } + ), + ) + }, + ); +} + +#[test] +fn rejects_impl_trait_parameter() { + assert_macro_rejects_with( + "cgp_computer with an impl Trait parameter", + "Computer function parameters cannot use `impl Trait`; declare a generic parameter instead", + || { + cgp_macro_extra_lib::cgp_computer( + quote!(), + quote!( + fn show(values: Vec) -> usize { + values.len() + } + ), + ) + }, + ); +} + +#[test] +fn rejects_impl_trait_return() { + assert_macro_rejects_with( + "cgp_computer returning impl Trait", + "Computer functions cannot return `impl Trait`", + || { + cgp_macro_extra_lib::cgp_computer( + quote!(), + quote!( + fn show(value: u64) -> impl core::fmt::Display { + value + } + ), + ) + }, + ); } diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs index d59a78ae5..9a5918cfd 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs @@ -7,7 +7,8 @@ //! arguments. Each is a rejection the macro makes during expansion (a returned //! `Err`), so it is pinned by driving the entrypoint directly here rather than by a //! `trybuild` compile-fail fixture, which is reserved for input the macro accepts -//! but whose expansion then fails to compile. +//! but whose expansion then fails to compile. One case pins a Known issue instead: +//! a function named with a raw identifier makes the macro panic. //! //! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_fn.md (Tests) for these failure cases, //! and cgp-knowledge-base/cgp/reference/attributes/implicit.md for the user-facing rules on where @@ -149,3 +150,20 @@ fn rejects_implicit_attr_name_value() { ) }); } + +#[test] +#[should_panic(expected = "is not a valid Ident")] +fn panics_on_raw_function_name() { + // A Known issue rather than an intended rejection: the default trait name is + // built from the raw spelling, so `fn r#type` yields `R#type`, which is not a + // valid identifier, and the macro panics instead of returning an error. The + // fix is to unraw the name first; this test then becomes a behavioral test. + let _ = cgp_macro_lib::cgp_fn( + quote!(), + quote! { + fn r#type(&self) -> u64 { + 1 + } + }, + ); +} diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_producer.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_producer.rs index 59e07e809..cf678887b 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_producer.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_producer.rs @@ -1,6 +1,8 @@ //! `#[cgp_producer]` rejects every signature that is not a synchronous, //! non-generic function with no parameters: a parameter, a `self` receiver, an -//! `async` function, and a generic parameter. +//! `async` function, and a generic parameter, plus an `impl Trait` return type, +//! which a provider impl cannot name, and a provider name that is a path rather +//! than an identifier. Each case pins the rejection's message. //! //! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_producer.md (Tests) for these //! failure cases, and cgp-knowledge-base/cgp/reference/macros/cgp_producer.md for the @@ -8,60 +10,112 @@ use quote::quote; -use super::assert_macro_rejects; +use super::assert_macro_rejects_with; #[test] fn rejects_parameter() { - assert_macro_rejects("cgp_producer with a parameter", || { - cgp_extra_macro_lib::cgp_producer( - quote!(), - quote!( - fn magic_number(seed: u64) -> u64 { - seed - } - ), - ) - }); + assert_macro_rejects_with( + "cgp_producer with a parameter", + "Producer functions cannot have parameters", + || { + cgp_macro_extra_lib::cgp_producer( + quote!(), + quote!( + fn magic_number(seed: u64) -> u64 { + seed + } + ), + ) + }, + ); } #[test] fn rejects_self_receiver() { - assert_macro_rejects("cgp_producer with a receiver", || { - cgp_extra_macro_lib::cgp_producer( - quote!(), - quote!( - fn magic_number(&self) -> u64 { - 42 - } - ), - ) - }); + assert_macro_rejects_with( + "cgp_producer with a receiver", + "Producer functions cannot have parameters", + || { + cgp_macro_extra_lib::cgp_producer( + quote!(), + quote!( + fn magic_number(&self) -> u64 { + 42 + } + ), + ) + }, + ); } #[test] fn rejects_async() { - assert_macro_rejects("cgp_producer on an async function", || { - cgp_extra_macro_lib::cgp_producer( - quote!(), - quote!( - async fn magic_number() -> u64 { - 42 - } - ), - ) - }); + assert_macro_rejects_with( + "cgp_producer on an async function", + "Producer functions cannot be async", + || { + cgp_macro_extra_lib::cgp_producer( + quote!(), + quote!( + async fn magic_number() -> u64 { + 42 + } + ), + ) + }, + ); } #[test] fn rejects_generic_parameter() { - assert_macro_rejects("cgp_producer with a generic parameter", || { - cgp_extra_macro_lib::cgp_producer( - quote!(), - quote!( - fn default_value() -> T { - T::default() - } - ), - ) - }); + assert_macro_rejects_with( + "cgp_producer with a generic parameter", + "Producer functions must have empty generic parameters", + || { + cgp_macro_extra_lib::cgp_producer( + quote!(), + quote!( + fn default_value() -> T { + T::default() + } + ), + ) + }, + ); +} + +#[test] +fn rejects_path_provider_name() { + assert_macro_rejects_with( + "cgp_producer with a path as the provider name", + "unexpected token", + || { + cgp_macro_extra_lib::cgp_producer( + quote!(providers::MagicNumber), + quote!( + fn magic_number() -> u64 { + 42 + } + ), + ) + }, + ); +} + +#[test] +fn rejects_impl_trait_return() { + assert_macro_rejects_with( + "cgp_producer returning impl Trait", + "Producer functions cannot return `impl Trait`", + || { + cgp_macro_extra_lib::cgp_producer( + quote!(), + quote!( + fn magic_number() -> impl core::fmt::Display { + 42 + } + ), + ) + }, + ); } diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs index 877862c2a..bd2b0e44e 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs @@ -1,6 +1,6 @@ //! Failure cases: inputs the CGP macros must reject. //! -//! A rejection test drives a `cgp-macro-lib` or `cgp-extra-macro-lib` +//! A rejection test drives a `cgp-macro-lib` or `cgp-macro-extra-lib` //! entrypoint (or a `cgp-macro-core` parser) with an invalid input and asserts it //! returns `Err` rather than producing tokens. This is how we pin down which code //! CGP deliberately refuses, and catch regressions where a macro starts accepting @@ -8,7 +8,9 @@ //! //! To add a case: //! 1. call the entrypoint, e.g. `cgp_macro_lib::cgp_component(attr, body)`; -//! 2. assert the result is `Err` with [`assert_macro_rejects`]; +//! 2. assert the result is `Err` with [`assert_macro_rejects`], or with +//! [`assert_macro_rejects_with`] to also pin the error message, so the case +//! cannot start passing because the input fails for an unrelated reason; //! 3. if the rejection corresponds to a documented limitation, note it in the //! owning reference document's `## Known issues` section and link to it here. @@ -23,6 +25,29 @@ pub fn assert_macro_rejects(label: &str, run: impl FnOnce() -> syn::Result syn::Result, +) { + match run() { + Ok(tokens) => { + panic!("expected `{label}` to be rejected, but it expanded to:\n{tokens}") + } + Err(error) => assert_eq!( + error.to_string(), + message, + "`{label}` was rejected with an unexpected message" + ), + } +} + pub mod blanket_trait; pub mod cgp_auto_dispatch; pub mod cgp_component; diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_generics.rs b/crates/tests/cgp-tests/tests/auto_dispatch/async_generics.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_generics.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/async_generics.rs diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_multi_args.rs b/crates/tests/cgp-tests/tests/auto_dispatch/async_multi_args.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_multi_args.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/async_multi_args.rs diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_multi_args_owned_self.rs b/crates/tests/cgp-tests/tests/auto_dispatch/async_multi_args_owned_self.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_multi_args_owned_self.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/async_multi_args_owned_self.rs diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_multi_args_ref.rs b/crates/tests/cgp-tests/tests/auto_dispatch/async_multi_args_ref.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_multi_args_ref.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/async_multi_args_ref.rs diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_self_mut_only.rs b/crates/tests/cgp-tests/tests/auto_dispatch/async_self_mut_only.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_self_mut_only.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/async_self_mut_only.rs diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_self_only.rs b/crates/tests/cgp-tests/tests/auto_dispatch/async_self_only.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_self_only.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/async_self_only.rs diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/async_self_ref_only.rs b/crates/tests/cgp-tests/tests/auto_dispatch/async_self_ref_only.rs new file mode 100644 index 000000000..0bdd76761 --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/async_self_ref_only.rs @@ -0,0 +1,154 @@ +//! `#[cgp_auto_dispatch]` combined with `#[async_trait]`: a `&self` async +//! method, dispatched over `FooBar`. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_auto_dispatch; +use futures::executor::block_on; + +use super::types::{Bar, Foo, FooBar}; + +snapshot_cgp_auto_dispatch! { + #[cgp_auto_dispatch] + #[async_trait] + pub trait CanCall { + async fn call(&self) -> &'static str; + } + + expand_async_self_ref_only(output) { + insta::assert_snapshot!(output, @" + #[async_trait] + pub trait CanCall { + async fn call(&self) -> &'static str; + } + impl<__Variants__> CanCall for __Variants__ + where + MatchWithValueHandlersRef< + ComputeCall, + >: for<'__a__> AsyncComputer<(), (), &'__a__ __Variants__, Output = &'static str>, + __Variants__: HasExtractor, + { + async fn call(&self) -> &'static str { + as AsyncComputer< + _, + _, + _, + >>::compute_async(&(), ::core::marker::PhantomData::<()>, self) + .await + } + } + async fn __compute_call__<'__a__, __Variants__: CanCall>( + __Variants__: &'__a__ __Variants__, + ) -> &'static str { + __Variants__.call().await + } + impl< + '__a__, + __Variants__: CanCall, + __Context__, + __Code__, + > AsyncComputer<__Context__, __Code__, (&'__a__ __Variants__)> for ComputeCall { + type Output = &'static str; + async fn compute_async( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (&'__a__ __Variants__), + ) -> Self::Output { + __compute_call__(arg_0).await + } + } + impl< + '__a__, + __Variants__: CanCall, + __Context__, + __Code__, + > IsProviderFor + for ComputeCall {} + pub struct ComputeCall; + impl DelegateComponent for ComputeCall + where + PromoteAsyncComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteAsyncComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteAsyncComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteAsyncComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteAsyncComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteAsyncComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +impl CanCall for Foo { + async fn call(&self) -> &'static str { + "foo" + } +} + +impl CanCall for Bar { + async fn call(&self) -> &'static str { + "bar" + } +} + +pub trait CheckCanCallFooBar: CanCall {} +impl CheckCanCallFooBar for FooBar {} + +#[test] +fn test_call_async_self_ref_only() { + assert_eq!(block_on(FooBar::Foo(Foo).call()), "foo"); +} diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/computer_by_name.rs b/crates/tests/cgp-tests/tests/auto_dispatch/computer_by_name.rs new file mode 100644 index 000000000..709798116 --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/computer_by_name.rs @@ -0,0 +1,49 @@ +//! The per-variant computer `#[cgp_auto_dispatch]` generates is a public provider +//! with a predictable name, `Compute` plus the method name in PascalCase, and it +//! can be wired by name. +//! +//! Code outside the macro relies on that name: the extensible-shapes example in +//! the knowledge base wires `ComputeArea` into a matcher by hand. This test pins +//! both uses, calling `ComputeCall` on a payload directly and running it through +//! the same value-handler matcher the generated blanket impl uses. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md. + +use cgp::extra::handler::Computer; +use cgp::prelude::*; + +use super::types::{Bar, Foo, FooBar}; + +#[cgp_auto_dispatch] +pub trait CanCall { + fn call(&self) -> &'static str; +} + +impl CanCall for Foo { + fn call(&self) -> &'static str { + "foo" + } +} + +impl CanCall for Bar { + fn call(&self) -> &'static str { + "bar" + } +} + +#[test] +fn test_computer_by_name() { + assert_eq!( + >::compute(&(), PhantomData, &Bar), + "bar", + ); + + assert_eq!( + as Computer<(), (), &FooBar>>::compute( + &(), + PhantomData, + &FooBar::Foo(Foo), + ), + "foo", + ); +} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_consumer_traits_in_scope.rs b/crates/tests/cgp-tests/tests/auto_dispatch/consumer_traits_in_scope.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_consumer_traits_in_scope.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/consumer_traits_in_scope.rs diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/cross_module.rs b/crates/tests/cgp-tests/tests/auto_dispatch/cross_module.rs new file mode 100644 index 000000000..82aad472a --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/cross_module.rs @@ -0,0 +1,53 @@ +//! A dispatch trait declared in one module, its payloads and enum in another, +//! and the dispatched method called from a third. +//! +//! The generated per-variant computer and the enum-level blanket impl are emitted +//! beside the trait, so they must stay reachable wherever the trait is: the +//! computer struct is public, and the helper function it calls is private to the +//! trait's module. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md. + +mod traits { + use cgp::prelude::*; + + #[cgp_auto_dispatch] + pub trait HasName { + fn name(&self) -> &'static str; + } +} + +mod shapes { + use cgp::prelude::*; + + use super::traits::HasName; + + pub struct Circle; + pub struct Square; + + #[derive(CgpData)] + pub enum Shape { + Circle(Circle), + Square(Square), + } + + impl HasName for Circle { + fn name(&self) -> &'static str { + "circle" + } + } + + impl HasName for Square { + fn name(&self) -> &'static str { + "square" + } + } +} + +use shapes::{Shape, Square}; +use traits::HasName; + +#[test] +fn test_dispatch_across_modules() { + assert_eq!(Shape::Square(Square).name(), "square"); +} diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/default_method.rs b/crates/tests/cgp-tests/tests/auto_dispatch/default_method.rs new file mode 100644 index 000000000..42263b248 --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/default_method.rs @@ -0,0 +1,32 @@ +//! A dispatch trait method with a default body. +//! +//! The macro keeps the trait unchanged, so a payload impl may rely on the default +//! while another overrides it. The enum-level blanket impl always dispatches, so +//! the enum calls whichever body its current variant's payload uses. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md. + +use cgp::prelude::*; + +use super::types::{Bar, Foo, FooBar}; + +#[cgp_auto_dispatch] +pub trait CanDescribe { + fn describe(&self) -> String { + "default".to_owned() + } +} + +impl CanDescribe for Foo {} + +impl CanDescribe for Bar { + fn describe(&self) -> String { + "bar".to_owned() + } +} + +#[test] +fn test_default_method() { + assert_eq!(FooBar::Foo(Foo).describe(), "default"); + assert_eq!(FooBar::Bar(Bar).describe(), "bar"); +} diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/elided_lifetimes.rs b/crates/tests/cgp-tests/tests/auto_dispatch/elided_lifetimes.rs new file mode 100644 index 000000000..d0b5bb5a0 --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/elided_lifetimes.rs @@ -0,0 +1,327 @@ +//! Dispatch methods whose signatures elide lifetimes, at any depth. +//! +//! The macro names each elided lifetime the way the compiler's elision rules +//! read it: every elided input lifetime is distinct, and an elided output +//! lifetime is the receiver's, or, for a by-value `self`, the one lifetime the +//! arguments use. So a borrow returned from `&self` outlives a shorter-lived +//! argument, as the trait's own signature promises. References nested inside +//! other types, such as `Option<&str>`, are named too, while a `Fn(&str)` bound +//! keeps the lifetime it binds itself. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_auto_dispatch; + +pub struct Circle { + pub name: String, +} + +pub struct Square { + pub name: String, +} + +#[derive(CgpData)] +pub enum Shape { + Circle(Circle), + Square(Square), +} + +snapshot_cgp_auto_dispatch! { + #[cgp_auto_dispatch] + pub trait CanLabel { + fn label(&self, suffix: &str) -> &str; + } + + expand_distinct_elided_lifetimes(output) { + insta::assert_snapshot!(output, @" + pub trait CanLabel { + fn label(&self, suffix: &str) -> &str; + } + impl<__Variants__> CanLabel for __Variants__ + where + MatchFirstWithValueHandlersRef< + ComputeLabel, + >: for<'__a__, '__a1__> Computer< + (), + (), + (&'__a__ __Variants__, (&'__a1__ str)), + Output = &'__a__ str, + >, + __Variants__: HasExtractor, + { + fn label(&self, arg_0: &str) -> &str { + as Computer< + _, + _, + _, + >>::compute(&(), ::core::marker::PhantomData::<()>, (self, (arg_0))) + } + } + fn __compute_label__<'__a__, '__a1__, __Variants__: CanLabel>( + __Variants__: &'__a__ __Variants__, + (arg_0): (&'__a1__ str), + ) -> &'__a__ str { + __Variants__.label(arg_0) + } + impl< + '__a__, + '__a1__, + __Variants__: CanLabel, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (&'__a__ __Variants__, (&'__a1__ str))> + for ComputeLabel { + type Output = &'__a__ str; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0, arg_1): (&'__a__ __Variants__, (&'__a1__ str)), + ) -> Self::Output { + __compute_label__(arg_0, arg_1) + } + } + impl< + '__a__, + '__a1__, + __Variants__: CanLabel, + __Context__, + __Code__, + > IsProviderFor< + ComputerComponent, + __Context__, + (__Code__, (&'__a__ __Variants__, (&'__a1__ str))), + > for ComputeLabel {} + pub struct ComputeLabel; + impl DelegateComponent for ComputeLabel + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeLabel + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeLabel + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeLabel + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeLabel + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeLabel + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeLabel + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeLabel + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeLabel + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeLabel + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeLabel + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeLabel + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeLabel + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeLabel + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +#[cgp_auto_dispatch] +pub trait CanFind { + fn find(&self, key: Option<&str>) -> Option<&str>; +} + +#[cgp_auto_dispatch] +pub trait CanPick { + fn pick(self, fallback: &str) -> &str; +} + +#[cgp_auto_dispatch] +pub trait CanMeasure { + fn measure(&self, measure: &dyn Fn(&str) -> usize) -> usize; +} + +impl CanLabel for Circle { + fn label(&self, _suffix: &str) -> &str { + &self.name + } +} + +impl CanLabel for Square { + fn label(&self, _suffix: &str) -> &str { + &self.name + } +} + +impl CanFind for Circle { + fn find(&self, key: Option<&str>) -> Option<&str> { + key.map(|_| self.name.as_str()) + } +} + +impl CanFind for Square { + fn find(&self, _key: Option<&str>) -> Option<&str> { + None + } +} + +impl CanPick for Circle { + fn pick(self, fallback: &str) -> &str { + fallback + } +} + +impl CanPick for Square { + fn pick(self, fallback: &str) -> &str { + fallback + } +} + +impl CanMeasure for Circle { + fn measure(&self, measure: &dyn Fn(&str) -> usize) -> usize { + measure(&self.name) + } +} + +impl CanMeasure for Square { + fn measure(&self, measure: &dyn Fn(&str) -> usize) -> usize { + measure(&self.name) + } +} + +#[test] +fn test_elided_lifetimes() { + let shape = Shape::Circle(Circle { + name: "circle".to_owned(), + }); + + // The returned borrow comes from `shape`, so it outlives `suffix`. + let label = { + let suffix = String::from("!"); + shape.label(&suffix) + }; + assert_eq!(label, "circle"); + + assert_eq!(shape.find(Some("key")), Some("circle")); + assert_eq!(shape.measure(&|name| name.len()), 6); + assert_eq!( + Shape::Square(Square { + name: "square".to_owned() + }) + .pick("fallback"), + "fallback" + ); +} diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/generics.rs b/crates/tests/cgp-tests/tests/auto_dispatch/generics.rs new file mode 100644 index 000000000..19c79d63a --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/generics.rs @@ -0,0 +1,448 @@ +//! `#[cgp_auto_dispatch]` on a generic trait (`CanCall`), where the per-variant +//! impls may add their own bounds on `T` (here `Foo` requires `T: Display`). +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. + +use core::fmt::Display; + +use cgp_macro_test_util::snapshot_cgp_auto_dispatch; + +use super::types::{Bar, Foo, FooBar}; + +snapshot_cgp_auto_dispatch! { + #[cgp_auto_dispatch] + pub trait CanCall { + fn call_a(&self, _a: u64, _b: &T) -> String; + + fn call_b(self, _a: u64, b: &mut T) -> &str; + } + + expand_generic_trait(output) { + insta::assert_snapshot!(output, @" + pub trait CanCall { + fn call_a(&self, _a: u64, _b: &T) -> String; + fn call_b(self, _a: u64, b: &mut T) -> &str; + } + impl<__Variants__, T> CanCall for __Variants__ + where + MatchFirstWithValueHandlersRef< + ComputeCallA, + >: for<'__a__, '__a1__> Computer< + (), + (), + (&'__a__ __Variants__, (u64, &'__a1__ T)), + Output = String, + >, + MatchFirstWithValueHandlers< + ComputeCallB, + >: for<'__a1__> Computer< + (), + (), + (__Variants__, (u64, &'__a1__ mut T)), + Output = &'__a1__ str, + >, + __Variants__: HasExtractor, + { + fn call_a(&self, arg_0: u64, arg_1: &T) -> String { + as Computer< + _, + _, + _, + >>::compute(&(), ::core::marker::PhantomData::<()>, (self, (arg_0, arg_1))) + } + fn call_b(self, arg_0: u64, arg_1: &mut T) -> &str { + as Computer< + _, + _, + _, + >>::compute(&(), ::core::marker::PhantomData::<()>, (self, (arg_0, arg_1))) + } + } + fn __compute_call_a__<'__a__, '__a1__, __Variants__: CanCall, T>( + __Variants__: &'__a__ __Variants__, + (arg_0, arg_1): (u64, &'__a1__ T), + ) -> String { + __Variants__.call_a(arg_0, arg_1) + } + impl< + '__a__, + '__a1__, + __Variants__: CanCall, + T, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (&'__a__ __Variants__, (u64, &'__a1__ T))> + for ComputeCallA { + type Output = String; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0, arg_1): (&'__a__ __Variants__, (u64, &'__a1__ T)), + ) -> Self::Output { + __compute_call_a__(arg_0, arg_1) + } + } + impl< + '__a__, + '__a1__, + __Variants__: CanCall, + T, + __Context__, + __Code__, + > IsProviderFor< + ComputerComponent, + __Context__, + (__Code__, (&'__a__ __Variants__, (u64, &'__a1__ T))), + > for ComputeCallA {} + pub struct ComputeCallA; + impl DelegateComponent for ComputeCallA + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallA + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallA + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallA + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallA + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallA + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallA + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallA + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallA + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallA + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallA + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallA + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallA + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallA + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + fn __compute_call_b__<'__a1__, __Variants__: CanCall, T>( + __Variants__: __Variants__, + (arg_0, arg_1): (u64, &'__a1__ mut T), + ) -> &'__a1__ str { + __Variants__.call_b(arg_0, arg_1) + } + impl< + '__a1__, + __Variants__: CanCall, + T, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (__Variants__, (u64, &'__a1__ mut T))> + for ComputeCallB { + type Output = &'__a1__ str; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0, arg_1): (__Variants__, (u64, &'__a1__ mut T)), + ) -> Self::Output { + __compute_call_b__(arg_0, arg_1) + } + } + impl< + '__a1__, + __Variants__: CanCall, + T, + __Context__, + __Code__, + > IsProviderFor< + ComputerComponent, + __Context__, + (__Code__, (__Variants__, (u64, &'__a1__ mut T))), + > for ComputeCallB {} + pub struct ComputeCallB; + impl DelegateComponent for ComputeCallB + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallB + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallB + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallB + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallB + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallB + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallB + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallB + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallB + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallB + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallB + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallB + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCallB + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCallB + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +impl CanCall for Foo { + fn call_a(&self, _a: u64, b: &T) -> String { + format!("foo-{}", b) + } + + fn call_b(self, _a: u64, _b: &mut T) -> &str { + "foo" + } +} + +impl CanCall for Bar { + fn call_a(&self, _a: u64, _b: &T) -> String { + "bar".to_owned() + } + + fn call_b(self, _a: u64, _b: &mut T) -> &str { + "bar" + } +} + +pub trait CheckCanCallFooBar: CanCall {} +impl CheckCanCallFooBar for FooBar {} + +#[test] +fn test_call_generics() { + assert_eq!(FooBar::Foo(Foo).call_a(42, &"extra"), "foo-extra"); +} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_method_name_in_scope.rs b/crates/tests/cgp-tests/tests/auto_dispatch/method_name_in_scope.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_method_name_in_scope.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/method_name_in_scope.rs diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/mod.rs b/crates/tests/cgp-tests/tests/auto_dispatch/mod.rs new file mode 100644 index 000000000..628b93ab8 --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/mod.rs @@ -0,0 +1,51 @@ +//! One unit test per file. Each file is self-contained: it defines its own +//! traits, providers, and context types at module scope so that the type-level +//! wiring of one test never leaks into another. +//! +//! The exception is `types`, a small shared fixture (the `Foo`/`Bar`/`FooBar` +//! enum) that the shape tests dispatch over. It is declared here and referenced +//! by siblings via `super::types`. + +// Shared fixture: the `Foo`/`Bar`/`FooBar` enum the shape tests route over. +pub mod types; + +// Method-shape coverage: one shape per file. Each defines per-variant impls and +// dispatches them over an extensible-data enum. +pub mod elided_lifetimes; +pub mod generics; +pub mod multi_args; +pub mod multi_args_owned_self; +pub mod multi_args_ref; +pub mod multi_methods; +pub mod named_lifetimes; +pub mod self_mut_only; +pub mod self_only; +pub mod self_ref_only; +pub mod self_ref_return_explicit_ref; +pub mod self_ref_return_implicit_ref; +pub mod shape; +pub mod supertrait; + +// Where the trait lives and what it declares: a trait used across modules, and a +// method with a default body. +pub mod cross_module; +pub mod default_method; + +// Combined with `#[async_trait]`: the async shapes. +pub mod async_generics; +pub mod async_multi_args; +pub mod async_multi_args_owned_self; +pub mod async_multi_args_ref; +pub mod async_self_mut_only; +pub mod async_self_only; +pub mod async_self_ref_only; + +// The names and items the macro generates: the per-variant computer wired by +// name, imported consumer traits that would make an unqualified matcher call +// ambiguous, a module item sharing a method's name, a method named with a raw +// identifier, and a module without the prelude, where only qualified paths resolve. +pub mod computer_by_name; +pub mod consumer_traits_in_scope; +pub mod method_name_in_scope; +pub mod raw_method_name; +pub mod without_prelude; diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_multi_args.rs b/crates/tests/cgp-tests/tests/auto_dispatch/multi_args.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_multi_args.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/multi_args.rs diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_multi_args_owned_self.rs b/crates/tests/cgp-tests/tests/auto_dispatch/multi_args_owned_self.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_multi_args_owned_self.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/multi_args_owned_self.rs diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/multi_args_ref.rs b/crates/tests/cgp-tests/tests/auto_dispatch/multi_args_ref.rs new file mode 100644 index 000000000..1cc312536 --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/multi_args_ref.rs @@ -0,0 +1,239 @@ +//! `#[cgp_auto_dispatch]` on a `&mut self` method with borrowed arguments and a +//! borrowed return, exercising the lifetime handling in the generated handler. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. + +use cgp_macro_test_util::snapshot_cgp_auto_dispatch; + +use super::types::{Bar, Foo, FooBar}; + +snapshot_cgp_auto_dispatch! { + #[cgp_auto_dispatch] + pub trait CanCall { + fn call<'a>(&'a mut self, _a: &'a u64, _b: bool) -> &'a str; + } + + expand_multi_args_ref(output) { + insta::assert_snapshot!(output, @" + pub trait CanCall { + fn call<'a>(&'a mut self, _a: &'a u64, _b: bool) -> &'a str; + } + impl<__Variants__> CanCall for __Variants__ + where + MatchFirstWithValueHandlersMut< + ComputeCall, + >: for<'a> Computer< + (), + (), + (&'a mut __Variants__, (&'a u64, bool)), + Output = &'a str, + >, + __Variants__: HasExtractor, + { + fn call<'a>(&'a mut self, arg_0: &'a u64, arg_1: bool) -> &'a str { + as Computer< + _, + _, + _, + >>::compute(&(), ::core::marker::PhantomData::<()>, (self, (arg_0, arg_1))) + } + } + fn __compute_call__<'a, __Variants__: CanCall>( + __Variants__: &'a mut __Variants__, + (arg_0, arg_1): (&'a u64, bool), + ) -> &'a str { + __Variants__.call(arg_0, arg_1) + } + impl< + 'a, + __Variants__: CanCall, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (&'a mut __Variants__, (&'a u64, bool))> + for ComputeCall { + type Output = &'a str; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0, arg_1): (&'a mut __Variants__, (&'a u64, bool)), + ) -> Self::Output { + __compute_call__(arg_0, arg_1) + } + } + impl< + 'a, + __Variants__: CanCall, + __Context__, + __Code__, + > IsProviderFor< + ComputerComponent, + __Context__, + (__Code__, (&'a mut __Variants__, (&'a u64, bool))), + > for ComputeCall {} + pub struct ComputeCall; + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +impl CanCall for Foo { + fn call(&mut self, _a: &u64, _b: bool) -> &str { + "foo" + } +} + +impl CanCall for Bar { + fn call(&mut self, _a: &u64, _b: bool) -> &str { + "bar" + } +} + +pub trait CheckCanCallFooBar: CanCall {} +impl CheckCanCallFooBar for FooBar {} + +#[test] +fn test_call_multi_args_ref() { + assert_eq!(FooBar::Foo(Foo).call(&42, true), "foo"); +} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_multi_methods.rs b/crates/tests/cgp-tests/tests/auto_dispatch/multi_methods.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_multi_methods.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/multi_methods.rs diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/named_lifetimes.rs b/crates/tests/cgp-tests/tests/auto_dispatch/named_lifetimes.rs new file mode 100644 index 000000000..ffddeb47e --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/named_lifetimes.rs @@ -0,0 +1,111 @@ +//! Dispatch methods whose signatures name lifetimes. +//! +//! The generated matcher bound is quantified over the method's own lifetime +//! parameters together with any lifetime the macro names for an elided one, in a +//! single `for<..>`, and never over the trait's lifetime parameters, which the +//! blanket impl already declares. Each trait here needs one of those properties: +//! a named receiver lifetime the return type does not mention, a named receiver +//! beside an elided argument (two lifetimes in one bound), a trait lifetime +//! parameter, and a named receiver whose lifetime an elided return type takes. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md. + +// `HasName::name` names its receiver's lifetime and elides the return's on +// purpose, which is the shape under test. +#![allow(mismatched_lifetime_syntaxes)] + +use cgp::prelude::*; + +pub struct Circle { + pub name: String, +} + +pub struct Square { + pub name: String, +} + +#[derive(CgpData)] +pub enum Shape { + Circle(Circle), + Square(Square), +} + +#[cgp_auto_dispatch] +pub trait HasSides { + fn sides<'a>(&'a self) -> u8; +} + +#[cgp_auto_dispatch] +pub trait CanLookup { + fn lookup<'a>(&'a self, key: &str) -> &'a str; +} + +#[cgp_auto_dispatch] +pub trait CanPrefix<'a> { + fn prefix(&self, prefix: &'a str) -> &'a str; +} + +#[cgp_auto_dispatch] +pub trait HasName { + fn name<'a>(&'a self) -> &str; +} + +impl HasSides for Circle { + fn sides<'a>(&'a self) -> u8 { + 0 + } +} + +impl HasSides for Square { + fn sides<'a>(&'a self) -> u8 { + 4 + } +} + +impl CanLookup for Circle { + fn lookup<'a>(&'a self, _key: &str) -> &'a str { + &self.name + } +} + +impl CanLookup for Square { + fn lookup<'a>(&'a self, _key: &str) -> &'a str { + &self.name + } +} + +impl<'a> CanPrefix<'a> for Circle { + fn prefix(&self, prefix: &'a str) -> &'a str { + prefix + } +} + +impl<'a> CanPrefix<'a> for Square { + fn prefix(&self, _prefix: &'a str) -> &'a str { + "square" + } +} + +impl HasName for Circle { + fn name<'a>(&'a self) -> &str { + &self.name + } +} + +impl HasName for Square { + fn name<'a>(&'a self) -> &str { + &self.name + } +} + +#[test] +fn test_named_lifetimes() { + let shape = Shape::Square(Square { + name: "square".to_owned(), + }); + + assert_eq!(shape.sides(), 4); + assert_eq!(shape.lookup("key"), "square"); + assert_eq!(shape.prefix("a"), "square"); + assert_eq!(shape.name(), "square"); +} diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/raw_method_name.rs b/crates/tests/cgp-tests/tests/auto_dispatch/raw_method_name.rs new file mode 100644 index 000000000..95d254afe --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/raw_method_name.rs @@ -0,0 +1,232 @@ +//! A dispatch trait whose method has a raw identifier as its name, `r#type`. The +//! generated computer and helper are named from the unrawed name (`ComputeType` and +//! `__compute_type__`), since `r#` cannot appear inside a longer identifier. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_auto_dispatch; + +#[derive(CgpData)] +pub enum Shape { + Circle(Circle), + Square(Square), +} + +pub struct Circle; + +pub struct Square; + +snapshot_cgp_auto_dispatch! { + #[cgp_auto_dispatch] + pub trait HasKind { + fn r#type(&self) -> &'static str; + } + + expand_raw_method_name(output) { + insta::assert_snapshot!(output, @" + pub trait HasKind { + fn r#type(&self) -> &'static str; + } + impl<__Variants__> HasKind for __Variants__ + where + MatchWithValueHandlersRef< + ComputeType, + >: for<'__a__> Computer<(), (), &'__a__ __Variants__, Output = &'static str>, + __Variants__: HasExtractor, + { + fn r#type(&self) -> &'static str { + as Computer<_, _, _>>::compute(&(), ::core::marker::PhantomData::<()>, self) + } + } + fn __compute_type__<'__a__, __Variants__: HasKind>( + __Variants__: &'__a__ __Variants__, + ) -> &'static str { + __Variants__.r#type() + } + impl< + '__a__, + __Variants__: HasKind, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (&'__a__ __Variants__)> for ComputeType { + type Output = &'static str; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (&'__a__ __Variants__), + ) -> Self::Output { + __compute_type__(arg_0) + } + } + impl< + '__a__, + __Variants__: HasKind, + __Context__, + __Code__, + > IsProviderFor + for ComputeType {} + pub struct ComputeType; + impl DelegateComponent for ComputeType + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeType + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeType + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeType + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeType + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeType + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeType + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeType + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeType + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeType + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeType + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeType + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeType + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeType + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +impl HasKind for Circle { + fn r#type(&self) -> &'static str { + "circle" + } +} + +impl HasKind for Square { + fn r#type(&self) -> &'static str { + "square" + } +} + +#[test] +fn test_raw_method_name_dispatches() { + assert_eq!(Shape::Circle(Circle).r#type(), "circle"); + assert_eq!(Shape::Square(Square).r#type(), "square"); +} diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/self_mut_only.rs b/crates/tests/cgp-tests/tests/auto_dispatch/self_mut_only.rs new file mode 100644 index 000000000..8a582762e --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/self_mut_only.rs @@ -0,0 +1,227 @@ +//! `#[cgp_auto_dispatch]` on a `&mut self` method with no extra arguments. +//! +//! The generated handler routes a `&mut FooBar` reference to the matching +//! variant impl of `CanCall`. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. + +use cgp_macro_test_util::snapshot_cgp_auto_dispatch; + +use super::types::{Bar, Foo, FooBar}; + +snapshot_cgp_auto_dispatch! { + #[cgp_auto_dispatch] + pub trait CanCall { + fn call(&mut self) -> &'static str; + } + + expand_self_mut_only(output) { + insta::assert_snapshot!(output, @" + pub trait CanCall { + fn call(&mut self) -> &'static str; + } + impl<__Variants__> CanCall for __Variants__ + where + MatchWithValueHandlersMut< + ComputeCall, + >: for<'__a__> Computer<(), (), &'__a__ mut __Variants__, Output = &'static str>, + __Variants__: HasExtractor, + { + fn call(&mut self) -> &'static str { + as Computer<_, _, _>>::compute(&(), ::core::marker::PhantomData::<()>, self) + } + } + fn __compute_call__<'__a__, __Variants__: CanCall>( + __Variants__: &'__a__ mut __Variants__, + ) -> &'static str { + __Variants__.call() + } + impl< + '__a__, + __Variants__: CanCall, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (&'__a__ mut __Variants__)> for ComputeCall { + type Output = &'static str; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (&'__a__ mut __Variants__), + ) -> Self::Output { + __compute_call__(arg_0) + } + } + impl< + '__a__, + __Variants__: CanCall, + __Context__, + __Code__, + > IsProviderFor + for ComputeCall {} + pub struct ComputeCall; + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +impl CanCall for Foo { + fn call(&mut self) -> &'static str { + "foo" + } +} + +impl CanCall for Bar { + fn call(&mut self) -> &'static str { + "bar" + } +} + +pub trait CheckCanCallFooBar: CanCall {} +impl CheckCanCallFooBar for FooBar {} + +#[test] +fn test_call_self_mut_only() { + assert_eq!(FooBar::Foo(Foo).call(), "foo"); +} diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/self_only.rs b/crates/tests/cgp-tests/tests/auto_dispatch/self_only.rs new file mode 100644 index 000000000..3fb2a39c1 --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/self_only.rs @@ -0,0 +1,223 @@ +//! `#[cgp_auto_dispatch]` on a by-value `self` method with no extra arguments. +//! +//! The generated handler routes a `FooBar` value to the `Foo`/`Bar` variant +//! impl of `CanCall`, consuming `self`. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. + +use cgp_macro_test_util::snapshot_cgp_auto_dispatch; + +use super::types::{Bar, Foo, FooBar}; + +snapshot_cgp_auto_dispatch! { + #[cgp_auto_dispatch] + pub trait CanCall { + fn call(self) -> &'static str; + } + + expand_self_only(output) { + insta::assert_snapshot!(output, @" + pub trait CanCall { + fn call(self) -> &'static str; + } + impl<__Variants__> CanCall for __Variants__ + where + MatchWithValueHandlers< + ComputeCall, + >: Computer<(), (), __Variants__, Output = &'static str>, + __Variants__: HasExtractor, + { + fn call(self) -> &'static str { + as Computer<_, _, _>>::compute(&(), ::core::marker::PhantomData::<()>, self) + } + } + fn __compute_call__<__Variants__: CanCall>(__Variants__: __Variants__) -> &'static str { + __Variants__.call() + } + impl< + __Variants__: CanCall, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (__Variants__)> for ComputeCall { + type Output = &'static str; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (__Variants__), + ) -> Self::Output { + __compute_call__(arg_0) + } + } + impl< + __Variants__: CanCall, + __Context__, + __Code__, + > IsProviderFor + for ComputeCall {} + pub struct ComputeCall; + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +impl CanCall for Foo { + fn call(self) -> &'static str { + "foo" + } +} + +impl CanCall for Bar { + fn call(self) -> &'static str { + "bar" + } +} + +pub trait CheckCanCallFooBar: CanCall {} +impl CheckCanCallFooBar for FooBar {} + +#[test] +fn test_call_self_only() { + assert_eq!(FooBar::Foo(Foo).call(), "foo"); +} diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/self_ref_only.rs b/crates/tests/cgp-tests/tests/auto_dispatch/self_ref_only.rs new file mode 100644 index 000000000..1c793f850 --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/self_ref_only.rs @@ -0,0 +1,227 @@ +//! `#[cgp_auto_dispatch]` on a `&self` method with no extra arguments. +//! +//! The generated handler routes a `&FooBar` reference to the matching variant +//! impl of `CanCall`. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. + +use cgp_macro_test_util::snapshot_cgp_auto_dispatch; + +use super::types::{Bar, Foo, FooBar}; + +snapshot_cgp_auto_dispatch! { + #[cgp_auto_dispatch] + pub trait CanCall { + fn call(&self) -> &'static str; + } + + expand_self_ref_only(output) { + insta::assert_snapshot!(output, @" + pub trait CanCall { + fn call(&self) -> &'static str; + } + impl<__Variants__> CanCall for __Variants__ + where + MatchWithValueHandlersRef< + ComputeCall, + >: for<'__a__> Computer<(), (), &'__a__ __Variants__, Output = &'static str>, + __Variants__: HasExtractor, + { + fn call(&self) -> &'static str { + as Computer<_, _, _>>::compute(&(), ::core::marker::PhantomData::<()>, self) + } + } + fn __compute_call__<'__a__, __Variants__: CanCall>( + __Variants__: &'__a__ __Variants__, + ) -> &'static str { + __Variants__.call() + } + impl< + '__a__, + __Variants__: CanCall, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (&'__a__ __Variants__)> for ComputeCall { + type Output = &'static str; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (&'__a__ __Variants__), + ) -> Self::Output { + __compute_call__(arg_0) + } + } + impl< + '__a__, + __Variants__: CanCall, + __Context__, + __Code__, + > IsProviderFor + for ComputeCall {} + pub struct ComputeCall; + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +impl CanCall for Foo { + fn call(&self) -> &'static str { + "foo" + } +} + +impl CanCall for Bar { + fn call(&self) -> &'static str { + "bar" + } +} + +pub trait CheckCanCallFooBar: CanCall {} +impl CheckCanCallFooBar for FooBar {} + +#[test] +fn test_call_self_ref_only() { + assert_eq!(FooBar::Foo(Foo).call(), "foo"); +} diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/self_ref_return_explicit_ref.rs b/crates/tests/cgp-tests/tests/auto_dispatch/self_ref_return_explicit_ref.rs new file mode 100644 index 000000000..e560e6b89 --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/self_ref_return_explicit_ref.rs @@ -0,0 +1,228 @@ +//! `#[cgp_auto_dispatch]` on a `&self` method whose return borrow is written +//! with an *explicit* lifetime (`fn call<'a>(&'a self) -> &'a str`). +//! +//! Companion to `self_ref_return_implicit_ref`, which writes the +//! same shape with an elided lifetime. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. + +use cgp_macro_test_util::snapshot_cgp_auto_dispatch; + +use super::types::{Bar, Foo, FooBar}; + +snapshot_cgp_auto_dispatch! { + #[cgp_auto_dispatch] + pub trait CanCall { + fn call<'a>(&'a self) -> &'a str; + } + + expand_explicit_return_lifetime(output) { + insta::assert_snapshot!(output, @" + pub trait CanCall { + fn call<'a>(&'a self) -> &'a str; + } + impl<__Variants__> CanCall for __Variants__ + where + MatchWithValueHandlersRef< + ComputeCall, + >: for<'a> Computer<(), (), &'a __Variants__, Output = &'a str>, + __Variants__: HasExtractor, + { + fn call<'a>(&'a self) -> &'a str { + as Computer<_, _, _>>::compute(&(), ::core::marker::PhantomData::<()>, self) + } + } + fn __compute_call__<'a, __Variants__: CanCall>( + __Variants__: &'a __Variants__, + ) -> &'a str { + __Variants__.call() + } + impl< + 'a, + __Variants__: CanCall, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (&'a __Variants__)> for ComputeCall { + type Output = &'a str; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (&'a __Variants__), + ) -> Self::Output { + __compute_call__(arg_0) + } + } + impl< + 'a, + __Variants__: CanCall, + __Context__, + __Code__, + > IsProviderFor + for ComputeCall {} + pub struct ComputeCall; + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeCall + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeCall + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +impl CanCall for Foo { + fn call(&self) -> &str { + "foo" + } +} + +impl CanCall for Bar { + fn call(&self) -> &str { + "bar" + } +} + +pub trait CheckCanCallFooBar: CanCall {} +impl CheckCanCallFooBar for FooBar {} + +#[test] +fn test_call_self_ref_return_explicit_ref() { + assert_eq!(FooBar::Foo(Foo).call(), "foo"); +} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_ref_return_implicit_ref.rs b/crates/tests/cgp-tests/tests/auto_dispatch/self_ref_return_implicit_ref.rs similarity index 90% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_ref_return_implicit_ref.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/self_ref_return_implicit_ref.rs index 66d3125a1..c624508aa 100644 --- a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_ref_return_implicit_ref.rs +++ b/crates/tests/cgp-tests/tests/auto_dispatch/self_ref_return_implicit_ref.rs @@ -1,7 +1,7 @@ //! `#[cgp_auto_dispatch]` on a `&self` method whose return borrow uses an //! *elided* (implicit) lifetime (`fn call(&self) -> &str`). //! -//! Companion to `auto_dispatch_self_ref_return_explicit_ref`, which writes the +//! Companion to `self_ref_return_explicit_ref`, which writes the //! same shape with an explicit lifetime. //! //! See cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_shape.rs b/crates/tests/cgp-tests/tests/auto_dispatch/shape.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/auto_dispatch_shape.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/shape.rs diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/supertrait.rs b/crates/tests/cgp-tests/tests/auto_dispatch/supertrait.rs new file mode 100644 index 000000000..baad14c1b --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/supertrait.rs @@ -0,0 +1,252 @@ +//! A dispatch trait with a supertrait. +//! +//! The blanket impl requires the trait's supertraits of the enum, so the enum +//! must provide them. Here the supertrait is itself a dispatch trait, so the enum +//! gets it through that trait's own blanket impl, and the subtrait's per-payload +//! impls can call the supertrait's method. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_auto_dispatch; + +pub struct Circle; + +pub struct Square; + +#[derive(CgpData)] +pub enum Shape { + Circle(Circle), + Square(Square), +} + +#[cgp_auto_dispatch] +pub trait HasName { + fn name(&self) -> &'static str; +} + +snapshot_cgp_auto_dispatch! { + #[cgp_auto_dispatch] + pub trait CanGreet: HasName { + fn greet(&self) -> String; + } + + expand_supertrait(output) { + insta::assert_snapshot!(output, @" + pub trait CanGreet: HasName { + fn greet(&self) -> String; + } + impl<__Variants__> CanGreet for __Variants__ + where + MatchWithValueHandlersRef< + ComputeGreet, + >: for<'__a__> Computer<(), (), &'__a__ __Variants__, Output = String>, + __Variants__: HasExtractor, + __Variants__: HasName, + { + fn greet(&self) -> String { + as Computer<_, _, _>>::compute(&(), ::core::marker::PhantomData::<()>, self) + } + } + fn __compute_greet__<'__a__, __Variants__: CanGreet>( + __Variants__: &'__a__ __Variants__, + ) -> String { + __Variants__.greet() + } + impl< + '__a__, + __Variants__: CanGreet, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (&'__a__ __Variants__)> for ComputeGreet { + type Output = String; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (&'__a__ __Variants__), + ) -> Self::Output { + __compute_greet__(arg_0) + } + } + impl< + '__a__, + __Variants__: CanGreet, + __Context__, + __Code__, + > IsProviderFor + for ComputeGreet {} + pub struct ComputeGreet; + impl DelegateComponent for ComputeGreet + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeGreet + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeGreet + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeGreet + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeGreet + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeGreet + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeGreet + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeGreet + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeGreet + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeGreet + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeGreet + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeGreet + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ComputeGreet + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ComputeGreet + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +impl HasName for Circle { + fn name(&self) -> &'static str { + "circle" + } +} + +impl HasName for Square { + fn name(&self) -> &'static str { + "square" + } +} + +impl CanGreet for Circle { + fn greet(&self) -> String { + format!("hello, {}", self.name()) + } +} + +impl CanGreet for Square { + fn greet(&self) -> String { + format!("hi, {}", self.name()) + } +} + +#[test] +fn test_supertrait() { + assert_eq!(Shape::Square(Square).greet(), "hi, square"); +} diff --git a/crates/tests/cgp-tests/tests/dispatching/types.rs b/crates/tests/cgp-tests/tests/auto_dispatch/types.rs similarity index 100% rename from crates/tests/cgp-tests/tests/dispatching/types.rs rename to crates/tests/cgp-tests/tests/auto_dispatch/types.rs diff --git a/crates/tests/cgp-tests/tests/auto_dispatch/without_prelude.rs b/crates/tests/cgp-tests/tests/auto_dispatch/without_prelude.rs new file mode 100644 index 000000000..19d8bdcc1 --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch/without_prelude.rs @@ -0,0 +1,57 @@ +//! `#[cgp_auto_dispatch]` in a module that does not import `cgp::prelude::*`. +//! +//! The expansion names the matchers, the computer traits, and `HasExtractor` +//! through their fully qualified `::cgp::macro_prelude` paths, so the macro works +//! when invoked by path alone, and items in the caller's module named like those +//! traits, here a local `Computer` and `HasExtractor`, do not capture them. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md. + +/// Local items named like the traits the expansion refers to. +pub struct Computer; +pub struct HasExtractor; + +pub struct Circle; + +pub struct Square; + +#[derive(cgp::prelude::CgpData)] +pub enum Shape { + Circle(Circle), + Square(Square), +} + +#[cgp::prelude::cgp_auto_dispatch] +pub trait HasSides { + fn sides(&self) -> u8; + + fn scale(&mut self, factor: u8) -> u8; +} + +impl HasSides for Circle { + fn sides(&self) -> u8 { + 0 + } + + fn scale(&mut self, factor: u8) -> u8 { + factor + } +} + +impl HasSides for Square { + fn sides(&self) -> u8 { + 4 + } + + fn scale(&mut self, factor: u8) -> u8 { + 4 * factor + } +} + +#[test] +fn test_dispatch_without_prelude() { + let mut shape = Shape::Square(Square); + + assert_eq!(shape.sides(), 4); + assert_eq!(shape.scale(2), 8); +} diff --git a/crates/tests/cgp-tests/tests/auto_dispatch_tests.rs b/crates/tests/cgp-tests/tests/auto_dispatch_tests.rs new file mode 100644 index 000000000..536e7447a --- /dev/null +++ b/crates/tests/cgp-tests/tests/auto_dispatch_tests.rs @@ -0,0 +1,19 @@ +//! Entrypoint for the `auto_dispatch` concept. +//! +//! Covers `#[cgp_auto_dispatch]`, which takes a trait implemented once per payload +//! type and generates its implementation for any extensible-data enum of those +//! payloads, routing each value to the matching variant's impl through a +//! value-handler matcher. One file per method shape: the receiver form, extra +//! arguments, reference lifetimes, generic traits, `async` methods stacked with +//! `#[async_trait]`, and the names the macro generates. +//! +//! This concept owns the `#[cgp_auto_dispatch]` snapshots; other macros used here +//! as incidental scaffolding are written in their plain form. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md, +//! cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md, and +//! cgp-knowledge-base/cgp/concepts/dispatching.md. +#![allow(dead_code)] +#![allow(clippy::needless_lifetimes)] + +pub mod auto_dispatch; diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_self_ref_only.rs b/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_self_ref_only.rs deleted file mode 100644 index 0ec0ae4e6..000000000 --- a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_async_self_ref_only.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! `#[cgp_auto_dispatch]` combined with `#[async_trait]`: a `&self` async -//! method, dispatched over `FooBar`. -//! -//! See cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. - -use cgp::prelude::*; -use futures::executor::block_on; - -use super::types::{Bar, Foo, FooBar}; - -#[cgp_auto_dispatch] -#[async_trait] -pub trait CanCall { - async fn call(&self) -> &'static str; -} - -impl CanCall for Foo { - async fn call(&self) -> &'static str { - "foo" - } -} - -impl CanCall for Bar { - async fn call(&self) -> &'static str { - "bar" - } -} - -pub trait CheckCanCallFooBar: CanCall {} -impl CheckCanCallFooBar for FooBar {} - -#[test] -fn test_call_async_self_ref_only() { - assert_eq!(block_on(FooBar::Foo(Foo).call()), "foo"); -} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_generics.rs b/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_generics.rs deleted file mode 100644 index 26b35e5c5..000000000 --- a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_generics.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! `#[cgp_auto_dispatch]` on a generic trait (`CanCall`), where the per-variant -//! impls may add their own bounds on `T` (here `Foo` requires `T: Display`). -//! -//! See cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. - -use core::fmt::Display; - -use cgp::prelude::*; - -use super::types::{Bar, Foo, FooBar}; - -#[cgp_auto_dispatch] -pub trait CanCall { - fn call_a(&self, _a: u64, _b: &T) -> String; - - fn call_b(self, _a: u64, b: &mut T) -> &str; -} - -impl CanCall for Foo { - fn call_a(&self, _a: u64, b: &T) -> String { - format!("foo-{}", b) - } - - fn call_b(self, _a: u64, _b: &mut T) -> &str { - "foo" - } -} - -impl CanCall for Bar { - fn call_a(&self, _a: u64, _b: &T) -> String { - "bar".to_owned() - } - - fn call_b(self, _a: u64, _b: &mut T) -> &str { - "bar" - } -} - -pub trait CheckCanCallFooBar: CanCall {} -impl CheckCanCallFooBar for FooBar {} - -#[test] -fn test_call_generics() { - assert_eq!(FooBar::Foo(Foo).call_a(42, &"extra"), "foo-extra"); -} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_multi_args_ref.rs b/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_multi_args_ref.rs deleted file mode 100644 index 37ff4a4b9..000000000 --- a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_multi_args_ref.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! `#[cgp_auto_dispatch]` on a `&mut self` method with borrowed arguments and a -//! borrowed return, exercising the lifetime handling in the generated handler. -//! -//! See cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. - -use cgp::prelude::*; - -use super::types::{Bar, Foo, FooBar}; - -#[cgp_auto_dispatch] -pub trait CanCall { - fn call<'a>(&'a mut self, _a: &'a u64, _b: bool) -> &'a str; -} - -impl CanCall for Foo { - fn call(&mut self, _a: &u64, _b: bool) -> &str { - "foo" - } -} - -impl CanCall for Bar { - fn call(&mut self, _a: &u64, _b: bool) -> &str { - "bar" - } -} - -pub trait CheckCanCallFooBar: CanCall {} -impl CheckCanCallFooBar for FooBar {} - -#[test] -fn test_call_multi_args_ref() { - assert_eq!(FooBar::Foo(Foo).call(&42, true), "foo"); -} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_raw_method_name.rs b/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_raw_method_name.rs deleted file mode 100644 index e7340545a..000000000 --- a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_raw_method_name.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! A dispatch trait whose method has a raw identifier as its name, `r#type`. The -//! generated computer and helper are named from the unrawed name (`ComputeType` and -//! `__compute_type__`), since `r#` cannot appear inside a longer identifier. -//! -//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_auto_dispatch.md. - -use cgp::prelude::*; - -#[derive(CgpData)] -pub enum Shape { - Circle(Circle), - Square(Square), -} - -pub struct Circle; - -pub struct Square; - -#[cgp_auto_dispatch] -pub trait HasKind { - fn r#type(&self) -> &'static str; -} - -impl HasKind for Circle { - fn r#type(&self) -> &'static str { - "circle" - } -} - -impl HasKind for Square { - fn r#type(&self) -> &'static str { - "square" - } -} - -#[test] -fn test_raw_method_name_dispatches() { - assert_eq!(Shape::Circle(Circle).r#type(), "circle"); - assert_eq!(Shape::Square(Square).r#type(), "square"); -} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_mut_only.rs b/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_mut_only.rs deleted file mode 100644 index b46ae2a87..000000000 --- a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_mut_only.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! `#[cgp_auto_dispatch]` on a `&mut self` method with no extra arguments. -//! -//! The generated handler routes a `&mut FooBar` reference to the matching -//! variant impl of `CanCall`. -//! -//! See cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. - -use cgp::prelude::*; - -use super::types::{Bar, Foo, FooBar}; - -#[cgp_auto_dispatch] -pub trait CanCall { - fn call(&mut self) -> &'static str; -} - -impl CanCall for Foo { - fn call(&mut self) -> &'static str { - "foo" - } -} - -impl CanCall for Bar { - fn call(&mut self) -> &'static str { - "bar" - } -} - -pub trait CheckCanCallFooBar: CanCall {} -impl CheckCanCallFooBar for FooBar {} - -#[test] -fn test_call_self_mut_only() { - assert_eq!(FooBar::Foo(Foo).call(), "foo"); -} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_only.rs b/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_only.rs deleted file mode 100644 index 42a7d3cd6..000000000 --- a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_only.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! `#[cgp_auto_dispatch]` on a by-value `self` method with no extra arguments. -//! -//! The generated handler routes a `FooBar` value to the `Foo`/`Bar` variant -//! impl of `CanCall`, consuming `self`. -//! -//! See cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. - -use cgp::prelude::*; - -use super::types::{Bar, Foo, FooBar}; - -#[cgp_auto_dispatch] -pub trait CanCall { - fn call(self) -> &'static str; -} - -impl CanCall for Foo { - fn call(self) -> &'static str { - "foo" - } -} - -impl CanCall for Bar { - fn call(self) -> &'static str { - "bar" - } -} - -pub trait CheckCanCallFooBar: CanCall {} -impl CheckCanCallFooBar for FooBar {} - -#[test] -fn test_call_self_only() { - assert_eq!(FooBar::Foo(Foo).call(), "foo"); -} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_ref_only.rs b/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_ref_only.rs deleted file mode 100644 index e6e2fec6d..000000000 --- a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_ref_only.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! `#[cgp_auto_dispatch]` on a `&self` method with no extra arguments. -//! -//! The generated handler routes a `&FooBar` reference to the matching variant -//! impl of `CanCall`. -//! -//! See cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. - -use cgp::prelude::*; - -use super::types::{Bar, Foo, FooBar}; - -#[cgp_auto_dispatch] -pub trait CanCall { - fn call(&self) -> &'static str; -} - -impl CanCall for Foo { - fn call(&self) -> &'static str { - "foo" - } -} - -impl CanCall for Bar { - fn call(&self) -> &'static str { - "bar" - } -} - -pub trait CheckCanCallFooBar: CanCall {} -impl CheckCanCallFooBar for FooBar {} - -#[test] -fn test_call_self_ref_only() { - assert_eq!(FooBar::Foo(Foo).call(), "foo"); -} diff --git a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_ref_return_explicit_ref.rs b/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_ref_return_explicit_ref.rs deleted file mode 100644 index f823b6451..000000000 --- a/crates/tests/cgp-tests/tests/dispatching/auto_dispatch_self_ref_return_explicit_ref.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! `#[cgp_auto_dispatch]` on a `&self` method whose return borrow is written -//! with an *explicit* lifetime (`fn call<'a>(&'a self) -> &'a str`). -//! -//! Companion to `auto_dispatch_self_ref_return_implicit_ref`, which writes the -//! same shape with an elided lifetime. -//! -//! See cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md. - -use cgp::prelude::*; - -use super::types::{Bar, Foo, FooBar}; - -#[cgp_auto_dispatch] -pub trait CanCall { - fn call<'a>(&'a self) -> &'a str; -} - -impl CanCall for Foo { - fn call(&self) -> &str { - "foo" - } -} - -impl CanCall for Bar { - fn call(&self) -> &str { - "bar" - } -} - -pub trait CheckCanCallFooBar: CanCall {} -impl CheckCanCallFooBar for FooBar {} - -#[test] -fn test_call_self_ref_return_explicit_ref() { - assert_eq!(FooBar::Foo(Foo).call(), "foo"); -} diff --git a/crates/tests/cgp-tests/tests/dispatching/mod.rs b/crates/tests/cgp-tests/tests/dispatching/mod.rs index 1720e6f9e..af21debb3 100644 --- a/crates/tests/cgp-tests/tests/dispatching/mod.rs +++ b/crates/tests/cgp-tests/tests/dispatching/mod.rs @@ -1,43 +1,6 @@ //! One unit test per file. Each file is self-contained: it defines its own //! traits, providers, and context types at module scope so that the type-level //! wiring of one test never leaks into another. -//! -//! The exception is `types`, a small shared fixture (the `Foo`/`Bar`/`FooBar` -//! enum) that the `#[cgp_auto_dispatch]` shape tests dispatch over. It is -//! declared here and referenced by siblings via `super::types`. - -// Shared fixture: the `Foo`/`Bar`/`FooBar` enum the auto-dispatch tests route over. -pub mod types; - -// `#[cgp_auto_dispatch]` shape coverage: one method-shape per file. Each defines -// per-variant impls and dispatches them over an extensible-data enum. Two files cover -// the generated names instead: a module item sharing a method's name, and a method -// named with a raw identifier. -pub mod auto_dispatch_generics; -pub mod auto_dispatch_method_name_in_scope; -pub mod auto_dispatch_multi_args; -pub mod auto_dispatch_multi_args_owned_self; -pub mod auto_dispatch_multi_args_ref; -pub mod auto_dispatch_multi_methods; -pub mod auto_dispatch_raw_method_name; -pub mod auto_dispatch_self_mut_only; -pub mod auto_dispatch_self_only; -pub mod auto_dispatch_self_ref_only; -pub mod auto_dispatch_self_ref_return_explicit_ref; -pub mod auto_dispatch_self_ref_return_implicit_ref; -pub mod auto_dispatch_shape; - -// `#[cgp_auto_dispatch]` combined with `#[async_trait]` — the async shapes. -pub mod auto_dispatch_async_generics; -pub mod auto_dispatch_async_multi_args; -pub mod auto_dispatch_async_multi_args_owned_self; -pub mod auto_dispatch_async_multi_args_ref; -pub mod auto_dispatch_async_self_mut_only; -pub mod auto_dispatch_async_self_only; -pub mod auto_dispatch_async_self_ref_only; - -// `#[cgp_auto_dispatch]` beside imported `CanCompute`/`CanComputeAsync`. -pub mod auto_dispatch_consumer_traits_in_scope; // The `UseDelegate` dispatch provider and the `UseDelegate`-table form of // `delegate_components!` (this concept owns those snapshots). diff --git a/crates/tests/cgp-tests/tests/dispatching_tests.rs b/crates/tests/cgp-tests/tests/dispatching_tests.rs index 6436d48f4..2de1a6897 100644 --- a/crates/tests/cgp-tests/tests/dispatching_tests.rs +++ b/crates/tests/cgp-tests/tests/dispatching_tests.rs @@ -1,18 +1,16 @@ //! Entrypoint for the `dispatching` concept. //! -//! Covers CGP's dispatch machinery: the `#[cgp_auto_dispatch]` macro (which -//! turns a per-variant trait into a handler that routes an extensible-data enum -//! to the matching variant impl), the `UseDelegate` dispatch provider and its +//! Covers CGP's dispatch machinery: the `UseDelegate` dispatch provider and its //! `UseDelegate`-table form of `delegate_components!`, and composing -//! handler/computer providers. +//! handler/computer providers. `#[cgp_auto_dispatch]` has its own +//! `auto_dispatch` target. //! //! This concept owns the `UseDelegate`-table snapshots of `delegate_components!` //! and `delegate_and_check_components!`; other macros used here as incidental //! scaffolding are written in their plain form (their expansions are pinned in //! their own owning targets). //! -//! See cgp-knowledge-base/cgp/reference/macros/cgp_auto_dispatch.md, -//! cgp-knowledge-base/cgp/reference/providers/use_delegate.md, +//! See cgp-knowledge-base/cgp/reference/providers/use_delegate.md, //! cgp-knowledge-base/cgp/reference/providers/dispatch_combinators.md, and //! cgp-knowledge-base/cgp/concepts/dispatching.md. #![allow(dead_code)] diff --git a/crates/tests/cgp-tests/tests/extensible_variants/shape_dispatch_ref.rs b/crates/tests/cgp-tests/tests/extensible_variants/shape_dispatch_ref.rs index 508a7b9eb..fc3468c88 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/shape_dispatch_ref.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/shape_dispatch_ref.rs @@ -13,9 +13,7 @@ use std::f64::consts::PI; -use cgp::extra::dispatch::{ - MatchFirstWithValueHandlersMut, MatchFirstWithValueHandlersRef, MatchWithValueHandlersRef, -}; +use cgp::extra::dispatch::MatchFirstWithValueHandlersMut; use cgp::extra::handler::NoCode; use cgp::prelude::*; diff --git a/crates/tests/cgp-tests/tests/handlers/computer_macro.rs b/crates/tests/cgp-tests/tests/handlers/computer_macro.rs index 8f445a9a3..9032854c3 100644 --- a/crates/tests/cgp-tests/tests/handlers/computer_macro.rs +++ b/crates/tests/cgp-tests/tests/handlers/computer_macro.rs @@ -14,7 +14,11 @@ //! `Handler` has an error type; it uses the plain `delegate_components!` (the //! error and wiring macros are owned by other concept targets). //! -//! See cgp-knowledge-base/cgp/reference/components/computer.md. +//! This target owns the `#[cgp_computer]` snapshots: the synchronous value and +//! `Result` cases and a generic function are pinned here. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md and +//! cgp-knowledge-base/cgp/reference/components/computer.md. use core::fmt::Display; @@ -22,16 +26,349 @@ use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent}; use cgp::extra::error::RaiseFrom; use cgp::extra::handler::{ComputerRef, HandlerRef, TryComputerRef}; use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_computer; use futures::executor::block_on; -#[cgp_computer] -fn add(a: u64, b: u64) -> u64 { - a + b +snapshot_cgp_computer! { + #[cgp_computer] + fn add(a: u64, b: u64) -> u64 { + a + b + } + + expand_add(output) { + insta::assert_snapshot!(output, @" + fn add(a: u64, b: u64) -> u64 { + a + b + } + impl<__Context__, __Code__> Computer<__Context__, __Code__, (u64, u64)> for Add { + type Output = u64; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0, arg_1): (u64, u64), + ) -> Self::Output { + add(arg_0, arg_1) + } + } + impl< + __Context__, + __Code__, + > IsProviderFor for Add {} + pub struct Add; + impl DelegateComponent for Add + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Add + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Add + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Add + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Add + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Add + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Add + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Add + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Add + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Add + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Add + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Add + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Add + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Add + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } } -#[cgp_computer] -fn add_with_error(a: u64, b: u64) -> Result { - a.checked_add(b).ok_or_else(|| "Overflow".to_string()) +snapshot_cgp_computer! { + #[cgp_computer] + fn add_with_error(a: u64, b: u64) -> Result { + a.checked_add(b).ok_or_else(|| "Overflow".to_string()) + } + + expand_add_with_error(output) { + insta::assert_snapshot!(output, @r#" + fn add_with_error(a: u64, b: u64) -> Result { + a.checked_add(b).ok_or_else(|| "Overflow".to_string()) + } + impl<__Context__, __Code__> Computer<__Context__, __Code__, (u64, u64)> + for AddWithError { + type Output = Result; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0, arg_1): (u64, u64), + ) -> Self::Output { + add_with_error(arg_0, arg_1) + } + } + impl< + __Context__, + __Code__, + > IsProviderFor + for AddWithError {} + pub struct AddWithError; + impl DelegateComponent for AddWithError + where + PromoteTryComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddWithError + where + PromoteTryComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddWithError + where + PromoteTryComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddWithError + where + PromoteTryComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddWithError + where + PromoteTryComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddWithError + where + PromoteTryComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddWithError + where + PromoteTryComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddWithError + where + PromoteTryComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddWithError + where + PromoteTryComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddWithError + where + PromoteTryComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddWithError + where + PromoteTryComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddWithError + where + PromoteTryComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddWithError + where + PromoteTryComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddWithError + where + PromoteTryComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + "#) + } } pub struct App; @@ -138,7 +475,177 @@ fn test_computer_ref() { ); } -#[cgp_computer] -pub fn add_generic>(a: T, b: T) -> T { - a + b +snapshot_cgp_computer! { + #[cgp_computer] + pub fn add_generic>(a: T, b: T) -> T { + a + b + } + + expand_add_generic(output) { + insta::assert_snapshot!(output, @" + pub fn add_generic>(a: T, b: T) -> T { + a + b + } + impl< + T: core::ops::Add, + __Context__, + __Code__, + > Computer<__Context__, __Code__, (T, T)> for AddGeneric { + type Output = T; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0, arg_1): (T, T), + ) -> Self::Output { + add_generic(arg_0, arg_1) + } + } + impl< + T: core::ops::Add, + __Context__, + __Code__, + > IsProviderFor for AddGeneric {} + pub struct AddGeneric; + impl DelegateComponent for AddGeneric + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddGeneric + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddGeneric + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddGeneric + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddGeneric + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddGeneric + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddGeneric + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddGeneric + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddGeneric + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddGeneric + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddGeneric + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddGeneric + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddGeneric + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddGeneric + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } } diff --git a/crates/tests/cgp-tests/tests/handlers/computer_macro_arity.rs b/crates/tests/cgp-tests/tests/handlers/computer_macro_arity.rs new file mode 100644 index 000000000..13290ccd6 --- /dev/null +++ b/crates/tests/cgp-tests/tests/handlers/computer_macro_arity.rs @@ -0,0 +1,375 @@ +//! `#[cgp_computer]` across parameter counts and patterns. +//! +//! The input type is the parameter types written inside parentheses, so no +//! parameters give `()`, one parameter gives the bare type (`(u64)` is `u64`), +//! and two or more give a tuple. Parameter patterns are discarded: each input is +//! rebound positionally as `arg_i`, so a `mut` binding or a destructuring pattern +//! still compiles, and the function itself keeps its own patterns. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_computer.md. + +use cgp::extra::handler::Computer; +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_computer; + +snapshot_cgp_computer! { + #[cgp_computer] + fn zero() -> u64 { + 0 + } + + expand_no_parameters(output) { + insta::assert_snapshot!(output, @" + fn zero() -> u64 { + 0 + } + impl<__Context__, __Code__> Computer<__Context__, __Code__, ()> for Zero { + type Output = u64; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (): (), + ) -> Self::Output { + zero() + } + } + impl<__Context__, __Code__> IsProviderFor + for Zero {} + pub struct Zero; + impl DelegateComponent for Zero + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Zero + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Zero + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Zero + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Zero + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Zero + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Zero + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Zero + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Zero + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Zero + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Zero + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Zero + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Zero + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Zero + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +snapshot_cgp_computer! { + #[cgp_computer] + fn increment(value: u64) -> u64 { + value + 1 + } + + expand_one_parameter(output) { + insta::assert_snapshot!(output, @" + fn increment(value: u64) -> u64 { + value + 1 + } + impl<__Context__, __Code__> Computer<__Context__, __Code__, (u64)> for Increment { + type Output = u64; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (u64), + ) -> Self::Output { + increment(arg_0) + } + } + impl< + __Context__, + __Code__, + > IsProviderFor for Increment {} + pub struct Increment; + impl DelegateComponent for Increment + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Increment + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Increment + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Increment + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Increment + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Increment + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Increment + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Increment + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Increment + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Increment + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Increment + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Increment + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Increment + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Increment + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +#[cgp_computer] +fn decrement(mut value: u64) -> u64 { + value -= 1; + value +} + +#[cgp_computer] +fn sum_pair((a, b): (u64, u64), scale: u64) -> u64 { + (a + b) * scale +} + +pub struct App; + +#[test] +fn test_computer_arity() { + let code = PhantomData::<()>; + + assert_eq!(Zero::compute(&App, code, ()), 0); + assert_eq!(Increment::compute(&App, code, 1), 2); + assert_eq!(Decrement::compute(&App, code, 2), 1); + assert_eq!(SumPair::compute(&App, code, ((1, 2), 3)), 9); +} diff --git a/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs b/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs new file mode 100644 index 000000000..cf29ce60d --- /dev/null +++ b/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs @@ -0,0 +1,553 @@ +//! `#[cgp_computer]` on functions with a `where` clause, a lifetime parameter, +//! and a const generic parameter. +//! +//! The function's generics and `where` clause move onto the generated impl, ahead +//! of the reserved `__Context__` and `__Code__` parameters. Each parameter here +//! appears in an input type, which is what keeps it constrained by the impl. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_computer.md. + +use core::fmt::Display; + +use cgp::extra::handler::Computer; +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_computer; + +snapshot_cgp_computer! { + #[cgp_computer] + fn describe(value: T) -> String + where + T: Display, + { + format!("<{value}>") + } + + expand_where_clause(output) { + insta::assert_snapshot!(output, @r#" + fn describe(value: T) -> String + where + T: Display, + { + format!("<{value}>") + } + impl Computer<__Context__, __Code__, (T)> for Describe + where + T: Display, + { + type Output = String; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (T), + ) -> Self::Output { + describe(arg_0) + } + } + impl< + T, + __Context__, + __Code__, + > IsProviderFor for Describe + where + T: Display, + {} + pub struct Describe; + impl DelegateComponent for Describe + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Describe + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Describe + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Describe + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Describe + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Describe + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Describe + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Describe + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Describe + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Describe + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Describe + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Describe + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Describe + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Describe + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + "#) + } +} + +snapshot_cgp_computer! { + #[cgp_computer] + fn first_char<'a>(value: &'a str) -> &'a str { + &value[..1] + } + + expand_lifetime_parameter(output) { + insta::assert_snapshot!(output, @" + fn first_char<'a>(value: &'a str) -> &'a str { + &value[..1] + } + impl<'a, __Context__, __Code__> Computer<__Context__, __Code__, (&'a str)> + for FirstChar { + type Output = &'a str; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (&'a str), + ) -> Self::Output { + first_char(arg_0) + } + } + impl< + 'a, + __Context__, + __Code__, + > IsProviderFor for FirstChar {} + pub struct FirstChar; + impl DelegateComponent for FirstChar + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for FirstChar + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for FirstChar + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for FirstChar + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for FirstChar + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for FirstChar + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for FirstChar + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for FirstChar + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for FirstChar + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for FirstChar + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for FirstChar + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for FirstChar + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for FirstChar + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for FirstChar + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +snapshot_cgp_computer! { + #[cgp_computer] + fn sum_array(values: [u64; N]) -> u64 { + values.iter().sum() + } + + expand_const_parameter(output) { + insta::assert_snapshot!(output, @" + fn sum_array(values: [u64; N]) -> u64 { + values.iter().sum() + } + impl Computer<__Context__, __Code__, ([u64; N])> + for SumArray { + type Output = u64; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): ([u64; N]), + ) -> Self::Output { + sum_array(arg_0) + } + } + impl< + const N: usize, + __Context__, + __Code__, + > IsProviderFor for SumArray {} + pub struct SumArray; + impl DelegateComponent for SumArray + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for SumArray + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for SumArray + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for SumArray + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for SumArray + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for SumArray + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for SumArray + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for SumArray + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for SumArray + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for SumArray + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for SumArray + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for SumArray + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for SumArray + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for SumArray + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +pub struct App; + +#[test] +fn test_computer_generics() { + let code = PhantomData::<()>; + + assert_eq!(Describe::compute(&App, code, 1), "<1>"); + assert_eq!(FirstChar::compute(&App, code, "abc"), "a"); + assert_eq!(SumArray::compute(&App, code, [1, 2, 3]), 6); +} diff --git a/crates/tests/cgp-tests/tests/handlers/computer_macro_named.rs b/crates/tests/cgp-tests/tests/handlers/computer_macro_named.rs new file mode 100644 index 000000000..6e80ecfd3 --- /dev/null +++ b/crates/tests/cgp-tests/tests/handlers/computer_macro_named.rs @@ -0,0 +1,186 @@ +//! `#[cgp_computer(Name)]`: an explicit provider name replaces the default, which +//! is the function name in PascalCase. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_computer.md. + +use cgp::extra::handler::Computer; +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_computer; + +snapshot_cgp_computer! { + #[cgp_computer(MyAdder)] + fn add(a: u64, b: u64) -> u64 { + a + b + } + + expand_named_computer(output) { + insta::assert_snapshot!(output, @" + fn add(a: u64, b: u64) -> u64 { + a + b + } + impl<__Context__, __Code__> Computer<__Context__, __Code__, (u64, u64)> for MyAdder { + type Output = u64; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0, arg_1): (u64, u64), + ) -> Self::Output { + add(arg_0, arg_1) + } + } + impl< + __Context__, + __Code__, + > IsProviderFor for MyAdder {} + pub struct MyAdder; + impl DelegateComponent for MyAdder + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MyAdder + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for MyAdder + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MyAdder + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for MyAdder + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MyAdder + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for MyAdder + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MyAdder + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for MyAdder + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MyAdder + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for MyAdder + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MyAdder + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for MyAdder + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MyAdder + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +pub struct App; + +#[test] +fn test_named_computer() { + assert_eq!(MyAdder::compute(&App, PhantomData::<()>, (1, 2)), 3); +} diff --git a/crates/tests/cgp-tests/tests/handlers/computer_macro_output.rs b/crates/tests/cgp-tests/tests/handlers/computer_macro_output.rs new file mode 100644 index 000000000..1ad8c1cc2 --- /dev/null +++ b/crates/tests/cgp-tests/tests/handlers/computer_macro_output.rs @@ -0,0 +1,374 @@ +//! How `#[cgp_computer]` reads the return type. +//! +//! An omitted return type is `()`. Only the bare two-argument form +//! `Result` selects the fallible promotion bundle; any other spelling, +//! such as the qualified `core::result::Result`, is treated as a plain +//! value, so `try_compute` wraps the whole result in `Ok` rather than +//! propagating its error. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_computer.md. + +use cgp::core::error::ErrorTypeProviderComponent; +use cgp::extra::handler::{Computer, TryComputer}; +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_computer; + +snapshot_cgp_computer! { + #[cgp_computer] + fn ignore(_value: u64) {} + + expand_unit_return(output) { + insta::assert_snapshot!(output, @" + fn ignore(_value: u64) {} + impl<__Context__, __Code__> Computer<__Context__, __Code__, (u64)> for Ignore { + type Output = (); + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (u64), + ) -> Self::Output { + ignore(arg_0) + } + } + impl< + __Context__, + __Code__, + > IsProviderFor for Ignore {} + pub struct Ignore; + impl DelegateComponent for Ignore + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Ignore + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Ignore + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Ignore + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Ignore + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Ignore + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Ignore + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Ignore + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Ignore + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Ignore + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Ignore + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Ignore + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Ignore + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Ignore + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +snapshot_cgp_computer! { + #[cgp_computer] + fn parse_qualified(value: String) -> core::result::Result { + value.parse().map_err(|_| value) + } + + expand_qualified_result(output) { + insta::assert_snapshot!(output, @" + fn parse_qualified(value: String) -> core::result::Result { + value.parse().map_err(|_| value) + } + impl<__Context__, __Code__> Computer<__Context__, __Code__, (String)> + for ParseQualified { + type Output = core::result::Result; + fn compute( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0): (String), + ) -> Self::Output { + parse_qualified(arg_0) + } + } + impl< + __Context__, + __Code__, + > IsProviderFor + for ParseQualified {} + pub struct ParseQualified; + impl DelegateComponent for ParseQualified + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ParseQualified + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + ComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ParseQualified + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ParseQualified + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ParseQualified + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ParseQualified + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + TryComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ParseQualified + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ParseQualified + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ParseQualified + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ParseQualified + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ParseQualified + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ParseQualified + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for ParseQualified + where + PromoteComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for ParseQualified + where + PromoteComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } +} + +pub struct App; + +delegate_components! { + App { + ErrorTypeProviderComponent: + UseType, + } +} + +#[test] +fn test_computer_output() { + let code = PhantomData::<()>; + + Ignore::compute(&App, code, 1); + + assert_eq!( + ParseQualified::try_compute(&App, code, "nope".to_owned()), + Ok(Err("nope".to_owned())), + ); +} diff --git a/crates/tests/cgp-tests/tests/handlers/computer_macro_visibility.rs b/crates/tests/cgp-tests/tests/handlers/computer_macro_visibility.rs new file mode 100644 index 000000000..b7525a343 --- /dev/null +++ b/crates/tests/cgp-tests/tests/handlers/computer_macro_visibility.rs @@ -0,0 +1,25 @@ +//! A `#[cgp_computer]` function declared in a module and used from outside it. +//! +//! The generated provider struct is public, so a `pub fn` computer defined in a +//! child module can be named and wired from its parent. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md. + +use cgp::extra::handler::Computer; +use cgp::prelude::*; + +mod math { + use cgp::prelude::*; + + #[cgp_computer] + pub fn double(value: u64) -> u64 { + value * 2 + } +} + +pub struct App; + +#[test] +fn test_computer_across_modules() { + assert_eq!(math::Double::compute(&App, PhantomData::<()>, 2), 4); +} diff --git a/crates/tests/cgp-tests/tests/handlers/handler_macro.rs b/crates/tests/cgp-tests/tests/handlers/handler_macro.rs index e2e24a4af..48e8d6de7 100644 --- a/crates/tests/cgp-tests/tests/handlers/handler_macro.rs +++ b/crates/tests/cgp-tests/tests/handlers/handler_macro.rs @@ -10,7 +10,10 @@ //! The error wiring on `App` is incidental scaffolding, so it uses the plain //! `delegate_components!`. //! -//! See cgp-knowledge-base/cgp/reference/components/handler.md and +//! The async value and `Result` expansions of `#[cgp_computer]` are pinned here. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md, +//! cgp-knowledge-base/cgp/reference/components/handler.md, and //! cgp-knowledge-base/cgp/reference/components/computer.md. use core::fmt::Display; @@ -19,16 +22,189 @@ use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent}; use cgp::extra::error::RaiseFrom; use cgp::extra::handler::HandlerRef; use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_computer; use futures::executor::block_on; -#[cgp_computer] -async fn add(a: u64, b: u64) -> u64 { - a + b +snapshot_cgp_computer! { + #[cgp_computer] + async fn add(a: u64, b: u64) -> u64 { + a + b + } + + expand_async_add(output) { + insta::assert_snapshot!(output, @" + async fn add(a: u64, b: u64) -> u64 { + a + b + } + impl<__Context__, __Code__> AsyncComputer<__Context__, __Code__, (u64, u64)> for Add { + type Output = u64; + async fn compute_async( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0, arg_1): (u64, u64), + ) -> Self::Output { + add(arg_0, arg_1).await + } + } + impl< + __Context__, + __Code__, + > IsProviderFor for Add {} + pub struct Add; + impl DelegateComponent for Add + where + PromoteAsyncComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Add + where + PromoteAsyncComputer: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Add + where + PromoteAsyncComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Add + where + PromoteAsyncComputer: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for Add + where + PromoteAsyncComputer: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for Add + where + PromoteAsyncComputer: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + ") + } } -#[cgp_computer] -async fn add_with_error(a: u64, b: u64) -> Result { - a.checked_add(b).ok_or_else(|| "Overflow".to_string()) +snapshot_cgp_computer! { + #[cgp_computer] + async fn add_with_error(a: u64, b: u64) -> Result { + a.checked_add(b).ok_or_else(|| "Overflow".to_string()) + } + + expand_async_add_with_error(output) { + insta::assert_snapshot!(output, @r#" + async fn add_with_error(a: u64, b: u64) -> Result { + a.checked_add(b).ok_or_else(|| "Overflow".to_string()) + } + impl<__Context__, __Code__> AsyncComputer<__Context__, __Code__, (u64, u64)> + for AddWithError { + type Output = Result; + async fn compute_async( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + (arg_0, arg_1): (u64, u64), + ) -> Self::Output { + add_with_error(arg_0, arg_1).await + } + } + impl< + __Context__, + __Code__, + > IsProviderFor + for AddWithError {} + pub struct AddWithError; + impl DelegateComponent for AddWithError + where + PromoteHandler: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddWithError + where + PromoteHandler: DelegateComponent, + as DelegateComponent< + AsyncComputerRefComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddWithError + where + PromoteHandler: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddWithError + where + PromoteHandler: DelegateComponent, + as DelegateComponent< + HandlerComponent, + >>::Delegate: IsProviderFor, + {} + impl DelegateComponent for AddWithError + where + PromoteHandler: DelegateComponent, + { + type Delegate = as DelegateComponent>::Delegate; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for AddWithError + where + PromoteHandler: DelegateComponent, + as DelegateComponent< + HandlerRefComponent, + >>::Delegate: IsProviderFor, + {} + "#) + } } pub struct App; diff --git a/crates/tests/cgp-tests/tests/handlers/macros_without_prelude.rs b/crates/tests/cgp-tests/tests/handlers/macros_without_prelude.rs new file mode 100644 index 000000000..cdbfeabdc --- /dev/null +++ b/crates/tests/cgp-tests/tests/handlers/macros_without_prelude.rs @@ -0,0 +1,51 @@ +//! `#[cgp_computer]` and `#[cgp_producer]` in a module that does not import +//! `cgp::prelude::*`. +//! +//! The expansion names every CGP item through its fully qualified +//! `::cgp::macro_prelude` path, so the macros work when invoked by path alone, +//! and an item of the same name as a CGP trait in the caller's module, here a +//! local `Computer` struct, does not capture it. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md. + +use core::marker::PhantomData; + +/// A local item named like the `Computer` trait the expansion implements. +pub struct Computer; + +#[cgp::prelude::cgp_computer] +fn add(a: u64, b: u64) -> u64 { + a + b +} + +#[cgp::prelude::cgp_computer] +async fn add_async(a: u64, b: u64) -> u64 { + a + b +} + +#[cgp::prelude::cgp_computer] +fn checked_add(a: u64, b: u64) -> Result { + a.checked_add(b).ok_or_else(|| "overflow".to_owned()) +} + +#[cgp::prelude::cgp_producer] +fn magic_number() -> u64 { + 42 +} + +pub struct App; + +#[test] +fn test_macros_without_prelude() { + use cgp::extra::handler::{AsyncComputer, Computer, Producer}; + use futures::executor::block_on; + + assert_eq!(Add::compute(&App, PhantomData::<()>, (1, 2)), 3); + assert_eq!(CheckedAdd::compute(&App, PhantomData::<()>, (1, 2)), Ok(3)); + assert_eq!(MagicNumber::produce(&App, PhantomData::<()>), 42); + + assert_eq!( + block_on(AddAsync::compute_async(&App, PhantomData::<()>, (1, 2))), + 3 + ); +} diff --git a/crates/tests/cgp-tests/tests/handlers/mod.rs b/crates/tests/cgp-tests/tests/handlers/mod.rs index 98a6c161b..126553073 100644 --- a/crates/tests/cgp-tests/tests/handlers/mod.rs +++ b/crates/tests/cgp-tests/tests/handlers/mod.rs @@ -7,8 +7,16 @@ // across the whole computation family (compute / try_compute / produce / handle // and their async and by-ref variants). pub mod computer_macro; +pub mod computer_macro_arity; +pub mod computer_macro_generics; +pub mod computer_macro_named; +pub mod computer_macro_output; +pub mod computer_macro_visibility; pub mod handler_macro; +pub mod macros_without_prelude; pub mod producer_macro; +pub mod producer_macro_output; +pub mod raw_function_names; // The `PipeHandlers` combinator: chaining computers, and chaining handlers with // the `Promote*` adapters that lift a simpler handler into a more capable one. diff --git a/crates/tests/cgp-tests/tests/handlers/producer_macro.rs b/crates/tests/cgp-tests/tests/handlers/producer_macro.rs index 729e8befb..873402f4a 100644 --- a/crates/tests/cgp-tests/tests/handlers/producer_macro.rs +++ b/crates/tests/cgp-tests/tests/handlers/producer_macro.rs @@ -10,16 +10,128 @@ //! The error type wiring on `App` is incidental scaffolding, so it uses the //! plain `delegate_components!`. //! -//! See cgp-knowledge-base/cgp/reference/components/producer.md. +//! This target owns the `#[cgp_producer]` snapshot, pinned here. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_producer.md and +//! cgp-knowledge-base/cgp/reference/components/producer.md. use cgp::core::error::ErrorTypeProviderComponent; use cgp::extra::handler::{ComputerRef, HandlerRef, TryComputerRef}; use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_producer; use futures::executor::block_on; -#[cgp_producer] -pub fn magic_number() -> u64 { - 42 +snapshot_cgp_producer! { + #[cgp_producer] + pub fn magic_number() -> u64 { + 42 + } + + expand_magic_number(output) { + insta::assert_snapshot!(output, @" + pub fn magic_number() -> u64 { + 42 + } + impl<__Context__, __Code__> Producer<__Context__, __Code__> for MagicNumber { + type Output = u64; + fn produce( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + ) -> Self::Output { + magic_number() + } + } + impl<__Context__, __Code__> IsProviderFor + for MagicNumber {} + pub struct MagicNumber; + impl DelegateComponent for MagicNumber { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MagicNumber + where + PromoteProducer: IsProviderFor, + {} + impl DelegateComponent for MagicNumber { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MagicNumber + where + PromoteProducer: IsProviderFor, + {} + impl DelegateComponent for MagicNumber { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MagicNumber + where + PromoteProducer: IsProviderFor, + {} + impl DelegateComponent for MagicNumber { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MagicNumber + where + PromoteProducer< + Self, + >: IsProviderFor, + {} + impl DelegateComponent for MagicNumber { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MagicNumber + where + PromoteProducer< + Self, + >: IsProviderFor, + {} + impl DelegateComponent for MagicNumber { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MagicNumber + where + PromoteProducer< + Self, + >: IsProviderFor, + {} + impl DelegateComponent for MagicNumber { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MagicNumber + where + PromoteProducer: IsProviderFor, + {} + impl DelegateComponent for MagicNumber { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for MagicNumber + where + PromoteProducer: IsProviderFor, + {} + ") + } } pub struct App; diff --git a/crates/tests/cgp-tests/tests/handlers/producer_macro_output.rs b/crates/tests/cgp-tests/tests/handlers/producer_macro_output.rs new file mode 100644 index 000000000..37ee801ba --- /dev/null +++ b/crates/tests/cgp-tests/tests/handlers/producer_macro_output.rs @@ -0,0 +1,159 @@ +//! `#[cgp_producer]` with an explicit name, an omitted return type, and a +//! `Result` return type. +//! +//! An explicit name replaces the PascalCase default. An omitted return type is +//! `()`. A producer has one expansion whatever it returns: a `Result` output is +//! a plain value, so `try_compute` wraps it in `Ok` instead of propagating its +//! error. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_producer.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_producer.md. + +use cgp::core::error::ErrorTypeProviderComponent; +use cgp::extra::handler::{Producer, TryComputer}; +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_producer; + +snapshot_cgp_producer! { + #[cgp_producer(TheAnswer)] + fn magic_number() -> u64 { + 42 + } + + expand_named_producer(output) { + insta::assert_snapshot!(output, @" + fn magic_number() -> u64 { + 42 + } + impl<__Context__, __Code__> Producer<__Context__, __Code__> for TheAnswer { + type Output = u64; + fn produce( + _context: &__Context__, + _code: ::core::marker::PhantomData<__Code__>, + ) -> Self::Output { + magic_number() + } + } + impl<__Context__, __Code__> IsProviderFor + for TheAnswer {} + pub struct TheAnswer; + impl DelegateComponent for TheAnswer { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for TheAnswer + where + PromoteProducer: IsProviderFor, + {} + impl DelegateComponent for TheAnswer { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for TheAnswer + where + PromoteProducer: IsProviderFor, + {} + impl DelegateComponent for TheAnswer { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for TheAnswer + where + PromoteProducer: IsProviderFor, + {} + impl DelegateComponent for TheAnswer { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for TheAnswer + where + PromoteProducer< + Self, + >: IsProviderFor, + {} + impl DelegateComponent for TheAnswer { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for TheAnswer + where + PromoteProducer< + Self, + >: IsProviderFor, + {} + impl DelegateComponent for TheAnswer { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for TheAnswer + where + PromoteProducer< + Self, + >: IsProviderFor, + {} + impl DelegateComponent for TheAnswer { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for TheAnswer + where + PromoteProducer: IsProviderFor, + {} + impl DelegateComponent for TheAnswer { + type Delegate = PromoteProducer; + } + impl< + __Context__, + __Params__: ?Sized, + > IsProviderFor for TheAnswer + where + PromoteProducer: IsProviderFor, + {} + ") + } +} + +#[cgp_producer] +fn nothing() {} + +#[cgp_producer] +fn failing() -> Result { + Err("nope".to_owned()) +} + +pub struct App; + +delegate_components! { + App { + ErrorTypeProviderComponent: + UseType, + } +} + +#[test] +fn test_producer_output() { + let code = PhantomData::<()>; + + assert_eq!(TheAnswer::produce(&App, code), 42); + + Nothing::produce(&App, code); + + assert_eq!( + Failing::try_compute(&App, code, ()), + Ok(Err("nope".to_owned())), + ); +} diff --git a/crates/tests/cgp-tests/tests/handlers/raw_function_names.rs b/crates/tests/cgp-tests/tests/handlers/raw_function_names.rs new file mode 100644 index 000000000..fed1a9bf4 --- /dev/null +++ b/crates/tests/cgp-tests/tests/handlers/raw_function_names.rs @@ -0,0 +1,30 @@ +//! `#[cgp_computer]` and `#[cgp_producer]` on functions named with a raw +//! identifier. +//! +//! The default provider name is the function name in PascalCase, read without +//! the `r#` prefix, which cannot begin a longer identifier: `r#type` names the +//! provider `Type`, and `r#loop` names it `Loop`. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md and +//! cgp-knowledge-base/cgp/implementation/entrypoints/cgp_producer.md. + +use cgp::extra::handler::{Computer, Producer}; +use cgp::prelude::*; + +#[cgp_computer] +fn r#type(value: u64) -> u64 { + value + 1 +} + +#[cgp_producer] +fn r#loop() -> u64 { + 42 +} + +pub struct App; + +#[test] +fn test_raw_function_names() { + assert_eq!(Type::compute(&App, PhantomData::<()>, 1), 2); + assert_eq!(Loop::produce(&App, PhantomData::<()>), 42); +} diff --git a/crates/tests/cgp-tests/tests/handlers_tests.rs b/crates/tests/cgp-tests/tests/handlers_tests.rs index 0d1c1875b..8b699baa8 100644 --- a/crates/tests/cgp-tests/tests/handlers_tests.rs +++ b/crates/tests/cgp-tests/tests/handlers_tests.rs @@ -6,14 +6,16 @@ //! the async, fallible `Handler`, and composing handlers into pipelines with the //! `PipeHandlers` combinator. //! -//! This concept does *not* own `#[cgp_component]`, `#[cgp_provider]`, -//! `check_components!`, or `delegate_components!` snapshots — those live in their -//! owning targets — so the scaffolding here uses the plain macros. +//! This concept owns the `#[cgp_computer]` and `#[cgp_producer]` snapshots. It +//! does *not* own `#[cgp_component]`, `#[cgp_provider]`, `check_components!`, or +//! `delegate_components!` snapshots — those live in their owning targets — so the +//! scaffolding here uses the plain macros. //! //! See cgp-knowledge-base/cgp/reference/components/computer.md, //! cgp-knowledge-base/cgp/reference/components/producer.md, //! cgp-knowledge-base/cgp/reference/components/handler.md, and //! cgp-knowledge-base/cgp/reference/providers/handler_combinators.md. #![allow(dead_code)] +#![allow(clippy::needless_lifetimes)] pub mod handlers;