diff --git a/AGENTS.md b/AGENTS.md index 5aa264a6..bea39459 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,12 +84,12 @@ lists every document in one page, which is the fastest way to find the few a tas `#[cgp_component]`/`#[cgp_impl]`/`#[cgp_fn]`, `delegate_components!`, `HasField`, `UseDelegate`, check traits, and so on). Re-invoke it whenever you move into an unfamiliar construct — the macros and core traits here are the ground truth the skill describes, so read the two together. -- **Always read [cgp-knowledge-base/cgp/README.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/README.md)** to orient in the knowledge base, then follow it - into the README of whichever section covers your task. -- **Read [cgp-knowledge-base/cgp/reference/README.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/reference/README.md) and the relevant reference documents - whenever the task requires understanding a CGP construct** — what it means, what syntax it accepts, - and what code it expands to. -- **Read [cgp-knowledge-base/cgp/implementation/README.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/README.md) and the relevant +- **Always read the knowledge base's [`cgp` section README](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/README.md)** to orient, + then follow it into the README of whichever part covers your task. +- **Read the [construct reference](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/reference/README.md) and the relevant reference + documents whenever the task requires understanding a CGP construct** — what it means, what syntax + it accepts, and what code it expands to. +- **Read the [implementation reference](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/README.md) and the relevant implementation documents whenever the task involves reading or modifying the CGP source code** — they map each macro to its `cgp-macro-core`/`cgp-macro-lib` internals, corner cases, and tests. - **Load the `/dual-reader-prose` skill whenever the task involves editing markdown documentation or @@ -187,13 +187,14 @@ as the behavior allows. ### Orient before touching anything Perform the standing steps in [Orient before any task](#orient-before-any-task) first, every -iteration. Then read the documentation specific to the macro under review, in the [knowledge base](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp): its -reference document under [cgp-knowledge-base/cgp/reference/](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp/reference), its implementation documents under -[cgp-knowledge-base/cgp/implementation/](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp/implementation) (the `entrypoints/` document, the `asts/` stack it -drives, and any `functions/` helpers it relies on), and the governing `AGENTS.md` files that define -how those documents stay in sync with the code: [cgp-knowledge-base/cgp/AGENTS.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/AGENTS.md), -[cgp-knowledge-base/cgp/implementation/AGENTS.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/AGENTS.md), and -[crates/macros/cgp-macro-core/AGENTS.md](crates/macros/cgp-macro-core/AGENTS.md). These establish +iteration. Then read the knowledge base's documentation for the macro under review: its +[reference document](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp/reference), its +[implementation documents](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp/implementation) — the `entrypoints/` document, the +`asts/` stack it drives, and any `functions/` helpers it relies on — and the `AGENTS.md` files that +define how those documents stay in sync with the code +([the `cgp` section's](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/AGENTS.md), +[the implementation tree's](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/AGENTS.md), and +[cgp-macro-core's](crates/macros/cgp-macro-core/AGENTS.md)). These establish that the source is the single source of truth and that reference, implementation, snapshot, and skill are four views of it that must never drift. diff --git a/Cargo.lock b/Cargo.lock index 6df59d02..74f19690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,7 @@ dependencies = [ "cgp-macro-core", "cgp-macro-lib", "cgp-macro-test-util", + "cgp-macro-test-util-lib", "insta", "proc-macro2", "quote", diff --git a/crates/macros/cgp-macro-core/src/types/attributes/use_type/attribute.rs b/crates/macros/cgp-macro-core/src/types/attributes/use_type/attribute.rs index 91f39165..1496bd73 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/use_type/attribute.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/use_type/attribute.rs @@ -4,7 +4,7 @@ use syn::{Ident, Type}; use crate::parse_internal; use crate::types::attributes::UseTypeIdent; -use crate::types::ident::PathWithTypeArgs; +use crate::types::ident::{PathWithTypeArgs, TypeArg}; /// One `#[use_type(...)]` import spec: the owning trait path, one or more /// associated types to import from it, and a rewrite target (`Self`, or a named @@ -17,6 +17,43 @@ pub struct UseTypeAttribute { } impl UseTypeAttribute { + /// The type positions in this spec that *grounding* resolves: the context the + /// imported types are projected against, and the generic arguments of the + /// owning trait path. + /// + /// Both positions end up inside the emitted `>::Assoc` + /// path, so an alias left bare in either is an identifier that resolves to + /// nothing. Naming the set once — rather than at each of the three places that + /// walk it — is what keeps the grounding pass, the emitted bounds, and the + /// cycle check in agreement about what grounding reaches. + pub fn groundable_types(&self) -> impl Iterator { + core::iter::once(&self.context_type).chain( + self.trait_path + .type_args + .args + .iter() + .filter_map(|arg| match arg { + TypeArg::Type(ty) => Some(ty), + // A lifetime or const argument can never name an abstract type. + TypeArg::Lifetime(_) | TypeArg::Const(_) => None, + }), + ) + } + + /// [`Self::groundable_types`] by mutable reference, for the grounding pass. + pub fn groundable_types_mut(&mut self) -> impl Iterator { + core::iter::once(&mut self.context_type).chain( + self.trait_path + .type_args + .args + .iter_mut() + .filter_map(|arg| match arg { + TypeArg::Type(ty) => Some(ty), + TypeArg::Lifetime(_) | TypeArg::Const(_) => None, + }), + ) + } + pub fn replace_ident(&self, ident: &Ident) -> Option { for type_ident in &self.type_idents { if type_ident.alias_ident() == ident { diff --git a/crates/macros/cgp-macro-core/src/types/attributes/use_type/attributes.rs b/crates/macros/cgp-macro-core/src/types/attributes/use_type/attributes.rs index ea81307a..2b9b6416 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/use_type/attributes.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/use_type/attributes.rs @@ -4,6 +4,7 @@ use syn::{ItemImpl, ItemTrait, Type}; use crate::functions::parse_internal; use crate::types::attributes::UseTypeAttribute; +use crate::types::attributes::use_type::grounding::ground_specs; use crate::types::attributes::use_type::type_predicates::{ derive_use_type_predicates, forbid_duplicate_aliases, }; @@ -15,37 +16,16 @@ pub struct UseTypeAttributes { } impl UseTypeAttributes { - /// Resolve every spec's context type into fully-qualified form before it is - /// used, so that both the body substitution and the appended bounds agree on - /// one grounded context. + /// Validate the import list and resolve every spec into fully-qualified form, + /// the two steps that must precede any substitution. /// - /// An `in Context` suffix whose `Context` is itself imported by another spec — - /// as in `#[use_type(HasTypes.Types, HasScalarType.Scalar in Types)]` — is - /// rewritten from the bare alias `Types` to `::Types`. - /// Contexts that name a real generic parameter or `Self` are left untouched. - /// The pass iterates to a fixpoint so a chain of links resolves fully; each - /// pass grounds one more level, so `attributes.len()` passes cover any - /// acyclic chain, and a cyclic reference simply stops making progress and - /// surfaces later as an ordinary unresolved-type error rather than looping. - fn grounded_specs(&self) -> Vec { - let mut grounded = self.attributes.clone(); - - for _ in 0..grounded.len() { - let snapshot = grounded.clone(); - let mut changed = false; - - for spec in grounded.iter_mut() { - let mut visitor = SubstituteAbstractTypes::new(&snapshot); - visitor.visit_type_mut(&mut spec.context_type); - changed |= visitor.is_changed; - } - - if !changed { - break; - } - } + /// Aliases are checked for uniqueness first, because [`ground_specs`] resolves a + /// reference through the spec that owns the alias and so needs one owner per + /// name; it then grounds each spec against its dependencies and rejects a cycle. + fn resolved_specs(&self) -> syn::Result> { + forbid_duplicate_aliases(&self.attributes)?; - grounded + ground_specs(&self.attributes) } pub fn transform_item_trait(&self, item_trait: &mut ItemTrait) -> syn::Result<()> { @@ -53,9 +33,7 @@ impl UseTypeAttributes { return Ok(()); } - forbid_duplicate_aliases(&self.attributes)?; - - let grounded = self.grounded_specs(); + let grounded = self.resolved_specs()?; SubstituteAbstractTypes::new(&grounded).visit_item_trait_mut(item_trait); @@ -98,9 +76,7 @@ impl UseTypeAttributes { return Ok(()); } - forbid_duplicate_aliases(&self.attributes)?; - - let grounded = self.grounded_specs(); + let grounded = self.resolved_specs()?; SubstituteAbstractTypes::new(&grounded).visit_item_impl_mut(item_impl); diff --git a/crates/macros/cgp-macro-core/src/types/attributes/use_type/grounding.rs b/crates/macros/cgp-macro-core/src/types/attributes/use_type/grounding.rs new file mode 100644 index 00000000..24666701 --- /dev/null +++ b/crates/macros/cgp-macro-core/src/types/attributes/use_type/grounding.rs @@ -0,0 +1,190 @@ +use std::collections::BTreeMap; + +use syn::Ident; +use syn::visit_mut::VisitMut; + +use crate::types::attributes::UseTypeAttribute; +use crate::visitors::{SubstituteAbstractTypes, collect_bare_aliases}; + +/// Resolve every `#[use_type]` spec's groundable positions into fully-qualified +/// form, rejecting an import list whose positions resolve through one another in a +/// cycle. +/// +/// Grounding reaches the positions named by +/// [`UseTypeAttribute::groundable_types`] — the context and the trait path's own +/// generic arguments — because an alias left bare in either ends up inside an +/// emitted `>::Assoc` path, resolving to nothing. So +/// `HasTypes.Types, HasScalarType.Scalar in Types` rewrites the second context to +/// `::Types`, and `HasDbType.Db, HasPoolType.Pool` projects +/// against `HasPoolType<::Db>`. A position naming a real generic +/// parameter or `Self` matches no alias and is left untouched. +/// +/// **Each spec is grounded against only the specs that are already grounded**, in +/// dependency order, which is what makes one pass per spec sufficient and makes a +/// half-resolved substitution impossible. A single depth-first walk of the +/// dependency graph gives that order and detects a cycle in the same traversal: a +/// spec is grounded once its dependencies return, and an edge back into a spec +/// still on the walk is a cycle. Both properties are therefore structural rather +/// than resting on an earlier validation pass. +/// +/// Aliases must already be unique (see `forbid_duplicate_aliases`), since the graph +/// assumes one owning spec per alias. +/// +/// The returned specs keep their **source order**, so the supertraits and `where` +/// predicates derived from them read in the order the author wrote the imports. +pub fn ground_specs(specs: &[UseTypeAttribute]) -> syn::Result> { + // Which spec imports each alias, so a reference can be resolved to the spec it + // depends on. + let mut owner_of_alias: BTreeMap = BTreeMap::new(); + + for (index, spec) in specs.iter().enumerate() { + for type_ident in spec.type_idents.iter() { + owner_of_alias.insert(type_ident.alias_ident().to_string(), index); + } + } + + // The dependency edges out of each spec: for every alias its groundable + // positions reference, the spec owning that alias, together with the referencing + // token so a rejected cycle can point at what the user wrote. + // + // Held outside `Grounding` so that indexing it inside the walk borrows the slice + // rather than `self`, leaving `self` free to mutate as the walk descends. + let edges: Vec> = specs + .iter() + .map(|spec| { + spec.groundable_types() + .flat_map(collect_bare_aliases) + .filter_map(|ident| Some((*owner_of_alias.get(&ident.to_string())?, ident))) + .collect() + }) + .collect(); + + let mut grounding = Grounding { + specs, + edges: &edges, + state: vec![VisitState::Unvisited; specs.len()], + grounded: vec![None; specs.len()], + labels: Vec::new(), + path: Vec::new(), + }; + + for index in 0..specs.len() { + grounding.resolve(index)?; + } + + Ok(grounding + .grounded + .into_iter() + .map(|spec| spec.expect("every spec is grounded once the walk has visited all of them")) + .collect()) +} + +#[derive(Clone, Copy, PartialEq)] +enum VisitState { + Unvisited, + /// On the current walk, so an edge back into it closes a cycle. + InProgress, + /// Grounded, along with everything it depends on. + Done, +} + +/// The depth-first walk that grounds each spec after its dependencies. +/// +/// `path` holds the specs on the current walk and `labels` the referencing tokens +/// that led between them, so a cycle can be reported in the alias names the author +/// wrote rather than as spec indices. +struct Grounding<'a, 'edges> { + specs: &'a [UseTypeAttribute], + edges: &'edges [Vec<(usize, &'a Ident)>], + state: Vec, + grounded: Vec>, + labels: Vec<&'a Ident>, + path: Vec, +} + +impl Grounding<'_, '_> { + fn resolve(&mut self, index: usize) -> syn::Result<()> { + // `Done` is the ordinary case: the spec and its dependencies are grounded, so + // there is nothing left to do. `InProgress` cannot arrive here, because the + // guard at the call site below reports a cycle before recursing — but + // descending on it anyway would recurse forever, so the test is written to + // stop on any state but `Unvisited`. A call site that ever skipped the guard + // then fails as an ungrounded spec at the end of the walk, which is + // diagnosable, rather than as a stack overflow inside the compiler. + if self.state[index] != VisitState::Unvisited { + return Ok(()); + } + + self.state[index] = VisitState::InProgress; + self.path.push(index); + + for (target, reference) in self.edges[index].iter() { + if self.state[*target] == VisitState::InProgress { + return Err(self.cycle_error(*target, reference)); + } + + self.labels.push(reference); + self.resolve(*target)?; + self.labels.pop(); + } + + // Every dependency has returned, so all of them are grounded and this spec + // can be resolved against them. + self.ground(index); + + self.path.pop(); + self.state[index] = VisitState::Done; + + Ok(()) + } + + /// Substitute one spec's groundable positions against the already-grounded + /// specs, and record the result. + fn ground(&mut self, index: usize) { + // Only grounded specs take part, so a replacement can never carry a bare + // alias of its own — which is what lets one pass per spec suffice. + let resolved: Vec = self.grounded.iter().flatten().cloned().collect(); + + let mut spec = self.specs[index].clone(); + let mut visitor = SubstituteAbstractTypes::new(&resolved); + + for ty in spec.groundable_types_mut() { + visitor.visit_type_mut(ty); + } + + self.grounded[index] = Some(spec); + } + + /// The rejection for an edge back into `target`, labelled `closing`. + /// + /// The cycle runs from wherever `target` sits on the current walk back around to + /// it, so its hops are the labels from that point on plus the closing reference — + /// which for a self-reference is the single `A` -> `A` hop. + fn cycle_error(&self, target: usize, closing: &Ident) -> syn::Error { + let entry = self + .path + .iter() + .position(|index| *index == target) + .unwrap_or(0); + + let mut hops: Vec = self.labels[entry..] + .iter() + .map(|ident| format!("`{ident}`")) + .collect(); + + hops.push(format!("`{closing}`")); + // Close the loop by restating where it started. + hops.push(hops[0].clone()); + + syn::Error::new_spanned( + closing, + format!( + "cannot ground `#[use_type]` imports: they resolve through one another \ + in a cycle {}. An `in Context` clause or a trait argument may name another \ + import's alias only if the resulting chain is acyclic, since a cycle has \ + no valid grounding order.", + hops.join(" -> "), + ), + ) + } +} diff --git a/crates/macros/cgp-macro-core/src/types/attributes/use_type/mod.rs b/crates/macros/cgp-macro-core/src/types/attributes/use_type/mod.rs index be818cb8..ca179fb5 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/use_type/mod.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/use_type/mod.rs @@ -1,5 +1,6 @@ mod attribute; mod attributes; +mod grounding; mod ident; mod type_predicates; diff --git a/crates/macros/cgp-macro-core/src/types/attributes/use_type/type_predicates.rs b/crates/macros/cgp-macro-core/src/types/attributes/use_type/type_predicates.rs index ebb9cfdd..27a41d0e 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/use_type/type_predicates.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/use_type/type_predicates.rs @@ -1,43 +1,36 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; -use syn::punctuated::Punctuated; -use syn::token::Comma; +use quote::quote; +use syn::visit_mut::VisitMut; use syn::{Ident, Type, WherePredicate}; use crate::functions::parse_internal; use crate::types::attributes::{UseTypeAttribute, UseTypeIdent}; +use crate::visitors::SubstituteAbstractTypes; /// Derive the impl-side `where` predicates a set of `#[use_type]` specs /// contributes: one `Context: Trait` bound per spec, carrying any type-equality -/// (`= T`) pins as associated-type bindings. The specs' contexts must already be -/// grounded (see [`UseTypeAttributes::grounded_specs`]), so this reads -/// `use_type.context_type` directly rather than re-resolving aliases. +/// (`= T`) pins as associated-type bindings on the trait. The specs must already be +/// grounded (see [`ground_specs`](super::grounding::ground_specs)), so this reads +/// each `context_type` and `trait_path` directly rather than re-resolving aliases. pub fn derive_use_type_predicates(specs: &[UseTypeAttribute]) -> syn::Result> { let mut predicates = Vec::new(); for use_type in specs.iter() { - let type_equalities = find_type_equalities(use_type, specs)?; - - let trait_path = &use_type.trait_path; + // The pins become associated-type bindings on the trait, which + // `to_bound_tokens` merges into whatever generic arguments the trait path + // already carries. An empty binding list renders the path unchanged, so the + // pinned and unpinned cases are one path through this code. + let bindings: Vec = find_type_equalities(use_type, specs) + .into_iter() + .map(|(alias_ident, equal_target)| quote! { #alias_ident = #equal_target }) + .collect(); + + let trait_bound = use_type.trait_path.to_bound_tokens(&bindings); let context_type = &use_type.context_type; - if type_equalities.is_empty() { - predicates.push(parse_internal! { - #context_type: #trait_path - }); - } else { - let mut constraints: Punctuated = Punctuated::new(); - - for (alias_ident, equal_target) in type_equalities.into_iter() { - constraints.push(quote! { - #alias_ident = #equal_target - }); - } - - predicates.push(parse_internal! { - #context_type: #trait_path < #constraints > - }); - } + predicates.push(parse_internal! { + #context_type: #trait_bound + }); } Ok(predicates) @@ -70,50 +63,47 @@ pub fn forbid_duplicate_aliases(specs: &[UseTypeAttribute]) -> syn::Result<()> { fn find_type_equalities( current_spec: &UseTypeAttribute, specs: &[UseTypeAttribute], -) -> syn::Result> { - let mut equalities = Vec::new(); - - for current_type_ident in current_spec.type_idents.iter() { - if let Some(equality) = find_type_equality(current_type_ident, current_spec, specs)? { - equalities.push(equality); - } - } - - Ok(equalities) +) -> Vec<(Ident, Type)> { + current_spec + .type_idents + .iter() + .filter_map(|current_type_ident| find_type_equality(current_type_ident, specs)) + .collect() } +/// Ground one `= T` pin: rewrite every imported alias appearing *anywhere +/// inside* the pin's right-hand side into its fully-qualified projection, so +/// `{Transaction = Tx}` grounds its nested `Db` exactly as +/// `{HashedPassword = Password}` grounds a bare one. Substituting through the +/// shared visitor rather than comparing the whole type is what makes the two +/// cases one rule — the right-hand side is an ordinary type, and an alias is +/// resolved wherever it occurs in it. +/// +/// The pinned alias itself is excluded from the substitution set, so a +/// degenerate self-pin (`{Foo = Foo}`) stays the unresolved-name error it +/// already was rather than silently becoming a vacuous bound. fn find_type_equality( current_ident: &UseTypeIdent, - current_spec: &UseTypeAttribute, specs: &[UseTypeAttribute], -) -> syn::Result> { - if let Some(equal_target) = current_ident.equals.clone() { - for spec in specs.iter() { - if core::ptr::eq(spec, current_spec) { - // Skip the current spec - continue; - } +) -> Option<(Ident, Type)> { + let mut equal_target = current_ident.equals.clone()?; - for match_use_type in spec.type_idents.iter() { - let match_type: Type = - parse_internal(match_use_type.alias_ident().to_token_stream())?; - if match_type == equal_target { - let trait_path = &spec.trait_path; - let current_type_ident = ¤t_ident.type_ident; - let match_type_ident = &match_use_type.type_ident; - let context_type = &spec.context_type; + let others = specs_excluding_alias(specs, current_ident.alias_ident()); + SubstituteAbstractTypes::new(&others).visit_type_mut(&mut equal_target); - let equal_target: Type = parse_internal! { - <#context_type as #trait_path>::#match_type_ident - }; - - return Ok(Some((current_type_ident.clone(), equal_target))); - } - } - } + Some((current_ident.type_ident.clone(), equal_target)) +} - Ok(Some((current_ident.type_ident.clone(), equal_target))) - } else { - Ok(None) - } +/// The grounded specs with one alias dropped, for substituting inside that +/// alias's own equality pin. +fn specs_excluding_alias(specs: &[UseTypeAttribute], alias: &Ident) -> Vec { + specs + .iter() + .map(|spec| { + let mut spec = spec.clone(); + spec.type_idents + .retain(|type_ident| type_ident.alias_ident() != alias); + spec + }) + .collect() } diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/entries.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/entries.rs index f21e41ff..26107525 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/entries.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/entries.rs @@ -82,9 +82,16 @@ impl DelegateEntries { impl ExtractInnerDelegateTables for DelegateEntries { fn extract_inner_tables(&self) -> Vec { - self.entries + // Statements are walked as well as mappings, because a `for` loop's body + // holds mappings whose values may open a nested table. + self.statements .iter() - .flat_map(|entry| entry.extract_inner_tables()) + .flat_map(|statement| statement.extract_inner_tables()) + .chain( + self.entries + .iter() + .flat_map(|entry| entry.extract_inner_tables()), + ) .collect() } } diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/combined.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/combined.rs index 4d4979ae..038f1531 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/combined.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/combined.rs @@ -4,8 +4,8 @@ use syn::{Error, Type}; use crate::traits::PeekKeyword; use crate::types::delegate_component::{ - EvalDelegateEntries, EvaluatedDelegateEntry, ForDelegateStatement, NamespaceDelegateStatement, - OpenDelegateStatement, + EvalDelegateEntries, EvaluatedDelegateEntry, ExtractInnerDelegateTables, ForDelegateStatement, + InnerDelegateTable, NamespaceDelegateStatement, OpenDelegateStatement, }; use crate::types::keywords::{Namespace, Open}; @@ -52,3 +52,13 @@ impl EvalDelegateEntries for DelegateStatement { } } } + +impl ExtractInnerDelegateTables for DelegateStatement { + fn extract_inner_tables(&self) -> Vec { + match self { + // `namespace` and `open` carry no values, so neither can open a table. + Self::Namespace(_) | Self::Open(_) => Vec::new(), + Self::For(statement) => statement.extract_inner_tables(), + } + } +} diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs index 96964916..88b95611 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs @@ -5,8 +5,8 @@ use syn::{Ident, Type, WhereClause, braced}; use crate::types::delegate_component::{ EvalDelegateEntries, EvalDelegateKey, EvalDelegateValue, EvalForEntries, - EvaluatedDelegateEntry, EvaluatedForEntry, NormalDelegateMapping, - eval_delegate_entries_via_for, + EvaluatedDelegateEntry, EvaluatedForEntry, ExtractInnerDelegateTables, InnerDelegateTable, + NormalDelegateMapping, eval_delegate_entries_via_for, }; use crate::types::ident::PathWithTypeArgs; @@ -106,3 +106,15 @@ impl EvalDelegateEntries for ForDelegateStatement { eval_delegate_entries_via_for(self, table_type) } } + +impl ExtractInnerDelegateTables for ForDelegateStatement { + fn extract_inner_tables(&self) -> Vec { + // A loop body holds ordinary `:` mappings, whose values may open a nested + // table just as a top-level mapping's can. Without this the inner table is + // parsed, named by the entry's `Delegate`, and never emitted. + self.mappings + .iter() + .flat_map(|mapping| mapping.extract_inner_tables()) + .collect() + } +} diff --git a/crates/macros/cgp-macro-core/src/types/ident/path_with_type_args.rs b/crates/macros/cgp-macro-core/src/types/ident/path_with_type_args.rs index d370ef1c..c95fa042 100644 --- a/crates/macros/cgp-macro-core/src/types/ident/path_with_type_args.rs +++ b/crates/macros/cgp-macro-core/src/types/ident/path_with_type_args.rs @@ -1,6 +1,8 @@ use proc_macro2::TokenStream; -use quote::ToTokens; +use quote::{ToTokens, quote}; use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::token::Comma; use syn::{Error, Ident, Path, PathArguments, Type, parse_quote, parse2}; use crate::traits::ToType; @@ -30,6 +32,40 @@ pub struct PathWithTypeArgs { } impl PathWithTypeArgs { + /// The path rendered with `bindings` — associated-type bindings such as + /// `Item = u8` — appended to its own generic arguments as **one** + /// angle-bracketed list, for use as a trait bound. + /// + /// A generic trait carrying an associated-type binding has exactly one valid + /// spelling, `Trait`; the path's arguments followed by a second + /// group, `Trait`, is not a trait bound in any position. Since + /// [`ToTokens`] emits this type's arguments as a *trailing* group, a caller that + /// appended its own bindings after the whole path would produce exactly that + /// invalid form — so the merge belongs here, with the rendering it has to agree + /// with, rather than at each call site. + /// + /// Bindings are passed as token streams because [`TypeArgs`] deliberately + /// rejects them (they are invalid in a plain type-argument position), so there is + /// no argument type able to carry one. + pub fn to_bound_tokens(&self, bindings: &[TokenStream]) -> TokenStream { + if bindings.is_empty() { + return self.to_token_stream(); + } + + let path = &self.path; + + let mut arguments: Punctuated = self + .type_args + .args + .iter() + .map(ToTokens::to_token_stream) + .collect(); + + arguments.extend(bindings.iter().cloned()); + + quote! { #path < #arguments > } + } + /// The identifier of the final path segment, e.g. `Foo` in /// `path::to::Foo`. pub fn ident(&self) -> &Ident { diff --git a/crates/macros/cgp-macro-core/src/types/namespace/eval.rs b/crates/macros/cgp-macro-core/src/types/namespace/eval.rs index 5fa14135..7b3afc14 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/eval.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/eval.rs @@ -2,10 +2,16 @@ use proc_macro2::TokenStream; use quote::ToTokens; use syn::{ItemImpl, ItemStruct, ItemTrait}; +use crate::types::empty_struct::EmptyStruct; + pub struct EvaluatedNamespaceTable { pub item_impls: Vec, pub item_trait: Option, pub item_struct: Option, + /// The structs declared for nested `Wrapper` values lifted out + /// of the body, mirroring `EvaluatedDelegateTable::item_structs`. Their + /// `DelegateComponent` impls are appended to `item_impls`. + pub inner_structs: Vec, } impl ToTokens for EvaluatedNamespaceTable { @@ -18,6 +24,10 @@ impl ToTokens for EvaluatedNamespaceTable { item_trait.to_tokens(tokens); } + for inner_struct in &self.inner_structs { + inner_struct.to_tokens(tokens); + } + for item_impl in &self.item_impls { item_impl.to_tokens(tokens); } 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 7a1f4580..8834afc9 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/table.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/table.rs @@ -6,6 +6,7 @@ use crate::functions::parse_internal; use crate::traits::ParseOptionalKeyword; use crate::types::delegate_component::{ DelegateEntries, EvalDelegateEntries, EvalDelegateEntry, EvalForEntry, + ExtractInnerDelegateTables, }; use crate::types::generics::ImplGenerics; use crate::types::ident::{IdentWithTypeArgs, PathWithTypeArgs}; @@ -162,10 +163,23 @@ impl NamespaceTable { item_impls.insert(0, item_impl); } + // Lift out each nested `Wrapper` value, exactly as + // `DelegateTable::eval` does. The entry's `Delegate` resolves to + // `Wrapper`, so without emitting `Inner` and its own + // `DelegateComponent` impls the entry would name a type nothing declares. + let mut inner_structs = Vec::new(); + + for inner_table in self.entries.extract_inner_tables() { + inner_structs.push(inner_table.build_table_struct()); + + item_impls.extend(inner_table.build_impls()?); + } + Ok(EvaluatedNamespaceTable { item_impls, item_trait, item_struct, + inner_structs, }) } } diff --git a/crates/macros/cgp-macro-core/src/visitors/collect_bare_aliases.rs b/crates/macros/cgp-macro-core/src/visitors/collect_bare_aliases.rs new file mode 100644 index 00000000..fc876993 --- /dev/null +++ b/crates/macros/cgp-macro-core/src/visitors/collect_bare_aliases.rs @@ -0,0 +1,42 @@ +use syn::visit::Visit; +use syn::{Ident, Type, visit}; + +use crate::visitors::bare_alias_ident; + +/// Collect every **bare alias reference** a type contains, in source order. +/// +/// This is the read-only counterpart of +/// [`SubstituteAbstractTypes`](crate::visitors::SubstituteAbstractTypes): where +/// that visitor *rewrites* each reference, this one only reports where they are, +/// which is what the `#[use_type]` grounding-cycle check needs to read one spec's +/// dependencies on another. Both decide what counts as a reference through the +/// shared [`bare_alias_ident`], so a position the substitution would rewrite is +/// always a position the cycle check can see. +/// +/// The collected identifiers borrow from the traversed type, so each one carries +/// the span of the token the user wrote — which is what lets a rejected cycle put +/// its caret on the offending alias rather than on the whole macro block. +#[derive(Default)] +pub struct CollectBareAliases<'ast> { + pub idents: Vec<&'ast Ident>, +} + +impl<'ast> Visit<'ast> for CollectBareAliases<'ast> { + fn visit_type(&mut self, ty: &'ast Type) { + if let Some(ident) = bare_alias_ident(ty) { + self.idents.push(ident); + // A bare alias is a single argument-free segment, so it has no nested + // types to descend into. + return; + } + + visit::visit_type(self, ty); + } +} + +/// Every bare alias reference in `ty`, in source order. +pub fn collect_bare_aliases(ty: &Type) -> Vec<&Ident> { + let mut visitor = CollectBareAliases::default(); + visitor.visit_type(ty); + visitor.idents +} diff --git a/crates/macros/cgp-macro-core/src/visitors/mod.rs b/crates/macros/cgp-macro-core/src/visitors/mod.rs index 04f460a7..5ab2ad6e 100644 --- a/crates/macros/cgp-macro-core/src/visitors/mod.rs +++ b/crates/macros/cgp-macro-core/src/visitors/mod.rs @@ -1,9 +1,11 @@ +mod collect_bare_aliases; mod remove_self_path; mod replace_provider; mod replace_self; mod self_assoc_type; mod substitute_abstract_type; +pub use collect_bare_aliases::*; pub use remove_self_path::*; pub use replace_provider::*; pub use replace_self::*; diff --git a/crates/macros/cgp-macro-core/src/visitors/substitute_abstract_type.rs b/crates/macros/cgp-macro-core/src/visitors/substitute_abstract_type.rs index 71d85f7c..f9f913f4 100644 --- a/crates/macros/cgp-macro-core/src/visitors/substitute_abstract_type.rs +++ b/crates/macros/cgp-macro-core/src/visitors/substitute_abstract_type.rs @@ -1,8 +1,41 @@ +use syn::punctuated::Punctuated; use syn::visit_mut::VisitMut; -use syn::{PathArguments, Type, TypePath, parse_quote, visit_mut}; +use syn::{ + ExprPath, Ident, PathArguments, PathSegment, Token, Type, TypePath, parse_quote, visit_mut, +}; use crate::types::attributes::UseTypeAttribute; +/// The identifier of a **bare alias reference** — an unqualified, single-segment, +/// argument-free type path such as `Scalar` — or `None` for any other type. +/// +/// This is the single rule that decides whether a type position *names* an +/// imported alias, and it is deliberately shared by everything that needs the +/// answer: the substitution below, which rewrites such a reference, and the +/// dependency graph `ground_specs` builds, which treats one as an edge between two +/// specs. Duplicating the guard would let the two disagree about what counts as a +/// reference, and a reference the graph cannot see is exactly the one whose +/// ordering — or cycle — grounding would then get wrong. +/// +/// The strictness is what keeps the rewrite from claiming syntax nobody wrote: a +/// path that already carries a qualifier, generic arguments, or more than one +/// segment is not a bare alias, so a genuine `Self::Error` or a `Foo` head +/// is left alone. +pub fn bare_alias_ident(ty: &Type) -> Option<&Ident> { + if let Type::Path(TypePath { qself: None, path }) = ty + && path.leading_colon.is_none() + && path.segments.len() == 1 + { + let segment = &path.segments[0]; + + if matches!(segment.arguments, PathArguments::None) { + return Some(&segment.ident); + } + } + + None +} + /// A single-pass `VisitMut` that rewrites every bare, single-segment, /// argument-free type path matching an imported alias into its fully-qualified /// `::AssocType` form. @@ -13,48 +46,84 @@ use crate::types::attributes::UseTypeAttribute; /// `forbid_duplicate_aliases`), at most one spec can match a given identifier, /// so the match order among specs is irrelevant. /// -/// Each spec's `context_type` must already be *grounded* — resolved to a fully -/// qualified path (`::Types`) with no remaining bare alias — -/// before the visitor runs. Grounding is what lets a single traversal suffice: -/// the replacement a spec emits contains no bare alias, so the visitor never has -/// to revisit its own output to finish a nested import. -/// -/// `is_changed` records whether any replacement was made during the traversal, -/// which the grounding fixpoint reads to decide when a further pass would be a -/// no-op. +/// Every spec passed in must already be *grounded* — its context and trait +/// arguments resolved to fully qualified paths (`::Types`) with +/// no remaining bare alias. That is what lets a single traversal suffice: the +/// replacement a spec emits contains no bare alias of its own, so the visitor never +/// has to revisit its own output to finish a nested import. `ground_specs` +/// establishes the property by resolving each spec against its dependencies. pub struct SubstituteAbstractTypes<'a> { pub specs: &'a [UseTypeAttribute], - pub is_changed: bool, } impl<'a> SubstituteAbstractTypes<'a> { pub fn new(specs: &'a [UseTypeAttribute]) -> Self { - Self { - specs, - is_changed: false, - } + Self { specs } } } impl VisitMut for SubstituteAbstractTypes<'_> { fn visit_type_mut(&mut self, ty: &mut Type) { - if let Type::Path(TypePath { qself: None, path }) = ty - && path.leading_colon.is_none() - && path.segments.len() == 1 - { - let segment = &path.segments[0]; - if matches!(segment.arguments, PathArguments::None) { - for spec in self.specs { - if let Some(replacement_ident) = spec.replace_ident(&segment.ident) { - let trait_path = &spec.trait_path; - let context_type = &spec.context_type; - *ty = parse_quote! { <#context_type as #trait_path>::#replacement_ident }; - self.is_changed = true; - return; - } - } + if let Some(ident) = bare_alias_ident(ty) { + // Resolve against the specs before mutating, so the replacement is not + // computed while the identifier it replaces is still borrowed. + let replacement = self.specs.iter().find_map(|spec| { + let replacement_ident = spec.replace_ident(ident)?; + let trait_path = &spec.trait_path; + let context_type = &spec.context_type; + Some(parse_quote! { <#context_type as #trait_path>::#replacement_ident }) + }); + + if let Some(replacement) = replacement { + *ty = replacement; + return; } } + visit_mut::visit_type_mut(self, ty); } + + /// Rewrite an alias that *qualifies* an expression path — `Transaction::begin_from(pool)` — + /// into the qualified-type form `<::Transaction>::begin_from(pool)`. + /// + /// The alias is already rewritten in every type position inside a body, a `let` annotation + /// included, so leaving it unresolved as the qualifier of an associated-function or + /// associated-const call was an inconsistency rather than a deliberate boundary: the author has + /// claimed the name by importing it, and `forbid_duplicate_aliases` guarantees nothing else in + /// the import list claims it too. + /// + /// Only a path of **two or more** segments is rewritten, which is what keeps the rewrite + /// unambiguous. A multi-segment `Transaction::foo` can only mean an associated item of the type, + /// while a bare single-segment `Transaction` in expression position is a *value* — a unit struct + /// or an enum variant the author means — and an abstract type can never be one, so it is left + /// alone. + fn visit_expr_path_mut(&mut self, expr: &mut ExprPath) { + if expr.qself.is_none() + && expr.path.leading_colon.is_none() + && expr.path.segments.len() > 1 + && matches!(expr.path.segments[0].arguments, PathArguments::None) + { + // Resolve against the specs before mutating, so the replacement is not computed while + // the path it replaces is still borrowed. + let replacement = self.specs.iter().find_map(|spec| { + let replacement_ident = spec.replace_ident(&expr.path.segments[0].ident)?; + let trait_path = &spec.trait_path; + let context_type = &spec.context_type; + Some(parse_quote! { <#context_type as #trait_path>::#replacement_ident }) + }); + + if let Some(ty) = replacement { + let ty: Type = ty; + let rest: Punctuated = + expr.path.segments.iter().skip(1).cloned().collect(); + + *expr = parse_quote! { <#ty>::#rest }; + // Fall through to the recursion rather than returning: a later segment may carry + // generic arguments of its own that name an alias — `Transaction::make::()` — + // and those are still to be substituted. Re-visiting the rewritten node cannot + // loop, because it now carries a `qself` and both guards require `qself: None`. + } + } + visit_mut::visit_expr_path_mut(self, expr); + } } diff --git a/crates/macros/cgp-macro-test-util-lib/src/entrypoints/mod.rs b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/mod.rs index ac64fa9b..aef0db18 100644 --- a/crates/macros/cgp-macro-test-util-lib/src/entrypoints/mod.rs +++ b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/mod.rs @@ -11,7 +11,10 @@ mod snapshot_cgp_type; mod snapshot_check_components; mod snapshot_delegate_and_check_components; mod snapshot_delegate_components; +mod snapshot_derive_build_field; mod snapshot_derive_cgp_data; +mod snapshot_derive_extract_field; +mod snapshot_derive_from_variant; mod snapshot_derive_has_field; mod snapshot_derive_has_fields; @@ -28,6 +31,9 @@ pub use snapshot_cgp_type::*; pub use snapshot_check_components::*; pub use snapshot_delegate_and_check_components::*; pub use snapshot_delegate_components::*; +pub use snapshot_derive_build_field::*; pub use snapshot_derive_cgp_data::*; +pub use snapshot_derive_extract_field::*; +pub use snapshot_derive_from_variant::*; pub use snapshot_derive_has_field::*; pub use snapshot_derive_has_fields::*; diff --git a/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_derive_build_field.rs b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_derive_build_field.rs new file mode 100644 index 00000000..bdf25675 --- /dev/null +++ b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_derive_build_field.rs @@ -0,0 +1,25 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ItemStruct, parse2}; + +use crate::keywords::BuildField; +use crate::types::DeriveMacroSnapshot; + +pub fn snapshot_derive_build_field(body: TokenStream) -> syn::Result { + let item: DeriveMacroSnapshot = parse2(body)?; + + let body = &item.body; + + let output = cgp_macro_lib::derive_build_field(quote! { + #[derive(BuildField)] + #body + })?; + + let wrapped = item.snapshot.wrap_output(output)?; + + Ok(quote! { + #body + + #wrapped + }) +} diff --git a/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_derive_extract_field.rs b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_derive_extract_field.rs new file mode 100644 index 00000000..df263ed6 --- /dev/null +++ b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_derive_extract_field.rs @@ -0,0 +1,25 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ItemEnum, parse2}; + +use crate::keywords::ExtractField; +use crate::types::DeriveMacroSnapshot; + +pub fn snapshot_derive_extract_field(body: TokenStream) -> syn::Result { + let item: DeriveMacroSnapshot = parse2(body)?; + + let body = &item.body; + + let output = cgp_macro_lib::derive_extract_field(quote! { + #[derive(ExtractField)] + #body + })?; + + let wrapped = item.snapshot.wrap_output(output)?; + + Ok(quote! { + #body + + #wrapped + }) +} diff --git a/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_derive_from_variant.rs b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_derive_from_variant.rs new file mode 100644 index 00000000..aab6ef9a --- /dev/null +++ b/crates/macros/cgp-macro-test-util-lib/src/entrypoints/snapshot_derive_from_variant.rs @@ -0,0 +1,25 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ItemEnum, parse2}; + +use crate::keywords::FromVariant; +use crate::types::DeriveMacroSnapshot; + +pub fn snapshot_derive_from_variant(body: TokenStream) -> syn::Result { + let item: DeriveMacroSnapshot = parse2(body)?; + + let body = &item.body; + + let output = cgp_macro_lib::derive_from_variant(quote! { + #[derive(FromVariant)] + #body + })?; + + let wrapped = item.snapshot.wrap_output(output)?; + + Ok(quote! { + #body + + #wrapped + }) +} diff --git a/crates/macros/cgp-macro-test-util-lib/src/keywords.rs b/crates/macros/cgp-macro-test-util-lib/src/keywords.rs index 6ae0ed20..104f5248 100644 --- a/crates/macros/cgp-macro-test-util-lib/src/keywords.rs +++ b/crates/macros/cgp-macro-test-util-lib/src/keywords.rs @@ -33,3 +33,9 @@ define_keyword!(HasField, "HasField"); define_keyword!(HasFields, "HasFields"); define_keyword!(CgpData, "CgpData"); + +define_keyword!(BuildField, "BuildField"); + +define_keyword!(ExtractField, "ExtractField"); + +define_keyword!(FromVariant, "FromVariant"); diff --git a/crates/macros/cgp-macro-test-util/README.md b/crates/macros/cgp-macro-test-util/README.md index 3ecbf08f..f3db5726 100644 --- a/crates/macros/cgp-macro-test-util/README.md +++ b/crates/macros/cgp-macro-test-util/README.md @@ -2,45 +2,27 @@ Snapshot-testing macros for the CGP procedural macros. -This crate exposes a family of `snapshot_*!` procedural macros that make it easy -to write **golden / snapshot tests** for the code generated by the core CGP -macros such as `#[cgp_component]`, `#[cgp_impl]`, `#[cgp_auto_getter]`, -`#[cgp_getter]`, `#[cgp_fn]`, `#[derive(HasField)]`, and -`delegate_components!`. - -The snapshots are asserted using the [`insta`](https://insta.rs) crate. - -## Why snapshot test the macros? - -The CGP macros generate a fair amount of boilerplate: consumer traits, provider -traits, blanket delegation impls, `IsProviderFor` impls, `UseContext` / -`UseField` / `RedirectLookup` providers, and so on. When we change the macro -internals, it is easy to accidentally change the generated output in an -unintended way. - -A snapshot test pins down the *exact* generated code as a human-readable string. -When the generated code changes, the snapshot test fails and shows a diff, -letting us review the change deliberately (and `cargo insta` makes accepting an -intended change a one-liner). - -Crucially, each snapshot macro does **two** things at once: - -1. It **emits the real generated code** into the surrounding module, exactly as - the underlying CGP macro would. The generated traits, structs, and impls are - therefore live and usable by the rest of the module — you can still wire them - up and assert their runtime behavior. -2. It **generates a `#[test]` function** that captures a pretty-printed string of - that same generated code and asserts it against an inline `insta` snapshot. - -Because of (1), migrating an existing test to a snapshot macro does not lose any -compile-time or runtime coverage — it only *adds* a snapshot assertion on top. +This crate exposes a family of `snapshot_*!` procedural macros that pin the code the +core CGP macros generate. Each one does **two** things at once: it emits the real +generated code into the surrounding module, exactly as the underlying CGP macro +would — so the traits, structs, and impls stay live and the rest of the module can +still wire them up and assert their runtime behavior — and it generates a `#[test]` +that asserts a pretty-printed string of that same code against an inline +[`insta`](https://insta.rs) snapshot. Adding or removing a snapshot therefore +changes only the golden assertion, never the compile-time or runtime coverage. + +Two documents carry what this README does not. The +[implementation document](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/entrypoints/snapshot_macros.md) +in the knowledge base explains how the macros are built and what an invocation +expands to; [crates/tests/AGENTS.md](../../tests/AGENTS.md) sets the convention for +*when* to snapshot — a macro's expansion is pinned only in the concept target that +owns that macro's feature, and written plainly everywhere else. ## Crate layout -This is the proc-macro crate. It is a thin shell: every macro simply forwards to -the implementation crate [`cgp-macro-test-util-lib`](../cgp-macro-test-util-lib), -which is a normal library crate so that the logic can be unit-tested without the -`proc-macro = true` restriction. +This is the proc-macro crate, a thin shell: every macro forwards to +[`cgp-macro-test-util-lib`](../cgp-macro-test-util-lib), a normal library crate so +the logic can be unit-tested without the `proc-macro = true` restriction. ``` cgp-macro-test-util/ # proc-macro entry points (#[proc_macro] fns) @@ -50,9 +32,8 @@ cgp-macro-test-util/ # proc-macro entry points (#[proc_macro] fns) └── functions/ # parse_attribute, pretty_format ``` -The implementation reuses the real macro logic from `cgp-macro-lib` -(`cgp_macro_lib::cgp_component`, `::cgp_impl`, `::cgp_getter`, etc.), so the -snapshot output is guaranteed to match what the production macros generate. +The implementation calls the real macro logic in `cgp-macro-lib`, not a copy of it, +so a snapshot is guaranteed to show what the production macros generate. ## Available macros @@ -74,29 +55,15 @@ snapshot output is guaranteed to match what the production macros generate. | `snapshot_delegate_and_check_components!` | `delegate_and_check_components!` | | `snapshot_cgp_namespace!` | `cgp_namespace!` | -## Anatomy of a snapshot invocation - -Every snapshot macro takes two parts: - -1. **The item under test**, written *exactly* as you would normally write the - underlying CGP macro invocation (the attribute plus the `trait` / `impl`, or - the full `delegate_components! { ... }` call). -2. **A test block** of the form: +Each accepts the same argument forms as the macro it wraps — `snapshot_cgp_component!` +takes both `#[cgp_component(Greeter)]` and the brace form, for instance — precisely +because it drives the production entry function. - ```text - () { - > - } - ``` +## Anatomy of an invocation - - `` becomes the name of the generated `#[test]` function, so it - must be unique within the module. - - `` is the identifier bound to the pretty-printed `&str` of the - generated code inside the test body. It is conventionally named `output`. - - The body is typically a single `insta::assert_snapshot!(, @"...")` - call with an inline snapshot. - -For example: +Every snapshot macro takes the item under test, written exactly as you would write +the underlying macro invocation, followed by a test block naming the generated +`#[test]` and the identifier bound to the pretty-printed expansion: ```rust use cgp::prelude::*; @@ -115,478 +82,31 @@ snapshot_cgp_getter! { } ``` -This expands to roughly: - -```rust -// (1) the real generated code, identical to what `#[cgp_getter]` produces: -pub trait HasName { /* ... */ } -impl<__Context__> HasName for __Context__ where /* ... */ { /* ... */ } -pub trait NameGetter<__Context__>: /* ... */ { /* ... */ } -pub struct NameGetterComponent; -/* ...UseContext / UseField / UseFields / WithProvider / RedirectLookup impls... */ - -// (2) the generated snapshot test: -#[test] -fn expand_has_name() { - let output = "...pretty-printed generated code..."; - assert_snapshot!(output, @"...generated code..."); -} -``` - -### `snapshot_cgp_component!` - -```rust -snapshot_cgp_component! { - #[cgp_component(Greeter)] - pub trait CanGreet: HasName { - fn greet(&self) -> String; - } - - expand_greeter(output) { - assert_snapshot!(output, @"...") - } -} -``` - -Both the parenthesized form `#[cgp_component(Greeter)]` and the brace form -`#[cgp_component { provider: Greeter, ... }]` are accepted, mirroring the real -macro. - -### `snapshot_cgp_impl!` - -```rust -snapshot_cgp_impl! { - #[cgp_impl(new ValueToString)] - impl FooProvider for Context { - fn foo(&self, value: u32) -> String { - value.to_string() - } - } - - expand_value_to_string(output) { - assert_snapshot!(output, @"...") - } -} -``` - -### `snapshot_cgp_provider!` - -Wraps `#[cgp_provider]` provider impls. The item under test is the full provider -`impl` written exactly as you would normally write it under `#[cgp_provider]` — -the provider struct itself is expected to already be defined elsewhere: - -```rust -pub struct GreetHello; - -snapshot_cgp_provider! { - #[cgp_provider] - impl Greeter for GreetHello - where - Context: HasName, - { - fn greet(context: &Context) { - println!("Hello, {}!", context.name()); - } - } - - expand_greet_hello(output) { - assert_snapshot!(output, @"...") - } -} -``` - -Both the default form `#[cgp_provider]` and the explicit component name form -`#[cgp_provider(GreeterComponent)]` are accepted, mirroring the real macro. In -addition to re-emitting the provider impl, the snapshot captures the generated -`IsProviderFor` impl. - -### `snapshot_cgp_new_provider!` - -Has the identical shape as `snapshot_cgp_provider!`, but wraps -`#[cgp_new_provider]`, which additionally defines the provider struct. The -snapshot therefore also captures the generated `struct` definition: - -```rust -snapshot_cgp_new_provider! { - #[cgp_new_provider] - impl Greeter for GreetHello - where - Context: HasName, - { - fn greet(context: &Context) { - println!("Hello, {}!", context.name()); - } - } - - expand_greet_hello(output) { - assert_snapshot!(output, @"...") - } -} -``` - -Both the default form `#[cgp_new_provider]` and the explicit component name form -`#[cgp_new_provider(GreeterComponent)]` are accepted, mirroring the real macro. - -### `snapshot_cgp_auto_getter!` / `snapshot_cgp_getter!` - -```rust -snapshot_cgp_auto_getter! { - #[cgp_auto_getter] - pub trait HasName { - fn name(&self) -> &str; - } - - expand_has_name(output) { - assert_snapshot!(output, @"...") - } -} -``` - -`snapshot_cgp_getter!` has the identical shape and also accepts the custom -provider name forms `#[cgp_getter(NameGetter)]` and -`#[cgp_getter { provider: ..., name: ... }]`. - -### `snapshot_cgp_fn!` - -The item under test is the full `#[cgp_fn]` function, written exactly as you -would normally write it — including any extra attributes such as `#[uses(...)]`, -`#[extend(...)]`, `#[extend_where(...)]`, `#[use_type(...)]`, `#[impl_generics(...)]`, -or `#[async_trait]`, all kept above the `fn` with `#[cgp_fn]` first: - -```rust -snapshot_cgp_fn! { - #[cgp_fn] - pub fn rectangle_area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 { - width * height - } - - expand_rectangle_area(output) { - assert_snapshot!(output, @"...") - } -} -``` - -Both the default form `#[cgp_fn]` and the custom trait name form -`#[cgp_fn(CanCalculateRectangleArea)]` are accepted, mirroring the real macro. - -### `snapshot_cgp_type!` - -Wraps `#[cgp_type]` abstract-type traits. The item under test is written exactly -as you would normally write the `#[cgp_type]` invocation: - -```rust -snapshot_cgp_type! { - #[cgp_type] - pub trait HasScalarType { - type Scalar; - } - - expand_has_scalar_type(output) { - assert_snapshot!(output, @"...") - } -} -``` - -Both the default form `#[cgp_type]` and the custom provider name forms -`#[cgp_type(ScalarTypeProvider)]` and -`#[cgp_type { provider: ... }]` are accepted, mirroring the -real macro. In addition to the usual `#[cgp_component]` output, the snapshot -captures the extra `UseType` / `WithProvider` providers that `#[cgp_type]` -generates. - -### `snapshot_derive_has_field!` - -Wraps the `#[derive(HasField)]` derive macro. The item under test is the -`struct` definition written exactly as you would normally write it under -`#[derive(HasField)]`: - -```rust -snapshot_derive_has_field! { - #[derive(HasField)] - pub struct Inner { - pub name: String, - } - - expand_inner(output) { - assert_snapshot!(output, @"...") - } -} -``` - -Unlike the attribute-based snapshot macros, the derive is re-emitted verbatim -above the struct (so the real `HasField` / `HasFieldMut` impls are still -generated by the compiler), while the snapshot captures the *derived* impls, -i.e. the `HasField` and `HasFieldMut` impls that `#[derive(HasField)]` produces -for each field. - -All struct shapes accepted by `#[derive(HasField)]` are accepted, since the body -is forwarded to the real derive verbatim: - -- **named-field structs**, keyed by `Symbol!("...")` field-name tags; -- **tuple structs**, keyed by `Index` positional tags; -- **structs with lifetime parameters**, whose lifetimes are propagated to the - generated impls. - -Because the snapshot macro emits a `#[test]` function, a `#[derive(HasField)]` -struct that currently lives *inside* a test function must be lifted out before it -can be wrapped — see [Migrating existing tests](#migrating-existing-tests). The -usual pattern is to move the struct into an inner `mod` alongside the snapshot -macro, and keep the original `#[test]` in that same module so its runtime -assertions are preserved. - -### `snapshot_derive_has_fields!` - -Wraps the `#[derive(HasFields)]` derive macro. The item under test is the type -definition written exactly as you would normally write it under -`#[derive(HasFields)]`, with any other derives kept alongside it: - -```rust -snapshot_derive_has_fields! { - #[derive(HasFields)] - #[derive(Clone, Debug, Eq, PartialEq)] - pub struct Person { - pub name: String, - } - - expand_person(output) { - assert_snapshot!(output, @"...") - } -} -``` - -Like the other derive snapshot macros, the type is re-emitted verbatim with its -derives (so the compiler still generates the real impls), while the snapshot -captures the `HasFields` / `HasFieldsRef` / `FromFields` / `ToFields` / -`ToFieldsRef` impls that `#[derive(HasFields)]` produces. - -Both **`struct`s** and **`enum`s** are accepted: - -- For a **`struct`** (a *record*), the field list is a product type - (`Cons` / `Nil`), and the conversions destructure/build the struct fields. -- For an **`enum`** (a *variant*), the field list is a sum type - (`Either` / `Void`), and the conversions `match` over the variants. - -Generic type parameters and lifetimes are propagated to the generated impls. -As with the other derive snapshot macros, a `#[derive(HasFields)]` type that -currently lives *inside* a test function must be lifted out before it can be -wrapped — see [Migrating existing tests](#migrating-existing-tests). The usual -pattern is to move each type into its own inner `mod` alongside the snapshot -macro, and keep the original `#[test]` in that same module so its runtime -assertions are preserved. - -### `snapshot_derive_cgp_data!` - -Wraps the `#[derive(CgpData)]` derive macro. `CgpData` is the umbrella derive for -extensible data types: rather than a single trait, it generates the *entire* -field/variant toolkit for a `struct` or `enum`. The item under test is the type -definition written exactly as you would normally write it under -`#[derive(CgpData)]`, with any other derives kept alongside it: - -```rust -snapshot_derive_cgp_data! { - #[derive(CgpData)] - #[derive(Debug, Eq, PartialEq)] - pub struct FooBarBaz { - pub foo: u64, - pub bar: String, - pub baz: bool, - } - - expand_foo_bar_baz(output) { - assert_snapshot!(output, @"...") - } -} -``` - -Like `snapshot_derive_has_field!`, the type is re-emitted verbatim with its -derives (so the compiler still generates the real impls), while the snapshot -captures the *derived* impls that `#[derive(CgpData)]` produces. Because -`CgpData` generates a large amount of code, these snapshots are correspondingly -large — but that is precisely what makes them valuable as golden tests. - -What gets captured depends on whether the item is a `struct` (a *record*) or an -`enum` (a *variant*): - -- For a **record `struct`**, the snapshot covers the per-field `HasField` / - `HasFieldMut` impls, the `HasFields` / `HasFieldsRef` field-list impls, the - `FromFields` / `ToFields` / `ToFieldsRef` conversions, and the generated - `__Partial*` builder struct together with its `HasBuilder` / `IntoBuilder` / - `PartialData` / `FinalizeBuild` / `UpdateField` impls. -- For a **variant `enum`**, the snapshot covers the `HasFields` / - `HasFieldsRef` impls, the `FromFields` / `ToFields` / `ToFieldsRef` - conversions, the per-variant `FromVariant` impls, the generated `__Partial*` - / `__PartialRef*` enums, and the `HasExtractor` / `HasExtractorRef` / - `HasExtractorMut` / `FinalizeExtract` / `ExtractField` extractor impls. - -All shapes accepted by `#[derive(CgpData)]` are accepted, since the body is -forwarded to the real derive verbatim — including generic type parameters (with -`where` clauses), tuple structs keyed by `Index`, and generic enums. - -As with the other derive snapshot macros, a `#[derive(CgpData)]` type that -currently lives *inside* a test function must be lifted out before it can be -wrapped — see [Migrating existing tests](#migrating-existing-tests). When several -auxiliary types share a file, the usual convention is to wrap only the primary -type under test in `snapshot_derive_cgp_data!`, and leave the auxiliary fixture -types as plain `#[derive(CgpData)]` definitions. - -### `snapshot_delegate_components!` - -Here the *whole* `delegate_components! { ... }` invocation is written verbatim, -followed by the test block: - -```rust -snapshot_delegate_components! { - delegate_components! { - new FooComponents { - Index<0>: u64, - Index<1>: String, - } - } - - expand_foo_components(output) { - assert_snapshot!(output, @"...") - } -} -``` - -### `snapshot_check_components!` - -The *whole* `check_components! { ... }` invocation is written verbatim, followed -by the test block: - -```rust -snapshot_check_components! { - check_components! { - App { - ErrorRaiserComponent: String, - } - } - - expand_check_app(output) { - assert_snapshot!(output, @"...") - } -} -``` - -The body is forwarded to the real macro verbatim, so every `check_components!` -form is accepted — including the `#[check_trait(...)]` and -`#[check_providers(...)]` attributes, `#[check_params(...)]` / generic -parameters, the array syntax for grouping components and params, and multiple -check specs in a single invocation. The snapshot captures the generated check -trait(s) and their `impl` blocks. - -Note that the snapshot macro emits the same check trait/impls into the -surrounding module, so the compile-time wiring check is preserved — it only -*adds* a snapshot assertion on top. - -### `snapshot_delegate_and_check_components!` - -Likewise, the *whole* `delegate_and_check_components! { ... }` invocation is -written verbatim, followed by the test block: - -```rust -snapshot_delegate_and_check_components! { - delegate_and_check_components! { - #[check_trait(CheckMyContext)] - MyContext { - NameTypeProviderComponent: UseType, - NameGetterComponent: UseField, - } - } - - expand_my_context(output) { - assert_snapshot!(output, @"...") - } -} -``` - -All `delegate_and_check_components!` forms are accepted, since the body is -forwarded to the real macro verbatim — including `#[check_trait(...)]`, -`#[check_params(...)]`, `#[skip_check]`, generic parameters, and array syntax. -The snapshot captures both the generated `DelegateComponent` / `IsProviderFor` -impls *and* the generated check trait + impls. - -### `snapshot_cgp_namespace!` - -Like `snapshot_delegate_components!`, the *whole* `cgp_namespace! { ... }` -invocation is written verbatim, followed by the test block: - -```rust -snapshot_cgp_namespace! { - cgp_namespace! { - new MyNamespace { - FooProviderComponent => - @MyApp.MyFooComponent, - } - } - - expand_my_namespace(output) { - assert_snapshot!(output, @"...") - } -} -``` - -All `cgp_namespace!` forms are accepted, since the body is forwarded to the real -macro verbatim — including parent namespaces (`new Extended: DefaultNamespace { ... }`), -symbol/type path keys (`@my_app.MyFooComponent`), and array keys. +That emits the real `#[cgp_getter]` expansion — the consumer trait and its blanket +impl, the provider trait, the marker struct, and the `UseField`/`UseFields`/ +`RedirectLookup` provider impls — and then a `fn expand_has_name()` holding the same +code as a string. The test-function name must be unique within its module, so give +each snapshot in a file a distinct one. ## Workflow with `insta` -Write the test with an **empty** inline snapshot first: - -```rust -expand_has_name(output) { - assert_snapshot!(output, @"") -} -``` - -Then let `insta` fill it in: +Write the test with an **empty** inline snapshot first (`assert_snapshot!(output, @"")`), +then let `insta` fill it in: ```bash -# review interactively -cargo insta test -p cgp-tests --test getter -cargo insta review - -# or accept everything non-interactively -cargo insta test -p cgp-tests --accept - -# or via the env var -INSTA_UPDATE=always cargo test -p cgp-tests +cargo insta test -p cgp-tests --test getter # then: cargo insta review +cargo insta test -p cgp-tests --accept # accept everything non-interactively +INSTA_UPDATE=always cargo test -p cgp-tests # or via the env var ``` -After the first accept, the inline `@"..."` is populated with the pretty-printed -generated code. On subsequent runs the test fails if the generated code changes, -showing a diff you can re-accept once you've confirmed the change is intended. - -## Migrating existing tests - -When migrating an existing macro test, two situations come up: - -- **Module-level macro use** — wrap the existing `#[cgp_component]` / `#[cgp_impl]` - / `#[cgp_getter]` / `#[cgp_auto_getter]` / `#[derive(HasField)]` / - `delegate_components!` in the matching `snapshot_*!` macro and append a test - block. Nothing else needs to change, since the snapshot macro re-emits the same - code. - -- **CGP components defined *inside* a test function** — the snapshot macro - generates a `#[test]` function, which cannot be nested inside another function. - So the CGP components must be lifted out of the function body: - - - If the test is **purely compile-time** (it only checks that the wiring - compiles, with no runtime assertions), turn the whole test into an inner - `mod` and place the snapshot macro there. - - If the test has **runtime assertions**, create an inner `mod` that holds the - extracted CGP components wrapped in the snapshot macro, and keep the original - `#[test]` function, now referencing the items from the inner module. This - preserves the runtime coverage while adding the snapshot assertion. +After the first accept, the inline `@"..."` holds the pretty-printed generated code. +On later runs the test fails if that code changes, showing a diff to re-accept once +you have confirmed the change is intended. -## Notes / limitations +## Notes and limitations -- The pretty-printing is done with - [`prettyplease`](https://crates.io/crates/prettyplease), and any - macro-prelude noise is stripped beforehand - (`cgp_macro_core::functions::strip_macro_prelude`), so the snapshot is stable, - readable Rust source. -- The `` must be unique within its module — when a single file - contains several snapshots, give each a distinct name (e.g. `expand_has_name`, - `expand_greeter`). +- Pretty-printing goes through [`prettyplease`](https://crates.io/crates/prettyplease), + with macro-prelude noise stripped first (`cgp_macro_core::functions::strip_macro_prelude`), + so a snapshot is stable, readable Rust source. +- A derive member re-emits the annotated item itself, since a derive macro produces + only the code it adds and not the struct or enum it decorates. diff --git a/crates/macros/cgp-macro-test-util/src/lib.rs b/crates/macros/cgp-macro-test-util/src/lib.rs index 99dc5b79..6d6c2510 100644 --- a/crates/macros/cgp-macro-test-util/src/lib.rs +++ b/crates/macros/cgp-macro-test-util/src/lib.rs @@ -112,3 +112,24 @@ pub fn snapshot_derive_cgp_data(body: TokenStream) -> TokenStream { .unwrap_or_else(syn::Error::into_compile_error) .into() } + +#[proc_macro] +pub fn snapshot_derive_build_field(body: TokenStream) -> TokenStream { + entrypoints::snapshot_derive_build_field(body.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro] +pub fn snapshot_derive_extract_field(body: TokenStream) -> TokenStream { + entrypoints::snapshot_derive_extract_field(body.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro] +pub fn snapshot_derive_from_variant(body: TokenStream) -> TokenStream { + entrypoints::snapshot_derive_from_variant(body.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} diff --git a/crates/main/cgp-base-extra/src/lib.rs b/crates/main/cgp-base-extra/src/lib.rs index 0683d3ad..ffdda3d0 100644 --- a/crates/main/cgp-base-extra/src/lib.rs +++ b/crates/main/cgp-base-extra/src/lib.rs @@ -2,5 +2,4 @@ pub mod macro_prelude; -pub use cgp_base::{base_types, component}; -pub use cgp_type as types; +pub use cgp_base::{component, types}; diff --git a/crates/main/cgp-base/src/lib.rs b/crates/main/cgp-base/src/lib.rs index b33a4759..c0e3c494 100644 --- a/crates/main/cgp-base/src/lib.rs +++ b/crates/main/cgp-base/src/lib.rs @@ -1,4 +1,4 @@ pub mod macro_prelude; -pub use cgp_base_types as base_types; +pub use cgp_base_types::*; pub use cgp_component as component; diff --git a/crates/main/cgp-core/src/lib.rs b/crates/main/cgp-core/src/lib.rs index 63d72983..f94af85e 100644 --- a/crates/main/cgp-core/src/lib.rs +++ b/crates/main/cgp-core/src/lib.rs @@ -6,6 +6,6 @@ pub mod prelude; pub use prelude as macro_prelude; #[doc(inline)] pub use { - cgp_async_macro::async_trait, cgp_component as component, cgp_error as error, + cgp_async_macro::async_trait, cgp_base as base, cgp_component as component, cgp_error as error, cgp_field as field, cgp_macro as macros, cgp_type as types, }; diff --git a/crates/main/cgp/README.md b/crates/main/cgp/README.md index d53d3ddd..8236647a 100644 --- a/crates/main/cgp/README.md +++ b/crates/main/cgp/README.md @@ -1,14 +1,13 @@ - -# `cgp` - Context-Generic Programming in Rust +# `cgp` — Context-Generic Programming in Rust ## Overview -The `cgp` project contains a collection of micro Rust crates that empowers _context-generic programming_ (CGP), a new modular programming paradigm in Rust. +Context-Generic Programming (CGP) is a language extension for Rust, with pluggable trait implementations at compile-time. In ordinary Rust a trait has one implementation per type; CGP lets one trait have many interchangeable implementations and lets each *context* choose which one it uses, through a small wiring table the compiler resolves statically — so the flexibility costs nothing at runtime. The `cgp` crate is the facade over a collection of micro crates that provide it, and it re-exports everything through `cgp::prelude`. -To learn more about context-generic programming, check out the our website [contextgeneric.dev](https://contextgeneric.dev/), and our book [Context-Generic Programming Patterns](https://patterns.contextgeneric.dev/). +To learn more, see the website [contextgeneric.dev](https://contextgeneric.dev/) and the book [Context-Generic Programming Patterns](https://patterns.contextgeneric.dev/).
-At the moment, the `cgp` crate its constructs are mostly undocumented within Rustdoc. The best way to understand CGP is to read the book [Context-Generic Programming Patterns](https://patterns.contextgeneric.dev/). +The `cgp` constructs are still mostly undocumented within Rustdoc. The best way to learn CGP today is the book [Context-Generic Programming Patterns](https://patterns.contextgeneric.dev/); for the exhaustive per-construct semantics, see the [CGP knowledge base](https://github.com/contextgeneric/cgp-knowledge-base).
diff --git a/crates/tests/AGENTS.md b/crates/tests/AGENTS.md index ba70f9db..2ffaf438 100644 --- a/crates/tests/AGENTS.md +++ b/crates/tests/AGENTS.md @@ -162,8 +162,8 @@ diagnostic *and* how cargo-cgp presents it, and links the backing `cargo-cgp` UI 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 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 +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 error catalog, and a macro's `## Failure modes` section links out to the catalog class and its `cargo-cgp` fixture rather than to a local fixture. diff --git a/crates/tests/README.md b/crates/tests/README.md index 7bf38235..22ee1392 100644 --- a/crates/tests/README.md +++ b/crates/tests/README.md @@ -1,16 +1,16 @@ # CGP test suite -This directory holds the test suite for Context-Generic Programming. The tests -are organized **by CGP concept** — basic delegation, abstract types, implicit -arguments, namespaces, and so on — rather than by the macro that implements each -concept, because a single macro (for example `delegate_components!`) serves many -concepts at once. If you are maintaining or extending the suite, read -[AGENTS.md](AGENTS.md) first; it is the authoritative guide to the conventions. -This README is the map. +This directory holds the test suite for Context-Generic Programming, organized **by +CGP concept** — basic delegation, abstract types, implicit arguments, namespaces, +and so on — rather than by the macro that implements each concept, because a single +macro such as `delegate_components!` serves many concepts at once. This README is +the map of what is here and how to run it; [AGENTS.md](AGENTS.md) is the +authoritative guide to the conventions, and you should read it before adding, +moving, or refactoring a test. ## The crates -The suite is split into two kinds of crate, each with a distinct job. +The suite is split into two crates, each with a distinct job. **`cgp-tests`** is the main suite: realistic example code that must compile and run. Because much of CGP is compile-time wiring, a test here often passes simply @@ -22,46 +22,28 @@ where the canonical macro-expansion snapshots live. refuses during expansion — and for pinning the invalid tokens a macro currently emits. -**Post-codegen compile failures live 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 need a companion crate — were -migrated to `cargo-cgp`'s UI test suite, which snapshots the readable, root-cause-first -errors `cargo-cgp` renders for each class (its `.rust.stderr` still records the raw -compiler output as the "before"). `cargo-cgp` is CGP's first-class error toolchain, so -those diagnostics are pinned where the tool that improves them lives; the -[error catalog](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/errors/README.md) links each class to the fixture that -backs it. See -[cargo-cgp's UI tests](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/README.md). +A third category lives in another repository: the cases where a macro *accepts* +input whose *expansion* then fails to compile are UI fixtures in +[`cargo-cgp`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/README.md), +so each is pinned as the readable error the tool renders for it. AGENTS.md's +"Adding a failure case" says which of the three a new case belongs in. ## How the tests are laid out Inside `cgp-tests`, each concept is one **integration test target**, which Cargo -compiles as its own crate (its own coherence scope). A target is an entrypoint file -`tests/_tests.rs` plus a module directory `tests//` holding one -`.rs` file per unit test. Each unit-test file is self-contained: it defines its own -components, providers, and context types at module scope, so the type-level wiring -of one test never leaks into another. `tests/basic_delegation/` is the reference -example of this layout. +compiles as its own crate — and therefore its own coherence scope. A target is an +entrypoint file `tests/_tests.rs` plus a module directory +`tests//` holding one `.rs` file per unit test, each self-contained so the +type-level wiring of one test never leaks into another. `tests/basic_delegation/` +is the reference example of the layout. The concept targets currently cover: basic delegation, impl-side dependencies, implicit arguments, higher-order providers, generic components, abstract types, getters, field access, extensible records, extensible variants, checking, dispatching, namespaces, handlers, monadic handlers, async and Send bounds, and -blanket traits. This set grows and subdivides over time — when a concept -accumulates too many cases to stay coherent, it is split into finer targets. - -`cgp-macro-tests` follows the same target/`_tests.rs` shape: `ident_with_type_params` -for parser corner cases, and the failure-case targets `parser_rejections` and -`invalid_expansion`. - -## Snapshots - -Many tests assert the exact code a macro generates, using the `snapshot_*!` macros -from `cgp-macro-test-util`. Each such macro emits the real generated code into the -module **and** generates a `#[test]` asserting a pretty-printed inline `insta` -snapshot of it. Snapshots are used deliberately: a macro's expansion is snapshotted -only in the concept target that owns that macro's feature, and written plainly -everywhere else (see [AGENTS.md](AGENTS.md) for the ownership rules). +blanket traits. 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 +failure-case targets `parser_rejections` and `invalid_expansion`. ## Running the tests @@ -74,5 +56,9 @@ cargo insta test -p cgp-tests --review # review snapshot diffs interact cargo insta test -p cgp-tests --accept # accept intended snapshot changes ``` -When a `snapshot_*!` test fails it prints a diff of the generated code; accept the -new output with `cargo insta` only after confirming the change is intended. +Many tests assert the exact code a macro generates, through the `snapshot_*!` +macros from `cgp-macro-test-util`: each emits the real generated code into the +module *and* generates a `#[test]` asserting a pretty-printed inline `insta` +snapshot of it. So a failing snapshot prints a diff of the generated code — accept +it with `cargo insta` only after confirming the change is intended. Which target +owns a given macro's snapshot is a convention AGENTS.md sets out. diff --git a/crates/tests/cgp-macro-tests/Cargo.toml b/crates/tests/cgp-macro-tests/Cargo.toml index 01baee1b..3e067545 100644 --- a/crates/tests/cgp-macro-tests/Cargo.toml +++ b/crates/tests/cgp-macro-tests/Cargo.toml @@ -18,6 +18,7 @@ insta = { version = "1.48.0" } [dev-dependencies] cgp-macro-core = { workspace = true } cgp-macro-lib = { workspace = true } +cgp-macro-test-util-lib = { workspace = true } syn = { version = "2.0.95" } quote = { version = "1.0.38" } proc-macro2 = { version = "1.0.92" } diff --git a/crates/tests/cgp-macro-tests/tests/ident_with_type_params/path_with_type_args.rs b/crates/tests/cgp-macro-tests/tests/ident_with_type_params/path_with_type_args.rs index c6431011..923f3382 100644 --- a/crates/tests/cgp-macro-tests/tests/ident_with_type_params/path_with_type_args.rs +++ b/crates/tests/cgp-macro-tests/tests/ident_with_type_params/path_with_type_args.rs @@ -3,7 +3,7 @@ use cgp_macro_core::types::ident::{PathWithTypeArgs, TypeArg}; use quote::quote; -use syn::parse2; +use syn::{WherePredicate, parse2}; use super::{assert_idempotent, assert_parses, assert_rejects}; @@ -91,6 +91,71 @@ fn single_segment_path_has_no_args_for_bare_ident() { assert_eq!(parsed.path.segments.len(), 3); } +#[test] +fn merges_bindings_into_the_argument_list() { + // Associated-type bindings are appended to the path's *own* arguments as one + // list, which is the only spelling Rust accepts for a bound that both + // instantiates a generic trait and pins its associated type. Rendering the + // path's arguments and then a second group — `Foo` — is not a trait + // bound in any position. + let bare: Subject = parse2(quote!(Foo)).unwrap(); + assert_eq!( + bare.to_bound_tokens(&[quote!(Item = X)]).to_string(), + quote!(Foo).to_string(), + ); + + let generic: Subject = parse2(quote!(Foo)).unwrap(); + assert_eq!( + generic.to_bound_tokens(&[quote!(Item = X)]).to_string(), + quote!(Foo).to_string(), + ); + + // A lifetime has to keep leading the list while the binding trails it. + let lifetime: Subject = parse2(quote!(Foo<'a>)).unwrap(); + assert_eq!( + lifetime.to_bound_tokens(&[quote!(Item = X)]).to_string(), + quote!(Foo<'a, Item = X>).to_string(), + ); + + let several: Subject = parse2(quote!(path::to::Foo)).unwrap(); + assert_eq!( + several + .to_bound_tokens(&[quote!(Item = X), quote!(Other = Y)]) + .to_string(), + quote!(path::to::Foo).to_string(), + ); +} + +#[test] +fn renders_unchanged_without_bindings() { + // An empty binding list is the unpinned case, and must leave the path exactly as + // `ToTokens` would render it, so one code path serves both. + for tokens in [quote!(Foo), quote!(Foo), quote!(::path::to::Foo<'a, A>)] { + let parsed: Subject = parse2(tokens.clone()).unwrap(); + assert_eq!( + parsed.to_bound_tokens(&[]).to_string(), + tokens.to_string(), + "empty bindings must not alter the rendering", + ); + } +} + +#[test] +fn bound_output_parses_as_a_trait_bound() { + // The property that matters: whatever the merge emits has to be usable as a + // trait bound. Emitting two argument groups instead once failed *inside* the + // macro with a bare "failed to parse internal tokens" naming no cause, so the + // malformed form never reached the compiler where it could be diagnosed. + for path in [quote!(Foo), quote!(Foo), quote!(path::to::Foo<'a, A>)] { + let parsed: Subject = parse2(path.clone()).unwrap(); + let bound = parsed.to_bound_tokens(&[quote!(Item = X)]); + + parse2::(quote!(Self: #bound)).unwrap_or_else(|error| { + panic!("`{path}` with a binding must parse as a trait bound, got: {error}") + }); + } +} + #[test] fn round_trips() { assert_idempotent::(quote!(Foo)); 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 28b00919..882bd535 100644 --- a/crates/tests/cgp-macro-tests/tests/invalid_expansion/mod.rs +++ b/crates/tests/cgp-macro-tests/tests/invalid_expansion/mod.rs @@ -1,7 +1,8 @@ //! Failure cases where a CGP macro emits invalid or incorrect Rust. //! //! See the entrypoint `invalid_expansion_tests.rs` for the pattern to follow when -//! adding a case. This module is intentionally empty until the first genuine -//! invalid-expansion case is captured; keeping the target in place means the -//! harness is ready and a future agent only has to add a `pub mod ;` line -//! and the snapshot file. +//! 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; 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 new file mode 100644 index 00000000..61f4b68d --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/invalid_expansion/reserved_variant_names.rs @@ -0,0 +1,127 @@ +//! 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 1f9570d8..ac548d1f 100644 --- a/crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs +++ b/crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs @@ -15,7 +15,9 @@ //! 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. //! -//! No cases are enumerated yet; see crates/tests/AGENTS.md ("Migration status"). +//! 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. #![allow(dead_code)] pub mod invalid_expansion; diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/delegate_and_check_components.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/delegate_and_check_components.rs new file mode 100644 index 00000000..961b82a8 --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/delegate_and_check_components.rs @@ -0,0 +1,91 @@ +//! `delegate_and_check_components!` rejects the check attributes it cannot make +//! sense of: a `#[skip_check]` merged with a `#[check_params]` across a list key +//! and its element, two check attributes on one key, `#[skip_check]` given +//! arguments, an unrecognized per-entry attribute, and a table attribute that is +//! not `#[check_trait]`. +//! +//! The merge case is the one worth pinning: a list key's attribute is combined +//! with each element's rather than overridden by it, so the conflict only exists +//! once the two are merged and cannot be caught by looking at either alone. +//! +//! See cgp-knowledge-base/cgp/reference/macros/delegate_and_check_components.md +//! for the merge rules these enforce. + +use quote::quote; + +use super::assert_macro_rejects; + +#[test] +fn rejects_skip_check_merged_with_check_params() { + assert_macro_rejects( + "delegate_and_check_components merging #[skip_check] with #[check_params]", + || { + cgp_macro_lib::delegate_and_check_components(quote!( + MyApp { + #[skip_check] + [ + #[check_params(Rectangle)] + AreaCalculatorComponent, + ]: ShapeProvider, + } + )) + }, + ); +} + +#[test] +fn rejects_two_check_attributes_on_one_key() { + assert_macro_rejects( + "delegate_and_check_components with two check attributes on one key", + || { + cgp_macro_lib::delegate_and_check_components(quote!(MyApp { + #[check_params(Rectangle)] + #[check_params(Circle)] + AreaCalculatorComponent: ShapeProvider, + })) + }, + ); +} + +#[test] +fn rejects_skip_check_with_arguments() { + assert_macro_rejects( + "delegate_and_check_components with #[skip_check(..)] taking arguments", + || { + cgp_macro_lib::delegate_and_check_components(quote!(MyApp { + #[skip_check(Rectangle)] + AreaCalculatorComponent: ShapeProvider, + })) + }, + ); +} + +#[test] +fn rejects_unknown_entry_attribute() { + assert_macro_rejects( + "delegate_and_check_components with an unrecognized entry attribute", + || { + cgp_macro_lib::delegate_and_check_components(quote!(MyApp { + #[check_provider(RectangleArea)] + AreaCalculatorComponent: ShapeProvider, + })) + }, + ); +} + +#[test] +fn rejects_unknown_table_attribute() { + // The table accepts only `#[check_trait]`; `#[check_providers]` belongs to + // the standalone `check_components!` and is refused here rather than ignored. + assert_macro_rejects( + "delegate_and_check_components with #[check_providers] on the table", + || { + cgp_macro_lib::delegate_and_check_components(quote!( + #[check_providers(RectangleArea)] + MyApp { + AreaCalculatorComponent: ShapeProvider, + } + )) + }, + ); +} diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/delegate_components.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/delegate_components.rs index d97080fa..44400d24 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/delegate_components.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/delegate_components.rs @@ -4,6 +4,11 @@ //! rather than silently drop. It also rejects a braceless `open` header that //! lists more than one component, since the braceless form opens exactly one. //! +//! Three further rejections come from the body grammar rather than from +//! attributes, and each is a documented diagnostic whose wording is misleading +//! enough to be worth pinning: a statement written after a mapping, a braced path +//! group followed by more path, and a bounded generic list on a nested table. +//! //! See cgp-knowledge-base/cgp/implementation/entrypoints/delegate_components.md (Tests) for these //! failure cases, and cgp-knowledge-base/cgp/reference/macros/delegate_components.md for the //! user-facing semantics. @@ -71,3 +76,66 @@ fn rejects_attribute_on_inner_table_key() { }, ); } + +#[test] +fn rejects_statement_after_mapping() { + // Statements must lead the block. Once the parser has moved on to mappings it + // reads `open` as a *key* type, then looks for an operator and finds the + // component name — so the rejection is real but the message (`expected `:``) + // blames the component rather than the misplaced statement. + assert_macro_rejects( + "delegate_components with a statement written after a mapping", + || { + cgp_macro_lib::delegate_components(quote!( + Context { + BarComponent: Bar, + + open FooComponent; + + @FooComponent.String: Foo, + } + )) + }, + ); +} + +#[test] +fn rejects_braced_path_group_followed_by_more_path() { + // A braced group holds whole path *tails* and therefore terminates the path, + // unlike a bracketed group, which holds alternatives for one segment and may + // be followed by more. The trailing `.bool` is left where the mapping's + // operator should be, so this too reports `expected `:``. + assert_macro_rejects( + "delegate_components with a braced path group followed by more path", + || { + cgp_macro_lib::delegate_components(quote!( + Context { + open FooComponent; + + @FooComponent.{String, u32}.bool: Foo, + } + )) + }, + ); +} + +#[test] +fn rejects_bounded_generics_on_inner_table() { + // A nested table's name takes a bound-free generic list; a bound belongs on + // the entry's own generics instead. The value parser tries the nested-table + // form speculatively and falls back to reading the whole value as a plain + // type when it fails, so the message is an unrelated `expected `,`` rather + // than anything about generics. + assert_macro_rejects( + "delegate_components with a bounded generic list on a nested table", + || { + cgp_macro_lib::delegate_components(quote!( + Context { + BarKey: UseDelegate { + BazKey: BazValue, + }>, + } + )) + }, + ); +} diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs index 5c619e89..b61d00cd 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs @@ -28,8 +28,10 @@ pub mod cgp_impl; pub mod cgp_namespace; pub mod cgp_provider; pub mod check_components; +pub mod delegate_and_check_components; pub mod delegate_components; pub mod derive_cgp_data; pub mod derive_from_variant; pub mod getters; +pub mod use_provider; pub mod use_type; diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/use_provider.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/use_provider.rs new file mode 100644 index 00000000..232b94c6 --- /dev/null +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/use_provider.rs @@ -0,0 +1,78 @@ +//! `#[use_provider]` binds exactly one inner provider per attribute, so a +//! comma-separated list of provider-and-trait pairs does not parse. +//! +//! The attribute's own argument ends in a `+`-separated bound list, which is +//! consumed to the end of the input. A comma after the first pair therefore lands +//! where the bound parser expects a `+`, and the whole attribute is rejected — +//! which makes this attribute the exception to the one-attribute-comma-separated +//! convention `#[uses]` and `#[use_type]` follow. Binding several inner providers +//! means stacking one attribute per provider. +//! +//! Both hosts that collect the attribute are covered, since the rejection comes +//! from the shared argument parser rather than from either host. +//! +//! See cgp-knowledge-base/cgp/reference/attributes/use_provider.md for the +//! user-facing rule and cgp-knowledge-base/cgp/implementation/asts/attributes/use_provider.md +//! for the parser. + +use quote::quote; + +use super::assert_macro_rejects; + +#[test] +fn rejects_comma_separated_pairs_on_impl() { + assert_macro_rejects("use_provider with a comma-separated pair list", || { + cgp_macro_lib::cgp_impl( + quote!(new ScaledAreaCalculator), + quote!( + #[use_provider(InnerA: AreaCalculator, InnerB: AreaCalculator)] + impl AreaCalculator { + fn area(&self) -> f64 { + InnerA::area(self) + InnerB::area(self) + } + } + ), + ) + }); +} + +#[test] +fn rejects_comma_separated_pairs_on_fn() { + assert_macro_rejects( + "use_provider with a comma-separated pair list on a fn", + || { + cgp_macro_lib::cgp_fn( + quote!(), + quote!( + #[use_provider(InnerA: AreaCalculator, InnerB: AreaCalculator)] + pub fn total_area(&self) -> f64 { + todo!() + } + ), + ) + }, + ); +} + +#[test] +fn accepts_plus_separated_bounds_on_one_provider() { + // The counterpart the rejection above is easy to confuse with: `+` continues + // *one* provider's bound list and is accepted, so the comma is what fails + // rather than the presence of two bounds. + let result = cgp_macro_lib::cgp_impl( + quote!(new BothCalculator), + quote!( + #[use_provider(Inner: AreaCalculator + PerimeterCalculator)] + impl AreaCalculator { + fn area(&self) -> f64 { + Inner::area(self) + } + } + ), + ); + + assert!( + result.is_ok(), + "expected `+`-separated bounds on one provider to be accepted", + ); +} diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/use_type.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/use_type.rs index 68e48a84..afc413cf 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/use_type.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/use_type.rs @@ -1,11 +1,12 @@ //! `#[use_type]` rejects imports it cannot lower unambiguously: a type-equality -//! constraint on a `#[cgp_component]` trait, and two imports that resolve to the -//! same identifier or alias — whether across specs or within one braced list, and -//! on any host macro. +//! constraint on a `#[cgp_component]` trait, two imports that resolve to the same +//! identifier or alias — whether across specs or within one braced list, and on any +//! host macro — and an import list whose contexts resolve through one another in a +//! cycle, which has no valid grounding order. //! -//! See cgp-knowledge-base/cgp/implementation/asts/attributes/README.md (Tests) for these -//! failure cases and cgp-knowledge-base/cgp/reference/attributes/use_type.md for the -//! user-facing semantics. +//! See cgp-knowledge-base/cgp/implementation/asts/attributes/use_type.md (Tests) for +//! these failure cases and cgp-knowledge-base/cgp/reference/attributes/use_type.md for +//! the user-facing semantics. use quote::quote; @@ -62,6 +63,92 @@ fn rejects_duplicate_alias_on_component() { }); } +#[test] +fn rejects_self_referential_context() { + // An import whose `in Context` names its own alias asks for the context to be + // grounded before itself — a one-node cycle. + assert_macro_rejects("use_type import whose context is its own alias", || { + cgp_macro_lib::cgp_fn( + quote!(), + quote!( + #[use_type(HasAType.A in A)] + pub fn get_a(&self) -> A { + todo!() + } + ), + ) + }); +} + +#[test] +fn rejects_context_cycle_between_specs() { + // Two imports each naming the other's alias as its context: every grounding + // order needs one of the two resolved first, so neither can be. + assert_macro_rejects("use_type contexts forming a two-spec cycle", || { + cgp_macro_lib::cgp_fn( + quote!(), + quote!( + #[use_type(HasAType.A in B, HasBType.B in A)] + pub fn deep(&self) -> A { + todo!() + } + ), + ) + }); +} + +#[test] +fn rejects_context_cycle_across_three_specs() { + // The cycle is found however long it is, so a three-hop loop is rejected the + // same way — a search that only compared adjacent pairs would miss this. + assert_macro_rejects("use_type contexts forming a three-spec cycle", || { + cgp_macro_lib::cgp_fn( + quote!(), + quote!( + #[use_type(HasAType.A in C, HasBType.B in A, HasCType.C in B)] + pub fn deep(&self) -> A { + todo!() + } + ), + ) + }); +} + +#[test] +fn rejects_cycle_through_a_trait_argument() { + // Grounding resolves an alias in a trait path's generic arguments as well as in + // an `in Context` clause, so a cycle running through that position is a cycle + // too: `HasPoolType` needs `Pool`, which is projected against it. + assert_macro_rejects("use_type cycle through a trait argument", || { + cgp_macro_lib::cgp_fn( + quote!(), + quote!( + #[use_type(HasPoolType.Pool)] + pub fn get_pool(&self) -> Pool { + todo!() + } + ), + ) + }); +} + +#[test] +fn rejects_cycle_on_component() { + // The cycle check runs on a component trait definition too, not only on impls + // and functions, since grounding is the same three-step transform there. + assert_macro_rejects("use_type context cycle on a component trait", || { + cgp_macro_lib::cgp_component( + quote!(FooProvider), + quote!( + #[use_type(HasAType.A in B, HasBType.B in A)] + pub trait CanFoo { + fn foo(&self) -> A; + } + ), + ) + }); +} + #[test] fn rejects_duplicate_alias_within_one_braced_list() { // Two entries of one braced list aliasing to the same name are also a diff --git a/crates/tests/cgp-tests/tests/abstract_types/mod.rs b/crates/tests/cgp-tests/tests/abstract_types/mod.rs index 612a5c40..2ec72b1b 100644 --- a/crates/tests/cgp-tests/tests/abstract_types/mod.rs +++ b/crates/tests/cgp-tests/tests/abstract_types/mod.rs @@ -19,21 +19,31 @@ pub mod use_type_component; pub mod use_type_foreign; pub mod use_type_generic_param; pub mod use_type_path_qualified; +pub mod use_type_trait_arg_component; // The `#[use_type]` attribute rewriting abstract types inside `#[cgp_fn]`: the -// bare alias, alias renaming, type-equality bounds, and foreign/nested type -// sources (including a two-hop foreign chain). These keep the `#[cgp_fn]` snapshot -// because the abstract-type rewrite is the point (the `#[cgp_fn]` expansion itself -// is owned by `implicit_arguments`). +// bare alias, alias renaming, type-equality bounds, foreign/nested type sources +// (including a two-hop foreign chain), the grounding of an alias that appears in +// the imported trait's own generic arguments, and the arrangements resolution must +// be indifferent to — a reversed chain, a shared context, and specs split across +// stacked attributes. These keep the `#[cgp_fn]` snapshot because the abstract-type +// rewrite is the point (the `#[cgp_fn]` expansion itself is owned by +// `implicit_arguments`). pub mod use_type_fn_alias; pub mod use_type_fn_deep_foreign; pub mod use_type_fn_equality; pub mod use_type_fn_equality_cross_trait; +pub mod use_type_fn_equality_nested; +pub mod use_type_fn_expr_path; pub mod use_type_fn_extend; pub mod use_type_fn_foreign; pub mod use_type_fn_foreign_equality; pub mod use_type_fn_foreign_equality_cross_trait; +pub mod use_type_fn_generic_trait_equality; pub mod use_type_fn_nested_foreign; pub mod use_type_fn_reverse_order; +pub mod use_type_fn_shared_context; +pub mod use_type_fn_stacked_attributes; +pub mod use_type_fn_trait_arg_alias; pub mod use_type_foreign_getter; pub mod use_type_uses_supertrait; diff --git a/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_equality_nested.rs b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_equality_nested.rs new file mode 100644 index 00000000..fc76d042 --- /dev/null +++ b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_equality_nested.rs @@ -0,0 +1,114 @@ +//! `#[use_type]` type-equality whose right-hand side *contains* an imported alias +//! rather than *being* one: `#[use_type(HasDbType.Db, HasTransactionType.{Transaction = Tx})]`. +//! +//! The cross-trait sibling `use_type_fn_equality_cross_trait` pins the case where +//! the pin's right-hand side *is* another alias (`{Bar as Baz = Foo}`). This file +//! pins the general rule that supersedes it: the right-hand side is an ordinary +//! type, and an imported alias is grounded wherever it occurs inside it. Both +//! shapes an alias can hide in are covered — nested in a generic argument list +//! (`Tx`) and inside a qualified path (`::Transaction`) — each +//! emitting a bound whose right-hand side names `::Db` rather +//! than a bare `Db` that resolves to nothing. +//! +//! The pinned alias itself is excluded from its own substitution, so a degenerate +//! self-pin stays the unresolved-name error it already was. +//! +//! See cgp-knowledge-base/cgp/reference/attributes/use_type.md and +//! cgp-knowledge-base/cgp/implementation/asts/attributes/use_type.md. + +use cgp_macro_test_util::snapshot_cgp_fn; + +pub trait Database: Sized { + type Transaction; +} + +pub struct Tx(pub core::marker::PhantomData); + +pub trait HasDbType { + type Db: Database; +} + +pub trait HasTransactionType { + type Transaction; +} + +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasDbType.Db, HasTransactionType.{Transaction = Tx})] + pub fn begin_nested(&self) -> Transaction { + todo!() + } + + expand_begin_nested(output) { + insta::assert_snapshot!(output, @" + pub trait BeginNested: HasDbType + HasTransactionType { + fn begin_nested(&self) -> ::Transaction; + } + impl<__Context__> BeginNested for __Context__ + where + Self: HasDbType, + Self: HasTransactionType::Db>>, + { + fn begin_nested(&self) -> ::Transaction { + todo!() + } + } + ") + } +} + +// The same nested pin with the imports written in the other order, so the pin's +// right-hand side names an alias declared *after* it. Resolution grounds each spec +// against the specs it depends on rather than the ones preceding it, so the emitted +// bound is identical; only the order the bounds are listed in follows the source. +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasTransactionType.{Transaction = Tx}, HasDbType.Db)] + pub fn begin_nested_reversed(&self) -> Transaction { + todo!() + } + + expand_begin_nested_reversed(output) { + insta::assert_snapshot!(output, @" + pub trait BeginNestedReversed: HasTransactionType + HasDbType { + fn begin_nested_reversed(&self) -> ::Transaction; + } + impl<__Context__> BeginNestedReversed for __Context__ + where + Self: HasTransactionType::Db>>, + Self: HasDbType, + { + fn begin_nested_reversed(&self) -> ::Transaction { + todo!() + } + } + ") + } +} + +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasDbType.Db, HasTransactionType.{Transaction = ::Transaction})] + pub fn begin_projected(&self) -> Transaction { + todo!() + } + + expand_begin_projected(output) { + insta::assert_snapshot!(output, @" + pub trait BeginProjected: HasDbType + HasTransactionType { + fn begin_projected(&self) -> ::Transaction; + } + impl<__Context__> BeginProjected for __Context__ + where + Self: HasDbType, + Self: HasTransactionType< + Transaction = <::Db as Database>::Transaction, + >, + { + fn begin_projected(&self) -> ::Transaction { + todo!() + } + } + ") + } +} diff --git a/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_expr_path.rs b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_expr_path.rs new file mode 100644 index 00000000..3e024416 --- /dev/null +++ b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_expr_path.rs @@ -0,0 +1,219 @@ +//! `#[use_type]` resolving an alias that *qualifies* an expression path: +//! `Transaction::begin_from(pool)` inside a `#[cgp_fn]` body. +//! +//! The substitution rewrites an imported alias in every type position, including a +//! `let` annotation inside the body, so leaving it unresolved as the qualifier of an +//! associated function or associated const was an inconsistency rather than a +//! boundary. Both are rewritten here into the qualified-type form +//! `<::Assoc>::item`, and the `let` annotation in the same body pins +//! that the type-position rewrite still works alongside it. +//! +//! The boundary the rewrite keeps is arity: a path of two or more segments can only +//! name an associated item of the type, while a bare single-segment path in +//! expression position is a *value* — a unit struct or an enum variant — which an +//! abstract type can never be, so it is left alone. `unit_struct_untouched` pins +//! that half: `Marker` in type position is the imported alias while `Marker` as an +//! expression stays the unit struct. +//! +//! See cgp-knowledge-base/cgp/reference/attributes/use_type.md and +//! cgp-knowledge-base/cgp/implementation/asts/attributes/use_type.md. + +use cgp_macro_test_util::snapshot_cgp_fn; + +pub trait Database: Sized { + type Row; +} + +pub struct Pool(pub core::marker::PhantomData); + +pub struct Tx(pub core::marker::PhantomData); + +pub struct Postgres; + +impl Database for Postgres { + type Row = String; +} + +pub trait CanBeginFrom { + const LABEL: &'static str; + + fn begin_from(pool: &Pool) -> Self; + + fn tagged(pool: &Pool) -> Self; +} + +impl CanBeginFrom for Tx { + const LABEL: &'static str = "pg"; + + fn begin_from(_pool: &Pool) -> Self { + Tx(core::marker::PhantomData) + } + + fn tagged(_pool: &Pool) -> Self { + Tx(core::marker::PhantomData) + } +} + +pub trait HasDbType { + type Db: Database; +} + +pub trait HasTransactionType { + type Transaction; +} + +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasDbType.Db, HasTransactionType.Transaction)] + pub fn begin_transaction(&self, #[implicit] db: &Pool) -> Transaction + where + Transaction: CanBeginFrom, + { + let started: Transaction = Transaction::begin_from(db); + started + } + + expand_begin_transaction(output) { + insta::assert_snapshot!(output, @" + pub trait BeginTransaction: HasDbType + HasTransactionType { + fn begin_transaction(&self) -> ::Transaction; + } + impl<__Context__> BeginTransaction for __Context__ + where + ::Transaction: CanBeginFrom<::Db>, + Self: HasField< + Symbol<2, Chars<'d', Chars<'b', Nil>>>, + Value = Pool<::Db>, + >, + Self: HasDbType, + Self: HasTransactionType, + { + fn begin_transaction(&self) -> ::Transaction { + let db: &Pool<::Db> = self + .get_field( + ::core::marker::PhantomData::>>>, + ); + let started: ::Transaction = <::Transaction>::begin_from( + db, + ); + started + } + } + ") + } +} + +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasDbType.Db, HasTransactionType.Transaction)] + pub fn transaction_label(&self) -> &'static str + where + Transaction: CanBeginFrom, + { + Transaction::LABEL + } + + expand_transaction_label(output) { + insta::assert_snapshot!(output, @" + pub trait TransactionLabel: HasDbType + HasTransactionType { + fn transaction_label(&self) -> &'static str; + } + impl<__Context__> TransactionLabel for __Context__ + where + ::Transaction: CanBeginFrom<::Db>, + Self: HasDbType, + Self: HasTransactionType, + { + fn transaction_label(&self) -> &'static str { + <::Transaction>::LABEL + } + } + ") + } +} + +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasDbType.Db, HasTransactionType.Transaction)] + pub fn tagged_transaction(&self, #[implicit] db: &Pool) -> Transaction + where + Transaction: CanBeginFrom, + { + // The alias appears twice: as the path qualifier, and inside a *later* segment's + // turbofish. Both are substituted — the rewrite recurses into the node it just + // replaced rather than stopping at it. + Transaction::tagged::(db) + } + + expand_tagged_transaction(output) { + insta::assert_snapshot!(output, @" + pub trait TaggedTransaction: HasDbType + HasTransactionType { + fn tagged_transaction(&self) -> ::Transaction; + } + impl<__Context__> TaggedTransaction for __Context__ + where + ::Transaction: CanBeginFrom<::Db>, + Self: HasField< + Symbol<2, Chars<'d', Chars<'b', Nil>>>, + Value = Pool<::Db>, + >, + Self: HasDbType, + Self: HasTransactionType, + { + fn tagged_transaction(&self) -> ::Transaction { + let db: &Pool<::Db> = self + .get_field( + ::core::marker::PhantomData::>>>, + ); + <::Transaction>::tagged::< + ::Db, + >(db) + } + } + ") + } +} + +pub struct Marker; + +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasDbType.{Db as Marker})] + pub fn unit_struct_untouched(&self, #[implicit] db: &Pool) -> Marker2 { + let _ = db; + Marker.into() + } + + expand_unit_struct_untouched(output) { + insta::assert_snapshot!(output, @" + pub trait UnitStructUntouched: HasDbType { + fn unit_struct_untouched(&self) -> Marker2; + } + impl<__Context__> UnitStructUntouched for __Context__ + where + Self: HasField< + Symbol<2, Chars<'d', Chars<'b', Nil>>>, + Value = Pool<::Db>, + >, + Self: HasDbType, + { + fn unit_struct_untouched(&self) -> Marker2 { + let db: &Pool<::Db> = self + .get_field( + ::core::marker::PhantomData::>>>, + ); + let _ = db; + Marker.into() + } + } + ") + } +} + +pub struct Marker2; + +impl From for Marker2 { + fn from(_: Marker) -> Self { + Marker2 + } +} diff --git a/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_generic_trait_equality.rs b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_generic_trait_equality.rs new file mode 100644 index 00000000..c34a0955 --- /dev/null +++ b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_generic_trait_equality.rs @@ -0,0 +1,135 @@ +//! `#[use_type]` type-equality on a trait path that already carries generic +//! arguments: `#[use_type(HasFooType.{Foo = u32})]`. +//! +//! A pin becomes an associated-type binding *inside* the trait path's own argument +//! list, appended after the arguments the path already carries, so the emitted +//! bound reads `Self: HasFooType`. It cannot be a second +//! angle-bracketed group after the path, because `HasFooType` is not +//! a trait bound at all — an arrangement that used to fail inside the macro with a +//! bare "failed to parse internal tokens" error naming no cause. +//! +//! The second case pins that the merge is per-spec and cumulative: two pins on one +//! generic trait land in the same argument list, while a plain generic import +//! beside them keeps its bare `Self: Trait` bound. +//! +//! See cgp-knowledge-base/cgp/implementation/asts/attributes/use_type.md and +//! cgp-knowledge-base/cgp/reference/attributes/use_type.md. + +use cgp_macro_test_util::snapshot_cgp_fn; + +pub trait HasFooType { + type Foo; +} + +pub trait HasPairType { + type Left; + + type Right; +} + +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasFooType.{Foo = u32})] + pub fn get_foo(&self) -> Foo { + 7 + } + + expand_get_foo(output) { + insta::assert_snapshot!(output, @" + pub trait GetFoo: HasFooType { + fn get_foo(&self) -> >::Foo; + } + impl<__Context__> GetFoo for __Context__ + where + Self: HasFooType, + { + fn get_foo(&self) -> >::Foo { + 7 + } + } + ") + } +} + +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasPairType.{Left = u32, Right = u64}, HasFooType.Foo)] + pub fn get_pair(&self) -> (Left, Right, Foo) { + todo!() + } + + expand_get_pair(output) { + insta::assert_snapshot!(output, @" + pub trait GetPair: HasPairType + HasFooType { + fn get_pair( + &self, + ) -> ( + >::Left, + >::Right, + >::Foo, + ); + } + impl<__Context__> GetPair for __Context__ + where + Self: HasPairType, + Self: HasFooType, + { + fn get_pair( + &self, + ) -> ( + >::Left, + >::Right, + >::Foo, + ) { + todo!() + } + } + ") + } +} + +pub trait HasRefType<'a> { + type Ref; +} + +// A pin merged after a *lifetime* argument, which is the ordering-sensitive case: +// Rust requires lifetimes to lead an argument list and associated-type bindings to +// trail it, so appending the binding to the existing arguments has to land on the +// far side of the lifetime. +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasRefType<'a>.{Ref = &'a str})] + pub fn get_ref<'a>(&self) -> Ref { + todo!() + } + + expand_get_ref(output) { + insta::assert_snapshot!(output, @" + pub trait GetRef<'a>: HasRefType<'a> { + fn get_ref(&self) -> >::Ref; + } + impl<'a, __Context__> GetRef<'a> for __Context__ + where + Self: HasRefType<'a, Ref = &'a str>, + { + fn get_ref(&self) -> >::Ref { + todo!() + } + } + ") + } +} + +pub struct App; + +impl HasFooType for App { + type Foo = u32; +} + +#[test] +fn test_pinned_generic_trait_import() { + // The pin makes the abstract `Foo` concrete, so the value flows out as a `u32` + // through a bound the macro could not previously even emit. + let foo: u32 = App.get_foo(); + assert_eq!(foo, 7); +} diff --git a/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_reverse_order.rs b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_reverse_order.rs index 0b72b656..31587097 100644 --- a/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_reverse_order.rs +++ b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_reverse_order.rs @@ -4,12 +4,14 @@ //! declared *after* it. //! //! This is the order-independence counterpart to `use_type_fn_deep_foreign`, which -//! writes the same chain front-to-back. Grounding iterates to a fixpoint over all -//! specs at once, so a spec may name a context imported by any other spec no matter -//! where it sits in the list; the bare `C` still rewrites to the three-hop +//! writes the same chain front-to-back. Grounding resolves each spec against the +//! specs it depends on rather than the ones preceding it, so a spec may name a +//! context imported by any other spec no matter where it sits in the list; the bare +//! `C` still rewrites to the three-hop //! `<<::A as HasB>::B as HasC>::C`. Only a genuine *cycle* — which has -//! no valid order at all — fails to ground; that acceptable failure is pinned in -//! the `use_type_cyclic_context` compile-fail fixture. +//! no valid order at all — cannot be grounded, and that is rejected at macro time +//! rather than lowered; the rejections are pinned in `cgp-macro-tests`' +//! `parser_rejections::use_type`. //! //! `deep` takes and returns a value of the deep type, so the test asserts a //! concrete value flows through the fully-grounded signature at runtime. diff --git a/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_shared_context.rs b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_shared_context.rs new file mode 100644 index 00000000..44b37a00 --- /dev/null +++ b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_shared_context.rs @@ -0,0 +1,66 @@ +//! `#[use_type]` where two imports name the *same* alias as their context — +//! `#[use_type(HasA.A, HasB.B in A, HasC.C in A)]` — a diamond in the grounding +//! graph rather than a chain. +//! +//! Grounding resolves each spec against every other, so a shared context grounds +//! once and both dependents pick it up: `B` and `C` are each projected against +//! `::A`. The case is worth pinning separately from the linear chain +//! because it is the shape a *cycle* check must not mistake for a cycle — two edges +//! into one node revisit that node without ever returning to a node still on the +//! search path, so the graph is acyclic and must be accepted. +//! +//! The chain cases are pinned by `use_type_fn_deep_foreign` (front-to-back) and +//! `use_type_fn_reverse_order` (back-to-front); the arrangements with no valid +//! grounding order at all are rejected at macro time and pinned in +//! `cgp-macro-tests`' `parser_rejections::use_type`. +//! +//! See cgp-knowledge-base/cgp/implementation/asts/attributes/use_type.md and +//! cgp-knowledge-base/cgp/reference/attributes/use_type.md. + +use cgp::prelude::*; + +#[cgp_type] +pub trait HasA { + type A; +} + +#[cgp_type] +pub trait HasB { + type B; +} + +#[cgp_type] +pub trait HasC { + type C; +} + +// Both `B` and `C` are imported from the same foreign context `A`, so grounding +// resolves `A` once and both specs project against the result. +#[cgp_fn] +#[use_type(HasA.A, HasB.B in A, HasC.C in A)] +pub fn combine(&self, left: B, right: C) -> (B, C) { + (left, right) +} + +pub struct Aa; + +impl HasB for Aa { + type B = u32; +} + +impl HasC for Aa { + type C = u64; +} + +pub struct App; + +impl HasA for App { + type A = Aa; +} + +#[test] +fn test_shared_context_grounds() { + // `B` grounds to `<::A as HasB>::B` (a `u32`) and `C` to the `u64` + // its sibling names, so both values flow through the shared context. + assert_eq!(App.combine(1u32, 2u64), (1u32, 2u64)); +} diff --git a/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_stacked_attributes.rs b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_stacked_attributes.rs new file mode 100644 index 00000000..73b559c8 --- /dev/null +++ b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_stacked_attributes.rs @@ -0,0 +1,103 @@ +//! Several `#[use_type]` attributes *stacked* on one item, rather than one +//! attribute carrying a comma-separated list. +//! +//! A host collector appends each attribute's specs to one list, so stacking is +//! expected to behave exactly like the combined form. The combined form is +//! recommended for readability, but the equivalence is a documented promise and is +//! pinned here — including for the case where it could plausibly break, a pin whose +//! right-hand side names an alias imported by a *different attribute* +//! (`#[use_type(HasFooType.{Foo = Vec})]` above `#[use_type(HasBarType.Bar)]`). +//! +//! Both stacking orders are pinned, because nothing about resolution may depend on +//! where a spec was written: it grounds each spec against the specs it depends on, +//! not against the ones that precede it. The only thing source order decides is the +//! order the emitted bounds are *listed* in, which the two snapshots below show. +//! +//! See cgp-knowledge-base/cgp/implementation/asts/attributes/use_type.md and +//! cgp-knowledge-base/cgp/reference/attributes/use_type.md. + +use cgp_macro_test_util::snapshot_cgp_fn; + +pub trait HasBarType { + type Bar; +} + +pub trait HasFooType { + type Foo; +} + +// The pinning attribute comes first, so its right-hand side names an alias the +// attribute *below* it imports. +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasFooType.{Foo = Vec})] + #[use_type(HasBarType.Bar)] + pub fn get_foo(&self) -> Foo { + Vec::new() + } + + expand_get_foo(output) { + insta::assert_snapshot!(output, @" + pub trait GetFoo: HasFooType + HasBarType { + fn get_foo(&self) -> ::Foo; + } + impl<__Context__> GetFoo for __Context__ + where + Self: HasFooType::Bar>>, + Self: HasBarType, + { + fn get_foo(&self) -> ::Foo { + Vec::new() + } + } + ") + } +} + +// The same two imports stacked the other way round. +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasBarType.Bar)] + #[use_type(HasFooType.{Foo = Vec})] + pub fn get_foo_reversed(&self) -> Foo { + Vec::new() + } + + expand_get_foo_reversed(output) { + insta::assert_snapshot!(output, @" + pub trait GetFooReversed: HasBarType + HasFooType { + fn get_foo_reversed(&self) -> ::Foo; + } + impl<__Context__> GetFooReversed for __Context__ + where + Self: HasBarType, + Self: HasFooType::Bar>>, + { + fn get_foo_reversed(&self) -> ::Foo { + Vec::new() + } + } + ") + } +} + +pub struct App; + +impl HasBarType for App { + type Bar = u32; +} + +impl HasFooType for App { + type Foo = Vec; +} + +#[test] +fn test_stacked_attributes_resolve() { + // `Foo` is pinned to `Vec<::Bar>`, so both functions return + // the context's `Vec` whichever order the attributes were stacked in. + let foo: Vec = App.get_foo(); + let reversed: Vec = App.get_foo_reversed(); + + assert!(foo.is_empty()); + assert!(reversed.is_empty()); +} diff --git a/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_trait_arg_alias.rs b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_trait_arg_alias.rs new file mode 100644 index 00000000..1e7eeca4 --- /dev/null +++ b/crates/tests/cgp-tests/tests/abstract_types/use_type_fn_trait_arg_alias.rs @@ -0,0 +1,143 @@ +//! `#[use_type]` grounding an imported alias that appears in a *trait path's own +//! generic arguments*: `#[use_type(HasDbType.Db, HasPoolType.Pool)]`. +//! +//! A trait argument ends up inside the emitted `>::Assoc` +//! path exactly as the context does, so an alias left bare in it names an +//! identifier that resolves to nothing. Grounding therefore resolves both +//! positions: `HasPoolType` becomes `HasPoolType<::Db>`, in +//! the rewritten signature and in the appended supertrait alike. +//! +//! This is the same rule that already grounds an `in Context` clause and a pin's +//! right-hand side, so the trait-argument position was an inconsistency rather than +//! a boundary — the alias resolved in two of the three positions it can occupy. +//! +//! The second case pins that grounding a trait argument composes with pinning the +//! type imported from it, since the merged argument list has to carry both the +//! grounded argument and the binding. +//! +//! See cgp-knowledge-base/cgp/implementation/asts/attributes/use_type.md and +//! cgp-knowledge-base/cgp/reference/attributes/use_type.md. + +use cgp_macro_test_util::snapshot_cgp_fn; + +pub trait HasDbType { + type Db; +} + +pub trait HasPoolType { + type Pool; +} + +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasDbType.Db, HasPoolType.Pool)] + pub fn get_pool(&self) -> Pool { + todo!() + } + + expand_get_pool(output) { + insta::assert_snapshot!(output, @" + pub trait GetPool: HasDbType + HasPoolType<::Db> { + fn get_pool(&self) -> ::Db>>::Pool; + } + impl<__Context__> GetPool for __Context__ + where + Self: HasDbType, + Self: HasPoolType<::Db>, + { + fn get_pool(&self) -> ::Db>>::Pool { + todo!() + } + } + ") + } +} + +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasDbType.Db, HasPoolType.{Pool = u32})] + pub fn get_pinned_pool(&self) -> Pool { + 11 + } + + expand_get_pinned_pool(output) { + insta::assert_snapshot!(output, @" + pub trait GetPinnedPool: HasDbType + HasPoolType<::Db> { + fn get_pinned_pool(&self) -> ::Db>>::Pool; + } + impl<__Context__> GetPinnedPool for __Context__ + where + Self: HasDbType, + Self: HasPoolType<::Db, Pool = u32>, + { + fn get_pinned_pool(&self) -> ::Db>>::Pool { + 11 + } + } + ") + } +} + +pub trait HasHandleType { + type Handle; +} + +// A three-hop chain threaded entirely through trait arguments rather than through +// `in Context` clauses, so each hop's argument must be grounded before the next can +// project against it. This is the trait-argument analogue of the context chain in +// `use_type_fn_deep_foreign`, and it exercises the same dependency-ordered +// resolution over the new position. +snapshot_cgp_fn! { + #[cgp_fn] + #[use_type(HasDbType.Db, HasPoolType.Pool, HasHandleType.Handle)] + pub fn get_handle(&self) -> Handle { + todo!() + } + + expand_get_handle(output) { + insta::assert_snapshot!(output, @" + pub trait GetHandle: HasDbType + HasPoolType< + ::Db, + > + HasHandleType<::Db>>::Pool> { + fn get_handle( + &self, + ) -> ::Db>>::Pool, + >>::Handle; + } + impl<__Context__> GetHandle for __Context__ + where + Self: HasDbType, + Self: HasPoolType<::Db>, + Self: HasHandleType<::Db>>::Pool>, + { + fn get_handle( + &self, + ) -> ::Db>>::Pool, + >>::Handle { + todo!() + } + } + ") + } +} + +pub struct Postgres; + +pub struct App; + +impl HasDbType for App { + type Db = Postgres; +} + +impl HasPoolType for App { + type Pool = u32; +} + +#[test] +fn test_grounded_trait_argument() { + // `HasPoolType<::Db>` resolves to `HasPoolType`, so + // both functions type-check against the one impl and return its `u32` pool. + assert_eq!(App.get_pinned_pool(), 11); +} diff --git a/crates/tests/cgp-tests/tests/abstract_types/use_type_trait_arg_component.rs b/crates/tests/cgp-tests/tests/abstract_types/use_type_trait_arg_component.rs new file mode 100644 index 00000000..e4492cde --- /dev/null +++ b/crates/tests/cgp-tests/tests/abstract_types/use_type_trait_arg_component.rs @@ -0,0 +1,71 @@ +//! A grounded trait argument across the idiomatic host pair: a `#[cgp_component]` +//! and a `#[cgp_impl]` provider that both import through +//! `#[use_type(HasDbType.Db, HasPoolType.Pool)]`, wired onto a real context. +//! +//! The two hosts consume the grounded spec through different paths — a component +//! turns a `Self` import into a *supertrait*, while an impl turns it into a `where` +//! predicate that also carries any pin — so this exercises both halves of the +//! transform on one shared abstract type. The provider additionally pins the +//! imported type (`{Pool = u32}`), which the impl-side path merges into the already +//! grounded trait argument as `HasPoolType<::Db, Pool = u32>`. +//! +//! The `#[cgp_fn]` counterparts, which pin the emitted tokens as snapshots, are +//! `use_type_fn_trait_arg_alias` and `use_type_fn_generic_trait_equality`; this file +//! pins that the same grounding survives component wiring and dispatches at run +//! time. +//! +//! See cgp-knowledge-base/cgp/implementation/asts/attributes/use_type.md and +//! cgp-knowledge-base/cgp/reference/attributes/use_type.md. + +use cgp::prelude::*; + +#[cgp_type] +pub trait HasDbType { + type Db; +} + +#[cgp_type] +pub trait HasPoolType { + type Pool; +} + +#[cgp_component(PoolSizeReader)] +#[use_type(HasDbType.Db, HasPoolType.Pool)] +pub trait CanReadPoolSize { + fn read_pool_size(&self) -> Pool; +} + +// The provider pins the abstract pool type to `u32` while still projecting it +// against the grounded `HasPoolType<::Db>`. +#[cgp_impl(new ReadPoolSize)] +#[use_type(HasDbType.Db, HasPoolType.{Pool = u32})] +impl PoolSizeReader { + fn read_pool_size(&self) -> Pool { + 42 + } +} + +pub struct Postgres; + +pub struct App; + +delegate_components! { + App { + DbTypeProviderComponent: UseType, + PoolTypeProviderComponent: UseType, + PoolSizeReaderComponent: ReadPoolSize, + } +} + +check_components! { + App { + PoolSizeReaderComponent, + } +} + +#[test] +fn test_grounded_trait_argument_wiring() { + // The context binds `Db` to `Postgres` and `Pool` to `u32`, which is what makes + // the provider's grounded-and-pinned bound hold. + assert_eq!(App.read_pool_size(), 42); +} diff --git a/crates/tests/cgp-tests/tests/basic_delegation/mod.rs b/crates/tests/cgp-tests/tests/basic_delegation/mod.rs index 750b88de..a0a4d6a0 100644 --- a/crates/tests/cgp-tests/tests/basic_delegation/mod.rs +++ b/crates/tests/cgp-tests/tests/basic_delegation/mod.rs @@ -19,6 +19,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_type; // `delegate_components!` shape variants (compile-time checks only). pub mod delegate_generic_nested_value; 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 new file mode 100644 index 00000000..8561e5f5 --- /dev/null +++ b/crates/tests/cgp-tests/tests/basic_delegation/self_local_assoc_type.rs @@ -0,0 +1,69 @@ +//! `Self::Assoc` inside a `#[cgp_impl]` body survives the rewrite when the block +//! declares `Assoc` itself. +//! +//! `#[cgp_impl]` rewrites `Self` into the context, which would break a component +//! whose provider supplies an associated type: `Self::Output` in a signature or a +//! body means the *provider's* own `type Output`, not something the context has. +//! The rewrite therefore collects the associated types the block declares and +//! skips any `Self::` path whose first segment names one of them, so the emitted +//! provider impl keeps `Self::Output` — where `Self` is now the provider struct +//! 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. +//! +//! 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::*; + +#[cgp_auto_getter] +pub trait HasName { + fn name(&self) -> &str; +} + +#[cgp_component(Labeller)] +pub trait CanLabel { + type Label; + + fn label(&self) -> Self::Label; +} + +#[cgp_impl(new LabelWithName)] +impl Labeller +where + Self: HasName, +{ + // The provider decides the associated type. Both the `Self::Label` in the + // return position and the one in the body name *this* item, so neither may be + // rewritten to the context — while `self.name()` must be. + type Label = String; + + fn label(&self) -> Self::Label { + let label: Self::Label = format!("<{}>", self.name()); + label + } +} + +#[derive(HasField)] +pub struct Person { + pub name: String, +} + +delegate_components! { + Person { + LabellerComponent: LabelWithName, + } +} + +#[test] +fn test_self_local_assoc_type() { + let person = Person { + name: "World".to_owned(), + }; + + assert_eq!(person.label(), ""); +} diff --git a/crates/tests/cgp-tests/tests/extensible_records/build_field_derive.rs b/crates/tests/cgp-tests/tests/extensible_records/build_field_derive.rs new file mode 100644 index 00000000..97a5c915 --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_records/build_field_derive.rs @@ -0,0 +1,367 @@ +//! `#[derive(BuildField)]` on its own: the builder slice of the record +//! machinery, and nothing else. +//! +//! The other record derives are supersets of this one, so it is easy to miss +//! what it emits by itself. The snapshot is the answer, and its shape is the +//! point: a `__Partial…` companion struct, the `HasBuilder`/`IntoBuilder` entry +//! points, `PartialData`, the all-present `FinalizeBuild`, a per-field +//! `UpdateField`, and a per-field `HasField` on the *partial* type — with no +//! `HasField` getters on the original struct and no `HasFields` representation +//! impls, which `#[derive(HasField)]` and `#[derive(HasFields)]` supply. +//! +//! Two capabilities come from field-crate blanket impls over the generated +//! `UpdateField`, rather than from the derive, and this file exercises both in +//! opposite directions: `BuildField` sets an absent field (`IsNothing` to +//! `IsPresent`) and `TakeField` removes a present one (`IsPresent` back to +//! `IsNothing`). Because presence lives in the partial type's parameters, a +//! premature `finalize_build` is a compile error rather than a runtime failure. +//! +//! See cgp-knowledge-base/cgp/reference/derives/derive_build_field.md and +//! cgp-knowledge-base/cgp/reference/traits/has_builder.md. + +use core::marker::PhantomData; + +use cgp::core::field::traits::TakeField; +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_derive_build_field; + +snapshot_derive_build_field! { + #[derive(BuildField)] + #[derive(Debug, Eq, PartialEq)] + pub struct Person { + pub first_name: String, + pub last_name: String, + } + + expand_person(output) { + insta::assert_snapshot!(output, @" + pub struct __PartialPerson<__F0__: MapType, __F1__: MapType> { + pub first_name: <__F0__ as MapType>::Map, + pub last_name: <__F1__ as MapType>::Map, + } + impl HasBuilder for Person { + type Builder = __PartialPerson; + fn builder() -> Self::Builder { + __PartialPerson { + first_name: (), + last_name: (), + } + } + } + impl IntoBuilder for Person { + type Builder = __PartialPerson; + fn into_builder(self) -> Self::Builder { + __PartialPerson { + first_name: self.first_name, + last_name: self.last_name, + } + } + } + impl<__F0__: MapType, __F1__: MapType> PartialData for __PartialPerson<__F0__, __F1__> { + type Target = Person; + } + impl FinalizeBuild for __PartialPerson { + fn finalize_build(self) -> Self::Target { + Person { + first_name: self.first_name, + last_name: self.last_name, + } + } + } + impl< + __M1__: MapType, + __M2__: MapType, + __F1__: MapType, + > UpdateField< + Symbol< + 10, + Chars< + 'f', + Chars< + 'i', + Chars< + 'r', + Chars< + 's', + Chars< + 't', + Chars< + '_', + Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + >, + __M2__, + > for __PartialPerson<__M1__, __F1__> { + type Value = String; + type Mapper = __M1__; + type Output = __PartialPerson<__M2__, __F1__>; + fn update_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 10, + Chars< + 'f', + Chars< + 'i', + Chars< + 'r', + Chars< + 's', + Chars< + 't', + Chars< + '_', + Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + >, + >, + value: __M2__::Map, + ) -> (__M1__::Map, Self::Output) { + ( + self.first_name, + __PartialPerson { + first_name: value, + last_name: self.last_name, + }, + ) + } + } + impl< + __F0__: MapType, + __M1__: MapType, + __M2__: MapType, + > UpdateField< + Symbol< + 9, + Chars< + 'l', + Chars< + 'a', + Chars< + 's', + Chars< + 't', + Chars<'_', Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>>, + >, + >, + >, + >, + >, + __M2__, + > for __PartialPerson<__F0__, __M1__> { + type Value = String; + type Mapper = __M1__; + type Output = __PartialPerson<__F0__, __M2__>; + fn update_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 9, + Chars< + 'l', + Chars< + 'a', + Chars< + 's', + Chars< + 't', + Chars< + '_', + Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + >, + value: __M2__::Map, + ) -> (__M1__::Map, Self::Output) { + ( + self.last_name, + __PartialPerson { + first_name: self.first_name, + last_name: value, + }, + ) + } + } + impl< + __F1__: MapType, + > HasField< + Symbol< + 10, + Chars< + 'f', + Chars< + 'i', + Chars< + 'r', + Chars< + 's', + Chars< + 't', + Chars< + '_', + Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + >, + > for __PartialPerson { + type Value = String; + fn get_field( + &self, + tag: ::core::marker::PhantomData< + Symbol< + 10, + Chars< + 'f', + Chars< + 'i', + Chars< + 'r', + Chars< + 's', + Chars< + 't', + Chars< + '_', + Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + >, + >, + ) -> &Self::Value { + &self.first_name + } + } + impl< + __F0__: MapType, + > HasField< + Symbol< + 9, + Chars< + 'l', + Chars< + 'a', + Chars< + 's', + Chars< + 't', + Chars<'_', Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>>, + >, + >, + >, + >, + >, + > for __PartialPerson<__F0__, IsPresent> { + type Value = String; + fn get_field( + &self, + tag: ::core::marker::PhantomData< + Symbol< + 9, + Chars< + 'l', + Chars< + 'a', + Chars< + 's', + Chars< + 't', + Chars< + '_', + Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + >, + ) -> &Self::Value { + &self.last_name + } + } + ") + } +} + +#[test] +fn test_build_field_by_field() { + let person: Person = Person::builder() + .build_field(PhantomData::, "Alice".to_owned()) + .build_field(PhantomData::, "Anderson".to_owned()) + .finalize_build(); + + assert_eq!( + person, + Person { + first_name: "Alice".to_owned(), + last_name: "Anderson".to_owned(), + } + ); +} + +#[test] +fn test_read_a_field_back_out_of_a_partial_value() { + // The per-field `HasField` impl on the partial type is in scope only once + // that field is present, so a field can be read back mid-build. + let partial = + Person::builder().build_field(PhantomData::, "Alice".to_owned()); + + assert_eq!( + partial.get_field(PhantomData::), + "Alice" + ); +} + +#[test] +fn test_round_trip_through_into_builder_and_take_field() { + let person1 = Person { + first_name: "Alice".to_owned(), + last_name: "Anderson".to_owned(), + }; + + // `IntoBuilder` goes the other way from `HasBuilder`: an existing value + // becomes an all-present partial value. + let builder = person1.into_builder(); + + // `TakeField` is `BuildField` reversed — it pulls a present field out and + // hands back the value alongside a partial value with that field absent. + let (first_name, remainder) = builder.take_field(PhantomData::); + assert_eq!(first_name, "Alice"); + + // Putting it back restores the all-present configuration, which is the only + // one `finalize_build` is implemented for. + let person2 = remainder + .build_field(PhantomData::, first_name) + .finalize_build(); + + assert_eq!( + person2, + Person { + first_name: "Alice".to_owned(), + last_name: "Anderson".to_owned(), + } + ); +} diff --git a/crates/tests/cgp-tests/tests/extensible_records/cgp_record_derive.rs b/crates/tests/cgp-tests/tests/extensible_records/cgp_record_derive.rs new file mode 100644 index 00000000..a11e3e3c --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_records/cgp_record_derive.rs @@ -0,0 +1,71 @@ +//! `#[derive(CgpRecord)]`, the struct-only face of `#[derive(CgpData)]`. +//! +//! `CgpRecord` and `CgpData`-on-a-struct run the same codegen, so this file +//! does not re-snapshot the expansion that `record_derive` already pins. What it +//! does check is that the one derive really delivers all three record slices at +//! once — the per-field getters, the whole-shape field list, and the builder — +//! since that is the reason to reach for it over deriving the three separately. +//! +//! The difference from `CgpData` is at the parser rather than in the output: +//! `CgpRecord` refuses a non-struct item outright, which is what makes it worth +//! writing when the type will always be a struct. +//! +//! See cgp-knowledge-base/cgp/reference/derives/derive_cgp_record.md. + +use core::marker::PhantomData; + +use cgp::prelude::*; + +#[derive(CgpRecord, Clone, Debug, Eq, PartialEq)] +pub struct Person { + pub first_name: String, + pub last_name: String, +} + +#[test] +fn test_per_field_getters() { + let mut person = Person { + first_name: "Alice".to_owned(), + last_name: "Anderson".to_owned(), + }; + + // The `HasField`/`HasFieldMut` slice. + assert_eq!( + person.get_field(PhantomData::), + "Alice" + ); + + *person.get_field_mut(PhantomData::) = "Baker".to_owned(); + assert_eq!(person.last_name, "Baker"); +} + +#[test] +fn test_whole_shape_round_trip() { + // The `HasFields`/`ToFields`/`FromFields` slice. + let person1 = Person { + first_name: "Alice".to_owned(), + last_name: "Anderson".to_owned(), + }; + + let fields = person1.clone().to_fields(); + let person2 = Person::from_fields(fields); + + assert_eq!(person1, person2); +} + +#[test] +fn test_incremental_builder() { + // The `BuildField` slice. + let person: Person = Person::builder() + .build_field(PhantomData::, "Alice".to_owned()) + .build_field(PhantomData::, "Anderson".to_owned()) + .finalize_build(); + + assert_eq!( + person, + Person { + first_name: "Alice".to_owned(), + last_name: "Anderson".to_owned(), + } + ); +} diff --git a/crates/tests/cgp-tests/tests/extensible_records/mod.rs b/crates/tests/cgp-tests/tests/extensible_records/mod.rs index a2886f90..37b1e3db 100644 --- a/crates/tests/cgp-tests/tests/extensible_records/mod.rs +++ b/crates/tests/cgp-tests/tests/extensible_records/mod.rs @@ -9,15 +9,25 @@ pub mod optional_builder; pub mod person_record; pub mod point_cast; pub mod record_derive; +pub mod record_empty; pub mod record_lifetime; pub mod tuple_record; +// The individual record derives, each on its own: `#[derive(BuildField)]` for +// the builder slice alone, and `#[derive(CgpRecord)]` for the struct-only face +// of `#[derive(CgpData)]`. +pub mod build_field_derive; +pub mod cgp_record_derive; + // Behavioral record building: assembling a record from other records and from // handler pipelines. These reuse `#[derive(CgpData)]` as plain scaffolding — // the derive expansion is already pinned by `record_derive`. pub mod record_build_from; pub mod record_build_with_handlers; +// The type-level product operations: growing, splicing, and re-marking a field list. +pub mod product_ops; + // The value-level `product!` macro: building a `Cons`/`Nil` value from expression // items, whose type is the matching `Product!`. pub mod product_value; @@ -30,3 +40,4 @@ pub mod struct_single_named_field; pub mod struct_single_unnamed_field; pub mod struct_tuple_fields; pub mod struct_two_named_fields; +pub mod struct_unit_field; diff --git a/crates/tests/cgp-tests/tests/extensible_records/product_ops.rs b/crates/tests/cgp-tests/tests/extensible_records/product_ops.rs new file mode 100644 index 00000000..b0f344ef --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_records/product_ops.rs @@ -0,0 +1,169 @@ +//! The type-level product operations: `AppendProduct`, `ConcatProduct`, and `MapFields`. +//! +//! These three are the list algebra the structural machinery is built from — growing a +//! field product by one entry, splicing two products together, and rewriting every entry +//! through a `MapType` marker. They are pure type-level functions evaluated by the trait +//! solver, so the assertions below are type equalities rather than runtime comparisons: +//! each `fn` body coerces the computed `Output`/`Mapped` type to the type the operation is +//! documented to produce, and the test compiles only if the two are the same type. +//! +//! `MapFields` is the one defined over *both* spines, so it is exercised over a `Cons`/`Nil` +//! product and an `Either`/`Void` sum, with the same marker applied to each. +//! +//! None of the three is in the prelude; they are imported from `cgp::core::field::traits`, +//! as is the `IsOptional` marker's home in `cgp::core::field::impls`. +//! +//! See cgp-knowledge-base/cgp/reference/traits/product_ops.md. + +use cgp::core::field::impls::IsOptional; +use cgp::core::field::traits::{AppendProduct, ConcatProduct, MapFields}; +use cgp::prelude::*; + +/// `AppendProduct` adds one entry at the end of a product, keeping the existing entries in +/// order. +#[test] +fn test_append_product() { + type Base = Product![Field]; + + type WithPort = >>::Output; + + fn assert_appended( + fields: WithPort, + ) -> Product![Field, Field] { + fields + } + + let appended = assert_appended(Cons( + "localhost".to_owned().into(), + Cons(8080_u16.into(), Nil), + )); + + assert_eq!(appended.0.value, "localhost"); +} + +/// Appending to the empty product yields a one-entry product. +#[test] +fn test_append_to_the_empty_product() { + type One = >>::Output; + + fn assert_one(fields: One) -> Product![Field] { + fields + } + + let one = assert_one(Cons("localhost".to_owned().into(), Nil)); + + assert_eq!(one.0.value, "localhost"); +} + +/// `ConcatProduct` splices a whole product onto the end of another, which is what makes +/// append the single-entry special case of concat. +#[test] +fn test_concat_product() { + type Left = Product![Field, Field]; + type Right = Product![Field]; + + type Joined = >::Output; + + fn assert_joined( + fields: Joined, + ) -> Product![ + Field, + Field, + Field, + ] { + fields + } + + let joined = assert_joined(Cons( + "localhost".to_owned().into(), + Cons(8080_u16.into(), Cons(true.into(), Nil)), + )); + + assert!(joined.1.1.0.value); +} + +/// Concatenating onto the empty product returns the other product unchanged, and +/// concatenating the empty product onto one leaves it unchanged — the two identity cases. +#[test] +fn test_concat_identities() { + type Fields = Product![Field]; + + fn assert_left_identity(fields: >::Output) -> Fields { + fields + } + + fn assert_right_identity(fields: >::Output) -> Fields { + fields + } + + let left = assert_left_identity(Cons("a".to_owned().into(), Nil)); + let right = assert_right_identity(Cons("b".to_owned().into(), Nil)); + + assert_eq!(left.0.value, "a"); + assert_eq!(right.0.value, "b"); +} + +/// `MapFields` leaves a product's length and order alone and rewrites each entry type +/// through the marker's `Map`. `IsPresent` is therefore the identity. +#[test] +fn test_map_fields_over_a_product() { + type Fields = Product![String, u16, bool]; + + fn assert_optional( + fields: >::Mapped, + ) -> Product![Option, Option, Option,] { + fields + } + + fn assert_nothing(fields: >::Mapped) -> Product![(), (), ()] { + fields + } + + fn assert_present(fields: >::Mapped) -> Fields { + fields + } + + let optional = assert_optional(Cons( + Some("a".to_owned()), + Cons(Some(1), Cons(Some(true), Nil)), + )); + assert_eq!(optional.0, Some("a".to_owned())); + + let _nothing = assert_nothing(Cons((), Cons((), Cons((), Nil)))); + let present = assert_present(Cons("a".to_owned(), Cons(1, Cons(true, Nil)))); + assert_eq!(present.0, "a"); +} + +/// Mapping the empty product is the empty product, whatever the marker. +#[test] +fn test_map_fields_over_the_empty_product() { + fn assert_empty(fields: >::Mapped) -> Nil { + fields + } + + assert_eq!(assert_empty(Nil), Nil); +} + +/// The same marker applies over the `Either`/`Void` sum spine, which is what lets one +/// operation produce both a partial record and a partial enum. +#[test] +fn test_map_fields_over_a_sum() { + type Variants = Sum![String, u16]; + + fn assert_optional( + variants: >::Mapped, + ) -> Sum![Option, Option] { + variants + } + + let mapped = assert_optional(Either::Left(Some("a".to_owned()))); + + assert_eq!(mapped, Either::Left(Some("a".to_owned()))); +} + +/// Mapping the empty sum is the empty sum. `Void` is uninhabited, so this is a type-level +/// assertion with no value to build — the function existing is the whole check. +#[allow(dead_code)] +fn assert_map_fields_over_the_empty_sum(variants: >::Mapped) -> Void { + variants +} diff --git a/crates/tests/cgp-tests/tests/extensible_records/record_empty.rs b/crates/tests/cgp-tests/tests/extensible_records/record_empty.rs new file mode 100644 index 00000000..f1ff6dd4 --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_records/record_empty.rs @@ -0,0 +1,92 @@ +//! `#[derive(CgpData)]` on a **fieldless struct**, the degenerate record shape. +//! +//! A struct with no fields is the record-side counterpart of the variantless +//! enum, and like it the expansion degenerates rather than failing: the +//! `__Partial…` companion struct takes no `MapType` parameters at all, so there +//! is exactly one configuration of it and `HasBuilder`, `IntoBuilder` and +//! `FinalizeBuild` all name the same type. `Fields` is the empty product `Nil`, +//! and no per-field `UpdateField`, `HasField`, or `HasFieldMut` impls are +//! emitted because there is no field to key one on. +//! +//! The practical consequence is that `builder()` is already finalizable — the +//! present/absent tracking that makes a premature `finalize_build` a compile +//! error has nothing to track. +//! +//! See cgp-knowledge-base/cgp/reference/derives/derive_cgp_data.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_derive_cgp_data; + +snapshot_derive_cgp_data! { + #[derive(CgpData)] + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct NoConfig {} + + expand_no_config(output) { + insta::assert_snapshot!(output, @" + impl HasFields for NoConfig { + type Fields = Nil; + } + impl HasFieldsRef for NoConfig { + type FieldsRef<'__a> = Nil where Self: '__a; + } + impl FromFields for NoConfig { + fn from_fields(Nil: Self::Fields) -> Self { + Self {} + } + } + impl ToFields for NoConfig { + fn to_fields(self) -> Self::Fields { + Nil + } + } + impl ToFieldsRef for NoConfig { + fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + where + Self: '__a, + { + Nil + } + } + pub struct __PartialNoConfig {} + impl HasBuilder for NoConfig { + type Builder = __PartialNoConfig; + fn builder() -> Self::Builder { + __PartialNoConfig {} + } + } + impl IntoBuilder for NoConfig { + type Builder = __PartialNoConfig; + fn into_builder(self) -> Self::Builder { + __PartialNoConfig {} + } + } + impl PartialData for __PartialNoConfig { + type Target = NoConfig; + } + impl FinalizeBuild for __PartialNoConfig { + fn finalize_build(self) -> Self::Target { + NoConfig {} + } + } + ") + } +} + +#[test] +fn test_empty_builder_finalizes_immediately() { + let config: NoConfig = NoConfig::builder().finalize_build(); + + assert_eq!(config, NoConfig {}); +} + +#[test] +fn test_empty_record_round_trip() { + let config1 = NoConfig {}; + + let fields = config1.clone().to_fields(); + assert_eq!(fields, Nil); + + let config2 = NoConfig::from_fields(fields); + assert_eq!(config1, config2); +} diff --git a/crates/tests/cgp-tests/tests/extensible_records/struct_unit_field.rs b/crates/tests/cgp-tests/tests/extensible_records/struct_unit_field.rs new file mode 100644 index 00000000..512ebafc --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_records/struct_unit_field.rs @@ -0,0 +1,72 @@ +//! The two field derives on a **unit struct**, the degenerate record shape. +//! +//! A unit struct has no fields, and the two derives answer that differently +//! rather than both erroring. `#[derive(HasField)]` emits *nothing at all*, +//! since there is no field to key an accessor on. `#[derive(HasFields)]` still +//! emits all five representation impls, with `Fields` being the empty product +//! `Nil` — so a unit struct is a valid, if trivial, extensible record and +//! round-trips through `to_fields`/`from_fields`. +//! +//! See cgp-knowledge-base/cgp/reference/derives/derive_has_field.md and +//! cgp-knowledge-base/cgp/reference/derives/derive_has_fields.md. + +use cgp::prelude::*; +use cgp_macro_test_util::{snapshot_derive_has_field, snapshot_derive_has_fields}; + +snapshot_derive_has_field! { + #[derive(HasField)] + pub struct NoFields; + + expand_has_field(output) { + insta::assert_snapshot!(output, @"") + } +} + +snapshot_derive_has_fields! { + #[derive(HasFields)] + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Unit; + + expand_has_fields(output) { + insta::assert_snapshot!(output, @" + impl HasFields for Unit { + type Fields = Nil; + } + impl HasFieldsRef for Unit { + type FieldsRef<'__a> = Nil where Self: '__a; + } + impl FromFields for Unit { + fn from_fields(Nil: Self::Fields) -> Self { + Self + } + } + impl ToFields for Unit { + fn to_fields(self) -> Self::Fields { + Nil + } + } + impl ToFieldsRef for Unit { + fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + where + Self: '__a, + { + Nil + } + } + ") + } +} + +#[test] +fn test_unit_struct_round_trip() { + let unit1 = Unit; + + let fields = unit1.clone().to_fields(); + assert_eq!(fields, Nil); + + let fields_ref = unit1.to_fields_ref(); + assert_eq!(fields_ref, Nil); + + let unit2 = Unit::from_fields(fields); + assert_eq!(unit1, unit2); +} 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 new file mode 100644 index 00000000..b7e985f9 --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_variants/extract_field_derive.rs @@ -0,0 +1,361 @@ +//! `#[derive(ExtractField)]` on its own: the extractor slice of the variant +//! machinery, and nothing else. +//! +//! The snapshot is the point, and what it does *not* contain matters as much as +//! what it does: two `__Partial…` companion enums (one owned, one borrowed), +//! `PartialData` for each, the three `HasExtractor` accessors, an all-`IsVoid` +//! `FinalizeExtract` for each, and a per-variant `ExtractField` — but no +//! `HasFields` representation impls and no `FromVariant` constructors, which +//! `#[derive(HasFields)]` and `#[derive(FromVariant)]` supply. So a value is +//! built here with the enum's ordinary constructor, not generically. +//! +//! The runtime tests walk the chain in both outcomes. Each failed +//! `extract_field` hands back a *remainder* whose type has one more variant +//! marked `IsVoid`, and once every variant has been tried the remainder is +//! uninhabited — which is what lets `finalize_extract_result` close the chain +//! with no wildcard arm. +//! +//! See cgp-knowledge-base/cgp/reference/derives/derive_extract_field.md and +//! cgp-knowledge-base/cgp/reference/traits/extract_field.md. + +use core::marker::PhantomData; + +use cgp::core::field::traits::FinalizeExtractResult; +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_derive_extract_field; + +#[derive(Debug, Eq, PartialEq)] +pub struct Circle { + pub radius: u32, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct Rectangle { + pub width: u32, + pub height: u32, +} + +snapshot_derive_extract_field! { + #[derive(ExtractField)] + #[derive(Debug, Eq, PartialEq)] + pub enum Shape { + Circle(Circle), + Rectangle(Rectangle), + } + + expand_shape(output) { + insta::assert_snapshot!(output, @" + pub enum __PartialShape<__F0__: MapType, __F1__: MapType> { + Circle(<__F0__ as MapType>::Map), + Rectangle(<__F1__ as MapType>::Map), + } + pub enum __PartialRefShape<'__a__, __R__: MapTypeRef, __F0__: MapType, __F1__: MapType> { + Circle(<__F0__ as MapType>::Map<<__R__ as MapTypeRef>::Map<'__a__, Circle>>), + Rectangle(<__F1__ as MapType>::Map<<__R__ as MapTypeRef>::Map<'__a__, Rectangle>>), + } + impl<__F0__: MapType, __F1__: MapType> PartialData for __PartialShape<__F0__, __F1__> { + type Target = Shape; + } + impl<'__a__, __R__: MapTypeRef, __F0__: MapType, __F1__: MapType> PartialData + for __PartialRefShape<'__a__, __R__, __F0__, __F1__> { + type Target = Shape; + } + impl HasExtractor for Shape { + type Extractor = __PartialShape; + fn to_extractor(self) -> Self::Extractor { + match self { + Self::Circle(value) => __PartialShape::Circle(value), + Self::Rectangle(value) => __PartialShape::Rectangle(value), + } + } + fn from_extractor(extractor: Self::Extractor) -> Self { + match extractor { + __PartialShape::Circle(value) => Self::Circle(value), + __PartialShape::Rectangle(value) => Self::Rectangle(value), + } + } + } + impl HasExtractorRef for Shape { + type ExtractorRef<'__a__> = __PartialRefShape<'__a__, IsRef, IsPresent, IsPresent> + where + Self: '__a__; + fn extractor_ref<'__a__>(&'__a__ self) -> Self::ExtractorRef<'__a__> { + match self { + Self::Circle(value) => __PartialRefShape::Circle(value), + Self::Rectangle(value) => __PartialRefShape::Rectangle(value), + } + } + } + impl HasExtractorMut for Shape { + type ExtractorMut<'__a__> = __PartialRefShape<'__a__, IsMut, IsPresent, IsPresent> + where + Self: '__a__; + fn extractor_mut<'__a__>(&'__a__ mut self) -> Self::ExtractorMut<'__a__> { + match self { + Self::Circle(value) => __PartialRefShape::Circle(value), + Self::Rectangle(value) => __PartialRefShape::Rectangle(value), + } + } + } + impl FinalizeExtract for __PartialShape { + fn finalize_extract<__T__>(self) -> __T__ { + match self {} + } + } + impl<'__a__, __R__: MapTypeRef> FinalizeExtract + for __PartialRefShape<'__a__, __R__, IsVoid, IsVoid> { + fn finalize_extract<__T__>(self) -> __T__ { + match self {} + } + } + impl< + __F1__: MapType, + > ExtractField< + Symbol< + 6, + Chars<'C', Chars<'i', Chars<'r', Chars<'c', Chars<'l', Chars<'e', Nil>>>>>>, + >, + > for __PartialShape { + type Value = Circle; + type Remainder = __PartialShape; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'C', + Chars<'i', Chars<'r', Chars<'c', Chars<'l', Chars<'e', Nil>>>>>, + >, + >, + >, + ) -> Result { + match self { + __PartialShape::Circle(value) => Ok(value), + __PartialShape::Rectangle(value) => Err(__PartialShape::Rectangle(value)), + } + } + } + impl< + __F0__: MapType, + > ExtractField< + Symbol< + 9, + Chars< + 'R', + Chars< + 'e', + Chars< + 'c', + Chars< + 't', + Chars<'a', Chars<'n', Chars<'g', Chars<'l', Chars<'e', Nil>>>>>, + >, + >, + >, + >, + >, + > for __PartialShape<__F0__, IsPresent> { + type Value = Rectangle; + type Remainder = __PartialShape<__F0__, IsVoid>; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 9, + Chars< + 'R', + Chars< + 'e', + Chars< + 'c', + Chars< + 't', + Chars< + 'a', + Chars<'n', Chars<'g', Chars<'l', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + >, + ) -> Result { + match self { + __PartialShape::Circle(value) => Err(__PartialShape::Circle(value)), + __PartialShape::Rectangle(value) => Ok(value), + } + } + } + impl< + '__a__, + __R__: MapTypeRef, + __F1__: MapType, + > ExtractField< + Symbol< + 6, + Chars<'C', Chars<'i', Chars<'r', Chars<'c', Chars<'l', Chars<'e', Nil>>>>>>, + >, + > for __PartialRefShape<'__a__, __R__, IsPresent, __F1__> { + type Value = <__R__ as MapTypeRef>::Map<'__a__, Circle>; + type Remainder = __PartialRefShape<'__a__, __R__, IsVoid, __F1__>; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'C', + Chars<'i', Chars<'r', Chars<'c', Chars<'l', Chars<'e', Nil>>>>>, + >, + >, + >, + ) -> Result { + match self { + __PartialRefShape::Circle(value) => Ok(value), + __PartialRefShape::Rectangle(value) => { + Err(__PartialRefShape::Rectangle(value)) + } + } + } + } + impl< + '__a__, + __R__: MapTypeRef, + __F0__: MapType, + > ExtractField< + Symbol< + 9, + Chars< + 'R', + Chars< + 'e', + Chars< + 'c', + Chars< + 't', + Chars<'a', Chars<'n', Chars<'g', Chars<'l', Chars<'e', Nil>>>>>, + >, + >, + >, + >, + >, + > for __PartialRefShape<'__a__, __R__, __F0__, IsPresent> { + type Value = <__R__ as MapTypeRef>::Map<'__a__, Rectangle>; + type Remainder = __PartialRefShape<'__a__, __R__, __F0__, IsVoid>; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 9, + Chars< + 'R', + Chars< + 'e', + Chars< + 'c', + Chars< + 't', + Chars< + 'a', + Chars<'n', Chars<'g', Chars<'l', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + >, + ) -> Result { + match self { + __PartialRefShape::Circle(value) => Err(__PartialRefShape::Circle(value)), + __PartialRefShape::Rectangle(value) => Ok(value), + } + } + } + ") + } +} + +/// Walk the chain to whichever variant the value holds, proving exhaustiveness +/// without a fallback arm. +fn describe(shape: Shape) -> String { + match shape + .to_extractor() + .extract_field(PhantomData::) + { + Ok(circle) => format!("circle of radius {}", circle.radius), + Err(remainder) => { + // `remainder` now has `Circle` ruled out. After the next extraction + // no variant is left, so the result cannot be an `Err`. + let rectangle = remainder + .extract_field(PhantomData::) + .finalize_extract_result(); + + format!("{}x{} rectangle", rectangle.width, rectangle.height) + } + } +} + +#[test] +fn test_extract_the_first_variant() { + assert_eq!( + describe(Shape::Circle(Circle { radius: 2 })), + "circle of radius 2" + ); +} + +#[test] +fn test_extract_through_a_remainder() { + assert_eq!( + describe(Shape::Rectangle(Rectangle { + width: 3, + height: 4, + })), + "3x4 rectangle" + ); +} + +#[test] +fn test_borrowed_and_mutable_extractors() { + let mut shape = Shape::Circle(Circle { radius: 2 }); + + // `HasExtractorRef` borrows each payload in place. + let radius = shape + .extractor_ref() + .extract_field(PhantomData::) + .map(|circle| circle.radius) + .ok(); + + assert_eq!(radius, Some(2)); + + // `HasExtractorMut` lends each payload mutably. + if let Ok(circle) = shape + .extractor_mut() + .extract_field(PhantomData::) + { + circle.radius = 5; + } + + assert_eq!(shape, Shape::Circle(Circle { radius: 5 })); +} + +#[test] +fn test_round_trip_through_the_owned_extractor() { + let shape1 = Shape::Rectangle(Rectangle { + width: 3, + height: 4, + }); + + let extractor = Shape::to_extractor(shape1); + let shape2 = Shape::from_extractor(extractor); + + assert_eq!( + shape2, + Shape::Rectangle(Rectangle { + width: 3, + height: 4, + }) + ); +} 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 new file mode 100644 index 00000000..f6fab96d --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_variants/from_variant_derive.rs @@ -0,0 +1,142 @@ +//! `#[derive(FromVariant)]` on its own: the variant-construction slice, and the +//! simplest derive in the extensible-data family. +//! +//! The snapshot shows the whole of what it emits — one `FromVariant` impl per +//! variant and nothing more. There is no companion type and no presence +//! tracking, because construction needs neither: each impl just wraps a payload +//! in its variant, keyed by the variant name's `Symbol!`. +//! +//! What that buys is a constructor a generic caller can select by tag. The test +//! below stays generic over the tag, so one function can build either variant, +//! which a `Shape::Circle(..)` call site cannot. +//! +//! See cgp-knowledge-base/cgp/reference/derives/derive_from_variant.md and +//! cgp-knowledge-base/cgp/reference/traits/from_variant.md. + +use core::marker::PhantomData; + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_derive_from_variant; + +#[derive(Debug, Eq, PartialEq)] +pub struct Circle { + pub radius: u32, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct Rectangle { + pub width: u32, + pub height: u32, +} + +snapshot_derive_from_variant! { + #[derive(FromVariant)] + #[derive(Debug, Eq, PartialEq)] + pub enum Shape { + Circle(Circle), + Rectangle(Rectangle), + } + + expand_shape(output) { + insta::assert_snapshot!(output, @" + impl FromVariant< + Symbol< + 6, + Chars<'C', Chars<'i', Chars<'r', Chars<'c', Chars<'l', Chars<'e', Nil>>>>>>, + >, + > for Shape { + type Value = Circle; + fn from_variant( + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'C', + Chars<'i', Chars<'r', Chars<'c', Chars<'l', Chars<'e', Nil>>>>>, + >, + >, + >, + value: Self::Value, + ) -> Self { + Self::Circle(value) + } + } + impl FromVariant< + Symbol< + 9, + Chars< + 'R', + Chars< + 'e', + Chars< + 'c', + Chars< + 't', + Chars<'a', Chars<'n', Chars<'g', Chars<'l', Chars<'e', Nil>>>>>, + >, + >, + >, + >, + >, + > for Shape { + type Value = Rectangle; + fn from_variant( + _tag: ::core::marker::PhantomData< + Symbol< + 9, + Chars< + 'R', + Chars< + 'e', + Chars< + 'c', + Chars< + 't', + Chars< + 'a', + Chars<'n', Chars<'g', Chars<'l', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + >, + value: Self::Value, + ) -> Self { + Self::Rectangle(value) + } + } + ") + } +} + +/// Build a `Shape` without naming which variant — the caller picks it with a tag. +fn wrap(tag: PhantomData, value: >::Value) -> Shape +where + Shape: FromVariant, +{ + Shape::from_variant(tag, value) +} + +#[test] +fn test_construct_each_variant_by_tag() { + assert_eq!( + wrap(PhantomData::, Circle { radius: 2 }), + Shape::Circle(Circle { radius: 2 }) + ); + + assert_eq!( + wrap( + PhantomData::, + Rectangle { + width: 3, + height: 4, + } + ), + Shape::Rectangle(Rectangle { + width: 3, + height: 4, + }) + ); +} 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 new file mode 100644 index 00000000..33366ab3 --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_variants/has_fields_enum_shapes.rs @@ -0,0 +1,402 @@ +//! `#[derive(HasFields)]` on an enum whose variants use **every variant shape**. +//! +//! The variant derives that deconstruct an enum — `#[derive(ExtractField)]` and +//! `#[derive(FromVariant)]`, and therefore `#[derive(CgpVariant)]` and +//! `#[derive(CgpData)]` — require every variant to be a single-unnamed-field +//! tuple variant, because each has to name one payload type. `HasFields` has no +//! such requirement: it only *describes* each variant, so it accepts all four +//! shapes and nests each variant's own fields as a product inside that +//! variant's `Field` entry. +//! +//! The four shapes map as follows, and this file pins each: +//! +//! - a unit variant becomes the empty product `Nil`; +//! - a single-unnamed-field variant becomes the payload type directly, the same +//! newtype special case a one-field tuple struct gets; +//! - a multi-field tuple variant becomes a product keyed by `Index`; +//! - a named-field (struct-style) variant becomes a product keyed by `Symbol!`. +//! +//! See cgp-knowledge-base/cgp/reference/derives/derive_has_fields.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_derive_has_fields; + +snapshot_derive_has_fields! { + #[derive(HasFields)] + #[derive(Clone, Debug, Eq, PartialEq)] + pub enum Shape { + Empty, + Circle(u32), + Rectangle(u32, u32), + Triangle { base: u32, height: u32 }, + } + + expand_shape(output) { + insta::assert_snapshot!(output, @" + impl HasFields for Shape { + type Fields = Either< + Field< + Symbol<5, Chars<'E', Chars<'m', Chars<'p', Chars<'t', Chars<'y', Nil>>>>>>, + Nil, + >, + Either< + Field< + Symbol< + 6, + Chars< + 'C', + Chars<'i', Chars<'r', Chars<'c', Chars<'l', Chars<'e', Nil>>>>>, + >, + >, + u32, + >, + Either< + Field< + Symbol< + 9, + Chars< + 'R', + Chars< + 'e', + Chars< + 'c', + Chars< + 't', + Chars< + 'a', + Chars<'n', Chars<'g', Chars<'l', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + Cons, u32>, Cons, u32>, Nil>>, + >, + Either< + Field< + Symbol< + 8, + Chars< + 'T', + Chars< + 'r', + Chars< + 'i', + Chars< + 'a', + Chars<'n', Chars<'g', Chars<'l', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + Cons< + Field< + Symbol< + 4, + Chars<'b', Chars<'a', Chars<'s', Chars<'e', Nil>>>>, + >, + u32, + >, + Cons< + Field< + Symbol< + 6, + Chars< + 'h', + Chars< + 'e', + Chars<'i', Chars<'g', Chars<'h', Chars<'t', Nil>>>>, + >, + >, + >, + u32, + >, + Nil, + >, + >, + >, + Void, + >, + >, + >, + >; + } + impl HasFieldsRef for Shape { + type FieldsRef<'__a> = Either< + Field< + Symbol<5, Chars<'E', Chars<'m', Chars<'p', Chars<'t', Chars<'y', Nil>>>>>>, + Nil, + >, + Either< + Field< + Symbol< + 6, + Chars< + 'C', + Chars<'i', Chars<'r', Chars<'c', Chars<'l', Chars<'e', Nil>>>>>, + >, + >, + &'__a u32, + >, + Either< + Field< + Symbol< + 9, + Chars< + 'R', + Chars< + 'e', + Chars< + 'c', + Chars< + 't', + Chars< + 'a', + Chars<'n', Chars<'g', Chars<'l', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + Cons< + Field, &'__a u32>, + Cons, &'__a u32>, Nil>, + >, + >, + Either< + Field< + Symbol< + 8, + Chars< + 'T', + Chars< + 'r', + Chars< + 'i', + Chars< + 'a', + Chars<'n', Chars<'g', Chars<'l', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + Cons< + Field< + Symbol< + 4, + Chars<'b', Chars<'a', Chars<'s', Chars<'e', Nil>>>>, + >, + &'__a u32, + >, + Cons< + Field< + Symbol< + 6, + Chars< + 'h', + Chars< + 'e', + Chars<'i', Chars<'g', Chars<'h', Chars<'t', Nil>>>>, + >, + >, + >, + &'__a u32, + >, + Nil, + >, + >, + >, + Void, + >, + >, + >, + > + where + Self: '__a; + } + impl FromFields for Shape { + fn from_fields(rest: Self::Fields) -> Self { + match rest { + Either::Left(field) => { + let Nil = field.value; + Self::Empty + } + Either::Right(rest) => { + match rest { + Either::Left(field) => { + let field = field.value; + Self::Circle(field) + } + Either::Right(rest) => { + match rest { + Either::Left(field) => { + let Cons(field_1, Cons(field_0, Nil)) = field.value; + Self::Rectangle(field_1.value, field_0.value) + } + Either::Right(rest) => { + match rest { + Either::Left(field) => { + let Cons(base, Cons(height, Nil)) = field.value; + Self::Triangle { + base: base.value, + height: height.value, + } + } + Either::Right(rest) => match rest {} + } + } + } + } + } + } + } + } + } + impl ToFields for Shape { + fn to_fields(self) -> Self::Fields { + match self { + Self::Empty => Either::Left(Nil.into()), + Self::Circle(field) => Either::Right(Either::Left(field.into())), + Self::Rectangle(field_0, field_1) => { + Either::Right( + Either::Right( + Either::Left( + Cons(field_0.into(), Cons(field_1.into(), Nil)).into(), + ), + ), + ) + } + Self::Triangle { base, height } => { + Either::Right( + Either::Right( + Either::Right( + Either::Left( + Cons(base.into(), Cons(height.into(), Nil)).into(), + ), + ), + ), + ) + } + } + } + } + impl ToFieldsRef for Shape { + fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + where + Self: '__a, + { + match self { + Self::Empty => Either::Left(Nil.into()), + Self::Circle(field) => Either::Right(Either::Left(field.into())), + Self::Rectangle(field_0, field_1) => { + Either::Right( + Either::Right( + Either::Left( + Cons(field_0.into(), Cons(field_1.into(), Nil)).into(), + ), + ), + ) + } + Self::Triangle { base, height } => { + Either::Right( + Either::Right( + Either::Right( + Either::Left( + Cons(base.into(), Cons(height.into(), Nil)).into(), + ), + ), + ), + ) + } + } + } + } + ") + } +} + +#[test] +fn test_unit_variant() { + let shape1 = Shape::Empty; + + let fields = shape1.clone().to_fields(); + assert_eq!(fields, Either::Left(Nil.into())); + + let shape2 = Shape::from_fields(fields); + assert_eq!(shape1, shape2); +} + +#[test] +fn test_newtype_variant() { + let shape1 = Shape::Circle(2); + + // The payload is the inner type directly, not a one-element product. + let fields = shape1.clone().to_fields(); + assert_eq!(fields, Either::Right(Either::Left(2.into()))); + + let shape2 = Shape::from_fields(fields); + assert_eq!(shape1, shape2); +} + +#[test] +fn test_multi_field_tuple_variant() { + let shape1 = Shape::Rectangle(3, 4); + + // Positional fields are keyed by `Index`, in declaration order. + let fields = shape1.clone().to_fields(); + assert_eq!( + fields, + Either::Right(Either::Right(Either::Left( + Cons(3.into(), Cons(4.into(), Nil)).into() + ))) + ); + + let shape2 = Shape::from_fields(fields); + assert_eq!(shape1, shape2); +} + +#[test] +fn test_named_field_variant() { + let shape1 = Shape::Triangle { base: 6, height: 5 }; + + // Named fields are keyed by `Symbol!`, in declaration order. + let fields = shape1.clone().to_fields(); + assert_eq!( + fields, + Either::Right(Either::Right(Either::Right(Either::Left( + Cons(6.into(), Cons(5.into(), Nil)).into() + )))) + ); + + let shape2 = Shape::from_fields(fields); + assert_eq!(shape1, shape2); +} + +#[test] +fn test_borrowed_fields_across_every_shape() { + // `to_fields_ref` borrows each payload in place, whatever the variant shape. + assert_eq!(Shape::Empty.to_fields_ref(), Either::Left(Nil.into())); + + assert_eq!( + Shape::Circle(2).to_fields_ref(), + Either::Right(Either::Left((&2).into())) + ); + + assert_eq!( + Shape::Rectangle(3, 4).to_fields_ref(), + Either::Right(Either::Right(Either::Left( + Cons((&3).into(), Cons((&4).into(), Nil)).into() + ))) + ); + + assert_eq!( + Shape::Triangle { base: 6, height: 5 }.to_fields_ref(), + Either::Right(Either::Right(Either::Right(Either::Left( + Cons((&6).into(), Cons((&5).into(), Nil)).into() + )))) + ); +} diff --git a/crates/tests/cgp-tests/tests/extensible_variants/mod.rs b/crates/tests/cgp-tests/tests/extensible_variants/mod.rs index b8b462e6..3487754f 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/mod.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/mod.rs @@ -6,10 +6,12 @@ pub mod sum_macro; // `#[derive(HasFields)]` snapshots for enums (this concept owns the enum -// expansion of the derive): the plain field list of an enum, and the generic -// variant. +// expansion of the derive): the plain field list of an enum, the generic +// variant, and every variant shape the derive accepts — which is all four, +// unlike the variant derives that deconstruct an enum. pub mod has_fields_enum; pub mod has_fields_enum_generic; +pub mod has_fields_enum_shapes; // `#[derive(CgpData)]` snapshots for enums (this concept owns the variant // expansion of the derive): the full extractor/extractor-ref machinery for a @@ -20,6 +22,12 @@ pub mod derive_cgp_data_empty; pub mod derive_cgp_data_generic; pub mod derive_cgp_data_shape; +// The individual variant derives, each on its own: `#[derive(ExtractField)]` +// for the extractor slice alone and `#[derive(FromVariant)]` for the +// constructor slice alone. +pub mod extract_field_derive; +pub mod from_variant_derive; + // Regression: `#[derive(CgpData)]` on an enum whose own lifetime is named `'a`, // which the borrowed extractor's reserved `'__a__` lifetime must not collide // with. A plain behavioral test — the expansion shape is pinned above. diff --git a/crates/tests/cgp-tests/tests/higher_order_providers/lifetime_inner_provider.rs b/crates/tests/cgp-tests/tests/higher_order_providers/lifetime_inner_provider.rs new file mode 100644 index 00000000..781645d5 --- /dev/null +++ b/crates/tests/cgp-tests/tests/higher_order_providers/lifetime_inner_provider.rs @@ -0,0 +1,87 @@ +//! A higher-order provider for a component that carries a **lifetime**, where the +//! inner-provider bound gets no `IsProviderFor` counterpart. +//! +//! Deriving a provider's `IsProviderFor` impl normally augments a bound naming the +//! same provider trait — the inner-provider bound of a higher-order provider — with +//! its `IsProviderFor` counterpart, so a dependency missing inside the inner +//! provider still propagates outward. That rewrite reads the provider trait's +//! *first* generic argument as the context, and Rust requires lifetime arguments to +//! come first, so on a lifetime-carrying component the first argument is a lifetime +//! and the rewrite finds no context to build the counterpart from. It then leaves +//! the bound alone rather than emitting something wrong. +//! +//! The snapshot pins that: the outer `IsProviderFor` impl is produced correctly, +//! with the lifetime lifted into `Life<'a>` in the params tuple, while the +//! `Inner: ReferenceGetter<'a, Context>` bound is copied verbatim with no +//! `IsProviderFor)>` beside it — +//! compare `use_provider_impl`, where the component has no lifetime and the +//! counterpart *is* added. The consequence is a weaker diagnostic rather than +//! broken code, and it is recorded under Known issues in the reference. +//! +//! See cgp-knowledge-base/cgp/reference/macros/cgp_provider.md (Known issues) and +//! cgp-knowledge-base/cgp/reference/types/life.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_new_provider; + +#[cgp_component(ReferenceGetter)] +pub trait HasReference<'a> { + fn get_reference(&self) -> &'a u32; +} + +snapshot_cgp_new_provider! { + #[cgp_new_provider] + impl<'a, Context, Inner> ReferenceGetter<'a, Context> for ForwardReference + where + Inner: ReferenceGetter<'a, Context>, + { + fn get_reference(context: &Context) -> &'a u32 { + Inner::get_reference(context) + } + } + + expand_forward_reference(output) { + insta::assert_snapshot!(output, @" + impl<'a, Context, Inner> ReferenceGetter<'a, Context> for ForwardReference + where + Inner: ReferenceGetter<'a, Context>, + { + fn get_reference(context: &Context) -> &'a u32 { + Inner::get_reference(context) + } + } + impl<'a, Context, Inner> IsProviderFor)> + for ForwardReference + where + Inner: ReferenceGetter<'a, Context>, + {} + pub struct ForwardReference(pub ::core::marker::PhantomData); + ") + } +} + +pub struct App<'a> { + pub value: &'a u32, +} + +#[cgp_impl(new GetReference)] +impl<'a> ReferenceGetter<'a> for App<'a> { + fn get_reference(&self) -> &'a u32 { + self.value + } +} + +delegate_components! { + <'a> App<'a> { + ReferenceGetterComponent: + ForwardReference, + } +} + +#[test] +fn test_lifetime_inner_provider() { + let value = 42; + let app = App { value: &value }; + + assert_eq!(app.get_reference(), &42); +} diff --git a/crates/tests/cgp-tests/tests/higher_order_providers/mod.rs b/crates/tests/cgp-tests/tests/higher_order_providers/mod.rs index c500454b..d97107d5 100644 --- a/crates/tests/cgp-tests/tests/higher_order_providers/mod.rs +++ b/crates/tests/cgp-tests/tests/higher_order_providers/mod.rs @@ -7,3 +7,7 @@ pub mod use_provider_impl; // The scaling pattern end-to-end: an outer calculator wraps an inner one. pub mod rectangle_or_circle; pub mod scaled_area; + +// The inner-provider bound of a lifetime-carrying component, which gets no +// `IsProviderFor` counterpart. +pub mod lifetime_inner_provider; diff --git a/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mref.rs b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mref.rs new file mode 100644 index 00000000..0e0dd774 --- /dev/null +++ b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mref.rs @@ -0,0 +1,47 @@ +//! An `#[implicit]` argument typed as `MRef<'_, T>`: the owned-or-borrowed form. +//! +//! `#[implicit]` picks its access mode from the argument's type, and `MRef<'a, T>` +//! is the one reference-shaped mode with no mutable mirror. It requires a plain +//! `T` field and wraps the borrow as `MRef::Ref(..)`, so the body receives a value +//! that may be either owned or borrowed without the context having to commit to +//! one — and, unlike a `&mut` argument, its access never depends on the receiver, +//! so it reads through `HasField` even under `&mut self`. +//! +//! The second function pins that receiver independence. The shape-directed parse +//! — `MRef` is recognized only as a single-segment path carrying exactly a +//! lifetime and a type — is not pinned here, because every way of writing it +//! differently (nested inside another type, or reached through a qualified path) +//! falls into the owned-and-cloned mode, and `MRef` is not `Clone`, so such a case +//! cannot compile at all. +//! +//! See cgp-knowledge-base/cgp/reference/attributes/implicit.md and +//! cgp-knowledge-base/cgp/reference/types/mref.md. + +use cgp::prelude::*; + +#[cgp_fn] +pub fn borrowed_name(&self, #[implicit] name: MRef<'_, String>) -> String { + name.as_ref().to_uppercase() +} + +#[cgp_fn] +pub fn borrowed_name_mut_self(&mut self, #[implicit] name: MRef<'_, String>) -> usize { + // A `&mut self` receiver does not make an `MRef` argument a mutable read, so + // this still resolves through `HasField` rather than `HasFieldMut`. + name.as_ref().len() +} + +#[derive(HasField)] +pub struct App { + pub name: String, +} + +#[test] +fn test_mref_implicit_argument() { + let mut app = App { + name: "world".to_owned(), + }; + + assert_eq!(app.borrowed_name(), "WORLD"); + assert_eq!(app.borrowed_name_mut_self(), 5); +} diff --git a/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs b/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs index f0904f0d..f5de7dc0 100644 --- a/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs +++ b/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs @@ -5,6 +5,7 @@ pub mod cgp_fn_calling_fn; pub mod cgp_fn_custom_trait_name; pub mod cgp_fn_greet; +pub mod cgp_fn_mref; pub mod cgp_fn_multi_and_use_type; pub mod cgp_fn_mut_slice; pub mod cgp_fn_mutable; diff --git a/crates/tests/cgp-tests/tests/namespaces/combined_forms.rs b/crates/tests/cgp-tests/tests/namespaces/combined_forms.rs new file mode 100644 index 00000000..c89c69ed --- /dev/null +++ b/crates/tests/cgp-tests/tests/namespaces/combined_forms.rs @@ -0,0 +1,303 @@ +//! Every `delegate_components!` body form in one block. +//! +//! The body's forms are independent choices — three mapping operators, three key +//! forms, and the leading statements — and nothing stops a table from using all of +//! them at once. Every other test in the suite stays inside one family, so this is +//! the only place the composition itself is pinned: an `open` statement, a `->` +//! forwarding entry into an aggregate provider, a bracketed list key sharing one +//! provider, and `@`-path keys using both grouping forms and a per-segment +//! generic, all against one context. +//! +//! The two grouping forms are the pair most easily confused, so both appear on the +//! same component: `{Circle, Ellipse}` groups whole path tails and ends the path, +//! while `[Square, Triangle]` groups alternatives for one segment. They fan out to +//! one impl pair per alternative either way. +//! +//! The `delegate_components!` snapshot is the golden output this file owns; the +//! components, the providers, and the `Bundle` aggregate are incidental +//! scaffolding. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/delegate_components.md and +//! cgp-knowledge-base/cgp/reference/macros/delegate_components.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_delegate_components; + +// Incidental: one generic component to dispatch per shape, and three plain ones +// wired through the other forms. +#[cgp_component(AreaCalculator)] +pub trait CanCalculateArea { + fn area(&self, shape: &Shape) -> f64; +} + +#[cgp_component(BarProvider)] +pub trait Bar { + fn bar(&self); +} + +#[cgp_component(BazProvider)] +pub trait Baz { + fn baz(&self); +} + +#[cgp_component(QuuxProvider)] +pub trait Quux { + fn quux(&self); +} + +pub struct Rectangle; +pub struct Circle; +pub struct Ellipse; +pub struct Square; +pub struct Triangle; + +// Incidental: per-shape and plain providers. +#[cgp_impl(new ShapeArea)] +impl AreaCalculator { + fn area(&self, _shape: &Shape) -> f64 { + 0.0 + } +} + +#[cgp_impl(new RefArea)] +impl AreaCalculator { + fn area(&self, _shape: &Shape) -> f64 { + 0.0 + } +} + +#[cgp_impl(new DummyBar)] +impl BarProvider { + fn bar(&self) {} +} + +#[cgp_impl(new DummyBaz)] +impl BazProvider { + fn baz(&self) {} +} + +#[cgp_impl(DummyBaz)] +impl QuuxProvider { + fn quux(&self) {} +} + +// Incidental: the aggregate provider the `->` entry forwards into. +delegate_components! { + new Bundle { + BarProviderComponent: DummyBar, + } +} + +pub struct App; + +snapshot_delegate_components! { + delegate_components! { + App { + // Statement, which must lead the block. + open AreaCalculatorComponent; + + // `->`: take whatever `Bundle` wires for this key. + BarProviderComponent -> + Bundle, + + // List key: one provider for two components. + [BazProviderComponent, QuuxProviderComponent]: + DummyBaz, + + // Path key with a braced group — whole tails, ends the path. + @AreaCalculatorComponent.{Rectangle, Circle}: + ShapeArea, + + // Path key with a bracketed group — alternatives for one segment. + @AreaCalculatorComponent.[Square, Triangle]: + ShapeArea, + + // Path key whose segment carries its own generics. + @AreaCalculatorComponent.<'a, T> &'a T: + RefArea, + } + } + + expand_combined_forms_app(output) { + insta::assert_snapshot!(output, @" + impl DelegateComponent for App { + type Delegate = RedirectLookup>; + } + impl< + __Context__, + __Params__, + > IsProviderFor for App + where + RedirectLookup< + App, + PathCons, + >: IsProviderFor, + {} + impl DelegateComponent for App + where + Bundle: DelegateComponent, + { + type Delegate = >::Delegate; + } + impl< + __Context__, + __Params__, + > IsProviderFor for App + where + Bundle: DelegateComponent, + >::Delegate: IsProviderFor, + {} + impl DelegateComponent for App { + type Delegate = DummyBaz; + } + impl< + __Context__, + __Params__, + > IsProviderFor for App + where + DummyBaz: IsProviderFor, + {} + impl DelegateComponent for App { + type Delegate = DummyBaz; + } + impl< + __Context__, + __Params__, + > IsProviderFor for App + where + DummyBaz: IsProviderFor, + {} + impl< + __Wildcard__, + > DelegateComponent>> + for App { + type Delegate = ShapeArea; + } + impl< + __Wildcard__, + __Context__, + __Params__, + > IsProviderFor< + PathCons>, + __Context__, + __Params__, + > for App + where + ShapeArea: IsProviderFor< + PathCons>, + __Context__, + __Params__, + >, + {} + impl< + __Wildcard__, + > DelegateComponent>> + for App { + type Delegate = ShapeArea; + } + impl< + __Wildcard__, + __Context__, + __Params__, + > IsProviderFor< + PathCons>, + __Context__, + __Params__, + > for App + where + ShapeArea: IsProviderFor< + PathCons>, + __Context__, + __Params__, + >, + {} + impl< + __Wildcard__, + > DelegateComponent>> + for App { + type Delegate = ShapeArea; + } + impl< + __Wildcard__, + __Context__, + __Params__, + > IsProviderFor< + PathCons>, + __Context__, + __Params__, + > for App + where + ShapeArea: IsProviderFor< + PathCons>, + __Context__, + __Params__, + >, + {} + impl< + __Wildcard__, + > DelegateComponent>> + for App { + type Delegate = ShapeArea; + } + impl< + __Wildcard__, + __Context__, + __Params__, + > IsProviderFor< + PathCons>, + __Context__, + __Params__, + > for App + where + ShapeArea: IsProviderFor< + PathCons>, + __Context__, + __Params__, + >, + {} + impl< + 'a, + T, + __Wildcard__, + > DelegateComponent>> + for App { + type Delegate = RefArea; + } + impl< + 'a, + T, + __Wildcard__, + __Context__, + __Params__, + > IsProviderFor< + PathCons>, + __Context__, + __Params__, + > for App + where + RefArea: IsProviderFor< + PathCons>, + __Context__, + __Params__, + >, + {} + ") + } +} + +check_components! { + App { + AreaCalculatorComponent: [ + Rectangle, + Circle, + Square, + Triangle, + <'a> &'a Ellipse, + ], + BarProviderComponent, + BazProviderComponent, + QuuxProviderComponent, + } +} diff --git a/crates/tests/cgp-tests/tests/namespaces/default_impls2.rs b/crates/tests/cgp-tests/tests/namespaces/default_impls2.rs new file mode 100644 index 00000000..79b7e1db --- /dev/null +++ b/crates/tests/cgp-tests/tests/namespaces/default_impls2.rs @@ -0,0 +1,64 @@ +//! `DefaultImpls2`, the two-type-parameter member of the default-lookup family. +//! +//! `DefaultNamespace`, `DefaultImpls1`, and `DefaultImpls2` differ only in how many types +//! take part in the key. Nothing in the library or the rest of this suite exercised +//! `DefaultImpls2`, so this file establishes that it is genuinely reachable rather than a +//! declared-but-unusable trait — both halves of the round trip work, and neither needed a +//! new construct. +//! +//! Registration is the ordinary `#[default_impl]` attribute: the attribute takes an +//! arbitrary namespace trait path and appends the table parameter, so naming +//! `DefaultImpls2` emits +//! `impl<__Components__> DefaultImpls2 for Key`. The key +//! written before `in` becomes the impl's `Self`, and the types written inside the path +//! become the trait's leading parameters — which is the opposite way round from what the +//! parameter names `T`/`T1`/`T2` suggest. +//! +//! Consumption is the ordinary `for … in` loop, whose generated bound is +//! `T: DefaultImpls2`. The loop variable is the +//! *key*, so the entry must mention `T` for it to be constrained. +//! +//! See cgp-knowledge-base/cgp/reference/traits/default_namespace.md. + +use cgp::core::component::DefaultImpls2; +use cgp::prelude::*; + +#[cgp_component(ShowImpl)] +pub trait CanShowPair { + fn show_pair(&self, first: &T, second: &U) -> String; +} + +/// Registers `ShowPair` as the default for the key `String` under the two-type key +/// `(ShowImplComponent, u64)`. +#[cgp_impl(new ShowPair)] +#[default_impl(String in DefaultImpls2)] +impl ShowImpl { + fn show_pair(&self, first: &String, second: &u64) -> String { + format!("{first}={second}") + } +} + +pub struct App; + +delegate_components! { + App { + open ShowImplComponent; + + for in DefaultImpls2 { + @ShowImplComponent.T: Provider, + } + } +} + +check_components! { + App { + ShowImplComponent: (String, u64), + } +} + +#[test] +fn test_the_two_type_default_resolves() { + let shown = App.show_pair(&"answer".to_owned(), &42); + + assert_eq!(shown, "answer=42"); +} diff --git a/crates/tests/cgp-tests/tests/namespaces/for_loop_nested_table.rs b/crates/tests/cgp-tests/tests/namespaces/for_loop_nested_table.rs new file mode 100644 index 00000000..4acf0642 --- /dev/null +++ b/crates/tests/cgp-tests/tests/namespaces/for_loop_nested_table.rs @@ -0,0 +1,66 @@ +//! A nested `UseDelegate` value inside a `for <..> in ..` loop body. +//! +//! A loop body holds ordinary `:` mappings, so its values can open a nested table +//! just as a top-level mapping's can — and the extraction that lifts such a table +//! out has to walk the body's statements, not only its mappings. A mapping-only +//! walk parses the table, names it in the entry's `Delegate`, and never emits it, +//! failing with an `E0425` on the dropped struct. +//! +//! What this file pins is that the table is emitted and carries its entry. It does +//! **not** wire the component through, because the combination is redundant by +//! construction: the loop already yields one provider per key, so a nested table +//! would dispatch the same parameter a second time. The form is worth keeping +//! correct rather than worth recommending. +//! +//! Note the inner table's own generic list. The loop binds a provider variable the +//! entry must mention, or the generated impl leaves it unconstrained (`E0207`); but +//! the loop's variables are not in scope inside the lifted table's impls, so +//! mentioning it only there fails on both counts (`E0207` and an `E0425` for the +//! `Provider` the inner impl cannot see). `new LoopInner` is what puts it +//! in both places at once. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/delegate_components.md. + +use cgp::prelude::*; + +// Incidental: a generic component routed into `DefaultNamespace` under a prefix. +#[cgp_component(FooProvider)] +#[prefix(@test in DefaultNamespace)] +#[derive_delegate(UseDelegate)] +pub trait Foo { + fn foo(&self, value: &T); +} + +#[cgp_impl(new DummyFoo)] +impl FooProvider { + fn foo(&self, _value: &T) {} +} + +// Incidental: the table the loop reads its (key, provider) pairs out of. +cgp_namespace! { + new FooSources { + String: DummyFoo, + } +} + +pub struct App; + +delegate_components! { + App { + namespace DefaultNamespace; + + for in FooSources { + @test.FooProviderComponent.T: UseDelegate { + String: Provider, + }>, + } + } +} + +// The lifted table exists and holds the entry the loop body gave it. Naming +// `LoopInner` at all is the regression this file guards: before the extraction +// walked statements, the struct was never declared and this bound would not +// resolve. +pub trait CheckLoopInner: DelegateComponent {} + +impl CheckLoopInner for LoopInner {} diff --git a/crates/tests/cgp-tests/tests/namespaces/mod.rs b/crates/tests/cgp-tests/tests/namespaces/mod.rs index d362862f..c7ede1c9 100644 --- a/crates/tests/cgp-tests/tests/namespaces/mod.rs +++ b/crates/tests/cgp-tests/tests/namespaces/mod.rs @@ -21,6 +21,14 @@ pub mod open_dispatch; pub mod prefix_default_namespace; pub mod redirect_lookup; +// The rest of the shared `delegate_components!` body grammar: the `=>` operator +// written on a context (and its equivalence to `open`), every form combined in one +// block, and a nested `UseDelegate` table lifted out of a `cgp_namespace!` body. +pub mod combined_forms; +pub mod for_loop_nested_table; +pub mod namespace_nested_table; +pub mod redirect_mapping; + // Namespace inheritance and per-type default impls. `default_impls` and // `extended` define reusable namespaces/providers (with `cgp_namespace!`, // `#[prefix]`, and `#[default_impl]` snapshots); the `*_wiring` modules consume @@ -28,6 +36,7 @@ pub mod redirect_lookup; // and namespace inheritance in `delegate_components!`. pub mod default_impl_use_type; pub mod default_impls; +pub mod default_impls2; pub mod default_impls_wiring; pub mod extended; pub mod extended_namespace_wiring; diff --git a/crates/tests/cgp-tests/tests/namespaces/namespace_nested_table.rs b/crates/tests/cgp-tests/tests/namespaces/namespace_nested_table.rs new file mode 100644 index 00000000..b2c3ae3b --- /dev/null +++ b/crates/tests/cgp-tests/tests/namespaces/namespace_nested_table.rs @@ -0,0 +1,106 @@ +//! A nested `UseDelegate` value inside a `cgp_namespace!` body. +//! +//! A namespace body is parsed by the same `DelegateEntries` type a +//! `delegate_components!` table is, so it accepts the legacy nested-table value — +//! and `cgp_namespace!` must lift that inner table out into its own struct and +//! `DelegateComponent` impls exactly as `delegate_components!` does. Drop that and +//! the entry's `Delegate` names a `FooTable` nothing declares, failing with an +//! `E0425` that reads like a typo rather than a dropped table — the regression +//! this file guards. +//! +//! Putting the dispatch table in the namespace rather than on the context is the +//! point of the form: `AppA` and `AppB` join `NestedNs` and both get the per-type +//! dispatch without either one spelling it out. The `cgp_namespace!` snapshot is +//! the golden output this file owns — it pins the lifted `FooTable` struct and its +//! two entries beside the namespace's own impl — and the component and provider +//! are incidental scaffolding. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_namespace.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_namespace.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_namespace; + +// Incidental: a generic component whose dispatch the nested table drives. The +// legacy nested-table form resolves through `#[derive_delegate]`, unlike `open`. +#[cgp_component(FooProvider)] +#[derive_delegate(UseDelegate)] +pub trait Foo { + fn foo(&self, value: &T); +} + +// Incidental: one per-value provider, shared by both dispatch entries. +#[cgp_impl(new DummyFoo)] +impl FooProvider { + fn foo(&self, _value: &T) {} +} + +snapshot_cgp_namespace! { + cgp_namespace! { + new NestedNs { + FooProviderComponent: + UseDelegate, + } + } + + expand_nested_table_namespace(output) { + insta::assert_snapshot!(output, @" + pub struct __NestedNsComponents; + pub trait NestedNs<__Table__> { + type Delegate; + } + pub struct FooTable; + impl<__Table__> NestedNs<__Table__> for FooProviderComponent { + type Delegate = UseDelegate; + } + impl DelegateComponent for FooTable { + type Delegate = DummyFoo; + } + impl<__Context__, __Params__> IsProviderFor for FooTable + where + DummyFoo: IsProviderFor, + {} + impl DelegateComponent for FooTable { + type Delegate = DummyFoo; + } + impl<__Context__, __Params__> IsProviderFor for FooTable + where + DummyFoo: IsProviderFor, + {} + ") + } +} + +pub struct AppA; + +delegate_components! { + AppA { + namespace NestedNs; + } +} + +check_components! { + AppA { + FooProviderComponent: [String, u64], + } +} + +// A second context joining the same namespace inherits the same dispatch table, +// which is what putting it in the namespace bought. +pub struct AppB; + +delegate_components! { + AppB { + namespace NestedNs; + } +} + +check_components! { + #[check_trait(__CheckAppB)] + AppB { + FooProviderComponent: [String, u64], + } +} diff --git a/crates/tests/cgp-tests/tests/namespaces/redirect_mapping.rs b/crates/tests/cgp-tests/tests/namespaces/redirect_mapping.rs new file mode 100644 index 00000000..e61cb722 --- /dev/null +++ b/crates/tests/cgp-tests/tests/namespaces/redirect_mapping.rs @@ -0,0 +1,299 @@ +//! The `=>` redirect operator written directly in a `delegate_components!` table. +//! +//! `=>` sets an entry's `Delegate` to a `RedirectLookup` along an `@`-path instead +//! of naming a provider, so the provider is decided wherever the path lands. Two +//! uses appear here. `FooProviderComponent => @FooProviderComponent` roots a +//! component's route at its own name, which is **exactly** what `open +//! FooProviderComponent;` generates — the snapshots below are the equivalence, +//! since `App` and `OpenApp` differ only in which spelling they use and their +//! golden output is identical. And `[BarProviderComponent, BazProviderComponent] +//! => @shared` points two components at one slot, answered by a single +//! `@shared: DummyImpl` entry. +//! +//! This is the only place the suite exercises `=>` outside a `cgp_namespace!` +//! body. The two `delegate_components!` snapshots are the golden output this file +//! owns; the components and providers are incidental scaffolding. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/delegate_components.md and +//! cgp-knowledge-base/cgp/reference/macros/delegate_components.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_delegate_components; + +// Incidental: one generic component dispatched per type, and two plain ones +// sharing a redirect slot. +#[cgp_component(FooProvider)] +pub trait Foo { + fn foo(&self, value: &T); +} + +#[cgp_component(BarProvider)] +pub trait Bar { + fn bar(&self); +} + +#[cgp_component(BazProvider)] +pub trait Baz { + fn baz(&self); +} + +// Incidental: plain providers for the three components. +#[cgp_impl(new DummyFoo)] +impl FooProvider { + fn foo(&self, _value: &T) {} +} + +#[cgp_impl(new DummyImpl)] +impl BarProvider { + fn bar(&self) {} +} + +#[cgp_impl(DummyImpl)] +impl BazProvider { + fn baz(&self) {} +} + +pub struct App; + +snapshot_delegate_components! { + delegate_components! { + App { + // The spelled-out form of `open FooProviderComponent;`. + FooProviderComponent => + @FooProviderComponent, + + @FooProviderComponent.String: + DummyFoo, + + // A list key redirecting two components onto one shared slot, which + // the single entry below answers. + [BarProviderComponent, BazProviderComponent] => + @shared, + + @shared: + DummyImpl, + } + } + + expand_redirect_mapping_app(output) { + insta::assert_snapshot!(output, @" + impl DelegateComponent for App { + type Delegate = RedirectLookup>; + } + impl< + __Context__, + __Params__, + > IsProviderFor for App + where + RedirectLookup< + App, + PathCons, + >: IsProviderFor, + {} + impl< + __Wildcard__, + > DelegateComponent>> + for App { + type Delegate = DummyFoo; + } + impl< + __Wildcard__, + __Context__, + __Params__, + > IsProviderFor< + PathCons>, + __Context__, + __Params__, + > for App + where + DummyFoo: IsProviderFor< + PathCons>, + __Context__, + __Params__, + >, + {} + impl DelegateComponent for App { + type Delegate = RedirectLookup< + App, + PathCons< + Symbol< + 6, + Chars< + 's', + Chars<'h', Chars<'a', Chars<'r', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + Nil, + >, + >; + } + impl< + __Context__, + __Params__, + > IsProviderFor for App + where + RedirectLookup< + App, + PathCons< + Symbol< + 6, + Chars< + 's', + Chars<'h', Chars<'a', Chars<'r', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + Nil, + >, + >: IsProviderFor, + {} + impl DelegateComponent for App { + type Delegate = RedirectLookup< + App, + PathCons< + Symbol< + 6, + Chars< + 's', + Chars<'h', Chars<'a', Chars<'r', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + Nil, + >, + >; + } + impl< + __Context__, + __Params__, + > IsProviderFor for App + where + RedirectLookup< + App, + PathCons< + Symbol< + 6, + Chars< + 's', + Chars<'h', Chars<'a', Chars<'r', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + Nil, + >, + >: IsProviderFor, + {} + impl< + __Wildcard__, + > DelegateComponent< + PathCons< + Symbol< + 6, + Chars<'s', Chars<'h', Chars<'a', Chars<'r', Chars<'e', Chars<'d', Nil>>>>>>, + >, + __Wildcard__, + >, + > for App { + type Delegate = DummyImpl; + } + impl< + __Wildcard__, + __Context__, + __Params__, + > IsProviderFor< + PathCons< + Symbol< + 6, + Chars<'s', Chars<'h', Chars<'a', Chars<'r', Chars<'e', Chars<'d', Nil>>>>>>, + >, + __Wildcard__, + >, + __Context__, + __Params__, + > for App + where + DummyImpl: IsProviderFor< + PathCons< + Symbol< + 6, + Chars< + 's', + Chars<'h', Chars<'a', Chars<'r', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + __Wildcard__, + >, + __Context__, + __Params__, + >, + {} + ") + } +} + +check_components! { + App { + FooProviderComponent: String, + BarProviderComponent, + BazProviderComponent, + } +} + +// The `open` spelling of the first entry above. Its `DelegateComponent` and +// `IsProviderFor` impls must match `App`'s `FooProviderComponent` pair exactly. +pub struct OpenApp; + +snapshot_delegate_components! { + delegate_components! { + OpenApp { + open FooProviderComponent; + + @FooProviderComponent.String: + DummyFoo, + } + } + + expand_redirect_mapping_open_app(output) { + insta::assert_snapshot!(output, @" + impl DelegateComponent for OpenApp { + type Delegate = RedirectLookup>; + } + impl< + __Context__, + __Params__, + > IsProviderFor for OpenApp + where + RedirectLookup< + OpenApp, + PathCons, + >: IsProviderFor, + {} + impl< + __Wildcard__, + > DelegateComponent>> + for OpenApp { + type Delegate = DummyFoo; + } + impl< + __Wildcard__, + __Context__, + __Params__, + > IsProviderFor< + PathCons>, + __Context__, + __Params__, + > for OpenApp + where + DummyFoo: IsProviderFor< + PathCons>, + __Context__, + __Params__, + >, + {} + ") + } +} + +check_components! { + #[check_trait(__CheckOpenApp)] + OpenApp { + FooProviderComponent: String, + } +}