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