From 128eee67c2ca9877259b89b5ecc80fbed1372eba Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 11 Oct 2026 16:15:38 +0200 Subject: [PATCH 01/25] Describe cargo-cgp's class-based fixture layout in the test guide Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SajznGLmpQzjPd1newVGDK --- crates/tests/AGENTS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/tests/AGENTS.md b/crates/tests/AGENTS.md index 6ead3abba..b50e5c0bc 100644 --- a/crates/tests/AGENTS.md +++ b/crates/tests/AGENTS.md @@ -144,11 +144,11 @@ user actually sees rather than the raw cascade. Add the fixture there — a stan cross-crate — following [cargo-cgp's UI-test guide](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/README.md); its harness records both `cargo-cgp`'s output (`.cgp.stderr`) and the raw compiler -baseline (`.rust.stderr`). cargo-cgp files a fixture by the **quality of the output** — -`acceptable/` when the tool already leads with the root cause, `usability/` when the -cause is present but buried, `ok/` for a clean compile — rather than by the -acceptable/problematic distinction this repository formerly used. Then catalog the class -and link the fixture (below). +baseline (`.rust.stderr`). cargo-cgp files a fixture under its **error class**, +mirroring the knowledge base's error catalog, and records the quality of its output in a +`//@quality:` header (`acceptable` when the tool already leads with the root cause, +`usability` when the cause is present but buried). Then catalog the class and link the +fixture (below). **Pinning the exact invalid output** is a separate, rarer need that *does* stay here: only when you must *inspect* the wrong tokens a macro emits (not merely assert they fail From 60aa1ea54718b3b5bd3514e446ae3411abd0a819 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 12 Oct 2026 17:36:09 +0200 Subject: [PATCH 02/25] Reject an optional reference to an unsized type in field reads A getter return or #[implicit] argument typed Option<&[T]> or Option<&dyn Trait> lowered by the Option<&T> rule to a HasField bound over an unsized Option<[T]>, which the compiler rejected on the macro attribute. The shared field-type parser now rejects both shapes with a message naming the sized container to read instead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SajznGLmpQzjPd1newVGDK --- .../src/functions/field/parse.rs | 31 ++++++++ .../tests/parser_rejections/cgp_fn.rs | 27 ++++++- .../tests/parser_rejections/getters.rs | 73 ++++++++++++++++++- 3 files changed, 125 insertions(+), 6 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/functions/field/parse.rs b/crates/macros/cgp-macro-core/src/functions/field/parse.rs index fafc9f890..672fce1ec 100644 --- a/crates/macros/cgp-macro-core/src/functions/field/parse.rs +++ b/crates/macros/cgp-macro-core/src/functions/field/parse.rs @@ -44,6 +44,8 @@ pub fn parse_field_type( } Type::Path(type_path) => { if let Some(type_ref) = try_parse_option_ref(type_path) { + reject_unsized_option_ref(type_path, type_ref)?; + // `Option<&T>` reads an `Option` field with `.as_ref()`; // `Option<&mut T>` reads it with `.as_mut()`. The read borrows the // field with the *inner* reference's mutability, so a `&mut` inner @@ -83,6 +85,35 @@ pub fn parse_field_type( } } +/// `Option<&T>` reads an `Option` field, and an `Option` holds only a sized value, so an optional +/// reference to a slice or a trait object has no field to read. Rejected here, naming the sized +/// container to read instead, rather than lowered to a bound over an unsized `Option`. +fn reject_unsized_option_ref(type_path: &TypePath, type_ref: &TypeReference) -> syn::Result<()> { + let mut referent = type_ref.elem.as_ref(); + while let Type::Paren(paren) = referent { + referent = paren.elem.as_ref(); + } + + let (written, kind, container) = match referent { + Type::Slice(_) => ("[T]", "a slice", "Vec"), + Type::TraitObject(_) => ("dyn Trait", "a trait object", "Box"), + _ => return Ok(()), + }; + + let reference = if type_ref.mutability.is_some() { + "&mut " + } else { + "&" + }; + + Err(Error::new_spanned( + type_path, + format!( + "`Option<{reference}{written}>` would read an `Option<{written}>` field, which cannot exist because {kind} is unsized; read an `Option<{container}>` field as `Option<{reference}{container}>` instead" + ), + )) +} + /// A mutable field read borrows the whole context exclusively through /// `get_field_mut`, so a `&mut` reference in the field type — the outer `&mut T` or /// the inner reference of an `Option<&mut T>` — is only valid under a `&mut self` 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 9a5918cfd..46bfc3a6d 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 @@ -3,8 +3,9 @@ //! implicit argument, a `&mut` implicit argument that is not the sole implicit //! (its exclusive borrow of the context would conflict with reading any other //! field), a `&mut`-reference implicit under a `&self` receiver (a mutable field -//! read requires `&mut self`), and a malformed `#[implicit]` attribute carrying -//! arguments. Each is a rejection the macro makes during expansion (a returned +//! read requires `&mut self`), an optional slice reference (the `Option<&T>` form +//! would read an unsized `Option<[T]>` field), and a malformed `#[implicit]` +//! attribute carrying 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. One case pins a Known issue instead: @@ -16,7 +17,7 @@ use quote::quote; -use super::assert_macro_rejects; +use super::{assert_macro_rejects, assert_macro_rejects_with}; #[test] fn rejects_implicit_arg_without_self() { @@ -117,6 +118,26 @@ fn rejects_mut_option_implicit_without_mut_self() { ); } +#[test] +fn rejects_optional_slice_implicit() { + // An implicit argument shares the getters' field-type forms, so `Option<&[u8]>` + // is rejected the same way: it would read an unsized `Option<[u8]>` field. + assert_macro_rejects_with( + "cgp_fn with an `Option<&[u8]>` implicit", + "`Option<&[T]>` would read an `Option<[T]>` field, which cannot exist because a slice is unsized; read an `Option>` field as `Option<&Vec>` instead", + || { + cgp_macro_lib::cgp_fn( + quote!(), + quote!( + fn count_items(&self, #[implicit] items: Option<&[u8]>) -> usize { + items.map_or(0, |items| items.len()) + } + ), + ) + }, + ); +} + #[test] fn rejects_implicit_attr_with_arguments() { // `#[implicit]` is a bare marker attribute; a list form such as diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs index 308e8a038..64c76bb81 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs @@ -5,8 +5,10 @@ //! `#[cgp_getter]`. A getter method must be a plain (non-const, non-async, //! non-unsafe, non-generic) method whose first argument is a reference; a mutable //! return requires a `&mut self` receiver; and at most one associated type is -//! allowed, only alongside exactly one method. `#[cgp_auto_getter]` additionally -//! rejects any attribute argument, since it has no provider name or keys to accept. +//! allowed, only alongside exactly one method. An optional reference to a slice or a +//! trait object is rejected too, since the `Option<&T>` form would read an unsized +//! `Option` field. `#[cgp_auto_getter]` additionally rejects any attribute argument, +//! since it has no provider name or keys to accept. //! //! See cgp-knowledge-base/cgp/implementation/asts/cgp_getter.md (Tests) for these failure cases and //! cgp-knowledge-base/cgp/reference/macros/cgp_auto_getter.md for the user-facing getter-method @@ -14,7 +16,7 @@ use quote::quote; -use super::assert_macro_rejects; +use super::{assert_macro_rejects, assert_macro_rejects_with}; #[test] fn rejects_const_getter_method() { @@ -107,6 +109,71 @@ fn rejects_mutable_return_without_mut_self() { }); } +#[test] +fn rejects_optional_slice_return() { + // `Option<&T>` reads an `Option` field, which an unsized slice cannot fill, so + // `Option<&[u8]>` is rejected with the sized container to read instead. + assert_macro_rejects_with( + "cgp_auto_getter returning `Option<&[u8]>`", + "`Option<&[T]>` would read an `Option<[T]>` field, which cannot exist because a slice is unsized; read an `Option>` field as `Option<&Vec>` instead", + || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasItems { + fn items(&self) -> Option<&[u8]>; + } + ), + ) + }, + ); +} + +#[test] +fn rejects_optional_mutable_slice_return() { + // The mutable form has the same unsized field, so it is rejected the same way, + // naming the mutable read. + assert_macro_rejects_with( + "cgp_auto_getter returning `Option<&mut [u8]>`", + "`Option<&mut [T]>` would read an `Option<[T]>` field, which cannot exist because a slice is unsized; read an `Option>` field as `Option<&mut Vec>` instead", + || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasItems { + fn items(&mut self) -> Option<&mut [u8]>; + } + ), + ) + }, + ); +} + +#[test] +fn rejects_optional_trait_object_return() { + // A trait object is unsized like a slice, so `Option<&dyn Debug>` has no field to + // read either; the parenthesized form is the same type. + for return_type in [ + quote!(Option<&dyn core::fmt::Debug>), + quote!(Option<&(dyn core::fmt::Debug + Send)>), + ] { + assert_macro_rejects_with( + "cgp_auto_getter returning an optional trait-object reference", + "`Option<&dyn Trait>` would read an `Option` field, which cannot exist because a trait object is unsized; read an `Option>` field as `Option<&Box>` instead", + || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasLabel { + fn label(&self) -> #return_type; + } + ), + ) + }, + ); + } +} + #[test] fn rejects_multiple_associated_types() { // A getter trait may declare at most one associated return type. From e22c2c2f76222680134a76cacbe6ae953b29b8a9 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 13 Oct 2026 10:03:44 +0200 Subject: [PATCH 03/25] Let a provider read its own associated const as `Self::CONST` Co-Authored-By: Claude Opus 5.5 --- .../src/functions/getter/parse.rs | 4 +- .../preprocessed/to_provider_trait.rs | 16 ++-- .../src/types/cgp_impl/lowered.rs | 44 ++++++--- .../src/visitors/replace_self/mod.rs | 2 + .../replace_self/qualify_self_const.rs | 96 +++++++++++++++++++ .../replace_self/replace_self_type.rs | 18 ++-- .../cgp-tests/tests/basic_delegation/mod.rs | 1 + .../self_local_assoc_const.rs | 76 +++++++++++++++ .../basic_delegation/self_local_assoc_type.rs | 5 +- 9 files changed, 224 insertions(+), 38 deletions(-) create mode 100644 crates/macros/cgp-macro-core/src/visitors/replace_self/qualify_self_const.rs create mode 100644 crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_const.rs diff --git a/crates/macros/cgp-macro-core/src/functions/getter/parse.rs b/crates/macros/cgp-macro-core/src/functions/getter/parse.rs index 095a221e5..8aaf51273 100644 --- a/crates/macros/cgp-macro-core/src/functions/getter/parse.rs +++ b/crates/macros/cgp-macro-core/src/functions/getter/parse.rs @@ -239,7 +239,7 @@ fn parse_receiver(context_ident: &Ident, arg: &FnArg) -> syn::Result<(ReceiverMo ReplaceSelfTypeVisitor { replaced_type: &parse_internal!(#context_ident), - skip_assoc_types: &Vec::new(), + skip_assoc_items: &Vec::new(), } .visit_type_mut(&mut receiver); @@ -264,7 +264,7 @@ fn parse_return_type( ReplaceSelfTypeVisitor { replaced_type: &parse_internal!(#context_type), - skip_assoc_types: &Vec::from_iter(field_assoc_type.clone()), + skip_assoc_items: &Vec::from_iter(field_assoc_type.clone()), } .visit_type_mut(&mut replaced_type); diff --git a/crates/macros/cgp-macro-core/src/types/cgp_component/preprocessed/to_provider_trait.rs b/crates/macros/cgp-macro-core/src/types/cgp_component/preprocessed/to_provider_trait.rs index 5bc236e6f..97fb88bf9 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_component/preprocessed/to_provider_trait.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_component/preprocessed/to_provider_trait.rs @@ -35,15 +35,15 @@ impl PreprocessedCgpComponent { .params .insert(0, parse_internal!(#context_type_ident)); - let local_assoc_types: Vec = provider_trait + // A default body's `Self::Item` naming the trait's own associated type or + // const means the provider's item, so the rewrite leaves it alone. + let local_assoc_items: Vec = provider_trait .items .iter() - .filter_map(|item| { - if let TraitItem::Type(assoc_type) = item { - Some(assoc_type.ident.clone()) - } else { - None - } + .filter_map(|item| match item { + TraitItem::Type(assoc_type) => Some(assoc_type.ident.clone()), + TraitItem::Const(assoc_const) => Some(assoc_const.ident.clone()), + _ => None, }) .collect(); @@ -72,7 +72,7 @@ impl PreprocessedCgpComponent { ReplaceSelfTypeVisitor { replaced_type: &context_type, - skip_assoc_types: &local_assoc_types, + skip_assoc_items: &local_assoc_items, } .visit_item_trait_mut(&mut provider_trait); diff --git a/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs b/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs index 85eeb09ec..8c7fcd174 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs @@ -1,16 +1,17 @@ use proc_macro2::Span; -use quote::ToTokens; +use quote::{ToTokens, quote}; use syn::spanned::Spanned; use syn::token::For; use syn::visit_mut::VisitMut; -use syn::{Error, Ident, ImplItem, ItemImpl, Type}; +use syn::{Error, Ident, ImplItem, ItemImpl, Path, Type}; use crate::functions::{parse_internal, to_snake_case_ident}; use crate::types::cgp_impl::{CgpProviderOrBareImpl, ImplArgs}; use crate::types::cgp_provider::{ItemCgpProvider, ProviderArgs}; use crate::types::ident::PathWithTypeArgs; use crate::visitors::{ - ReplaceSelfReceiverVisitor, ReplaceSelfTypeVisitor, ReplaceSelfValueVisitor, + QualifySelfConstVisitor, ReplaceSelfReceiverVisitor, ReplaceSelfTypeVisitor, + ReplaceSelfValueVisitor, }; /// The header-normalized stage of `#[cgp_impl]`, carrying the resolved context @@ -72,15 +73,22 @@ impl LoweredCgpImpl { Ident::new("__context__", Span::call_site()) }; + // `Self::Item` naming an associated type or const the block declares + // means the provider's own item, so the rewrite leaves it alone. let local_assoc_types: Vec = item_impl .items .iter() - .filter_map(|item| { - if let ImplItem::Type(assoc_type) = item { - Some(assoc_type.ident.clone()) - } else { - None - } + .filter_map(|item| match item { + ImplItem::Type(assoc_type) => Some(assoc_type.ident.clone()), + _ => None, + }) + .collect(); + let local_assoc_consts: Vec = item_impl + .items + .iter() + .filter_map(|item| match item { + ImplItem::Const(assoc_const) => Some(assoc_const.ident.clone()), + _ => None, }) .collect(); @@ -108,15 +116,21 @@ impl LoweredCgpImpl { None => For(self.provider_trait_path.span()), }; - out_impl.trait_ = Some(( - None, - parse_internal(provider_trait_path.to_token_stream())?, - for_token, - )); + let provider_trait: Path = parse_internal(provider_trait_path.to_token_stream())?; + + // A local const is named through the provider trait, since a bare + // `Self::CONST` on the provider is ambiguous with the consumer trait's. + QualifySelfConstVisitor { + local_consts: &local_assoc_consts, + qualified_self: "e!(<#provider_type as #provider_trait>), + } + .visit_item_impl_mut(&mut out_impl); + + out_impl.trait_ = Some((None, provider_trait, for_token)); ReplaceSelfTypeVisitor { replaced_type: context_type, - skip_assoc_types: &local_assoc_types, + skip_assoc_items: &local_assoc_types, } .visit_item_impl_mut(&mut out_impl); diff --git a/crates/macros/cgp-macro-core/src/visitors/replace_self/mod.rs b/crates/macros/cgp-macro-core/src/visitors/replace_self/mod.rs index 0cf5c5a6b..0c7181364 100644 --- a/crates/macros/cgp-macro-core/src/visitors/replace_self/mod.rs +++ b/crates/macros/cgp-macro-core/src/visitors/replace_self/mod.rs @@ -1,7 +1,9 @@ +mod qualify_self_const; mod replace_self_receiver; mod replace_self_type; mod replace_self_value; +pub use qualify_self_const::*; pub use replace_self_receiver::*; pub use replace_self_type::*; pub use replace_self_value::*; diff --git a/crates/macros/cgp-macro-core/src/visitors/replace_self/qualify_self_const.rs b/crates/macros/cgp-macro-core/src/visitors/replace_self/qualify_self_const.rs new file mode 100644 index 000000000..485c96c47 --- /dev/null +++ b/crates/macros/cgp-macro-core/src/visitors/replace_self/qualify_self_const.rs @@ -0,0 +1,96 @@ +use itertools::Itertools; +use proc_macro2::{Group, TokenStream, TokenTree}; +use syn::visit_mut::{self, VisitMut}; +use syn::{ExprPath, Ident, Item, Macro, parse_quote}; + +/// Rewrites `Self::CONST`, for a const the `#[cgp_impl]` block declares, to the +/// qualified `>::CONST`. +/// +/// Once the block's `Self` is the provider, a bare `Self::CONST` is ambiguous: +/// the consumer trait's blanket impl gives the provider the same const. +pub struct QualifySelfConstVisitor<'a> { + pub local_consts: &'a Vec, + /// The qualified self, `>`. + pub qualified_self: &'a TokenStream, +} + +impl QualifySelfConstVisitor<'_> { + fn local_const<'p>(&self, expr: &'p ExprPath) -> Option<&'p Ident> { + let segments = &expr.path.segments; + if expr.qself.is_some() || expr.path.leading_colon.is_some() || segments.len() != 2 { + return None; + } + let (first, second) = (&segments[0], &segments[1]); + (first.ident == "Self" + && first.arguments.is_none() + && second.arguments.is_none() + && self.local_consts.contains(&second.ident)) + .then_some(&second.ident) + } +} + +impl VisitMut for QualifySelfConstVisitor<'_> { + fn visit_expr_path_mut(&mut self, expr: &mut ExprPath) { + if let Some(ident) = self.local_const(expr) { + let qualified_self = self.qualified_self; + let mut qualified: ExprPath = parse_quote!(#qualified_self :: #ident); + qualified.attrs = core::mem::take(&mut expr.attrs); + *expr = qualified; + return; + } + visit_mut::visit_expr_path_mut(self, expr); + } + + fn visit_macro_mut(&mut self, mac: &mut Macro) { + mac.tokens = qualify_self_const_in_token_stream( + core::mem::take(&mut mac.tokens), + self.qualified_self, + self.local_consts, + ); + } + + fn visit_item_mut(&mut self, _item: &mut Item) { + // A block-nested item has its own `Self`; leave it untouched. + } +} + +/// The token-level form of [`QualifySelfConstVisitor`], for macro bodies. +pub fn qualify_self_const_in_token_stream( + stream: TokenStream, + qualified_self: &TokenStream, + local_consts: &Vec, +) -> TokenStream { + let mut result_stream: Vec = Vec::new(); + + let mut token_iter = stream.into_iter().multipeek(); + + while let Some(tree) = token_iter.next() { + match tree { + TokenTree::Ident(ident) if ident == "Self" => { + let names_local_const = matches!(token_iter.peek(), Some(TokenTree::Punct(p)) if p.as_char() == ':') + && matches!(token_iter.peek(), Some(TokenTree::Punct(p)) if p.as_char() == ':') + && matches!(token_iter.peek(), Some(TokenTree::Ident(name)) if local_consts.contains(name)); + + if names_local_const { + result_stream.extend(qualified_self.clone()); + } else { + result_stream.push(TokenTree::Ident(ident)); + } + } + TokenTree::Group(group) => { + let replaced_stream = qualify_self_const_in_token_stream( + group.stream(), + qualified_self, + local_consts, + ); + let mut replaced_group = Group::new(group.delimiter(), replaced_stream); + replaced_group.set_span(group.span()); + + result_stream.push(TokenTree::Group(replaced_group)); + } + tree => result_stream.push(tree), + } + } + + result_stream.into_iter().collect() +} diff --git a/crates/macros/cgp-macro-core/src/visitors/replace_self/replace_self_type.rs b/crates/macros/cgp-macro-core/src/visitors/replace_self/replace_self_type.rs index 7c7ad2cad..1564579a8 100644 --- a/crates/macros/cgp-macro-core/src/visitors/replace_self/replace_self_type.rs +++ b/crates/macros/cgp-macro-core/src/visitors/replace_self/replace_self_type.rs @@ -5,11 +5,11 @@ use syn::visit_mut::{self, VisitMut}; use syn::{Item, Macro, Path, Type}; /// Rewrites the `Self` type (and `Self::Foo` paths) to the context type, -/// skipping any `Self::Assoc` whose associated type is declared locally in the -/// block so the trait's own associated types are left intact. +/// skipping any `Self::Item` whose associated type or const is declared locally +/// in the block, so the block's own associated items are left intact. pub struct ReplaceSelfTypeVisitor<'a> { pub replaced_type: &'a Type, - pub skip_assoc_types: &'a Vec, + pub skip_assoc_items: &'a Vec, } impl<'a> ReplaceSelfTypeVisitor<'a> { @@ -20,7 +20,7 @@ impl<'a> ReplaceSelfTypeVisitor<'a> { if first.ident != "Self" { return; } - if path.segments.len() >= 2 && self.skip_assoc_types.contains(&path.segments[1].ident) { + if path.segments.len() >= 2 && self.skip_assoc_items.contains(&path.segments[1].ident) { return; } if let Type::Path(replaced) = self.replaced_type @@ -60,7 +60,7 @@ impl VisitMut for ReplaceSelfTypeVisitor<'_> { mac.tokens = replace_self_type_in_token_stream( core::mem::take(&mut mac.tokens), self.replaced_type.to_token_stream(), - self.skip_assoc_types, + self.skip_assoc_items, ); } @@ -74,7 +74,7 @@ impl VisitMut for ReplaceSelfTypeVisitor<'_> { pub fn replace_self_type_in_token_stream( stream: TokenStream, replaced_ident: TokenStream, - local_assoc_types: &Vec, + local_assoc_items: &Vec, ) -> TokenStream { let self_type = format_ident!("Self"); @@ -88,14 +88,14 @@ pub fn replace_self_type_in_token_stream( if ident == self_type { let replaced_ident = replaced_ident.clone(); - // Do not replace self if it is an associated type expression that refers to local associated type + // Do not replace `Self` in a `Self::Item` path naming a local associated type or const let replaced = match token_iter.peek() { Some(TokenTree::Punct(p)) if p.as_char() == ':' => { match token_iter.peek() { Some(TokenTree::Punct(p)) if p.as_char() == ':' => { match token_iter.peek() { Some(TokenTree::Ident(assoc_type)) - if local_assoc_types.contains(assoc_type) => + if local_assoc_items.contains(assoc_type) => { ident.to_token_stream() } @@ -117,7 +117,7 @@ pub fn replace_self_type_in_token_stream( let replaced_stream = replace_self_type_in_token_stream( group.stream(), replaced_ident.clone(), - local_assoc_types, + local_assoc_items, ); let replaced_group = Group::new(group.delimiter(), replaced_stream); diff --git a/crates/tests/cgp-tests/tests/basic_delegation/mod.rs b/crates/tests/cgp-tests/tests/basic_delegation/mod.rs index 34d9d2763..98124d729 100644 --- a/crates/tests/cgp-tests/tests/basic_delegation/mod.rs +++ b/crates/tests/cgp-tests/tests/basic_delegation/mod.rs @@ -20,6 +20,7 @@ pub mod impl_self; pub mod owned_receiver; pub mod self_in_macro; pub mod self_in_nested_item; +pub mod self_local_assoc_const; pub mod self_local_assoc_type; // `delegate_components!` shape variants (compile-time checks only). diff --git a/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_const.rs b/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_const.rs new file mode 100644 index 000000000..376d53ace --- /dev/null +++ b/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_const.rs @@ -0,0 +1,76 @@ +//! `Self::CONST` naming an associated const the block declares survives the +//! `Self` rewrite, in a `#[cgp_impl]` body and in a component's default method. +//! +//! The rewrite turns `Self` into the context, but a `Self::LIMIT` where the block +//! itself declares `const LIMIT` means the provider's own const, as with a local +//! associated type. The macros collect the consts a block declares beside its +//! associated types and leave such a path alone, so `AllowUnderLimit` reads its +//! own `LIMIT` and the provider trait's default body reads the wired provider's. +//! In a `#[cgp_impl]` body the path is also qualified through the provider +//! trait, which the snapshot pins. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_impl.md (Behavior and corner cases) +//! and cgp-knowledge-base/cgp/reference/macros/cgp_impl.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_impl; + +#[cgp_component(RateLimiter)] +pub trait CanRateLimit { + const LIMIT: u32; + + fn allow(&self, count: u32) -> bool; + + // The default body is copied into the provider trait, where `Self::LIMIT` + // must stay the provider's const rather than become the unbounded context's. + fn remaining(&self, count: u32) -> u32 { + Self::LIMIT.saturating_sub(count) + } +} + +// The provider's `Self::LIMIT` becomes the qualified path through its provider +// trait, in plain code and inside a macro body alike: left as `Self::LIMIT` on +// the provider, it would be ambiguous with the consumer trait's `LIMIT`, which +// the consumer blanket impl also gives the provider. +snapshot_cgp_impl! { + #[cgp_impl(new AllowUnderLimit)] + impl RateLimiter { + const LIMIT: u32 = 10; + + fn allow(&self, count: u32) -> bool { + count < Self::LIMIT && matches!(count, 0..Self::LIMIT) + } + } + + expand_allow_under_limit(output) { + insta::assert_snapshot!(output, @" + impl<__Context__> RateLimiter<__Context__> for AllowUnderLimit { + const LIMIT: u32 = 10; + fn allow(__context__: &__Context__, count: u32) -> bool { + count < >::LIMIT + && matches!( + count, 0.. < AllowUnderLimit as RateLimiter < __Context__ > > ::LIMIT + ) + } + } + impl<__Context__> IsProviderFor + for AllowUnderLimit {} + pub struct AllowUnderLimit; + ") + } +} + +pub struct App; + +delegate_components! { + App { + RateLimiterComponent: AllowUnderLimit, + } +} + +#[test] +fn test_self_local_assoc_const() { + assert!(App.allow(9)); + assert!(!App.allow(10)); + assert_eq!(App.remaining(4), 6); +} diff --git a/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_type.rs b/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_type.rs index 8561e5f53..4560ab161 100644 --- a/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_type.rs +++ b/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_type.rs @@ -10,10 +10,7 @@ //! that declares it. Every other `Self` in the same block, including the `Self` //! of an inherited abstract type, is still rewritten to the context. //! -//! An associated *const* is deliberately not covered by the skip, since only -//! `ImplItem::Type` items are collected; `#[cgp_impl]`'s Known issues records -//! that asymmetry and the `>::CONST` form that works -//! around it. +//! `self_local_assoc_const` covers the same skip for an associated const. //! //! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_impl.md (Behavior and corner cases) //! and cgp-knowledge-base/cgp/reference/macros/cgp_impl.md. From b0c83f911d179dd7cdad1bf2be012d7f8e770a29 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 13 Oct 2026 10:09:36 +0200 Subject: [PATCH 04/25] Name the variant derives' associated types through qualified projections Co-Authored-By: Claude Opus 5.5 --- .../derive_extractor/extract_field_impls.rs | 5 +- .../derive_extractor/has_extractor_impl.rs | 8 +- .../src/types/cgp_data/derive_from_variant.rs | 2 +- .../derive_has_fields/from_fields_enum.rs | 4 +- .../derive_has_fields/to_fields_enum.rs | 4 +- .../derive_has_fields/to_fields_ref_enum.rs | 4 +- .../tests/invalid_expansion/mod.rs | 4 +- .../reserved_variant_names.rs | 127 -------- .../tests/invalid_expansion_tests.rs | 4 +- .../extensible_variants/derive_cgp_data.rs | 84 ++++-- .../derive_cgp_data_empty.rs | 18 +- .../derive_cgp_data_empty_variants.rs | 271 ++++++++++++++++-- .../derive_cgp_data_generic.rs | 84 ++++-- .../derive_cgp_data_shape.rs | 182 +++++++++++- .../extract_field_derive.rs | 144 +++++++++- .../from_variant_derive.rs | 32 ++- .../extensible_variants/has_fields_enum.rs | 6 +- .../has_fields_enum_generic.rs | 6 +- .../has_fields_enum_shapes.rs | 6 +- .../tests/extensible_variants/mod.rs | 5 + .../reserved_variant_names.rs | 85 ++++++ 21 files changed, 850 insertions(+), 235 deletions(-) delete mode 100644 crates/tests/cgp-macro-tests/tests/invalid_expansion/reserved_variant_names.rs create mode 100644 crates/tests/cgp-tests/tests/extensible_variants/reserved_variant_names.rs diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs index cc38959aa..e08952cae 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs @@ -109,7 +109,10 @@ pub fn derive_extract_field_impls( type Remainder = #output_type; - fn extract_field(self, _tag: ::core::marker::PhantomData< #tag_type >) -> Result { + fn extract_field(self, _tag: ::core::marker::PhantomData< #tag_type >) -> Result< + >::Value, + >::Remainder, + > { match self { #(#match_arms)* } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs index 6154a6b64..98a44e9b0 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs @@ -54,13 +54,13 @@ pub fn derive_has_extractor_impl( { type Extractor = #extractor_ident #extractor_generics; - fn to_extractor(self) -> Self::Extractor { + fn to_extractor(self) -> ::Extractor { match self { #(#to_match_arms)* } } - fn from_extractor(extractor: Self::Extractor) -> Self { + fn from_extractor(extractor: ::Extractor) -> Self { match extractor { #(#from_match_arms)* } @@ -142,7 +142,7 @@ pub fn derive_has_extractor_ref_impl( where Self: '__a__; - fn extractor_ref<'__a__>(&'__a__ self) -> Self::ExtractorRef<'__a__> { + fn extractor_ref<'__a__>(&'__a__ self) -> ::ExtractorRef<'__a__> { #body } } @@ -219,7 +219,7 @@ pub fn derive_has_extractor_mut_impl( where Self: '__a__; - fn extractor_mut<'__a__>(&'__a__ mut self) -> Self::ExtractorMut<'__a__> { + fn extractor_mut<'__a__>(&'__a__ mut self) -> ::ExtractorMut<'__a__> { #body } } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs index 1f3c4ef5c..2c577e909 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs @@ -30,7 +30,7 @@ pub fn derive_from_variant_from_enum(item_enum: &ItemEnum) -> syn::Result, #binding: Self::Value) -> Self { + fn from_variant(_tag: ::core::marker::PhantomData<#variant_tag>, #binding: >::Value) -> Self { #constructor } } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/from_fields_enum.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/from_fields_enum.rs index dffb5bc5c..5653632b4 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/from_fields_enum.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/from_fields_enum.rs @@ -1,7 +1,7 @@ use quote::quote; use syn::{ItemEnum, ItemImpl}; -use crate::exports::{Either, FromFields}; +use crate::exports::{Either, FromFields, HasFields}; use crate::functions::parse_internal; use crate::types::cgp_data::derive_from_field_params; @@ -37,7 +37,7 @@ pub fn derive_from_fields_for_enum(item_enum: &ItemEnum) -> syn::Result::Fields, ) -> Self { #match_expr } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/to_fields_enum.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/to_fields_enum.rs index 2439db3d3..77d115956 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/to_fields_enum.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/to_fields_enum.rs @@ -5,7 +5,7 @@ use syn::spanned::Spanned; use syn::token::Comma; use syn::{Error, Fields, Ident, ItemEnum, ItemImpl, Variant}; -use crate::exports::{Either, ToFields}; +use crate::exports::{Either, HasFields, ToFields}; use crate::functions::parse_internal; use crate::types::cgp_data::{FieldLabel, derive_to_fields_constructor}; @@ -22,7 +22,7 @@ pub fn derive_to_fields_for_enum(item_enum: &ItemEnum) -> syn::Result { fn to_fields( self, - ) -> Self::Fields { + ) -> ::Fields { match self { #match_arms } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/to_fields_ref_enum.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/to_fields_ref_enum.rs index 733f08a9b..3b63de157 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/to_fields_ref_enum.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/to_fields_ref_enum.rs @@ -1,7 +1,7 @@ use quote::quote; use syn::{ItemEnum, ItemImpl}; -use crate::exports::ToFieldsRef; +use crate::exports::{HasFieldsRef, ToFieldsRef}; use crate::functions::parse_internal; use crate::types::cgp_data::derive_to_fields_match_arms; @@ -29,7 +29,7 @@ pub fn derive_to_fields_ref_for_enum(item_enum: &ItemEnum) -> syn::Result( & #life self, - ) -> Self::FieldsRef< #life > + ) -> ::FieldsRef< #life > where Self: #life, { diff --git a/crates/tests/cgp-macro-tests/tests/invalid_expansion/mod.rs b/crates/tests/cgp-macro-tests/tests/invalid_expansion/mod.rs index 882bd535a..e50adb241 100644 --- a/crates/tests/cgp-macro-tests/tests/invalid_expansion/mod.rs +++ b/crates/tests/cgp-macro-tests/tests/invalid_expansion/mod.rs @@ -3,6 +3,4 @@ //! See the entrypoint `invalid_expansion_tests.rs` for the pattern to follow when //! adding a case. -// The variant derives name their own associated types through `Self::…`, so a variant whose name -// collides with one of them makes the emitted path ambiguous and the expansion does not compile. -pub mod reserved_variant_names; +// No case is captured at present. diff --git a/crates/tests/cgp-macro-tests/tests/invalid_expansion/reserved_variant_names.rs b/crates/tests/cgp-macro-tests/tests/invalid_expansion/reserved_variant_names.rs deleted file mode 100644 index 61f4b68d8..000000000 --- a/crates/tests/cgp-macro-tests/tests/invalid_expansion/reserved_variant_names.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! The variant derives emit `Self::` to name their own associated types, so a variant -//! whose name collides with one of those makes the generated path ambiguous and the expansion does -//! not compile. -//! -//! **Why the output is wrong.** `#[derive(FromVariant)]` writes the payload's type as `Self::Value`, -//! and `#[derive(ExtractField)]` writes `Self::Value`, `Self::Remainder`, `Self::Extractor`, -//! `Self::ExtractorRef`, and `Self::ExtractorMut`. Inside an impl for an enum, `Self::Value` can -//! resolve to either the trait's associated type or a *variant* of that name, so an enum carrying a -//! variant called `Value` makes the path ambiguous and `rustc` reports -//! `error: ambiguous associated item`, headlined at the `#[derive(...)]` attribute. -//! -//! How readable that error is depends on the derive. `#[derive(HasFields)]` and -//! `#[derive(FromVariant)]` write their impls `for` the user's enum, so the colliding variant keeps its -//! own span and a note points straight at it. `#[derive(ExtractField)]` writes its impls for the -//! generated `__Partial…` companions, whose variant identifiers the codegen rebuilds — so both notes -//! land on the derive attribute and nothing in the output names the variant to rename. That is the case -//! worth guarding against. `#[derive(HasFields)]` also reserves `FieldsRef`, so an enum deriving the -//! whole family has seven names it cannot use. -//! -//! **What the correct output should be.** The codegen should write each associated type as a fully -//! qualified projection — `>::Value` rather than `Self::Value` — which is -//! unambiguous whatever the enum's variants are named. That is the same hygiene discipline the rest of -//! the suite follows for CGP's own paths, applied to the traits these derives implement. -//! -//! The snapshots below capture the emitted expansion as a string, so this test compiles even though -//! the code it describes would not. Only the offending lines are pinned; the surrounding items are the -//! ordinary output the reference documents already describe. -//! -//! Recorded in cgp-knowledge-base/cgp/reference/derives/derive_from_variant.md and -//! derive_extract_field.md, under `## Known issues`. - -use cgp_macro_test_util_lib::functions::pretty_format; -use quote::quote; - -/// `#[derive(FromVariant)]` on an enum with a variant named `Value`. -/// -/// The emitted `value: Self::Value` parameter is the ambiguous path: `Self::Value` could be the -/// `FromVariant::Value` associated type or the `Value` variant. -#[test] -fn test_from_variant_reserves_value() { - let output = cgp_macro_lib::derive_from_variant(quote! { - #[derive(FromVariant)] - pub enum Tagged { - Value(u32), - } - }) - .unwrap(); - - let formatted = pretty_format(output).unwrap(); - - assert!( - formatted.contains("value: Self::Value"), - "expected the ambiguous `Self::Value` parameter, got:\n{formatted}" - ); - - insta::assert_snapshot!(formatted, @r" - impl FromVariant< - Symbol<5, Chars<'V', Chars<'a', Chars<'l', Chars<'u', Chars<'e', Nil>>>>>>, - > for Tagged { - type Value = u32; - fn from_variant( - _tag: ::core::marker::PhantomData< - Symbol<5, Chars<'V', Chars<'a', Chars<'l', Chars<'u', Chars<'e', Nil>>>>>>, - >, - value: Self::Value, - ) -> Self { - Self::Value(value) - } - } - "); -} - -/// `#[derive(ExtractField)]` on an enum with a variant named `Remainder`. -/// -/// `Self::Remainder` in the `extract_field` return type is the ambiguous path here. The same enum -/// would also collide on `Value`, `Extractor`, `ExtractorRef`, and `ExtractorMut`. -#[test] -fn test_extract_field_reserves_remainder() { - let output = cgp_macro_lib::derive_extract_field(quote! { - #[derive(ExtractField)] - pub enum Tagged { - Remainder(u32), - } - }) - .unwrap(); - - let formatted = pretty_format(output).unwrap(); - - assert!( - formatted.contains("Result"), - "expected the ambiguous `Self::Remainder` return type, got:\n{formatted}" - ); - - // `Self::Extractor` appears in the owned accessor for the same reason. - assert!( - formatted.contains("-> Self::Extractor"), - "expected the ambiguous `Self::Extractor` return type, got:\n{formatted}" - ); -} - -/// `#[derive(HasFields)]` on an enum with a variant named `Fields`. -/// -/// `Self::Fields` in the `from_fields` parameter and `Self::FieldsRef` in the borrowed accessor are -/// the ambiguous paths. This is the same defect in the representation slice, which is why an enum -/// deriving the whole family has seven reserved names rather than five. -#[test] -fn test_has_fields_reserves_fields() { - let output = cgp_macro_lib::derive_has_fields(quote! { - #[derive(HasFields)] - pub enum Tagged { - Fields(u32), - } - }) - .unwrap(); - - let formatted = pretty_format(output).unwrap(); - - assert!( - formatted.contains("rest: Self::Fields"), - "expected the ambiguous `Self::Fields` parameter, got:\n{formatted}" - ); - - assert!( - formatted.contains("Self::FieldsRef<'__a>"), - "expected the ambiguous `Self::FieldsRef` projection, got:\n{formatted}" - ); -} diff --git a/crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs b/crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs index ac548d1f3..bc118daf1 100644 --- a/crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs +++ b/crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs @@ -15,9 +15,7 @@ //! 3. record the limitation in the owning reference document's `## Known issues` //! section (per cgp-knowledge-base/cgp/AGENTS.md), and link from the test to that document. //! -//! One case is captured so far: `reserved_variant_names`, where the variant derives name their -//! own associated types through `Self::…` and so cannot be applied to an enum with a variant of -//! a colliding name. +//! No case is captured at present. #![allow(dead_code)] pub mod invalid_expansion; diff --git a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data.rs b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data.rs index c215ea9a7..57f66eb8c 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data.rs @@ -55,7 +55,7 @@ snapshot_derive_cgp_data! { Self: '__a; } impl FromFields for FooBarBaz { - fn from_fields(rest: Self::Fields) -> Self { + fn from_fields(rest: ::Fields) -> Self { match rest { Either::Left(field) => { let field = field.value; @@ -82,7 +82,7 @@ snapshot_derive_cgp_data! { } } impl ToFields for FooBarBaz { - fn to_fields(self) -> Self::Fields { + fn to_fields(self) -> ::Fields { match self { Self::Foo(field) => Either::Left(field.into()), Self::Bar(field) => Either::Right(Either::Left(field.into())), @@ -91,7 +91,7 @@ snapshot_derive_cgp_data! { } } impl ToFieldsRef for FooBarBaz { - fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + fn to_fields_ref<'__a>(&'__a self) -> ::FieldsRef<'__a> where Self: '__a, { @@ -108,7 +108,9 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'F', Chars<'o', Chars<'o', Nil>>>>, >, - value: Self::Value, + value: >>>, + >>::Value, ) -> Self { Self::Foo(value) } @@ -119,7 +121,9 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'r', Nil>>>>, >, - value: Self::Value, + value: >>>, + >>::Value, ) -> Self { Self::Bar(value) } @@ -130,7 +134,9 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'z', Nil>>>>, >, - value: Self::Value, + value: >>>, + >>::Value, ) -> Self { Self::Baz(value) } @@ -166,14 +172,14 @@ snapshot_derive_cgp_data! { } impl HasExtractor for FooBarBaz { type Extractor = __PartialFooBarBaz; - fn to_extractor(self) -> Self::Extractor { + fn to_extractor(self) -> ::Extractor { match self { Self::Foo(value) => __PartialFooBarBaz::Foo(value), Self::Bar(value) => __PartialFooBarBaz::Bar(value), Self::Baz(value) => __PartialFooBarBaz::Baz(value), } } - fn from_extractor(extractor: Self::Extractor) -> Self { + fn from_extractor(extractor: ::Extractor) -> Self { match extractor { __PartialFooBarBaz::Foo(value) => Self::Foo(value), __PartialFooBarBaz::Bar(value) => Self::Bar(value), @@ -191,7 +197,9 @@ snapshot_derive_cgp_data! { > where Self: '__a__; - fn extractor_ref<'__a__>(&'__a__ self) -> Self::ExtractorRef<'__a__> { + fn extractor_ref<'__a__>( + &'__a__ self, + ) -> ::ExtractorRef<'__a__> { match self { Self::Foo(value) => __PartialRefFooBarBaz::Foo(value), Self::Bar(value) => __PartialRefFooBarBaz::Bar(value), @@ -209,7 +217,9 @@ snapshot_derive_cgp_data! { > where Self: '__a__; - fn extractor_mut<'__a__>(&'__a__ mut self) -> Self::ExtractorMut<'__a__> { + fn extractor_mut<'__a__>( + &'__a__ mut self, + ) -> ::ExtractorMut<'__a__> { match self { Self::Foo(value) => __PartialRefFooBarBaz::Foo(value), Self::Bar(value) => __PartialRefFooBarBaz::Bar(value), @@ -240,7 +250,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'F', Chars<'o', Chars<'o', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialFooBarBaz::Foo(value) => Ok(value), __PartialFooBarBaz::Bar(value) => Err(__PartialFooBarBaz::Bar(value)), @@ -260,7 +277,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'r', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialFooBarBaz::Foo(value) => Err(__PartialFooBarBaz::Foo(value)), __PartialFooBarBaz::Bar(value) => Ok(value), @@ -280,7 +304,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'z', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialFooBarBaz::Foo(value) => Err(__PartialFooBarBaz::Foo(value)), __PartialFooBarBaz::Bar(value) => Err(__PartialFooBarBaz::Bar(value)), @@ -302,7 +333,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'F', Chars<'o', Chars<'o', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialRefFooBarBaz::Foo(value) => Ok(value), __PartialRefFooBarBaz::Bar(value) => Err(__PartialRefFooBarBaz::Bar(value)), @@ -324,7 +362,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'r', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialRefFooBarBaz::Foo(value) => Err(__PartialRefFooBarBaz::Foo(value)), __PartialRefFooBarBaz::Bar(value) => Ok(value), @@ -346,7 +391,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'z', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialRefFooBarBaz::Foo(value) => Err(__PartialRefFooBarBaz::Foo(value)), __PartialRefFooBarBaz::Bar(value) => Err(__PartialRefFooBarBaz::Bar(value)), diff --git a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty.rs b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty.rs index 1e6c23158..6aef481c0 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty.rs @@ -29,17 +29,17 @@ snapshot_derive_cgp_data! { type FieldsRef<'__a> = Void where Self: '__a; } impl FromFields for Never { - fn from_fields(rest: Self::Fields) -> Self { + fn from_fields(rest: ::Fields) -> Self { match rest {} } } impl ToFields for Never { - fn to_fields(self) -> Self::Fields { + fn to_fields(self) -> ::Fields { match self {} } } impl ToFieldsRef for Never { - fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + fn to_fields_ref<'__a>(&'__a self) -> ::FieldsRef<'__a> where Self: '__a, { @@ -56,22 +56,26 @@ snapshot_derive_cgp_data! { } impl HasExtractor for Never { type Extractor = __PartialNever; - fn to_extractor(self) -> Self::Extractor { + fn to_extractor(self) -> ::Extractor { match self {} } - fn from_extractor(extractor: Self::Extractor) -> Self { + fn from_extractor(extractor: ::Extractor) -> Self { match extractor {} } } impl HasExtractorRef for Never { type ExtractorRef<'__a__> = __PartialRefNever where Self: '__a__; - fn extractor_ref<'__a__>(&'__a__ self) -> Self::ExtractorRef<'__a__> { + fn extractor_ref<'__a__>( + &'__a__ self, + ) -> ::ExtractorRef<'__a__> { match *self {} } } impl HasExtractorMut for Never { type ExtractorMut<'__a__> = __PartialRefNever where Self: '__a__; - fn extractor_mut<'__a__>(&'__a__ mut self) -> Self::ExtractorMut<'__a__> { + fn extractor_mut<'__a__>( + &'__a__ mut self, + ) -> ::ExtractorMut<'__a__> { match *self {} } } diff --git a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty_variants.rs b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty_variants.rs index 0783cec8d..482f16101 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty_variants.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty_variants.rs @@ -155,7 +155,7 @@ snapshot_derive_cgp_data! { Self: '__a; } impl FromFields for Status { - fn from_fields(rest: Self::Fields) -> Self { + fn from_fields(rest: ::Fields) -> Self { match rest { Either::Left(field) => { let field = field.value; @@ -190,7 +190,7 @@ snapshot_derive_cgp_data! { } } impl ToFields for Status { - fn to_fields(self) -> Self::Fields { + fn to_fields(self) -> ::Fields { match self { Self::Active(field) => Either::Left(field.into()), Self::Closed => Either::Right(Either::Left(Nil.into())), @@ -202,7 +202,7 @@ snapshot_derive_cgp_data! { } } impl ToFieldsRef for Status { - fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + fn to_fields_ref<'__a>(&'__a self) -> ::FieldsRef<'__a> where Self: '__a, { @@ -233,7 +233,15 @@ snapshot_derive_cgp_data! { >, >, >, - value: Self::Value, + value: >>>>, + >, + >, + >>::Value, ) -> Self { Self::Active(value) } @@ -255,7 +263,15 @@ snapshot_derive_cgp_data! { >, >, >, - _: Self::Value, + _: >>>>, + >, + >, + >>::Value, ) -> Self { Self::Closed {} } @@ -277,7 +293,15 @@ snapshot_derive_cgp_data! { >, >, >, - _: Self::Value, + _: >>>>, + >, + >, + >>::Value, ) -> Self { Self::Paused {} } @@ -317,7 +341,24 @@ snapshot_derive_cgp_data! { >, >, >, - _: Self::Value, + _: >>>, + >, + >, + >, + >, + >, + >>::Value, ) -> Self { Self::Archived {} } @@ -362,7 +403,7 @@ snapshot_derive_cgp_data! { } impl HasExtractor for Status { type Extractor = __PartialStatus; - fn to_extractor(self) -> Self::Extractor { + fn to_extractor(self) -> ::Extractor { match self { Self::Active(value) => __PartialStatus::Active(value), Self::Closed { .. } => __PartialStatus::Closed(Nil), @@ -370,7 +411,7 @@ snapshot_derive_cgp_data! { Self::Archived { .. } => __PartialStatus::Archived(Nil), } } - fn from_extractor(extractor: Self::Extractor) -> Self { + fn from_extractor(extractor: ::Extractor) -> Self { match extractor { __PartialStatus::Active(value) => Self::Active(value), __PartialStatus::Closed(_) => Self::Closed {}, @@ -390,7 +431,9 @@ snapshot_derive_cgp_data! { > where Self: '__a__; - fn extractor_ref<'__a__>(&'__a__ self) -> Self::ExtractorRef<'__a__> { + fn extractor_ref<'__a__>( + &'__a__ self, + ) -> ::ExtractorRef<'__a__> { match self { Self::Active(value) => __PartialRefStatus::Active(value), Self::Closed { .. } => __PartialRefStatus::Closed(&Nil), @@ -410,7 +453,9 @@ snapshot_derive_cgp_data! { > where Self: '__a__; - fn extractor_mut<'__a__>(&'__a__ mut self) -> Self::ExtractorMut<'__a__> { + fn extractor_mut<'__a__>( + &'__a__ mut self, + ) -> ::ExtractorMut<'__a__> { match self { Self::Active(value) => __PartialRefStatus::Active(value), Self::Closed { .. } => __PartialRefStatus::Closed(Box::leak(Box::new(Nil))), @@ -455,7 +500,26 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>>, + >, + >, + >>::Value, + >>>>, + >, + >, + >>::Remainder, + > { match self { __PartialStatus::Active(value) => Ok(value), __PartialStatus::Closed(value) => Err(__PartialStatus::Closed(value)), @@ -487,7 +551,26 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>>, + >, + >, + >>::Value, + >>>>, + >, + >, + >>::Remainder, + > { match self { __PartialStatus::Active(value) => Err(__PartialStatus::Active(value)), __PartialStatus::Closed(value) => Ok(value), @@ -519,7 +602,26 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>>, + >, + >, + >>::Value, + >>>>, + >, + >, + >>::Remainder, + > { match self { __PartialStatus::Active(value) => Err(__PartialStatus::Active(value)), __PartialStatus::Closed(value) => Err(__PartialStatus::Closed(value)), @@ -569,7 +671,44 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>, + >, + >, + >, + >, + >, + >>::Value, + >>>, + >, + >, + >, + >, + >, + >>::Remainder, + > { match self { __PartialStatus::Active(value) => Err(__PartialStatus::Active(value)), __PartialStatus::Closed(value) => Err(__PartialStatus::Closed(value)), @@ -603,7 +742,26 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>>, + >, + >, + >>::Value, + >>>>, + >, + >, + >>::Remainder, + > { match self { __PartialRefStatus::Active(value) => Ok(value), __PartialRefStatus::Closed(value) => Err(__PartialRefStatus::Closed(value)), @@ -639,7 +797,26 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>>, + >, + >, + >>::Value, + >>>>, + >, + >, + >>::Remainder, + > { match self { __PartialRefStatus::Active(value) => Err(__PartialRefStatus::Active(value)), __PartialRefStatus::Closed(value) => Ok(value), @@ -675,7 +852,26 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>>, + >, + >, + >>::Value, + >>>>, + >, + >, + >>::Remainder, + > { match self { __PartialRefStatus::Active(value) => Err(__PartialRefStatus::Active(value)), __PartialRefStatus::Closed(value) => Err(__PartialRefStatus::Closed(value)), @@ -729,7 +925,44 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>, + >, + >, + >, + >, + >, + >>::Value, + >>>, + >, + >, + >, + >, + >, + >>::Remainder, + > { match self { __PartialRefStatus::Active(value) => Err(__PartialRefStatus::Active(value)), __PartialRefStatus::Closed(value) => Err(__PartialRefStatus::Closed(value)), diff --git a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_generic.rs b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_generic.rs index be7942ce9..0578f3523 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_generic.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_generic.rs @@ -50,7 +50,7 @@ snapshot_derive_cgp_data! { Self: '__a; } impl FromFields for FooBarBazGeneric { - fn from_fields(rest: Self::Fields) -> Self { + fn from_fields(rest: ::Fields) -> Self { match rest { Either::Left(field) => { let field = field.value; @@ -77,7 +77,7 @@ snapshot_derive_cgp_data! { } } impl ToFields for FooBarBazGeneric { - fn to_fields(self) -> Self::Fields { + fn to_fields(self) -> ::Fields { match self { Self::Foo(field) => Either::Left(field.into()), Self::Bar(field) => Either::Right(Either::Left(field.into())), @@ -86,7 +86,7 @@ snapshot_derive_cgp_data! { } } impl ToFieldsRef for FooBarBazGeneric { - fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + fn to_fields_ref<'__a>(&'__a self) -> ::FieldsRef<'__a> where Self: '__a, { @@ -104,7 +104,9 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'F', Chars<'o', Chars<'o', Nil>>>>, >, - value: Self::Value, + value: >>>, + >>::Value, ) -> Self { Self::Foo(value) } @@ -116,7 +118,9 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'r', Nil>>>>, >, - value: Self::Value, + value: >>>, + >>::Value, ) -> Self { Self::Bar(value) } @@ -128,7 +132,9 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'z', Nil>>>>, >, - value: Self::Value, + value: >>>, + >>::Value, ) -> Self { Self::Baz(value) } @@ -185,14 +191,14 @@ snapshot_derive_cgp_data! { IsPresent, IsPresent, >; - fn to_extractor(self) -> Self::Extractor { + fn to_extractor(self) -> ::Extractor { match self { Self::Foo(value) => __PartialFooBarBazGeneric::Foo(value), Self::Bar(value) => __PartialFooBarBazGeneric::Bar(value), Self::Baz(value) => __PartialFooBarBazGeneric::Baz(value), } } - fn from_extractor(extractor: Self::Extractor) -> Self { + fn from_extractor(extractor: ::Extractor) -> Self { match extractor { __PartialFooBarBazGeneric::Foo(value) => Self::Foo(value), __PartialFooBarBazGeneric::Bar(value) => Self::Bar(value), @@ -213,7 +219,9 @@ snapshot_derive_cgp_data! { > where Self: '__a__; - fn extractor_ref<'__a__>(&'__a__ self) -> Self::ExtractorRef<'__a__> { + fn extractor_ref<'__a__>( + &'__a__ self, + ) -> ::ExtractorRef<'__a__> { match self { Self::Foo(value) => __PartialRefFooBarBazGeneric::Foo(value), Self::Bar(value) => __PartialRefFooBarBazGeneric::Bar(value), @@ -234,7 +242,9 @@ snapshot_derive_cgp_data! { > where Self: '__a__; - fn extractor_mut<'__a__>(&'__a__ mut self) -> Self::ExtractorMut<'__a__> { + fn extractor_mut<'__a__>( + &'__a__ mut self, + ) -> ::ExtractorMut<'__a__> { match self { Self::Foo(value) => __PartialRefFooBarBazGeneric::Foo(value), Self::Bar(value) => __PartialRefFooBarBazGeneric::Bar(value), @@ -269,7 +279,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'F', Chars<'o', Chars<'o', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialFooBarBazGeneric::Foo(value) => Ok(value), __PartialFooBarBazGeneric::Bar(value) => { @@ -296,7 +313,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'r', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialFooBarBazGeneric::Foo(value) => { Err(__PartialFooBarBazGeneric::Foo(value)) @@ -323,7 +347,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'z', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialFooBarBazGeneric::Foo(value) => { Err(__PartialFooBarBazGeneric::Foo(value)) @@ -370,7 +401,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'F', Chars<'o', Chars<'o', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialRefFooBarBazGeneric::Foo(value) => Ok(value), __PartialRefFooBarBazGeneric::Bar(value) => { @@ -417,7 +455,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'r', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialRefFooBarBazGeneric::Foo(value) => { Err(__PartialRefFooBarBazGeneric::Foo(value)) @@ -464,7 +509,14 @@ snapshot_derive_cgp_data! { _tag: ::core::marker::PhantomData< Symbol<3, Chars<'B', Chars<'a', Chars<'z', Nil>>>>, >, - ) -> Result { + ) -> Result< + >>>, + >>::Value, + >>>, + >>::Remainder, + > { match self { __PartialRefFooBarBazGeneric::Foo(value) => { Err(__PartialRefFooBarBazGeneric::Foo(value)) diff --git a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_shape.rs b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_shape.rs index 70c6c4600..0e9ecad53 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_shape.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_shape.rs @@ -111,7 +111,7 @@ snapshot_derive_cgp_data! { Self: '__a; } impl FromFields for Shape { - fn from_fields(rest: Self::Fields) -> Self { + fn from_fields(rest: ::Fields) -> Self { match rest { Either::Left(field) => { let field = field.value; @@ -130,7 +130,7 @@ snapshot_derive_cgp_data! { } } impl ToFields for Shape { - fn to_fields(self) -> Self::Fields { + fn to_fields(self) -> ::Fields { match self { Self::Circle(field) => Either::Left(field.into()), Self::Rectangle(field) => Either::Right(Either::Left(field.into())), @@ -138,7 +138,7 @@ snapshot_derive_cgp_data! { } } impl ToFieldsRef for Shape { - fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + fn to_fields_ref<'__a>(&'__a self) -> ::FieldsRef<'__a> where Self: '__a, { @@ -165,7 +165,15 @@ snapshot_derive_cgp_data! { >, >, >, - value: Self::Value, + value: >>>>, + >, + >, + >>::Value, ) -> Self { Self::Circle(value) } @@ -211,7 +219,27 @@ snapshot_derive_cgp_data! { >, >, >, - value: Self::Value, + value: >>>, + >, + >, + >, + >, + >, + >, + >>::Value, ) -> Self { Self::Rectangle(value) } @@ -233,13 +261,13 @@ snapshot_derive_cgp_data! { } impl HasExtractor for Shape { type Extractor = __PartialShape; - fn to_extractor(self) -> Self::Extractor { + fn to_extractor(self) -> ::Extractor { match self { Self::Circle(value) => __PartialShape::Circle(value), Self::Rectangle(value) => __PartialShape::Rectangle(value), } } - fn from_extractor(extractor: Self::Extractor) -> Self { + fn from_extractor(extractor: ::Extractor) -> Self { match extractor { __PartialShape::Circle(value) => Self::Circle(value), __PartialShape::Rectangle(value) => Self::Rectangle(value), @@ -250,7 +278,9 @@ snapshot_derive_cgp_data! { type ExtractorRef<'__a__> = __PartialRefShape<'__a__, IsRef, IsPresent, IsPresent> where Self: '__a__; - fn extractor_ref<'__a__>(&'__a__ self) -> Self::ExtractorRef<'__a__> { + fn extractor_ref<'__a__>( + &'__a__ self, + ) -> ::ExtractorRef<'__a__> { match self { Self::Circle(value) => __PartialRefShape::Circle(value), Self::Rectangle(value) => __PartialRefShape::Rectangle(value), @@ -261,7 +291,9 @@ snapshot_derive_cgp_data! { type ExtractorMut<'__a__> = __PartialRefShape<'__a__, IsMut, IsPresent, IsPresent> where Self: '__a__; - fn extractor_mut<'__a__>(&'__a__ mut self) -> Self::ExtractorMut<'__a__> { + fn extractor_mut<'__a__>( + &'__a__ mut self, + ) -> ::ExtractorMut<'__a__> { match self { Self::Circle(value) => __PartialRefShape::Circle(value), Self::Rectangle(value) => __PartialRefShape::Rectangle(value), @@ -300,7 +332,26 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>>, + >, + >, + >>::Value, + >>>>, + >, + >, + >>::Remainder, + > { match self { __PartialShape::Circle(value) => Ok(value), __PartialShape::Rectangle(value) => Err(__PartialShape::Rectangle(value)), @@ -352,7 +403,50 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>, + >, + >, + >, + >, + >, + >, + >>::Value, + >>>, + >, + >, + >, + >, + >, + >, + >>::Remainder, + > { match self { __PartialShape::Circle(value) => Err(__PartialShape::Circle(value)), __PartialShape::Rectangle(value) => Ok(value), @@ -382,7 +476,26 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>>, + >, + >, + >>::Value, + >>>>, + >, + >, + >>::Remainder, + > { match self { __PartialRefShape::Circle(value) => Ok(value), __PartialRefShape::Rectangle(value) => { @@ -438,7 +551,50 @@ snapshot_derive_cgp_data! { >, >, >, - ) -> Result { + ) -> Result< + >>>, + >, + >, + >, + >, + >, + >, + >>::Value, + >>>, + >, + >, + >, + >, + >, + >, + >>::Remainder, + > { match self { __PartialRefShape::Circle(value) => Err(__PartialRefShape::Circle(value)), __PartialRefShape::Rectangle(value) => Ok(value), diff --git a/crates/tests/cgp-tests/tests/extensible_variants/extract_field_derive.rs b/crates/tests/cgp-tests/tests/extensible_variants/extract_field_derive.rs index b7e985f9e..00191f3d9 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/extract_field_derive.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/extract_field_derive.rs @@ -62,13 +62,13 @@ snapshot_derive_extract_field! { } impl HasExtractor for Shape { type Extractor = __PartialShape; - fn to_extractor(self) -> Self::Extractor { + fn to_extractor(self) -> ::Extractor { match self { Self::Circle(value) => __PartialShape::Circle(value), Self::Rectangle(value) => __PartialShape::Rectangle(value), } } - fn from_extractor(extractor: Self::Extractor) -> Self { + fn from_extractor(extractor: ::Extractor) -> Self { match extractor { __PartialShape::Circle(value) => Self::Circle(value), __PartialShape::Rectangle(value) => Self::Rectangle(value), @@ -79,7 +79,9 @@ snapshot_derive_extract_field! { type ExtractorRef<'__a__> = __PartialRefShape<'__a__, IsRef, IsPresent, IsPresent> where Self: '__a__; - fn extractor_ref<'__a__>(&'__a__ self) -> Self::ExtractorRef<'__a__> { + fn extractor_ref<'__a__>( + &'__a__ self, + ) -> ::ExtractorRef<'__a__> { match self { Self::Circle(value) => __PartialRefShape::Circle(value), Self::Rectangle(value) => __PartialRefShape::Rectangle(value), @@ -90,7 +92,9 @@ snapshot_derive_extract_field! { type ExtractorMut<'__a__> = __PartialRefShape<'__a__, IsMut, IsPresent, IsPresent> where Self: '__a__; - fn extractor_mut<'__a__>(&'__a__ mut self) -> Self::ExtractorMut<'__a__> { + fn extractor_mut<'__a__>( + &'__a__ mut self, + ) -> ::ExtractorMut<'__a__> { match self { Self::Circle(value) => __PartialRefShape::Circle(value), Self::Rectangle(value) => __PartialRefShape::Rectangle(value), @@ -129,7 +133,26 @@ snapshot_derive_extract_field! { >, >, >, - ) -> Result { + ) -> Result< + >>>>, + >, + >, + >>::Value, + >>>>, + >, + >, + >>::Remainder, + > { match self { __PartialShape::Circle(value) => Ok(value), __PartialShape::Rectangle(value) => Err(__PartialShape::Rectangle(value)), @@ -181,7 +204,50 @@ snapshot_derive_extract_field! { >, >, >, - ) -> Result { + ) -> Result< + >>>, + >, + >, + >, + >, + >, + >, + >>::Value, + >>>, + >, + >, + >, + >, + >, + >, + >>::Remainder, + > { match self { __PartialShape::Circle(value) => Err(__PartialShape::Circle(value)), __PartialShape::Rectangle(value) => Ok(value), @@ -211,7 +277,26 @@ snapshot_derive_extract_field! { >, >, >, - ) -> Result { + ) -> Result< + >>>>, + >, + >, + >>::Value, + >>>>, + >, + >, + >>::Remainder, + > { match self { __PartialRefShape::Circle(value) => Ok(value), __PartialRefShape::Rectangle(value) => { @@ -267,7 +352,50 @@ snapshot_derive_extract_field! { >, >, >, - ) -> Result { + ) -> Result< + >>>, + >, + >, + >, + >, + >, + >, + >>::Value, + >>>, + >, + >, + >, + >, + >, + >, + >>::Remainder, + > { match self { __PartialRefShape::Circle(value) => Err(__PartialRefShape::Circle(value)), __PartialRefShape::Rectangle(value) => Ok(value), diff --git a/crates/tests/cgp-tests/tests/extensible_variants/from_variant_derive.rs b/crates/tests/cgp-tests/tests/extensible_variants/from_variant_derive.rs index f6fab96de..cef62201d 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/from_variant_derive.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/from_variant_derive.rs @@ -56,7 +56,15 @@ snapshot_derive_from_variant! { >, >, >, - value: Self::Value, + value: >>>>, + >, + >, + >>::Value, ) -> Self { Self::Circle(value) } @@ -102,7 +110,27 @@ snapshot_derive_from_variant! { >, >, >, - value: Self::Value, + value: >>>, + >, + >, + >, + >, + >, + >, + >>::Value, ) -> Self { Self::Rectangle(value) } diff --git a/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum.rs b/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum.rs index c5c96c9e1..261218871 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum.rs @@ -98,7 +98,7 @@ snapshot_derive_has_fields! { Self: '__a; } impl FromFields for Person { - fn from_fields(rest: Self::Fields) -> Self { + fn from_fields(rest: ::Fields) -> Self { match rest { Either::Left(field) => { let field = field.value; @@ -117,7 +117,7 @@ snapshot_derive_has_fields! { } } impl ToFields for Person { - fn to_fields(self) -> Self::Fields { + fn to_fields(self) -> ::Fields { match self { Self::Anonymous(field) => Either::Left(field.into()), Self::Named(field) => Either::Right(Either::Left(field.into())), @@ -125,7 +125,7 @@ snapshot_derive_has_fields! { } } impl ToFieldsRef for Person { - fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + fn to_fields_ref<'__a>(&'__a self) -> ::FieldsRef<'__a> where Self: '__a, { diff --git a/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum_generic.rs b/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum_generic.rs index 6723890da..79c681c67 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum_generic.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum_generic.rs @@ -96,7 +96,7 @@ snapshot_derive_has_fields! { Self: '__a; } impl<'a, Name> FromFields for Person<'a, Name> { - fn from_fields(rest: Self::Fields) -> Self { + fn from_fields(rest: ::Fields) -> Self { match rest { Either::Left(field) => { let field = field.value; @@ -115,7 +115,7 @@ snapshot_derive_has_fields! { } } impl<'a, Name> ToFields for Person<'a, Name> { - fn to_fields(self) -> Self::Fields { + fn to_fields(self) -> ::Fields { match self { Self::Anonymous(field) => Either::Left(field.into()), Self::Named(field) => Either::Right(Either::Left(field.into())), @@ -123,7 +123,7 @@ snapshot_derive_has_fields! { } } impl<'a, Name> ToFieldsRef for Person<'a, Name> { - fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + fn to_fields_ref<'__a>(&'__a self) -> ::FieldsRef<'__a> where Self: '__a, { diff --git a/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum_shapes.rs b/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum_shapes.rs index 33366ab31..31d95510a 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum_shapes.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum_shapes.rs @@ -219,7 +219,7 @@ snapshot_derive_has_fields! { Self: '__a; } impl FromFields for Shape { - fn from_fields(rest: Self::Fields) -> Self { + fn from_fields(rest: ::Fields) -> Self { match rest { Either::Left(field) => { let Nil = field.value; @@ -257,7 +257,7 @@ snapshot_derive_has_fields! { } } impl ToFields for Shape { - fn to_fields(self) -> Self::Fields { + fn to_fields(self) -> ::Fields { match self { Self::Empty => Either::Left(Nil.into()), Self::Circle(field) => Either::Right(Either::Left(field.into())), @@ -285,7 +285,7 @@ snapshot_derive_has_fields! { } } impl ToFieldsRef for Shape { - fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + fn to_fields_ref<'__a>(&'__a self) -> ::FieldsRef<'__a> where Self: '__a, { diff --git a/crates/tests/cgp-tests/tests/extensible_variants/mod.rs b/crates/tests/cgp-tests/tests/extensible_variants/mod.rs index 7c144848d..cec1226e0 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/mod.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/mod.rs @@ -34,6 +34,11 @@ pub mod from_variant_derive; // with. A plain behavioral test — the expansion shape is pinned above. pub mod derive_cgp_data_lifetime; +// Variants named after the associated types the variant derives implement +// (`Value`, `Remainder`, `Extractor`, …), which the derives' qualified +// projections keep unambiguous. +pub mod reserved_variant_names; + // Variants with no fields (`V`, `V()`, `V {}`), carrying the payload `Nil` // through construction, extraction, casts, and both kinds of dispatch. pub mod empty_variants; diff --git a/crates/tests/cgp-tests/tests/extensible_variants/reserved_variant_names.rs b/crates/tests/cgp-tests/tests/extensible_variants/reserved_variant_names.rs new file mode 100644 index 000000000..9380d9463 --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_variants/reserved_variant_names.rs @@ -0,0 +1,85 @@ +//! An enum may name its variants after the associated types the variant derives implement. +//! +//! `#[derive(CgpData)]` implements traits whose associated types are `Value`, `Remainder`, +//! `Extractor`, `ExtractorRef`, `ExtractorMut`, `Fields`, and `FieldsRef`. Inside an impl for an +//! enum, a bare `Self::Value` could name the associated type or a variant called `Value`, so the +//! derives write each associated type as a qualified projection, such as +//! `>::Value`. An enum with all seven names as variants therefore derives +//! the whole family, and each slice works on it. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/derive_extract_field.md and +//! cgp-knowledge-base/cgp/implementation/entrypoints/derive_has_fields.md. + +use core::marker::PhantomData; + +use cgp::prelude::*; + +#[derive(Debug, PartialEq, CgpData)] +pub enum Reserved { + Value(u32), + Remainder(u64), + Extractor(i8), + ExtractorRef(i16), + ExtractorMut(i32), + Fields(bool), + FieldsRef(char), +} + +#[test] +fn test_from_variant_builds_reserved_names() { + assert_eq!( + Reserved::from_variant(PhantomData::, 1), + Reserved::Value(1) + ); + assert_eq!( + Reserved::from_variant(PhantomData::, 'x'), + Reserved::FieldsRef('x') + ); +} + +#[test] +fn test_extract_field_reaches_reserved_names() { + let remainder = Reserved::Remainder(2) + .to_extractor() + .extract_field(PhantomData::) + .unwrap_err(); + + assert_eq!( + remainder + .extract_field(PhantomData::) + .ok(), + Some(2) + ); + + assert_eq!( + Reserved::from_extractor(Reserved::Extractor(3).to_extractor()), + Reserved::Extractor(3) + ); +} + +#[test] +fn test_borrowed_extractors_reach_reserved_names() { + let reserved = Reserved::ExtractorRef(4); + let extracted = reserved + .extractor_ref() + .extract_field(PhantomData::); + assert_eq!(extracted.ok(), Some(&4)); + + let mut reserved = Reserved::ExtractorMut(5); + if let Ok(value) = reserved + .extractor_mut() + .extract_field(PhantomData::) + { + *value += 1; + } + assert_eq!(reserved, Reserved::ExtractorMut(6)); +} + +#[test] +fn test_fields_round_trip_reserved_names() { + let fields = Reserved::Fields(true).to_fields(); + assert_eq!(Reserved::from_fields(fields), Reserved::Fields(true)); + + let reserved = Reserved::FieldsRef('y'); + let _fields_ref = reserved.to_fields_ref(); +} From 3b78226ab5aac82b075f82c6d48bf6a24cb75222 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 13 Oct 2026 10:13:02 +0200 Subject: [PATCH 05/25] Keep other derives' helper attributes off the data derives' companions Co-Authored-By: Claude Opus 5.5 --- .../src/types/cgp_data/companion_attrs.rs | 9 +++ .../cgp_data/derive_builder/builder_struct.rs | 4 +- .../derive_extractor/extractor_enum.rs | 8 ++- .../cgp-macro-core/src/types/cgp_data/mod.rs | 2 + crates/tests/README.md | 3 +- .../companion_attrs/helper_attributes.rs | 58 +++++++++++++++++++ .../tests/companion_attrs/mod.rs | 3 + .../tests/companion_attrs_tests.rs | 12 ++++ .../tests/extensible_variants/mod.rs | 4 ++ .../variant_helper_attribute.rs | 29 ++++++++++ 10 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 crates/macros/cgp-macro-core/src/types/cgp_data/companion_attrs.rs create mode 100644 crates/tests/cgp-macro-tests/tests/companion_attrs/helper_attributes.rs create mode 100644 crates/tests/cgp-macro-tests/tests/companion_attrs/mod.rs create mode 100644 crates/tests/cgp-macro-tests/tests/companion_attrs_tests.rs create mode 100644 crates/tests/cgp-tests/tests/extensible_variants/variant_helper_attribute.rs diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/companion_attrs.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/companion_attrs.rs new file mode 100644 index 000000000..84f9e8fef --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/companion_attrs.rs @@ -0,0 +1,9 @@ +use syn::Attribute; + +/// Keep only the attributes a generated companion type can carry from a field or variant: `#[cfg]`, +/// so the companion has the same members as the input, and documentation. Every other attribute, +/// such as another derive's helper (`#[default]`, `#[serde(…)]`), belongs to a derive the companion +/// does not have and would fail to resolve. +pub fn retain_companion_attrs(attrs: &mut Vec) { + attrs.retain(|attr| attr.path().is_ident("cfg") || attr.path().is_ident("doc")); +} diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/builder_struct.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/builder_struct.rs index 782dc1322..5913ee51d 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/builder_struct.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/builder_struct.rs @@ -2,7 +2,7 @@ use syn::{GenericParam, Ident, ItemStruct, Type, TypeParam}; use crate::exports::MapType; use crate::parse_internal; -use crate::types::cgp_data::index_to_generic_ident; +use crate::types::cgp_data::{index_to_generic_ident, retain_companion_attrs}; pub fn derive_builder_struct( context_struct: &ItemStruct, @@ -16,6 +16,8 @@ pub fn derive_builder_struct( let generics = &mut builder_struct.generics; for (i, field) in builder_struct.fields.iter_mut().enumerate() { + retain_companion_attrs(&mut field.attrs); + let generic_param_name = index_to_generic_ident(i); let generic_param: TypeParam = parse_internal! { diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extractor_enum.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extractor_enum.rs index ce73c9bbc..e180ae0f0 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extractor_enum.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extractor_enum.rs @@ -3,7 +3,9 @@ use syn::{GenericParam, Ident, ItemEnum, Lifetime, LifetimeParam, Type, TypePara use crate::exports::{MapType, MapTypeRef}; use crate::parse_internal; -use crate::types::cgp_data::{get_variant_type, index_to_generic_ident, type_to_variant_fields}; +use crate::types::cgp_data::{ + get_variant_type, index_to_generic_ident, retain_companion_attrs, type_to_variant_fields, +}; /// Build the owned `__Partial{Name}` enum: a clone of the input enum that gains /// one `__F{i}__: MapType` parameter per variant, wrapping each payload so a @@ -21,6 +23,8 @@ pub fn derive_extractor_enum( let generics = &mut extractor_enum.generics; for (i, variant) in extractor_enum.variants.iter_mut().enumerate() { + retain_companion_attrs(&mut variant.attrs); + let generic_param_name = index_to_generic_ident(i); let generic_param: TypeParam = parse_internal! { @@ -99,6 +103,8 @@ pub fn derive_extractor_enum_ref( ); for (i, variant) in extractor_enum.variants.iter_mut().enumerate() { + retain_companion_attrs(&mut variant.attrs); + let generic_param_name = index_to_generic_ident(i); let generic_param: TypeParam = parse_internal! { diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/mod.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/mod.rs index e5eb39412..673a1ca34 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/mod.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/mod.rs @@ -1,3 +1,4 @@ +mod companion_attrs; mod derive_builder; mod derive_extractor; mod derive_from_variant; @@ -7,6 +8,7 @@ mod item; mod record; mod variant; +pub use companion_attrs::*; pub use derive_builder::*; pub use derive_extractor::*; pub use derive_from_variant::*; diff --git a/crates/tests/README.md b/crates/tests/README.md index 0c10cef19..e6d4b9cae 100644 --- a/crates/tests/README.md +++ b/crates/tests/README.md @@ -44,7 +44,8 @@ dispatching, namespaces, handlers, monadic handlers, async and Send bounds, blanket traits, and the standalone error backends (`error_backends`). The crate's build script turns the example in each error backend's README into a test module, which the `readme_*.rs` files in `error_backends` include, so those examples are tested without being copied. The set grows and subdivides over time. `cgp-macro-tests` follows -the same shape, with `ident_with_type_params` for parser corner cases and the +the same shape, with `ident_with_type_params` and `shape_macro_parsing` for parser corner cases, +`companion_attrs` for the attributes the data derives copy onto their companion types, and the failure-case targets `parser_rejections` and `invalid_expansion`. ## Running the tests diff --git a/crates/tests/cgp-macro-tests/tests/companion_attrs/helper_attributes.rs b/crates/tests/cgp-macro-tests/tests/companion_attrs/helper_attributes.rs new file mode 100644 index 000000000..3bafbcdd4 --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/companion_attrs/helper_attributes.rs @@ -0,0 +1,58 @@ +//! The builder and extractor companions drop another derive's helper attributes and keep the +//! documentation of each field or variant. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/derive_build_field.md and +//! cgp-knowledge-base/cgp/implementation/entrypoints/derive_extract_field.md. + +use cgp_macro_test_util_lib::functions::pretty_format; +use quote::quote; + +#[test] +fn test_builder_companion_drops_field_helper_attributes() { + let output = cgp_macro_lib::derive_build_field(quote! { + pub struct Person { + /// The person's name. + #[serde(rename = "full_name")] + pub name: String, + } + }) + .unwrap(); + + let formatted = pretty_format(output).unwrap(); + + assert!( + !formatted.contains("serde"), + "expected no `#[serde]` on the companion, got:\n{formatted}" + ); + assert!( + formatted.contains("The person's name."), + "expected the field's documentation on the companion, got:\n{formatted}" + ); +} + +#[test] +fn test_extractor_companions_drop_variant_helper_attributes() { + let output = cgp_macro_lib::derive_extract_field(quote! { + pub enum Mode { + /// The mode is off. + #[default] + Off, + #[serde(rename = "on")] + On(u64), + } + }) + .unwrap(); + + let formatted = pretty_format(output).unwrap(); + + assert!( + !formatted.contains("#[default]") && !formatted.contains("serde"), + "expected no helper attribute on the companions, got:\n{formatted}" + ); + // Once on the owned companion and once on the borrowed one. + assert_eq!( + formatted.matches("The mode is off.").count(), + 2, + "expected the variant's documentation on both companions, got:\n{formatted}" + ); +} diff --git a/crates/tests/cgp-macro-tests/tests/companion_attrs/mod.rs b/crates/tests/cgp-macro-tests/tests/companion_attrs/mod.rs new file mode 100644 index 000000000..0dfb37fa8 --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/companion_attrs/mod.rs @@ -0,0 +1,3 @@ +//! One unit test per file. + +pub mod helper_attributes; diff --git a/crates/tests/cgp-macro-tests/tests/companion_attrs_tests.rs b/crates/tests/cgp-macro-tests/tests/companion_attrs_tests.rs new file mode 100644 index 000000000..fd51792ae --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/companion_attrs_tests.rs @@ -0,0 +1,12 @@ +//! Entrypoint for tests of the attributes the extensible-data derives copy onto their generated +//! companion types. +//! +//! The builder's `__Partial{Name}` struct and the extractor's `__Partial{Name}` and +//! `__PartialRef{Name}` enums clone the input's fields or variants. A helper attribute of another +//! derive on one of them (`#[default]`, `#[serde(…)]`) would fail to resolve on a companion that +//! lacks that derive, so the companions keep only `#[cfg]` and documentation. These call the derive +//! entrypoints directly, since a test crate cannot compile a helper attribute without the derive +//! that declares it. +#![allow(dead_code)] + +pub mod companion_attrs; diff --git a/crates/tests/cgp-tests/tests/extensible_variants/mod.rs b/crates/tests/cgp-tests/tests/extensible_variants/mod.rs index cec1226e0..231681223 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/mod.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/mod.rs @@ -39,6 +39,10 @@ pub mod derive_cgp_data_lifetime; // projections keep unambiguous. pub mod reserved_variant_names; +// A variant helper attribute of another derive, which the extractor's companion +// enums must not copy. +pub mod variant_helper_attribute; + // Variants with no fields (`V`, `V()`, `V {}`), carrying the payload `Nil` // through construction, extraction, casts, and both kinds of dispatch. pub mod empty_variants; diff --git a/crates/tests/cgp-tests/tests/extensible_variants/variant_helper_attribute.rs b/crates/tests/cgp-tests/tests/extensible_variants/variant_helper_attribute.rs new file mode 100644 index 000000000..94a473e88 --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_variants/variant_helper_attribute.rs @@ -0,0 +1,29 @@ +//! A variant helper attribute of another derive, here `Default`'s `#[default]`, on an enum that also +//! derives `CgpData`. +//! +//! The extractor's companion enums copy the input's variants, but keep only their `#[cfg]` and +//! documentation attributes, so the `#[default]` stays on `Mode` alone and does not reach a +//! companion that lacks `#[derive(Default)]`. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/derive_extract_field.md. + +use core::marker::PhantomData; + +use cgp::prelude::*; + +#[derive(Debug, Default, PartialEq, CgpData)] +pub enum Mode { + #[default] + Off, + On(u64), +} + +#[test] +fn test_variant_helper_attribute() { + assert_eq!(Mode::default(), Mode::Off); + + let extracted = Mode::On(3) + .to_extractor() + .extract_field(PhantomData::); + assert_eq!(extracted.ok(), Some(3)); +} From df71fac72522a9f3a965df4466e57691a2b19903 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 13 Oct 2026 10:16:58 +0200 Subject: [PATCH 06/25] Keep only real primitive names as types in `Path!` segments Co-Authored-By: Claude Opus 5.5 --- .../src/types/path/path_element.rs | 32 ++++++++++++------- .../cgp-tests/tests/namespaces/path_macro.rs | 15 ++++++++- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/types/path/path_element.rs b/crates/macros/cgp-macro-core/src/types/path/path_element.rs index 76ee53aae..fdc387696 100644 --- a/crates/macros/cgp-macro-core/src/types/path/path_element.rs +++ b/crates/macros/cgp-macro-core/src/types/path/path_element.rs @@ -68,16 +68,26 @@ impl ToTokens for PathElement { } } +/// Whether `ident` is a Rust primitive type name, which a path segment keeps as the type rather +/// than reading as a `Symbol`. Only the real names match, so `f` or `u2` is a symbol. fn is_primitive_type(ident: &str) -> bool { - if (ident.starts_with("i") || ident.starts_with("u") || ident.starts_with("f")) - && ident[1..].chars().all(|c| c.is_numeric()) - { - return true; - } - - if ["char", "bool", "usize", "isize", "str"].contains(&ident) { - return true; - } - - false + matches!( + ident, + "u8" | "u16" + | "u32" + | "u64" + | "u128" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "i128" + | "isize" + | "f32" + | "f64" + | "char" + | "bool" + | "str" + ) } diff --git a/crates/tests/cgp-tests/tests/namespaces/path_macro.rs b/crates/tests/cgp-tests/tests/namespaces/path_macro.rs index 08c749e1b..2ea76b887 100644 --- a/crates/tests/cgp-tests/tests/namespaces/path_macro.rs +++ b/crates/tests/cgp-tests/tests/namespaces/path_macro.rs @@ -2,7 +2,8 @@ //! entry or `#[prefix]` attribute. Each `assert_same_type` call compiles only if //! the two types are identical, pinning the segment classification: a lowercase //! segment becomes a `Symbol!`, a capitalized segment stays a type, a primitive -//! name such as `u32` stays the primitive, and the chain ends in `Nil`. +//! name such as `u32` stays the primitive while a near miss such as `f` or `u2` +//! becomes a symbol, and the chain ends in `Nil`. //! //! See cgp-knowledge-base/cgp/reference/macros/path.md. @@ -35,4 +36,16 @@ fn test_path_macro_segments() { PhantomData::, PhantomData::>>>, ); + + // Only real primitive names stay types: `f`, `u2`, and `i7` look like + // primitives but are none, so each becomes a symbol. + assert_same_type( + PhantomData::, + PhantomData::< + PathCons< + Symbol!("f"), + PathCons>>, + >, + >, + ); } From c813d90ae895872a87404d08993503e494f6d98d Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 13 Oct 2026 10:25:29 +0200 Subject: [PATCH 07/25] Point a `->` entry's errors at the entry Co-Authored-By: Claude Opus 5.5 --- .../src/functions/override_span.rs | 27 +++++++++ .../delegate_component/mapping/direct.rs | 31 +++++++--- .../types/delegate_component/mapping/eval.rs | 56 +++++++++++++++---- .../delegate_component/mapping/normal.rs | 2 + .../delegate_component/mapping/redirect.rs | 2 + .../delegate_component/statement/eval.rs | 2 + .../delegate_component/statement/open.rs | 2 + 7 files changed, 104 insertions(+), 18 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/functions/override_span.rs b/crates/macros/cgp-macro-core/src/functions/override_span.rs index 0aa6b3bce..5e7019d85 100644 --- a/crates/macros/cgp-macro-core/src/functions/override_span.rs +++ b/crates/macros/cgp-macro-core/src/functions/override_span.rs @@ -77,6 +77,33 @@ where parse2(trees.into_iter().collect()) } +/// Re-span every *synthesized* top-level token of `body` onto `span`, leaving the user's tokens at +/// their own spans, and re-parse into `T`. +/// +/// This is for a fragment whose own span a diagnostic reads, such as a `where` bound or a type, +/// when the fragment mixes user tokens with synthesized ones: the synthesized ends otherwise widen +/// the fragment's span to the whole invocation. `span` must be a user token no resolvable name +/// shares (an operator such as `->`), since a synthesized reference moved onto it becomes, for an +/// editor, that token. +pub fn override_synthesized_span(span: Span, body: &T) -> syn::Result +where + T: Parse + ToTokens, +{ + let call_site_text = Span::call_site().source_text(); + + parse2( + body.to_token_stream() + .into_iter() + .map(|mut tree| { + if is_synthesized(tree.span(), &call_site_text) { + tree.set_span(span); + } + tree + }) + .collect(), + ) +} + /// Whether `token_span` was stamped by `quote!`/`parse_internal!` rather than /// carried in from user input. A synthesized token carries the macro's /// `call_site` span, whose source text is the entire invocation; a user token's diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/direct.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/direct.rs index ff2b1d078..fe254d5f0 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/direct.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/direct.rs @@ -1,7 +1,9 @@ -use syn::Type; +use syn::spanned::Spanned; use syn::token::RArrow; +use syn::{Type, WherePredicate}; use crate::exports::DelegateComponent; +use crate::functions::override_synthesized_span; use crate::parse_internal; use crate::types::delegate_component::{ DelegateKey, DelegateValue, EvalDelegateEntries, EvalDelegateKey, EvalDelegateValue, @@ -22,6 +24,7 @@ impl EvalDelegateEntries for DirectDelegateMapping { fn eval_entries(&self, table_type: &Type) -> syn::Result> { let keys = self.key.eval()?; let value_type = self.value.eval()?; + let arrow_span = self.arrow.span(); let mut entries = Vec::new(); @@ -30,18 +33,29 @@ impl EvalDelegateEntries for DirectDelegateMapping { let span = key.span; let mut generics = key.generics; - let where_predicate = parse_internal! { - #value_type: #DelegateComponent< #key_type > - }; + // The bound and the projection mix the user's key and value with + // synthesized tokens, so an error on either (the table lacking the key) + // would span the whole block. Their synthesized tokens move onto the `->` + // the user wrote, which no resolvable name shares, so the key and the + // value stay navigable. + let where_predicate: WherePredicate = override_synthesized_span( + arrow_span, + &parse_internal! { + #value_type: #DelegateComponent< #key_type > + }, + )?; generics .make_where_clause() .predicates .push(where_predicate); - let direct_value_type = parse_internal! { - < #value_type as #DelegateComponent< #key_type > >::Delegate - }; + let direct_value_type: Type = override_synthesized_span( + arrow_span, + &parse_internal! { + < #value_type as #DelegateComponent< #key_type > >::Delegate + }, + )?; entries.push(EvaluatedDelegateEntry { table_type: table_type.clone(), @@ -49,6 +63,9 @@ impl EvalDelegateEntries for DirectDelegateMapping { key: key_type, value: direct_value_type, span, + delegate_span: Some(arrow_span), + // The `-` alone, which does not contain the arrow's span the bounds sit on. + params_span: Some(self.arrow.spans[0]), }); } diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs index dc83203c9..17315f22b 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs @@ -1,8 +1,10 @@ use proc_macro2::Span; -use syn::{Generics, ItemImpl, Type}; +use syn::{GenericParam, Generics, ItemImpl, Type, WherePredicate}; use crate::exports::{DelegateComponent, IsProviderFor}; -use crate::functions::{merge_generics, override_item_span, parse_internal}; +use crate::functions::{ + merge_generics, override_item_span, override_synthesized_span, parse_internal, +}; /// The flat form every key, value, and statement collapses to; it renders the /// impl pair for one wiring entry. @@ -15,6 +17,17 @@ pub struct EvaluatedDelegateEntry { /// reported on the entry the user wrote. Sourced from the key's original token /// rather than the (possibly synthesized) `key` type — see `respan_impl`. pub span: Span, + /// Where the synthesized tokens of the `type Delegate = …;` item and of the + /// `IsProviderFor` impl's bound on the value go, when the value mixes them with the + /// user's: a `->` entry's projection, whose errors (the table lacking the key) would + /// otherwise span the whole block. `None` leaves them as built. + pub delegate_span: Option, + /// Where the `IsProviderFor` impl's `__Context__` and `__Params__: ?Sized` parameters go + /// for a `->` entry. rustc reports a false global bound at the first `where` predicate + /// whose span contains it, and an inline parameter bound is such a predicate, so this + /// span must not contain `delegate_span`, or the parameter would claim the entry's + /// errors. `None` leaves the parameters as built. + pub params_span: Option, } /// Lower a construct into a single evaluated entry. @@ -43,7 +56,7 @@ impl EvaluatedDelegateEntry { let (impl_generics, _, where_clause) = generics.split_for_impl(); - let item_impl = parse_internal! { + let mut item_impl: ItemImpl = parse_internal! { impl #impl_generics #DelegateComponent< #key > for #table_type @@ -53,6 +66,12 @@ impl EvaluatedDelegateEntry { } }; + if let Some(delegate_span) = self.delegate_span { + for item in item_impl.items.iter_mut() { + *item = override_synthesized_span(delegate_span, item)?; + } + } + self.respan_impl(item_impl) } @@ -69,15 +88,30 @@ impl EvaluatedDelegateEntry { let key = &self.key; let value = &self.value; - generics.params.push(parse_internal!(__Context__)); - generics.params.push(parse_internal!(__Params__: ?Sized)); + let mut context_param: GenericParam = parse_internal!(__Context__); + let mut params_param: GenericParam = parse_internal!(__Params__: ?Sized); + + // Left spanning the whole block, `__Params__: ?Sized` would claim a `->` entry's + // false-bound errors; see `params_span`. + if let Some(params_span) = self.params_span { + context_param = override_synthesized_span(params_span, &context_param)?; + params_param = override_synthesized_span(params_span, ¶ms_param)?; + } + + generics.params.push(context_param); + generics.params.push(params_param); + + let mut provider_bound: WherePredicate = parse_internal! { + #value: #IsProviderFor<#key, __Context__, __Params__> + }; + + // A `->` entry's bound is ill-formed when the table lacks the key, so its + // synthesized tokens go where the `type Delegate` item's do. + if let Some(delegate_span) = self.delegate_span { + provider_bound = override_synthesized_span(delegate_span, &provider_bound)?; + } - generics - .make_where_clause() - .predicates - .push(parse_internal! { - #value: #IsProviderFor<#key, __Context__, __Params__> - }); + generics.make_where_clause().predicates.push(provider_bound); let (impl_generics, _, where_clause) = generics.split_for_impl(); diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/normal.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/normal.rs index a131be04f..e3f96a1d7 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/normal.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/normal.rs @@ -40,6 +40,8 @@ impl EvalDelegateEntries for NormalDelegateMapping { key: key.key, value: value_type.clone(), span: key.span, + delegate_span: None, + params_span: None, }) } diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/redirect.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/redirect.rs index 815fd0bfd..ba0ac6dea 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/redirect.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/redirect.rs @@ -41,6 +41,8 @@ impl EvalDelegateEntries for RedirectDelegateMapping { key: key.key, value: value_type.clone(), span: key.span, + delegate_span: None, + params_span: None, }; entries.push(entry); diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs index 5d4f7e375..10506aa15 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs @@ -88,6 +88,8 @@ impl EvalDelegateEntry for EvaluatedForEntry { key: self.mapping_key.clone(), value: self.mapping_value.clone(), span: self.span, + delegate_span: None, + params_span: None, }; Ok(entry) diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/open.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/open.rs index e38b7fb44..f9f156f6f 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/open.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/open.rs @@ -65,6 +65,8 @@ impl EvalDelegateEntries for OpenDelegateStatement { table_type: table_type.clone(), generics: Default::default(), span: component.span(), + delegate_span: None, + params_span: None, key, value, }) From 25ffa741f5933715a1c933c474ad603719edb110 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 13 Oct 2026 10:30:10 +0200 Subject: [PATCH 08/25] Make an `#[async_trait]` default body the returned future Co-Authored-By: Claude Opus 5.5 --- .../macros/cgp-async-macro/src/impl_async.rs | 43 ++++++++++- .../tests/async_and_send/default_body.rs | 73 +++++++++++++++++++ .../cgp-tests/tests/async_and_send/mod.rs | 4 + 3 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 crates/tests/cgp-tests/tests/async_and_send/default_body.rs diff --git a/crates/macros/cgp-async-macro/src/impl_async.rs b/crates/macros/cgp-async-macro/src/impl_async.rs index bec3b8311..dce50d9b9 100644 --- a/crates/macros/cgp-async-macro/src/impl_async.rs +++ b/crates/macros/cgp-async-macro/src/impl_async.rs @@ -1,6 +1,8 @@ +use alloc::vec::Vec; + use proc_macro2::TokenStream; -use quote::ToTokens; -use syn::{ItemTrait, ReturnType, TraitItem, Type, parse_quote}; +use quote::{ToTokens, quote}; +use syn::{FnArg, ItemTrait, Pat, ReturnType, Signature, TraitItem, Type, parse_quote}; pub fn impl_async(item: TokenStream) -> TokenStream { match syn::parse2::(item.clone()) { @@ -22,6 +24,22 @@ pub fn impl_async(item: TokenStream) -> TokenStream { trait_fn.sig.output = impl_return; trait_fn.sig.asyncness = None; + + // A default body becomes the future the method returns, as the body of an + // `async fn` is. + if let Some(body) = &mut trait_fn.default { + let rebinds = move_params_into_future(&mut trait_fn.sig); + let stmts = &body.stmts; + + *body = parse_quote! { + { + async move { + #(#rebinds)* + #(#stmts)* + } + } + }; + } } } @@ -30,3 +48,24 @@ pub fn impl_async(item: TokenStream) -> TokenStream { _ => item, } } + +/// Rebind each by-value identifier parameter inside the future, so the future owns it and drops it +/// when it completes, as an `async fn` does, rather than when the method returns. A `mut` moves +/// from the parameter to the rebinding, where the body mutates it. +fn move_params_into_future(sig: &mut Signature) -> Vec { + let mut rebinds = Vec::new(); + + for input in sig.inputs.iter_mut() { + if let FnArg::Typed(arg) = input + && let Pat::Ident(pat) = arg.pat.as_mut() + && pat.by_ref.is_none() + && pat.subpat.is_none() + { + let ident = &pat.ident; + let mutability = pat.mutability.take(); + rebinds.push(quote! { let #mutability #ident = #ident; }); + } + } + + rebinds +} diff --git a/crates/tests/cgp-tests/tests/async_and_send/default_body.rs b/crates/tests/cgp-tests/tests/async_and_send/default_body.rs new file mode 100644 index 000000000..52fbd2556 --- /dev/null +++ b/crates/tests/cgp-tests/tests/async_and_send/default_body.rs @@ -0,0 +1,73 @@ +//! An `async` method with a default body under `#[async_trait]`. +//! +//! The attribute rewrites the method to return `impl Future`, and its default body +//! becomes that future: `async move { … }` around the body, with each by-value +//! parameter rebound inside it. The future therefore owns the parameters and drops +//! them when it completes, as an `async fn` does, and a `mut` parameter is mutable +//! inside. A component's default body reaches its provider trait the same way, so +//! an empty provider inherits it. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/async_trait.md. + +use core::sync::atomic::{AtomicBool, Ordering}; + +use cgp::prelude::*; +use futures::executor::block_on; + +pub struct Guard<'a>(&'a AtomicBool); + +impl Drop for Guard<'_> { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } +} + +#[async_trait] +pub trait CanCount { + async fn add(&self, mut value: u32, extra: u32) -> u32 { + value += extra; + value + } + + async fn hold(&self, _guard: Guard<'_>) {} +} + +pub struct Counter; + +impl CanCount for Counter {} + +#[cgp_component(Greeter)] +#[async_trait] +pub trait CanGreet { + async fn greet(&self, name: &str) -> String { + format!("Hello, {name}!") + } +} + +#[cgp_impl(new GreetByDefault)] +impl Greeter {} + +pub struct App; + +delegate_components! { + App { + GreeterComponent: GreetByDefault, + } +} + +#[test] +fn test_default_body_runs_as_future() { + assert_eq!(block_on(Counter.add(2, 3)), 5); + assert_eq!(block_on(App.greet("World")), "Hello, World!"); +} + +#[test] +fn test_default_body_owns_its_parameters() { + let dropped = AtomicBool::new(false); + + let future = Counter.hold(Guard(&dropped)); + assert!(!dropped.load(Ordering::SeqCst)); + + block_on(future); + assert!(dropped.load(Ordering::SeqCst)); +} diff --git a/crates/tests/cgp-tests/tests/async_and_send/mod.rs b/crates/tests/cgp-tests/tests/async_and_send/mod.rs index 0791df477..43cba9284 100644 --- a/crates/tests/cgp-tests/tests/async_and_send/mod.rs +++ b/crates/tests/cgp-tests/tests/async_and_send/mod.rs @@ -15,3 +15,7 @@ pub mod component_async; // Async `#[cgp_fn]` expansion: this concept owns the async variant of the // `#[cgp_fn]` macro snapshot (the `#[async_trait]` handling is the feature). pub mod cgp_fn_async; + +// A default `async` body: `#[async_trait]` turns it into the returned future, +// which owns the by-value parameters. +pub mod default_body; From 70bec20f863c01606ec59abcfa6cffb745b60cb4 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 13 Oct 2026 10:32:38 +0200 Subject: [PATCH 09/25] Reject a namespace that inherits itself Co-Authored-By: Claude Opus 5.5 --- .../src/types/namespace/table.rs | 29 ++++++++++++++++++- .../tests/parser_rejections/cgp_namespace.rs | 22 +++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/types/namespace/table.rs b/crates/macros/cgp-macro-core/src/types/namespace/table.rs index a06d08326..2a0ab1ceb 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/table.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/table.rs @@ -1,6 +1,7 @@ +use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::token::Colon; -use syn::{Ident, ItemImpl, ItemStruct, ItemTrait, Type, braced}; +use syn::{Error, Ident, ItemImpl, ItemStruct, ItemTrait, Type, braced}; use crate::functions::parse_internal; use crate::traits::ParseOptionalKeyword; @@ -32,6 +33,7 @@ impl Parse for NamespaceTable { let parent_namespace = if input.peek(Colon) { let colon: Colon = input.parse()?; let parent_namespace_type = input.parse()?; + reject_self_inheritance(&namespace_type, &parent_namespace_type)?; Some((colon, parent_namespace_type)) } else { None @@ -56,6 +58,31 @@ impl Parse for NamespaceTable { } } +/// Reject a namespace that names itself as its parent, `new Ns: Ns`, a cycle visible in this one +/// invocation. A cycle across several namespaces is not visible here. +fn reject_self_inheritance( + namespace: &IdentWithTypeArgs, + parent: &PathWithTypeArgs, +) -> syn::Result<()> { + let names_itself = parent.path.leading_colon.is_none() + && parent.path.segments.len() == 1 + && parent.path.segments[0].ident == namespace.ident + && parent.type_args.to_token_stream().to_string() + == namespace.type_args.to_token_stream().to_string(); + + if names_itself { + return Err(Error::new_spanned( + parent, + format!( + "the namespace `{}` cannot inherit from itself; name another namespace as its parent, or omit the parent", + namespace.ident + ), + )); + } + + Ok(()) +} + impl NamespaceTable { pub fn build_namespace_trait(&self) -> syn::Result { let mut namespace = self.namespace.clone(); diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs index d10891f5b..a7cd97889 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs @@ -4,6 +4,8 @@ //! discarding it. It rejects an attribute on a `:` mapping key, on a `=>` redirect //! key, and on a key inside a `for <..> in ..` loop. //! +//! A namespace naming itself as its parent (`new Ns: Ns`) is rejected. +//! //! The header may stand alone: `new Ns` or `new Child: Parent` with no brace pair //! parses as an empty table. That relaxation applies only to an input that ends //! at the header, so any other token in the body position is still rejected. @@ -14,7 +16,7 @@ use quote::quote; -use super::assert_macro_rejects; +use super::{assert_macro_rejects, assert_macro_rejects_with}; #[test] fn rejects_attribute_on_mapping_key() { @@ -78,3 +80,21 @@ fn rejects_stray_token_after_name() { )) }); } + +/// A namespace that names itself as its parent is a cycle visible in one +/// invocation, so the macro rejects it, with or without type arguments, rather +/// than emitting impls the compiler rejects with an unconstrained `__Value__`. +#[test] +fn rejects_self_inheritance() { + assert_macro_rejects_with( + "cgp_namespace inheriting itself", + "the namespace `SelfNs` cannot inherit from itself; name another namespace as its parent, or omit the parent", + || cgp_macro_lib::cgp_namespace(quote!(new SelfNs: SelfNs {})), + ); + + assert_macro_rejects_with( + "cgp_namespace with type arguments inheriting itself", + "the namespace `SelfNs` cannot inherit from itself; name another namespace as its parent, or omit the parent", + || cgp_macro_lib::cgp_namespace(quote!( new SelfNs: SelfNs {})), + ); +} From 6a2e6aa7c131c7cdd7f77b6935c41e19a4594a93 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 13 Oct 2026 10:35:39 +0200 Subject: [PATCH 10/25] Reject `#[uses]` on a component trait Co-Authored-By: Claude Opus 5.5 --- .../attributes/cgp_component_attributes.rs | 10 +++++++ .../tests/parser_rejections/cgp_component.rs | 29 +++++++++++++++++-- .../tests/parser_rejections/getters.rs | 24 ++++++++++++++- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/types/attributes/cgp_component_attributes.rs b/crates/macros/cgp-macro-core/src/types/attributes/cgp_component_attributes.rs index ddf61e5fb..e429c991d 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/cgp_component_attributes.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/cgp_component_attributes.rs @@ -61,6 +61,16 @@ impl CgpComponentAttributes { } else if ident == "prefix" { let namespace_specs = attribute.parse_args_with(PrefixAttribute::parse)?; parsed_attributes.prefixes.push(namespace_specs); + } else if ident == "uses" { + // `#[uses]` imports a provider's dependencies; on a trait definition it would + // only be left for the compiler, which knows no such attribute. + let bounds = &attribute.meta.require_list()?.tokens; + return Err(syn::Error::new_spanned( + &attribute, + format!( + "`#[uses]` declares the dependencies of a provider, not of a trait; write `#[extend({bounds})]` to make it a supertrait" + ), + )); } else if ident == "derive_delegate" { let derive_delegate_attribute = attribute.parse_args_with(DeriveDelegateAttribute::parse)?; diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs index 5f88ad0f1..538a1a072 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs @@ -1,7 +1,8 @@ //! `#[cgp_component]` rejects inputs it cannot lower into a component: a //! non-trait item, a trait carrying a const generic parameter, attribute -//! arguments with a repeated key, an unknown key, or no `provider` key, and a -//! `name` whose parameters the trait does not declare. +//! arguments with a repeated key, an unknown key, or no `provider` key, a +//! `name` whose parameters the trait does not declare, and a `#[uses]` +//! attribute, which belongs on a provider (`#[extend]` adds a supertrait). //! //! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_component.md (Tests) for these failure //! cases, and cgp-knowledge-base/cgp/reference/macros/cgp_component.md for the user-facing @@ -9,7 +10,7 @@ use quote::quote; -use super::assert_macro_rejects; +use super::{assert_macro_rejects, assert_macro_rejects_with}; #[test] fn rejects_non_trait_item() { @@ -117,3 +118,25 @@ fn rejects_missing_provider_key() { ) }); } + +/// `#[uses]` declares a provider's dependencies, so on a component trait the +/// macro rejects it and names the `#[extend]` form, rather than leaving an +/// attribute the compiler does not know (with its `#[used]` suggestion). +#[test] +fn rejects_uses_attribute() { + assert_macro_rejects_with( + "cgp_component with a #[uses] attribute", + "`#[uses]` declares the dependencies of a provider, not of a trait; write `#[extend(HasName)]` to make it a supertrait", + || { + cgp_macro_lib::cgp_component( + quote!(Greeter), + quote!( + #[uses(HasName)] + pub trait CanGreet { + fn greet(&self); + } + ), + ) + }, + ); +} diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs index 64c76bb81..3af5b3b89 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs @@ -8,7 +8,8 @@ //! allowed, only alongside exactly one method. An optional reference to a slice or a //! trait object is rejected too, since the `Option<&T>` form would read an unsized //! `Option` field. `#[cgp_auto_getter]` additionally rejects any attribute argument, -//! since it has no provider name or keys to accept. +//! since it has no provider name or keys to accept, and a `#[uses]` attribute, which +//! belongs on a provider. //! //! See cgp-knowledge-base/cgp/implementation/asts/cgp_getter.md (Tests) for these failure cases and //! cgp-knowledge-base/cgp/reference/macros/cgp_auto_getter.md for the user-facing getter-method @@ -244,3 +245,24 @@ fn rejects_attribute_argument() { ) }); } + +/// `#[cgp_auto_getter]` shares the component's attribute parser, so it rejects a +/// `#[uses]` the same way, naming the `#[extend]` form. +#[test] +fn rejects_uses_on_auto_getter() { + assert_macro_rejects_with( + "cgp_auto_getter with a #[uses] attribute", + "`#[uses]` declares the dependencies of a provider, not of a trait; write `#[extend(HasId)]` to make it a supertrait", + || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + #[uses(HasId)] + pub trait HasName { + fn name(&self) -> &str; + } + ), + ) + }, + ); +} From 7b407e92b07c92c2a2fd4aa3a79b589e7bc9b9c9 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 13 Oct 2026 10:37:58 +0200 Subject: [PATCH 11/25] Reject a component method parameter that is not a plain name Co-Authored-By: Claude Opus 5.5 --- .../functions/delegated_impls/signature.rs | 26 +++++++--- .../tests/parser_rejections/cgp_component.rs | 51 ++++++++++++++++++- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs b/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs index a724dda95..4e314f5a3 100644 --- a/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs +++ b/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs @@ -1,8 +1,8 @@ use proc_macro2::{Span, TokenStream}; -use quote::{ToTokens, quote}; +use quote::quote; use syn::punctuated::Punctuated; use syn::token::Comma; -use syn::{FnArg, Ident, ImplItemFn, Signature, Type, Visibility}; +use syn::{Error, FnArg, Ident, ImplItemFn, Pat, Signature, Type, Visibility}; use crate::functions::parse_internal; @@ -43,11 +43,21 @@ fn signature_to_idents(sig: &Signature) -> syn::Result> sig.inputs.iter().map(arg_to_ident).collect() } +/// The name an argument is forwarded by. A parameter that is not a plain name (`_`, `mut value`, +/// a destructuring pattern) has none, so it is rejected with a message saying what to write. fn arg_to_ident(arg: &FnArg) -> syn::Result { - let ident = match arg { - FnArg::Receiver(_) => Ident::new("self", Span::call_site()), - FnArg::Typed(pat) => parse_internal(pat.pat.to_token_stream())?, - }; - - Ok(ident) + match arg { + FnArg::Receiver(_) => Ok(Ident::new("self", Span::call_site())), + FnArg::Typed(arg) => match arg.pat.as_ref() { + Pat::Ident(pat) + if pat.by_ref.is_none() && pat.mutability.is_none() && pat.subpat.is_none() => + { + Ok(pat.ident.clone()) + } + pat => Err(Error::new_spanned( + pat, + "a component method parameter needs a plain name, since the generated impls forward each argument by name; write a name such as `_value`, and destructure inside a default body", + )), + }, + } } diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs index 538a1a072..c0d29626c 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs @@ -1,8 +1,10 @@ //! `#[cgp_component]` rejects inputs it cannot lower into a component: a //! non-trait item, a trait carrying a const generic parameter, attribute //! arguments with a repeated key, an unknown key, or no `provider` key, a -//! `name` whose parameters the trait does not declare, and a `#[uses]` -//! attribute, which belongs on a provider (`#[extend]` adds a supertrait). +//! `name` whose parameters the trait does not declare, a `#[uses]` +//! attribute, which belongs on a provider (`#[extend]` adds a supertrait), and +//! a method parameter that is not a plain name, which the generated impls could +//! not forward. //! //! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_component.md (Tests) for these failure //! cases, and cgp-knowledge-base/cgp/reference/macros/cgp_component.md for the user-facing @@ -140,3 +142,48 @@ fn rejects_uses_attribute() { }, ); } + +const UNNAMED_PARAMETER: &str = "a component method parameter needs a plain name, since the generated impls forward each argument by name; write a name such as `_value`, and destructure inside a default body"; + +/// The generated impls forward each argument to the provider by name, so a `_` +/// parameter, which Rust allows in a method without a body, is rejected with a +/// message saying what to write rather than an internal parse error. +#[test] +fn rejects_unnamed_parameter() { + assert_macro_rejects_with( + "cgp_component with a `_` parameter", + UNNAMED_PARAMETER, + || { + cgp_macro_lib::cgp_component( + quote!(Counter), + quote!( + pub trait CanCount { + fn count(&self, _: u32) -> u32; + } + ), + ) + }, + ); +} + +/// A destructuring pattern in a default method has no single name to forward +/// either. +#[test] +fn rejects_destructuring_parameter() { + assert_macro_rejects_with( + "cgp_component with a destructuring parameter", + UNNAMED_PARAMETER, + || { + cgp_macro_lib::cgp_component( + quote!(Adder), + quote!( + pub trait CanAdd { + fn add(&self, (a, b): (u32, u32)) -> u32 { + a + b + } + } + ), + ) + }, + ); +} From d868fd1971807fe758270bf32d0feec7d988ce74 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 13 Oct 2026 10:42:19 +0200 Subject: [PATCH 12/25] Reject a `#[cgp_computer]` parameter only the return type uses Co-Authored-By: Claude Opus 5.5 --- .../cgp-macro-extra-core/src/functions/mod.rs | 2 + .../src/functions/output_only_params.rs | 104 ++++++++++++++++++ .../src/types/cgp_computer/item.rs | 4 +- .../tests/parser_rejections/cgp_computer.rs | 46 +++++++- .../tests/handlers/computer_macro_generics.rs | 11 +- 5 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 crates/macros/cgp-macro-extra-core/src/functions/output_only_params.rs diff --git a/crates/macros/cgp-macro-extra-core/src/functions/mod.rs b/crates/macros/cgp-macro-extra-core/src/functions/mod.rs index 7b9916734..1809371de 100644 --- a/crates/macros/cgp-macro-extra-core/src/functions/mod.rs +++ b/crates/macros/cgp-macro-extra-core/src/functions/mod.rs @@ -1,5 +1,7 @@ +mod output_only_params; mod provider_ident; mod return_type; +pub use output_only_params::*; pub use provider_ident::*; pub use return_type::*; diff --git a/crates/macros/cgp-macro-extra-core/src/functions/output_only_params.rs b/crates/macros/cgp-macro-extra-core/src/functions/output_only_params.rs new file mode 100644 index 000000000..39806d498 --- /dev/null +++ b/crates/macros/cgp-macro-extra-core/src/functions/output_only_params.rs @@ -0,0 +1,104 @@ +use syn::visit::{self, Visit}; +use syn::{Error, FnArg, GenericParam, Ident, Signature, WherePredicate}; + +/// Reject a type or const parameter that no argument type names and no other bound names. +/// +/// The generated `Computer` impl carries the function's generics, but its trait arguments carry +/// only the input types, so a parameter only the return type uses is constrained by nothing and +/// the impl fails with `E0207`. A parameter another bound names may still be constrained (an +/// associated-type bound such as `I: Iterator` binds `T`), so it is left to the compiler. +pub fn reject_output_only_params(sig: &Signature) -> syn::Result<()> { + for param in sig.generics.params.iter() { + let ident = match param { + GenericParam::Type(param) => ¶m.ident, + GenericParam::Const(param) => ¶m.ident, + GenericParam::Lifetime(_) => continue, + }; + + if named_by_inputs(sig, ident) || named_by_other_bounds(sig, ident) { + continue; + } + + return Err(Error::new_spanned( + ident, + format!( + "the parameter `{ident}` appears in no argument type, so the generated `Computer` impl cannot determine it; take an argument whose type names `{ident}`, or write the provider by hand as a `Computer` impl on a provider generic over `{ident}`" + ), + )); + } + + Ok(()) +} + +fn named_by_inputs(sig: &Signature, ident: &Ident) -> bool { + let mut finder = FindIdent::new(ident); + + for input in sig.inputs.iter() { + if let FnArg::Typed(arg) = input { + finder.visit_type(&arg.ty); + } + } + + finder.found +} + +/// Whether a bound other than the parameter's own names it: an inline bound of another parameter, +/// or a `where` predicate whose bounded type is not the parameter alone. +fn named_by_other_bounds(sig: &Signature, ident: &Ident) -> bool { + let mut finder = FindIdent::new(ident); + + for param in sig.generics.params.iter() { + match param { + GenericParam::Type(other) if other.ident != *ident => { + for bound in other.bounds.iter() { + finder.visit_type_param_bound(bound); + } + } + GenericParam::Const(other) if other.ident != *ident => { + finder.visit_type(&other.ty); + } + _ => {} + } + } + + if let Some(where_clause) = &sig.generics.where_clause { + for predicate in where_clause.predicates.iter() { + if let WherePredicate::Type(predicate) = predicate { + let bounds_itself = matches!( + &predicate.bounded_ty, + syn::Type::Path(path) if path.qself.is_none() && path.path.is_ident(ident) + ); + + if !bounds_itself { + finder.visit_predicate_type(predicate); + } + } + } + } + + finder.found +} + +struct FindIdent<'a> { + target: &'a Ident, + found: bool, +} + +impl<'a> FindIdent<'a> { + fn new(target: &'a Ident) -> Self { + Self { + target, + found: false, + } + } +} + +impl<'ast> Visit<'ast> for FindIdent<'_> { + fn visit_ident(&mut self, ident: &'ast Ident) { + if ident == self.target { + self.found = true; + } + + visit::visit_ident(self, ident); + } +} 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 index 24488dc2b..4d835fa72 100644 --- 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 @@ -3,7 +3,7 @@ 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::functions::{derive_provider_ident, reject_output_only_params, return_type}; use crate::types::cgp_computer::{MaybeResultType, PreprocessedCgpComputer}; use crate::visitors::find_impl_trait; @@ -54,6 +54,8 @@ impl ItemCgpComputer { let output = return_type(&sig.output)?; + reject_output_only_params(sig)?; + if let Some(impl_trait) = find_impl_trait(&output) { return Err(Error::new_spanned( impl_trait, 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 a63b54c3b..516e49ab0 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,8 +1,10 @@ //! `#[cgp_computer]` rejects a function with a `self` receiver, since a handler //! 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. +//! return type, which a provider impl cannot name, a provider name that is a +//! path rather than an identifier, and a type or const parameter only the return +//! type uses, which the generated impl cannot determine. 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 @@ -105,3 +107,43 @@ fn rejects_impl_trait_return() { }, ); } + +/// The generated `Computer` impl's trait arguments carry only the input types, so +/// a parameter that only the return type names is constrained by nothing; the +/// macro rejects it on the parameter rather than emitting an `E0207`. +#[test] +fn rejects_output_only_type_parameter() { + assert_macro_rejects_with( + "cgp_computer with a type parameter only the return type uses", + "the parameter `T` appears in no argument type, so the generated `Computer` impl cannot determine it; take an argument whose type names `T`, or write the provider by hand as a `Computer` impl on a provider generic over `T`", + || { + cgp_macro_extra_lib::cgp_computer( + quote!(), + quote!( + fn parse(value: String) -> Option { + value.parse().ok() + } + ), + ) + }, + ); +} + +/// A const parameter only the return type uses is just as undetermined. +#[test] +fn rejects_output_only_const_parameter() { + assert_macro_rejects_with( + "cgp_computer with a const parameter only the return type uses", + "the parameter `N` appears in no argument type, so the generated `Computer` impl cannot determine it; take an argument whose type names `N`, or write the provider by hand as a `Computer` impl on a provider generic over `N`", + || { + cgp_macro_extra_lib::cgp_computer( + quote!(), + quote!( + fn zeros(value: u8) -> [u8; N] { + [value; N] + } + ), + ) + }, + ); +} diff --git a/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs b/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs index cf29ce60d..f6d7cb441 100644 --- a/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs +++ b/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs @@ -3,7 +3,8 @@ //! //! 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. +//! appears in an input type, or is bound through one (`first_item`'s `T`), 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. @@ -541,6 +542,13 @@ snapshot_cgp_computer! { } } +// `T` appears in no input type, but the bound `I: IntoIterator` binds it +// through the input `I`, so the macro accepts it and the impl compiles. +#[cgp_computer] +fn first_item, T>(items: I) -> Option { + items.into_iter().next() +} + pub struct App; #[test] @@ -550,4 +558,5 @@ fn test_computer_generics() { 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); + assert_eq!(FirstItem::compute(&App, code, vec![4, 5]), Some(4)); } From 50c69c7bb2aed2d372b2a9ed255c7c29d931b743 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 26 Oct 2026 18:54:12 +0100 Subject: [PATCH 13/25] Pin which const a provider's `Self::CONST` names A `#[cgp_impl]` block's `Self::LIMIT` names the block's own const even when a `#[uses]` trait gives the context a const of the same name, a nested item's `Self::LIMIT` names that item's own, and a `Self::CEILING` naming a const only the context has reads the context's, in code and in a macro body. No test showed the first or the last, so disabling the name checks of the qualifying visitor passed the suite. Co-Authored-By: Claude Opus 5.5 --- .../cgp-tests/tests/basic_delegation/mod.rs | 1 + .../self_local_assoc_const_scope.rs | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_const_scope.rs diff --git a/crates/tests/cgp-tests/tests/basic_delegation/mod.rs b/crates/tests/cgp-tests/tests/basic_delegation/mod.rs index 98124d729..b2abb74f2 100644 --- a/crates/tests/cgp-tests/tests/basic_delegation/mod.rs +++ b/crates/tests/cgp-tests/tests/basic_delegation/mod.rs @@ -21,6 +21,7 @@ pub mod owned_receiver; pub mod self_in_macro; pub mod self_in_nested_item; pub mod self_local_assoc_const; +pub mod self_local_assoc_const_scope; pub mod self_local_assoc_type; // `delegate_components!` shape variants (compile-time checks only). diff --git a/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_const_scope.rs b/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_const_scope.rs new file mode 100644 index 000000000..06e77b104 --- /dev/null +++ b/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_const_scope.rs @@ -0,0 +1,83 @@ +//! `Self::LIMIT` in a `#[cgp_impl]` block names the nearest `LIMIT`: a nested item's own, then the +//! block's own, before a const of the same name that a `#[uses]` trait gives the context. +//! +//! `Self` in the block means the context, but a `Self::LIMIT` whose `LIMIT` the block declares +//! names the provider's own const, whatever the context's traits declare. Here the context also +//! has `HasLimit::LIMIT`, imported through `#[uses]`, so `allow` reads the block's 10 and +//! `context_allows` reads the context's 100 through the qualified ``. A local +//! `Floor` inside `allow` has its own `Self` and its own `LIMIT`, which the rewrite leaves alone, +//! and `Floor::LIMIT` names `Floor`'s const although the block declares one of the same name. A +//! `Self::CEILING` naming a const only the context has is rewritten to the context, in code and +//! in a macro body alike. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_impl.md (Behavior and corner cases) +//! and cgp-knowledge-base/cgp/reference/macros/cgp_impl.md. + +use cgp::prelude::*; + +pub trait HasLimit { + const LIMIT: u32; +} + +pub trait HasCeiling { + const CEILING: u32; +} + +#[cgp_component(RateLimiter)] +pub trait CanRateLimit { + const LIMIT: u32; + + fn allow(&self, count: u32) -> bool; + + fn context_allows(&self, count: u32) -> bool; +} + +#[cgp_impl(new AllowUnderLimit)] +#[uses(HasLimit, HasCeiling)] +impl RateLimiter { + const LIMIT: u32 = 10; + + fn allow(&self, count: u32) -> bool { + struct Floor; + + impl Floor { + const LIMIT: u32 = 1; + + fn limit() -> u32 { + Self::LIMIT + } + } + + count >= Floor::LIMIT && count >= Floor::limit() && count < Self::LIMIT + } + + fn context_allows(&self, count: u32) -> bool { + count < ::LIMIT + && count < Self::CEILING + && matches!(Self::CEILING, ceiling if count < ceiling) + } +} + +pub struct App; + +impl HasLimit for App { + const LIMIT: u32 = 100; +} + +impl HasCeiling for App { + const CEILING: u32 = 1000; +} + +delegate_components! { + App { + RateLimiterComponent: AllowUnderLimit, + } +} + +#[test] +fn test_self_local_assoc_const_scope() { + assert!(!App.allow(0)); + assert!(App.allow(5)); + assert!(!App.allow(50)); + assert!(App.context_allows(50)); +} From 876e7a5a628f26ba4091d627d1bb9fd107f4c680 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 26 Oct 2026 18:57:14 +0100 Subject: [PATCH 14/25] Keep only documentation on the data derives' companions The companion filter also kept `#[cfg]` so a companion would have the same members as the input, but the compiler evaluates `#[cfg]` and `#[cfg_attr]` before a derive sees its input, so no `#[cfg]` ever reached it. The filter now keeps documentation alone, and its comment and the tests' headers say why. Co-Authored-By: Claude Opus 5.5 --- .../src/types/cgp_data/companion_attrs.rs | 10 +++++----- .../cgp-macro-tests/tests/companion_attrs_tests.rs | 6 +++--- .../extensible_variants/variant_helper_attribute.rs | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/companion_attrs.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/companion_attrs.rs index 84f9e8fef..78d6ea9ab 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/companion_attrs.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/companion_attrs.rs @@ -1,9 +1,9 @@ use syn::Attribute; -/// Keep only the attributes a generated companion type can carry from a field or variant: `#[cfg]`, -/// so the companion has the same members as the input, and documentation. Every other attribute, -/// such as another derive's helper (`#[default]`, `#[serde(…)]`), belongs to a derive the companion -/// does not have and would fail to resolve. +/// Keep only the documentation of a field or variant on a generated companion type. Every other +/// attribute, such as another derive's helper (`#[default]`, `#[serde(…)]`), belongs to a derive +/// the companion does not have and would fail to resolve. A `#[cfg]` or `#[cfg_attr]` never +/// reaches here: the compiler evaluates both before a derive sees its input. pub fn retain_companion_attrs(attrs: &mut Vec) { - attrs.retain(|attr| attr.path().is_ident("cfg") || attr.path().is_ident("doc")); + attrs.retain(|attr| attr.path().is_ident("doc")); } diff --git a/crates/tests/cgp-macro-tests/tests/companion_attrs_tests.rs b/crates/tests/cgp-macro-tests/tests/companion_attrs_tests.rs index fd51792ae..32663333a 100644 --- a/crates/tests/cgp-macro-tests/tests/companion_attrs_tests.rs +++ b/crates/tests/cgp-macro-tests/tests/companion_attrs_tests.rs @@ -4,9 +4,9 @@ //! The builder's `__Partial{Name}` struct and the extractor's `__Partial{Name}` and //! `__PartialRef{Name}` enums clone the input's fields or variants. A helper attribute of another //! derive on one of them (`#[default]`, `#[serde(…)]`) would fail to resolve on a companion that -//! lacks that derive, so the companions keep only `#[cfg]` and documentation. These call the derive -//! entrypoints directly, since a test crate cannot compile a helper attribute without the derive -//! that declares it. +//! lacks that derive, so the companions keep only documentation. These call the derive entrypoints +//! directly, since a test crate cannot compile a helper attribute without the derive that declares +//! it. #![allow(dead_code)] pub mod companion_attrs; diff --git a/crates/tests/cgp-tests/tests/extensible_variants/variant_helper_attribute.rs b/crates/tests/cgp-tests/tests/extensible_variants/variant_helper_attribute.rs index 94a473e88..1ac4600f3 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/variant_helper_attribute.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/variant_helper_attribute.rs @@ -1,9 +1,9 @@ //! A variant helper attribute of another derive, here `Default`'s `#[default]`, on an enum that also //! derives `CgpData`. //! -//! The extractor's companion enums copy the input's variants, but keep only their `#[cfg]` and -//! documentation attributes, so the `#[default]` stays on `Mode` alone and does not reach a -//! companion that lacks `#[derive(Default)]`. +//! The extractor's companion enums copy the input's variants, but keep only their documentation, +//! so the `#[default]` stays on `Mode` alone and does not reach a companion that lacks +//! `#[derive(Default)]`. //! //! See cgp-knowledge-base/cgp/implementation/entrypoints/derive_extract_field.md. From 048dc0de8d0682b07e055b86d00b8b021f7cccb5 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 26 Oct 2026 19:05:04 +0100 Subject: [PATCH 15/25] Pin every primitive name a `Path!` segment keeps as a type The segment rule is a list of the seventeen primitive names, and the test asserted three of them; dropping any of eleven others from the list passed the suite. Each name is now asserted on its own. Co-Authored-By: Claude Opus 5.5 --- .../cgp-tests/tests/namespaces/path_macro.rs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/tests/cgp-tests/tests/namespaces/path_macro.rs b/crates/tests/cgp-tests/tests/namespaces/path_macro.rs index 2ea76b887..63c426f84 100644 --- a/crates/tests/cgp-tests/tests/namespaces/path_macro.rs +++ b/crates/tests/cgp-tests/tests/namespaces/path_macro.rs @@ -2,8 +2,8 @@ //! entry or `#[prefix]` attribute. Each `assert_same_type` call compiles only if //! the two types are identical, pinning the segment classification: a lowercase //! segment becomes a `Symbol!`, a capitalized segment stays a type, a primitive -//! name such as `u32` stays the primitive while a near miss such as `f` or `u2` -//! becomes a symbol, and the chain ends in `Nil`. +//! name such as `u32` stays the primitive (each of the seventeen is asserted) while a near miss +//! such as `f` or `u2` becomes a symbol, and the chain ends in `Nil`. //! //! See cgp-knowledge-base/cgp/reference/macros/path.md. @@ -13,7 +13,18 @@ use cgp::prelude::*; pub struct ErrorRaiserComponent; -fn assert_same_type(_: PhantomData, _: PhantomData) {} +fn assert_same_type(_: PhantomData, _: PhantomData) {} + +macro_rules! assert_primitive_segments { + ($($primitive:ident),*) => { + $( + assert_same_type( + PhantomData::, + PhantomData::>, + ); + )* + }; +} #[test] fn test_path_macro_segments() { @@ -48,4 +59,9 @@ fn test_path_macro_segments() { >, >, ); + + // Every primitive name stays the primitive type, `str` included. + assert_primitive_segments!( + u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, char, bool, str + ); } From 8e8e92168c54ebfe8c3bff58c97cd352fb26ceca Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 26 Oct 2026 19:15:33 +0100 Subject: [PATCH 16/25] Reject a namespace that inherits another instance of itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cgp_namespace!` rejected a parent naming the namespace only when its type arguments matched, so ` new Ns: Ns` and ` new Ns: Ns>` reached the compiler, which reports an overflow on stable, once without a location, and accepts both on the nightly. Every instance of a namespace has the same parent rule, so such a parent always asks the namespace again. The rejection now ignores the arguments, and points at the parent's path: the rebuilt `<…>` carried the macro's span and widened the caret to the whole invocation for any parent with arguments. Co-Authored-By: Claude Opus 5.5 --- .../cgp-macro-core/src/types/namespace/table.rs | 13 +++++++------ .../tests/parser_rejections/cgp_namespace.rs | 13 ++++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/types/namespace/table.rs b/crates/macros/cgp-macro-core/src/types/namespace/table.rs index 2a0ab1ceb..48d9db11c 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/table.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/table.rs @@ -1,4 +1,3 @@ -use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::token::Colon; use syn::{Error, Ident, ItemImpl, ItemStruct, ItemTrait, Type, braced}; @@ -59,20 +58,22 @@ impl Parse for NamespaceTable { } /// Reject a namespace that names itself as its parent, `new Ns: Ns`, a cycle visible in this one -/// invocation. A cycle across several namespaces is not visible here. +/// invocation. The parent's arguments do not matter: every instance of the namespace has the same +/// parent rule, so ` new Ns: Ns` asks `Ns` again for a key its table lacks, and +/// `Ns>` asks an ever larger instance. A cycle across several namespaces is not visible here. fn reject_self_inheritance( namespace: &IdentWithTypeArgs, parent: &PathWithTypeArgs, ) -> syn::Result<()> { let names_itself = parent.path.leading_colon.is_none() && parent.path.segments.len() == 1 - && parent.path.segments[0].ident == namespace.ident - && parent.type_args.to_token_stream().to_string() - == namespace.type_args.to_token_stream().to_string(); + && parent.path.segments[0].ident == namespace.ident; if names_itself { + // On the path alone: the parent's `<…>` is rebuilt from its arguments with the macro's + // span, which would widen the caret to the whole invocation. return Err(Error::new_spanned( - parent, + &parent.path, format!( "the namespace `{}` cannot inherit from itself; name another namespace as its parent, or omit the parent", namespace.ident diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs index a7cd97889..28184ebd6 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs @@ -4,7 +4,8 @@ //! discarding it. It rejects an attribute on a `:` mapping key, on a `=>` redirect //! key, and on a key inside a `for <..> in ..` loop. //! -//! A namespace naming itself as its parent (`new Ns: Ns`) is rejected. +//! A namespace naming itself as its parent (`new Ns: Ns`, or another instance of itself such as +//! `Ns`) is rejected. //! //! The header may stand alone: `new Ns` or `new Child: Parent` with no brace pair //! parses as an empty table. That relaxation applies only to an input that ends @@ -97,4 +98,14 @@ fn rejects_self_inheritance() { "the namespace `SelfNs` cannot inherit from itself; name another namespace as its parent, or omit the parent", || cgp_macro_lib::cgp_namespace(quote!( new SelfNs: SelfNs {})), ); + + // Another instance of the namespace is the same parent rule again, at a fixed or a growing + // type, so the arguments do not matter. + for parent in [quote!(SelfNs), quote!(SelfNs>)] { + assert_macro_rejects_with( + "cgp_namespace inheriting another instance of itself", + "the namespace `SelfNs` cannot inherit from itself; name another namespace as its parent, or omit the parent", + || cgp_macro_lib::cgp_namespace(quote!( new SelfNs: #parent {})), + ); + } } From 166db410df0470f3a8ec1302cd38fa49cf56ea88 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 26 Oct 2026 19:21:41 +0100 Subject: [PATCH 17/25] Forward a component method's `mut` parameter by its name A default method may declare `mut count`, which the macro rejected as a parameter without a plain name, though the forwarding impls only need the name. They now forward `count` and leave the `mut` off their own signatures, where it would be unused; the default body, which an empty provider inherits, keeps it. A `ref` binding and a binding with a subpattern stay rejected, now with tests, as do `_` and destructuring patterns. Co-Authored-By: Claude Opus 5.5 --- .../functions/delegated_impls/signature.rs | 22 +++++--- .../tests/parser_rejections/cgp_component.rs | 25 +++++++++ .../default_method_mut_parameter.rs | 54 +++++++++++++++++++ .../cgp-tests/tests/basic_delegation/mod.rs | 1 + 4 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 crates/tests/cgp-tests/tests/basic_delegation/default_method_mut_parameter.rs diff --git a/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs b/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs index 4e314f5a3..5d56541e1 100644 --- a/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs +++ b/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs @@ -28,11 +28,22 @@ pub fn signature_to_delegated_impl_item_fn( ) #await_expr }); + // A `mut` on a parameter belongs to the default body that mutates it; the forwarding body + // only passes the argument on, where the `mut` would be unused. + let mut sig = signature.clone(); + for input in sig.inputs.iter_mut() { + if let FnArg::Typed(arg) = input + && let Pat::Ident(pat) = arg.pat.as_mut() + { + pat.mutability = None; + } + } + let item = ImplItemFn { attrs: Vec::new(), vis: Visibility::Inherited, defaultness: None, - sig: signature.clone(), + sig, block: body, }; @@ -43,15 +54,14 @@ fn signature_to_idents(sig: &Signature) -> syn::Result> sig.inputs.iter().map(arg_to_ident).collect() } -/// The name an argument is forwarded by. A parameter that is not a plain name (`_`, `mut value`, -/// a destructuring pattern) has none, so it is rejected with a message saying what to write. +/// The name an argument is forwarded by, a `mut value` parameter's included. A parameter without a +/// name of its own (`_`, `ref value`, a destructuring pattern) has none, so it is rejected with a +/// message saying what to write. fn arg_to_ident(arg: &FnArg) -> syn::Result { match arg { FnArg::Receiver(_) => Ok(Ident::new("self", Span::call_site())), FnArg::Typed(arg) => match arg.pat.as_ref() { - Pat::Ident(pat) - if pat.by_ref.is_none() && pat.mutability.is_none() && pat.subpat.is_none() => - { + Pat::Ident(pat) if pat.by_ref.is_none() && pat.subpat.is_none() => { Ok(pat.ident.clone()) } pat => Err(Error::new_spanned( diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs index c0d29626c..cf9b6cee2 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_component.rs @@ -166,6 +166,31 @@ fn rejects_unnamed_parameter() { ); } +/// A `ref` binding and a binding with a subpattern are no plain name either: the +/// forwarding impls would pass a reference, or leave the subpattern's bindings +/// unused. +#[test] +fn rejects_ref_and_subpattern_parameters() { + for parameter in [quote!(ref count: u32), quote!(pair @ (a, b): (u32, u32))] { + assert_macro_rejects_with( + "cgp_component with a `ref` or `@` parameter", + UNNAMED_PARAMETER, + || { + cgp_macro_lib::cgp_component( + quote!(Counter), + quote!( + pub trait CanCount { + fn count(&self, #parameter) -> u32 { + 0 + } + } + ), + ) + }, + ); + } +} + /// A destructuring pattern in a default method has no single name to forward /// either. #[test] diff --git a/crates/tests/cgp-tests/tests/basic_delegation/default_method_mut_parameter.rs b/crates/tests/cgp-tests/tests/basic_delegation/default_method_mut_parameter.rs new file mode 100644 index 000000000..ced8febc5 --- /dev/null +++ b/crates/tests/cgp-tests/tests/basic_delegation/default_method_mut_parameter.rs @@ -0,0 +1,54 @@ +//! A `#[cgp_component]` method with a default body may take a `mut` parameter. +//! +//! The generated impls forward each argument by name, so a parameter written `mut count` is +//! forwarded as `count`, and the forwarding impls leave the `mut` off their own signatures, where +//! nothing mutates it. The default body keeps its `mut`: an empty provider inherits the body, and a +//! provider that writes the method may declare the parameter either way. +//! +//! See cgp-knowledge-base/cgp/implementation/functions/derive/delegated_impls.md (Rejected +//! parameters) and cgp-knowledge-base/cgp/reference/macros/cgp_component.md. + +use cgp::prelude::*; + +#[cgp_component(Counter)] +pub trait CanCount { + fn count_up(&self, mut count: u32, steps: u32) -> u32 { + for _ in 0..steps { + count += 1; + } + count + } +} + +#[cgp_impl(new CountByDefault)] +impl Counter {} + +#[cgp_impl(new CountTwice)] +impl Counter { + fn count_up(&self, mut count: u32, steps: u32) -> u32 { + count += 2 * steps; + count + } +} + +pub struct App; + +delegate_components! { + App { + CounterComponent: CountByDefault, + } +} + +pub struct FastApp; + +delegate_components! { + FastApp { + CounterComponent: CountTwice, + } +} + +#[test] +fn test_default_method_mut_parameter() { + assert_eq!(App.count_up(2, 3), 5); + assert_eq!(FastApp.count_up(2, 3), 8); +} diff --git a/crates/tests/cgp-tests/tests/basic_delegation/mod.rs b/crates/tests/cgp-tests/tests/basic_delegation/mod.rs index b2abb74f2..619ee48eb 100644 --- a/crates/tests/cgp-tests/tests/basic_delegation/mod.rs +++ b/crates/tests/cgp-tests/tests/basic_delegation/mod.rs @@ -15,6 +15,7 @@ pub mod provider_macro; // Behavioral wiring: a consumer trait becomes usable once its component is wired. pub mod consumer_delegate_generic; pub mod consumer_delegate_getter; +pub mod default_method_mut_parameter; pub mod default_methods; pub mod impl_self; pub mod owned_receiver; From 291135c7956121c0d4c59ff2f1d97058e73da5c6 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 26 Oct 2026 19:24:41 +0100 Subject: [PATCH 18/25] Accept a `#[cgp_computer]` parameter named inside a type macro The output-only check searched the argument types for the parameter with `syn`'s visitor, which does not look inside a type macro, so `fn first_of_pair(pair: Product![T, T]) -> T` was rejected as using `T` only in its return type, though it compiled before the check existed. The search now reads a macro's tokens too, counting any token of the parameter's name and leaving the doubt to the compiler. Co-Authored-By: Claude Opus 5.5 --- .../src/functions/output_only_params.rs | 20 ++++++++++++++++++- .../tests/handlers/computer_macro_generics.rs | 14 +++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/macros/cgp-macro-extra-core/src/functions/output_only_params.rs b/crates/macros/cgp-macro-extra-core/src/functions/output_only_params.rs index 39806d498..04ad4b68b 100644 --- a/crates/macros/cgp-macro-extra-core/src/functions/output_only_params.rs +++ b/crates/macros/cgp-macro-extra-core/src/functions/output_only_params.rs @@ -1,5 +1,6 @@ +use proc_macro2::{TokenStream, TokenTree}; use syn::visit::{self, Visit}; -use syn::{Error, FnArg, GenericParam, Ident, Signature, WherePredicate}; +use syn::{Error, FnArg, GenericParam, Ident, Macro, Signature, WherePredicate}; /// Reject a type or const parameter that no argument type names and no other bound names. /// @@ -101,4 +102,21 @@ impl<'ast> Visit<'ast> for FindIdent<'_> { visit::visit_ident(self, ident); } + + /// A type macro such as `Product![T, u32]` is only tokens to `syn`, so its tokens are searched + /// for the name; a token of that name counts as naming the parameter, which leaves any doubt + /// to the compiler. + fn visit_macro(&mut self, mac: &'ast Macro) { + if tokens_name(&mac.tokens, self.target) { + self.found = true; + } + } +} + +fn tokens_name(tokens: &TokenStream, target: &Ident) -> bool { + tokens.clone().into_iter().any(|tree| match tree { + TokenTree::Ident(ident) => ident == *target, + TokenTree::Group(group) => tokens_name(&group.stream(), target), + _ => false, + }) } diff --git a/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs b/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs index f6d7cb441..cb4a02e5b 100644 --- a/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs +++ b/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs @@ -3,8 +3,9 @@ //! //! 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, or is bound through one (`first_item`'s `T`), which -//! is what keeps it constrained by the impl. +//! appears in an input type, inside a type macro there (`first_of_pair`'s `T`), or +//! is bound through one (`first_item`'s `T`), 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. @@ -549,6 +550,14 @@ fn first_item, T>(items: I) -> Option { items.into_iter().next() } +// `T` appears in the input only inside a type macro, `Product![T, T]`, whose tokens the macro +// reads as well, so it accepts it. +#[cgp_computer] +fn first_of_pair(pair: Product![T, T]) -> T { + let Cons(first, _) = pair; + first +} + pub struct App; #[test] @@ -559,4 +568,5 @@ fn test_computer_generics() { assert_eq!(FirstChar::compute(&App, code, "abc"), "a"); assert_eq!(SumArray::compute(&App, code, [1, 2, 3]), 6); assert_eq!(FirstItem::compute(&App, code, vec![4, 5]), Some(4)); + assert_eq!(FirstOfPair::compute(&App, code, product![6, 7]), 6); } From 8ed2287f9dfc609a25a2dc059debbb65620a6f29 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 26 Oct 2026 19:25:58 +0100 Subject: [PATCH 19/25] Pin the `where` and tuple cases of the computer output-only check Disabling the `where`-predicate search, its skip of a predicate on the parameter itself, or the search inside a type macro's groups passed the suite. The tests now reject an output-only parameter bounded in a `where` clause, accept one bound through another parameter's `where` predicate, and accept one named only inside a tuple in a `Product!`. Co-Authored-By: Claude Opus 5.5 --- .../tests/parser_rejections/cgp_computer.rs | 37 ++++++++++++------- .../tests/handlers/computer_macro_generics.rs | 18 +++++++-- 2 files changed, 37 insertions(+), 18 deletions(-) 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 516e49ab0..11065fcb0 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 @@ -113,20 +113,29 @@ fn rejects_impl_trait_return() { /// macro rejects it on the parameter rather than emitting an `E0207`. #[test] fn rejects_output_only_type_parameter() { - assert_macro_rejects_with( - "cgp_computer with a type parameter only the return type uses", - "the parameter `T` appears in no argument type, so the generated `Computer` impl cannot determine it; take an argument whose type names `T`, or write the provider by hand as a `Computer` impl on a provider generic over `T`", - || { - cgp_macro_extra_lib::cgp_computer( - quote!(), - quote!( - fn parse(value: String) -> Option { - value.parse().ok() - } - ), - ) - }, - ); + // A bound on the parameter itself, inline or in a `where` clause, names it but does not + // determine it. + for function in [ + quote!( + fn parse(value: String) -> Option { + value.parse().ok() + } + ), + quote!( + fn parse(value: String) -> Option + where + T: core::str::FromStr, + { + value.parse().ok() + } + ), + ] { + assert_macro_rejects_with( + "cgp_computer with a type parameter only the return type uses", + "the parameter `T` appears in no argument type, so the generated `Computer` impl cannot determine it; take an argument whose type names `T`, or write the provider by hand as a `Computer` impl on a provider generic over `T`", + || cgp_macro_extra_lib::cgp_computer(quote!(), function.clone()), + ); + } } /// A const parameter only the return type uses is just as undetermined. diff --git a/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs b/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs index cb4a02e5b..95d0f5b98 100644 --- a/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs +++ b/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs @@ -550,11 +550,20 @@ fn first_item, T>(items: I) -> Option { items.into_iter().next() } -// `T` appears in the input only inside a type macro, `Product![T, T]`, whose tokens the macro +// The same binding written as a `where` predicate. +#[cgp_computer] +fn last_item(items: I) -> Option +where + I: IntoIterator, +{ + items.into_iter().last() +} + +// `T` appears in the input only inside a type macro, in a tuple there, whose tokens the macro // reads as well, so it accepts it. #[cgp_computer] -fn first_of_pair(pair: Product![T, T]) -> T { - let Cons(first, _) = pair; +fn first_of_pair(pair: Product![(T, T)]) -> T { + let Cons((first, _), _) = pair; first } @@ -568,5 +577,6 @@ fn test_computer_generics() { assert_eq!(FirstChar::compute(&App, code, "abc"), "a"); assert_eq!(SumArray::compute(&App, code, [1, 2, 3]), 6); assert_eq!(FirstItem::compute(&App, code, vec![4, 5]), Some(4)); - assert_eq!(FirstOfPair::compute(&App, code, product![6, 7]), 6); + assert_eq!(LastItem::compute(&App, code, vec![4, 5]), Some(5)); + assert_eq!(FirstOfPair::compute(&App, code, product![(6, 7)]), 6); } From 9bcdc7d0f324bf17728c559b010f7f0038704634 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 1 Nov 2026 20:27:44 +0100 Subject: [PATCH 20/25] State what cargo-cgp offers and where its fixtures live The README's install command lacked `--locked` and claimed any toolchain builds the front-end, which needs Rust 1.96, and it described the tool as a lone `check` in an early release. The agent guides called it a way to build CGP code, filed fixtures by quality rather than by class, and narrated how the compile-fail suite moved. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 18 ++++++++---------- README.md | 8 ++++---- crates/tests/AGENTS.md | 37 +++++++++++++++++-------------------- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 57d5162cf..b8658a5de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,15 +32,14 @@ versus linking to it. Three of them matter constantly. There is no `docs/` directory here. The construct reference, the concepts, the guides, the error catalog, and the macro-implementation notes are all in that repository's [`cgp/`](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp) directory, and the worked -examples sit at its top level. It is a *consolidated* base — `cargo-cgp`'s documentation is the -neighbouring section — which is why an error class here and the tool's handling of it are now one -directory apart. Treat the knowledge base as part of this repository's surface: **a change to a +examples sit at its top level. It is a *consolidated* base, with `cargo-cgp`'s documentation as the +neighbouring section, so an error class here and the tool's handling of it are one directory apart. Treat the knowledge base as part of this repository's surface: **a change to a construct's behavior carries its documentation change**, and when the checkout is missing, say plainly what needs updating there. **[`cargo-cgp`](https://github.com/contextgeneric/cargo-cgp) is CGP's first-class toolchain**: a cargo subcommand that rewrites the compiler's diagnostics into readable, root-cause-first CGP errors. It is -the recommended way to build and check CGP code and to read a CGP compile error, so recommend it +the recommended way to check CGP code and to read a CGP compile error, so recommend it wherever CGP errors are discussed, and prefer it over raw `cargo check` when diagnosing a wiring failure here. When a construct change alters a diagnostic it has a UI fixture for, update the fixture in the same change, following that project's own conventions. @@ -65,10 +64,9 @@ a doc pointer in a source comment, which names its knowledge-base path (`cgp-knowledge-base/cgp/implementation/entrypoints/cgp_impl.md`). **The code dependency stays one-way.** No `cgp` *crate* may depend on a `cargo-cgp` crate: -`cargo-cgp` reads `cgp`, never the reverse. Documentation is the deliberate exception — the knowledge -base and the skill both reference `cargo-cgp` as the recommended toolchain — so it is the *code* graph -that stays acyclic, not the documentation. The post-codegen compile-fail cases this repository used to -keep in `cgp-compile-fail-tests` now live as `cargo-cgp` UI fixtures, and the +`cargo-cgp` reads `cgp`, never the reverse. Documentation is the deliberate exception, since the knowledge +base and the skill both recommend `cargo-cgp`, so the *code* graph stays acyclic and the documentation +need not. Post-codegen compile-fail cases are `cargo-cgp` UI fixtures, and the [error catalog](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/errors/README.md) links each error class to the fixture that backs it. @@ -120,7 +118,7 @@ do). - **Single test crate / test:** `cargo nextest run -p cgp-tests` or target one file with the standard test harness, e.g. `cargo test -p cgp-tests --test component` - **Post-codegen compile failures** (input a macro accepts but whose expansion fails to compile) are - no longer tested here: they are UI fixtures in [`cargo-cgp`](https://github.com/contextgeneric/cargo-cgp), + tested elsewhere: they are UI fixtures in [`cargo-cgp`](https://github.com/contextgeneric/cargo-cgp), the first-class error toolchain, and cataloged in the [error catalog](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/errors/README.md). See [crates/tests/AGENTS.md](crates/tests/AGENTS.md) for the workflow. - Many "tests" are **compile-time wiring checks** (`check_components!` / @@ -275,7 +273,7 @@ Walk the shapes the expansion can take across the whole input space and confirm Rust — no conflicting `impl` blocks from a cartesian expansion, no unbound or doubly-declared generic, no empty expansion that checks nothing, no clash on a generated identifier. When an expansion fails to compile, capture it as a [`cargo-cgp` UI fixture](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/README.md) -(post-codegen compile-fail cases live there now, filed by the quality of the output the tool produces) +(post-codegen compile-fail cases live there, filed by their error class) and catalog the class in the [error catalog](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/errors/README.md); when the failure is a CGP *defect* the macro should have rejected, also record it under the macro's `## Known issues`, per [crates/tests/AGENTS.md](crates/tests/AGENTS.md) and [cgp-knowledge-base/cgp/implementation/AGENTS.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/AGENTS.md). diff --git a/README.md b/README.md index 97deef9b6..1e5efed99 100644 --- a/README.md +++ b/README.md @@ -110,13 +110,13 @@ CGP requires **Rust 1.89+** and runs on the **stable** toolchain — no nightly, CGP compiles to ordinary Rust, so a wiring mistake surfaces as a compiler error about generated types — often a wall of them with the real cause buried. **When you start with CGP, install [`cargo-cgp`](https://github.com/contextgeneric/cargo-cgp) alongside it.** cargo-cgp is CGP's error toolchain: a drop-in for `cargo check` that rewrites those errors into a compact, root-cause-first form, leading with the field or wiring that actually failed. It is the single biggest quality-of-life improvement for writing CGP, so treat it as part of your getting-started setup. ```bash -cargo install cargo-cgp # the front-end (installs on any toolchain) -cargo cgp setup # provisions the compiler + driver it needs +cargo install --locked cargo-cgp # the front-end (needs Rust 1.96 or newer) +cargo cgp setup # provisions the compiler + driver it needs ``` -Then run `cargo cgp check` wherever you would run `cargo check`; it forces the pinned nightly that `cargo cgp setup` installed only for its own check, so your project keeps its toolchain untouched. (Prefer Nix? `nix run github:contextgeneric/cargo-cgp/v0.1.0 -- check` runs it without installing.) cargo-cgp is an early release that already makes the common wiring errors readable and is steadily covering more; see its [README](https://github.com/contextgeneric/cargo-cgp) for the full guide. +Then run `cargo cgp check` wherever you would run `cargo check`; it forces the pinned nightly that `cargo cgp setup` installed only for its own check, so your project keeps its toolchain untouched. (Prefer Nix? `nix run github:contextgeneric/cargo-cgp/v0.1.0 -- check` runs it without installing.) See its [README](https://github.com/contextgeneric/cargo-cgp) for the full guide. -cargo-cgp is optional and adds only a diagnostic `check`: keep building, running, and testing with ordinary cargo — CGP is a plain library needing only stable Rust (1.89+) — and reach for `cargo cgp check` when you want a wiring error made readable. Plain `cargo check` still works too, and is fine when you are not expecting a compile error. +cargo-cgp is optional and adds only the reading commands `check` and `expand`: keep building, running, and testing with ordinary cargo, since CGP is a plain library needing only stable Rust (1.89+), and reach for `cargo cgp check` when you want a wiring error made readable. Plain `cargo check` still works too, and is fine when you are not expecting a compile error. ## When to use CGP — and when not diff --git a/crates/tests/AGENTS.md b/crates/tests/AGENTS.md index b50e5c0bc..e78e1a3eb 100644 --- a/crates/tests/AGENTS.md +++ b/crates/tests/AGENTS.md @@ -22,12 +22,11 @@ The test suite has two jobs, split across crates: invalid tokens** a macro emits (`invalid_expansion` string snapshots). **Post-codegen compile failures are tested in `cargo-cgp`, not here.** The cases where a -macro *accepts* input but its *expansion* then fails to compile — and the cross-crate -coherence and orphan-rule fixtures that once lived in `cgp-test-crate-a`/`-b` — were -migrated to `cargo-cgp`'s UI test suite, which pins the readable errors the tool renders -for each class. `cargo-cgp` is CGP's first-class error toolchain, so those diagnostics -belong where the tool that improves them lives; see "Adding a failure case" for the new -workflow. +macro *accepts* input but its *expansion* then fails to compile, and the cross-crate +coherence and orphan-rule fixtures, are in `cargo-cgp`'s UI test suite, which pins the +readable errors the tool renders for each class. `cargo-cgp` is CGP's first-class error +toolchain, so those diagnostics belong where the tool that improves them lives; see +"Adding a failure case" for the workflow. ## Organize by concept, not by construct @@ -134,20 +133,20 @@ which is enough to pin a rejection and gives a precise check of the macro's own diagnostic. This is the right tool for a structural error the macro is expected to catch, and such a case does **not** also need a compile-fail test. -**Input a macro accepts whose expansion fails to compile — a UI fixture in +**Input a macro accepts whose expansion fails to compile: a UI fixture in `cargo-cgp`.** Reserve these for input a CGP macro *accepts* but whose *expansion* then -fails to type- or borrow-check — the failure lands on the emitted Rust, not inside the -macro. These **no longer live in this repository**: they are UI fixtures in `cargo-cgp`, +fails to type- or borrow-check, so the failure lands on the emitted Rust, not inside the +macro. These **live in another repository**: they are UI fixtures in `cargo-cgp`, the tool that rewrites such errors into readable form, so the pinned snapshot is what a -user actually sees rather than the raw cascade. Add the fixture there — a standalone +user actually sees rather than the raw cascade. Add the fixture there, a standalone `.rs` program with `fn main`, plus a `//@aux-build: ` directive when the case is -cross-crate — following +cross-crate, following [cargo-cgp's UI-test guide](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/README.md); its harness records both `cargo-cgp`'s output (`.cgp.stderr`) and the raw compiler baseline (`.rust.stderr`). cargo-cgp files a fixture under its **error class**, mirroring the knowledge base's error catalog, and records the quality of its output in a -`//@quality:` header (`acceptable` when the tool already leads with the root cause, -`usability` when the cause is present but buried). Then catalog the class and link the +`//@quality:` header (`acceptable` when the tool leads with the root cause, `usability` +when the cause is present but buried, `hidden` when it is absent). Then catalog the class and link the fixture (below). **Pinning the exact invalid output** is a separate, rarer need that *does* stay here: @@ -163,7 +162,7 @@ reader-facing documentation for these errors (see [cgp-knowledge-base/cgp/errors/AGENTS.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/errors/AGENTS.md)): the class doc describes the raw diagnostic *and* how cargo-cgp presents it, and links the backing `cargo-cgp` UI fixture as a GitHub URL. When a construct change alters such a diagnostic, the cross-project -[sync rule](../../AGENTS.md) applies — update the `cargo-cgp` fixture and the class doc +[sync rule](../../AGENTS.md) applies: update the `cargo-cgp` fixture and the class doc here together when both repos are checked out. A macro's **implementation document** still records the *rejection* cases it catches and its behavioral tests (its `## Known issues` and `## Tests` sections); the accept-then-fail classes are documented in the @@ -201,10 +200,8 @@ macro's accepted input or its *expansion*, also check the post-codegen behavior `cargo-cgp`'s UI suite (its fixtures compile CGP code through the tool), per the cross-project [sync rule](../../AGENTS.md). -## Migration status +## Growing the suite -The suite was reorganized from a by-construct layout to this by-concept layout. As -categories grow, keep splitting them per the rule above, and keep expanding rejection -coverage in `cgp-macro-tests`. Post-codegen compile-fail coverage and cross-crate -coverage now grow in `cargo-cgp`'s UI suite rather than here (the former -`cgp-compile-fail-tests` and `cgp-test-crate-*` packages were migrated there). +As categories grow, keep splitting them per the rule above, and keep expanding rejection coverage in +`cgp-macro-tests`. Post-codegen compile-fail coverage and cross-crate coverage grow in `cargo-cgp`'s UI +suite rather than here. From 1616e32acf733f7d6765e0235cf3822d5b2519ca Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 1 Nov 2026 20:43:27 +0100 Subject: [PATCH 21/25] State the sibling list plainly and without its move Co-Authored-By: Claude Opus 5.5 --- sibling-projects.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sibling-projects.md b/sibling-projects.md index c96bb6239..022de2b54 100644 --- a/sibling-projects.md +++ b/sibling-projects.md @@ -2,25 +2,25 @@ The Context-Generic Programming ecosystem is split across several repositories that are developed together, and this file records where each one lives and which revision of it to read. An agent -working here routinely needs one of them — to verify a claim against another project's source, to +working here routinely needs one of them, to verify a claim against another project's source, to revise a document a change here affects, or to keep a fixture and the prose that describes it in -step — so treat this table as the authoritative list of what exists alongside this repository. +step, so treat this table as the authoritative list of what exists alongside this repository. This repository's own documentation lives in the knowledge base, under its `cgp/` directory: the -construct reference, the concepts, the guides, the error catalog, and the macro-implementation notes -all moved there, and the worked examples sit at the knowledge base's top level. +construct reference, the concepts, the guides, the error catalog, and the macro-implementation notes, +and the worked examples sit at the knowledge base's top level. | Project | Repository | Branch/tag to read | What it is | |---|---|---|---| | `cargo-cgp` | | `main` | CGP's first-class toolchain: the cargo subcommand that makes CGP compile errors readable and expands CGP macros. | -| `cgp-skills` | | `main` | The agent skills for CGP, deployed on their own — the `/cgp` skill among them. | +| `cgp-skills` | | `main` | The agent skills for CGP, deployed on their own, the `/cgp` skill among them. | | `cgp-knowledge-base` | | `main` | The consolidated documentation for every CGP project, written by and for AI agents. | ## Finding a sibling Look for a sibling in the parent directory first, at `../`. Every project in this list, including this one, is expected to sit side by side under one parent directory in a local -environment, so `../cargo-cgp` is the fastest and most current reference — it reflects uncommitted work +environment, so `../cargo-cgp` is the fastest and most current reference, reflecting uncommitted work that GitHub does not. When the checkout is absent, fetch the file you need from the repository above instead, at the revision the table records. @@ -40,4 +40,4 @@ The branch or tag recorded above changes **only on explicit instruction**, such cuts an official release and the ecosystem should pin it. During ordinary development every entry stays `main`, and it deliberately does *not* track a feature branch in progress: work in flight is read from the local sibling checkout, not from a revision recorded here. The same rule governs the -`main` in a cross-project link — update it only when told to, as part of a release. +`main` in a cross-project link: update it only when told to, as part of a release. From 9f750c882f4508aee57fd5a0f9d06a7fe0279c75 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 6 Nov 2026 08:38:28 +0100 Subject: [PATCH 22/25] Reject a namespace that inherits itself through a self:: path `cgp_namespace!` compared the parent with the namespace only for a bare name, so `new SelfNs: self::SelfNs` reached the compiler as an `E0207` on the generated `__Value__`. A `self::` path names the namespace in the module the macro expands in, so it is now rejected like the bare form. Any other qualified parent, such as `crate::Ns`, names an item the macro cannot resolve and stays unchecked, which the function's doc states. Co-Authored-By: Claude Opus 5.5 --- .../src/types/namespace/table.rs | 20 +++++++++++++------ .../tests/parser_rejections/cgp_namespace.rs | 8 ++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/types/namespace/table.rs b/crates/macros/cgp-macro-core/src/types/namespace/table.rs index 48d9db11c..db22b3a76 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/table.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/table.rs @@ -57,17 +57,25 @@ impl Parse for NamespaceTable { } } -/// Reject a namespace that names itself as its parent, `new Ns: Ns`, a cycle visible in this one -/// invocation. The parent's arguments do not matter: every instance of the namespace has the same -/// parent rule, so ` new Ns: Ns` asks `Ns` again for a key its table lacks, and -/// `Ns>` asks an ever larger instance. A cycle across several namespaces is not visible here. +/// Reject a namespace that names itself as its parent, `new Ns: Ns` or `new Ns: self::Ns`, a cycle +/// visible in this one invocation. The parent's arguments do not matter: every instance of the +/// namespace has the same parent rule, so ` new Ns: Ns` asks `Ns` again for a key +/// its table lacks, and `Ns>` asks an ever larger instance. Any other qualified parent, such +/// as `crate::Ns` or `super::Ns`, is not checked, since the macro cannot tell which item the path +/// resolves to; the compiler then rejects a cycle with an unconstrained `__Value__`. A cycle across +/// several namespaces is not visible here. fn reject_self_inheritance( namespace: &IdentWithTypeArgs, parent: &PathWithTypeArgs, ) -> syn::Result<()> { + let segments = &parent.path.segments; + let name = match segments.len() { + 1 => Some(&segments[0]), + 2 if segments[0].ident == "self" => Some(&segments[1]), + _ => None, + }; let names_itself = parent.path.leading_colon.is_none() - && parent.path.segments.len() == 1 - && parent.path.segments[0].ident == namespace.ident; + && name.is_some_and(|segment| segment.ident == namespace.ident); if names_itself { // On the path alone: the parent's `<…>` is rebuilt from its arguments with the macro's diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs index 28184ebd6..96ae6252a 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs @@ -99,6 +99,14 @@ fn rejects_self_inheritance() { || cgp_macro_lib::cgp_namespace(quote!( new SelfNs: SelfNs {})), ); + // A `self::` path names the namespace in the module the macro expands in, so it is the same + // cycle as the bare name. + assert_macro_rejects_with( + "cgp_namespace inheriting itself through a self:: path", + "the namespace `SelfNs` cannot inherit from itself; name another namespace as its parent, or omit the parent", + || cgp_macro_lib::cgp_namespace(quote!(new SelfNs: self::SelfNs {})), + ); + // Another instance of the namespace is the same parent rule again, at a fixed or a growing // type, so the arguments do not matter. for parent in [quote!(SelfNs), quote!(SelfNs>)] { From 6103a0644a9865b3671e37c10a719f77373912bc Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 6 Nov 2026 08:42:14 +0100 Subject: [PATCH 23/25] Name the written type in the unsized-optional rejection The rejection of an optional reference to a slice or a trait object worded its message from placeholders, so `Option<&[u8]>` read `Option<&[T]>` and `Option<&dyn Debug>` read `dyn Trait`. It now quotes the reference as written and names the referent inside each container, `Option>` or `Option>`, through a small renderer that tightens the token printer's spacing. Co-Authored-By: Claude Opus 5.5 --- .../src/functions/field/parse.rs | 38 ++++++++++++++++--- .../tests/parser_rejections/cgp_fn.rs | 2 +- .../tests/parser_rejections/getters.rs | 24 ++++++++---- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/functions/field/parse.rs b/crates/macros/cgp-macro-core/src/functions/field/parse.rs index 672fce1ec..498c9f4f1 100644 --- a/crates/macros/cgp-macro-core/src/functions/field/parse.rs +++ b/crates/macros/cgp-macro-core/src/functions/field/parse.rs @@ -87,16 +87,19 @@ pub fn parse_field_type( /// `Option<&T>` reads an `Option` field, and an `Option` holds only a sized value, so an optional /// reference to a slice or a trait object has no field to read. Rejected here, naming the sized -/// container to read instead, rather than lowered to a bound over an unsized `Option`. +/// container to read instead, rather than lowered to a bound over an unsized `Option`. The message +/// quotes the reference as written and names the referent inside each container, so +/// `Option<&[u8]>` points to an `Option>` field and `Option<&(dyn Debug + Send)>` to an +/// `Option>` field. fn reject_unsized_option_ref(type_path: &TypePath, type_ref: &TypeReference) -> syn::Result<()> { let mut referent = type_ref.elem.as_ref(); while let Type::Paren(paren) = referent { referent = paren.elem.as_ref(); } - let (written, kind, container) = match referent { - Type::Slice(_) => ("[T]", "a slice", "Vec"), - Type::TraitObject(_) => ("dyn Trait", "a trait object", "Box"), + let (kind, container) = match referent { + Type::Slice(slice) => ("a slice", format!("Vec<{}>", type_text(&slice.elem))), + Type::TraitObject(_) => ("a trait object", format!("Box<{}>", type_text(referent))), _ => return Ok(()), }; @@ -109,11 +112,36 @@ fn reject_unsized_option_ref(type_path: &TypePath, type_ref: &TypeReference) -> Err(Error::new_spanned( type_path, format!( - "`Option<{reference}{written}>` would read an `Option<{written}>` field, which cannot exist because {kind} is unsized; read an `Option<{container}>` field as `Option<{reference}{container}>` instead" + "`Option<{}>` would read an `Option<{}>` field, which cannot exist because {kind} is unsized; read an `Option<{container}>` field as `Option<{reference}{container}>` instead", + type_text(type_ref), + type_text(referent), ), )) } +/// A type as the reader writes it, from its tokens: the spaces the token printer puts around `::`, +/// `<`, `>`, `,`, brackets, parentheses, and after `&` removed, so `core :: fmt :: Debug` reads +/// `core::fmt::Debug` and `& mut [u8]` reads `&mut [u8]`. +fn type_text(ty: &impl ToTokens) -> String { + let mut text = ty.to_token_stream().to_string(); + for (spaced, tight) in [ + (" :: ", "::"), + (" < ", "<"), + ("< ", "<"), + (" <", "<"), + (" >", ">"), + (" ,", ","), + ("[ ", "["), + (" ]", "]"), + ("( ", "("), + (" )", ")"), + ("& ", "&"), + ] { + text = text.replace(spaced, tight); + } + text +} + /// A mutable field read borrows the whole context exclusively through /// `get_field_mut`, so a `&mut` reference in the field type — the outer `&mut T` or /// the inner reference of an `Option<&mut T>` — is only valid under a `&mut self` 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 46bfc3a6d..ffb3dc7d0 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 @@ -124,7 +124,7 @@ fn rejects_optional_slice_implicit() { // is rejected the same way: it would read an unsized `Option<[u8]>` field. assert_macro_rejects_with( "cgp_fn with an `Option<&[u8]>` implicit", - "`Option<&[T]>` would read an `Option<[T]>` field, which cannot exist because a slice is unsized; read an `Option>` field as `Option<&Vec>` instead", + "`Option<&[u8]>` would read an `Option<[u8]>` field, which cannot exist because a slice is unsized; read an `Option>` field as `Option<&Vec>` instead", || { cgp_macro_lib::cgp_fn( quote!(), diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs index 3af5b3b89..a203f7275 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs @@ -113,10 +113,11 @@ fn rejects_mutable_return_without_mut_self() { #[test] fn rejects_optional_slice_return() { // `Option<&T>` reads an `Option` field, which an unsized slice cannot fill, so - // `Option<&[u8]>` is rejected with the sized container to read instead. + // `Option<&[u8]>` is rejected with the sized container to read instead, both named with + // the element type the getter wrote. assert_macro_rejects_with( "cgp_auto_getter returning `Option<&[u8]>`", - "`Option<&[T]>` would read an `Option<[T]>` field, which cannot exist because a slice is unsized; read an `Option>` field as `Option<&Vec>` instead", + "`Option<&[u8]>` would read an `Option<[u8]>` field, which cannot exist because a slice is unsized; read an `Option>` field as `Option<&Vec>` instead", || { cgp_macro_lib::cgp_auto_getter( quote!(), @@ -136,7 +137,7 @@ fn rejects_optional_mutable_slice_return() { // naming the mutable read. assert_macro_rejects_with( "cgp_auto_getter returning `Option<&mut [u8]>`", - "`Option<&mut [T]>` would read an `Option<[T]>` field, which cannot exist because a slice is unsized; read an `Option>` field as `Option<&mut Vec>` instead", + "`Option<&mut [u8]>` would read an `Option<[u8]>` field, which cannot exist because a slice is unsized; read an `Option>` field as `Option<&mut Vec>` instead", || { cgp_macro_lib::cgp_auto_getter( quote!(), @@ -153,14 +154,21 @@ fn rejects_optional_mutable_slice_return() { #[test] fn rejects_optional_trait_object_return() { // A trait object is unsized like a slice, so `Option<&dyn Debug>` has no field to - // read either; the parenthesized form is the same type. - for return_type in [ - quote!(Option<&dyn core::fmt::Debug>), - quote!(Option<&(dyn core::fmt::Debug + Send)>), + // read either; the parenthesized form is the same type, which the message quotes as + // written and names inside the containers without its parentheses. + for (return_type, message) in [ + ( + quote!(Option<&dyn core::fmt::Debug>), + "`Option<&dyn core::fmt::Debug>` would read an `Option` field, which cannot exist because a trait object is unsized; read an `Option>` field as `Option<&Box>` instead", + ), + ( + quote!(Option<&(dyn core::fmt::Debug + Send)>), + "`Option<&(dyn core::fmt::Debug + Send)>` would read an `Option` field, which cannot exist because a trait object is unsized; read an `Option>` field as `Option<&Box>` instead", + ), ] { assert_macro_rejects_with( "cgp_auto_getter returning an optional trait-object reference", - "`Option<&dyn Trait>` would read an `Option` field, which cannot exist because a trait object is unsized; read an `Option>` field as `Option<&Box>` instead", + message, || { cgp_macro_lib::cgp_auto_getter( quote!(), From b6a69058d93eca0644c960c91525901d5123f0f2 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 6 Nov 2026 08:45:51 +0100 Subject: [PATCH 24/25] Drop the mut of a mut self receiver in the forwarding impls The forwarding impls left the `mut` off each typed parameter of a default method but kept it on a `mut self` receiver, where nothing mutates it, so a correct component compiled with rustc's `unused_mut` warning, whose help to remove the `mut` would break the default body. The receiver's `mut` is now dropped too, only for a by-value receiver, since a `&mut self` receiver carries `mutability` as part of its type. The test of default-method `mut` parameters covers the receiver, and a test comment's cleft becomes a direct statement. Co-Authored-By: Claude Opus 5.5 --- .../functions/delegated_impls/signature.rs | 20 +++++++--- .../default_method_mut_parameter.rs | 39 +++++++++++++++++-- .../tests/handlers/computer_macro_generics.rs | 3 +- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs b/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs index 5d56541e1..ffd5d9f00 100644 --- a/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs +++ b/crates/macros/cgp-macro-core/src/functions/delegated_impls/signature.rs @@ -28,14 +28,22 @@ pub fn signature_to_delegated_impl_item_fn( ) #await_expr }); - // A `mut` on a parameter belongs to the default body that mutates it; the forwarding body - // only passes the argument on, where the `mut` would be unused. + // A `mut` on a parameter or a by-value receiver belongs to the default body that mutates it; + // the forwarding body only passes the argument on, where the `mut` would be unused. A + // `&mut self` receiver also carries `mutability`, as part of its type, so it is kept. let mut sig = signature.clone(); for input in sig.inputs.iter_mut() { - if let FnArg::Typed(arg) = input - && let Pat::Ident(pat) = arg.pat.as_mut() - { - pat.mutability = None; + match input { + FnArg::Typed(arg) => { + if let Pat::Ident(pat) = arg.pat.as_mut() { + pat.mutability = None; + } + } + FnArg::Receiver(receiver) => { + if receiver.reference.is_none() { + receiver.mutability = None; + } + } } } diff --git a/crates/tests/cgp-tests/tests/basic_delegation/default_method_mut_parameter.rs b/crates/tests/cgp-tests/tests/basic_delegation/default_method_mut_parameter.rs index ced8febc5..72f6406da 100644 --- a/crates/tests/cgp-tests/tests/basic_delegation/default_method_mut_parameter.rs +++ b/crates/tests/cgp-tests/tests/basic_delegation/default_method_mut_parameter.rs @@ -1,9 +1,12 @@ -//! A `#[cgp_component]` method with a default body may take a `mut` parameter. +//! A `#[cgp_component]` method with a default body may take a `mut` parameter or a `mut self` +//! receiver. //! //! The generated impls forward each argument by name, so a parameter written `mut count` is //! forwarded as `count`, and the forwarding impls leave the `mut` off their own signatures, where -//! nothing mutates it. The default body keeps its `mut`: an empty provider inherits the body, and a -//! provider that writes the method may declare the parameter either way. +//! nothing mutates it; a `mut self` receiver is forwarded as `self` the same way. The default body +//! keeps its `mut`: an empty provider inherits the body, and a provider that writes the method may +//! declare the parameter either way. rustc's `unused_mut` lint, which the clippy runs deny, guards +//! the forwarding signatures. //! //! See cgp-knowledge-base/cgp/implementation/functions/derive/delegated_impls.md (Rejected //! parameters) and cgp-knowledge-base/cgp/reference/macros/cgp_component.md. @@ -20,9 +23,24 @@ pub trait CanCount { } } +#[cgp_component(Consumer)] +pub trait CanConsume { + fn consume(mut self, extra: u32) -> u32 + where + Self: Sized + Into, + { + let receiver = &mut self; + let _ = receiver; + self.into() + extra + } +} + #[cgp_impl(new CountByDefault)] impl Counter {} +#[cgp_impl(new ConsumeByDefault)] +impl Consumer {} + #[cgp_impl(new CountTwice)] impl Counter { fn count_up(&self, mut count: u32, steps: u32) -> u32 { @@ -39,6 +57,20 @@ delegate_components! { } } +pub struct Tally(u32); + +impl From for u32 { + fn from(tally: Tally) -> u32 { + tally.0 + } +} + +delegate_components! { + Tally { + ConsumerComponent: ConsumeByDefault, + } +} + pub struct FastApp; delegate_components! { @@ -51,4 +83,5 @@ delegate_components! { fn test_default_method_mut_parameter() { assert_eq!(App.count_up(2, 3), 5); assert_eq!(FastApp.count_up(2, 3), 8); + assert_eq!(Tally(1).consume(2), 3); } diff --git a/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs b/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs index 95d0f5b98..ff298302e 100644 --- a/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs +++ b/crates/tests/cgp-tests/tests/handlers/computer_macro_generics.rs @@ -4,8 +4,7 @@ //! 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, inside a type macro there (`first_of_pair`'s `T`), or -//! is bound through one (`first_item`'s `T`), which is what keeps it constrained by -//! the impl. +//! is bound through one (`first_item`'s `T`), so the impl constrains it. //! //! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_computer.md and //! cgp-knowledge-base/cgp/reference/macros/cgp_computer.md. From d62a381bb6430303c65bc46d3ae9f48c1902f278 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 6 Nov 2026 17:18:28 +0100 Subject: [PATCH 25/25] Spell arrays and Fn arguments as written in the optional rejection The unsized-optional rejection quoted `[u8 ; 4]` and `dyn Fn (u8)` with the token printer's spaces. The type printer now joins `;` to its element and a parenthesized argument list to its name, and keeps the space before a parenthesized type after `->`, a lifetime, `mut`, `dyn`, or `impl`, so `&mut (dyn Debug)` keeps its form. Co-Authored-By: Claude Opus 5.5 --- .../src/functions/field/parse.rs | 32 +++++++++++-- .../tests/parser_rejections/getters.rs | 45 +++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/crates/macros/cgp-macro-core/src/functions/field/parse.rs b/crates/macros/cgp-macro-core/src/functions/field/parse.rs index 498c9f4f1..0006a9d1b 100644 --- a/crates/macros/cgp-macro-core/src/functions/field/parse.rs +++ b/crates/macros/cgp-macro-core/src/functions/field/parse.rs @@ -120,8 +120,8 @@ fn reject_unsized_option_ref(type_path: &TypePath, type_ref: &TypeReference) -> } /// A type as the reader writes it, from its tokens: the spaces the token printer puts around `::`, -/// `<`, `>`, `,`, brackets, parentheses, and after `&` removed, so `core :: fmt :: Debug` reads -/// `core::fmt::Debug` and `& mut [u8]` reads `&mut [u8]`. +/// `<`, `>`, `,`, `;`, brackets, parentheses, and after `&` removed, so `core :: fmt :: Debug` reads +/// `core::fmt::Debug`, `& mut [u8 ; 4]` reads `&mut [u8; 4]`, and `Fn (u8)` reads `Fn(u8)`. fn type_text(ty: &impl ToTokens) -> String { let mut text = ty.to_token_stream().to_string(); for (spaced, tight) in [ @@ -131,6 +131,7 @@ fn type_text(ty: &impl ToTokens) -> String { (" <", "<"), (" >", ">"), (" ,", ","), + (" ;", ";"), ("[ ", "["), (" ]", "]"), ("( ", "("), @@ -139,7 +140,32 @@ fn type_text(ty: &impl ToTokens) -> String { ] { text = text.replace(spaced, tight); } - text + join_argument_lists(&text) +} + +/// Removes the space before a parenthesized argument list, which follows a name, as in `Fn (u8)`. +/// A parenthesized type keeps its space, so `-> (u8, u16)`, `&'a (dyn Debug)`, and +/// `&mut (dyn Debug)` are unchanged: after a lifetime or the keyword `mut`, `dyn`, or `impl`, the +/// group is not an argument list. +fn join_argument_lists(text: &str) -> String { + let mut joined = String::with_capacity(text.len()); + let mut rest = text; + while let Some(at) = rest.find(" (") { + let before = &rest[..at]; + let prefix = before.trim_end_matches(|c: char| c.is_alphanumeric() || c == '_'); + let word = &before[prefix.len()..]; + let is_argument_list = + !word.is_empty() && !prefix.ends_with('\'') && !matches!(word, "mut" | "dyn" | "impl"); + + joined.push_str(before); + if !is_argument_list { + joined.push(' '); + } + joined.push('('); + rest = &rest[at + 2..]; + } + joined.push_str(rest); + joined } /// A mutable field read borrows the whole context exclusively through diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs index a203f7275..698cf686a 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs @@ -183,6 +183,51 @@ fn rejects_optional_trait_object_return() { } } +#[test] +fn quotes_written_type_with_its_spacing() { + // Property: the rejection quotes each type as Rust code spells it. A parenthesized + // argument list joins its name (`Fn(u8)`) and `;` joins its element (`[u8; 4]`), while a + // parenthesized type keeps its space after `->`, a lifetime, and `mut`. The container is + // borrowed as the getter's reference, `&` or `&mut`, without the written lifetime. + for (receiver, return_type, message) in [ + ( + quote!(&self), + quote!(Option<&[[u8; 4]]>), + "`Option<&[[u8; 4]]>` would read an `Option<[[u8; 4]]>` field, which cannot exist because a slice is unsized; read an `Option>` field as `Option<&Vec<[u8; 4]>>` instead", + ), + ( + quote!(&self), + quote!(Option<&dyn Fn(u8) -> (u8, u16)>), + "`Option<&dyn Fn(u8) -> (u8, u16)>` would read an `Option (u8, u16)>` field, which cannot exist because a trait object is unsized; read an `Option (u8, u16)>>` field as `Option<&Box (u8, u16)>>` instead", + ), + ( + quote!(&mut self), + quote!(Option<&mut (dyn FnMut(u8) + Send)>), + "`Option<&mut (dyn FnMut(u8) + Send)>` would read an `Option` field, which cannot exist because a trait object is unsized; read an `Option>` field as `Option<&mut Box>` instead", + ), + ( + quote!(&self), + quote!(Option<&'a (dyn core::fmt::Debug)>), + "`Option<&'a (dyn core::fmt::Debug)>` would read an `Option` field, which cannot exist because a trait object is unsized; read an `Option>` field as `Option<&Box>` instead", + ), + ] { + assert_macro_rejects_with( + "cgp_auto_getter returning an optional unsized reference", + message, + || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasValue { + fn value(#receiver) -> #return_type; + } + ), + ) + }, + ); + } +} + #[test] fn rejects_multiple_associated_types() { // A getter trait may declare at most one associated return type.