Skip to content

Extra macro refactoring - #274

Merged
soareschen merged 15 commits into
mainfrom
macro-extra-refactoring
Oct 4, 2026
Merged

soareschen merged 15 commits into
mainfrom
macro-extra-refactoring

Conversation

@soareschen

@soareschen soareschen commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

This PR rebuilds CGP's three extra-feature macros, #[cgp_computer], #[cgp_producer], and
#[cgp_auto_dispatch], on the same foundation as the core macros, and fixes the defects that review
of them turned up. The macros now generate fully qualified code that works without
use cgp::prelude::*. #[cgp_auto_dispatch] accepts supertraits and the lifetime shapes it used to
reject, and inputs that previously miscompiled or crashed the macro now fail with an error on the
code the user wrote.

What changes for users

Most users need to change nothing. Code that reaches these macros through cgp::prelude keeps
compiling, and the generated code behaves the same apart from the fixes below. Three consequences are
visible, and all are deliberate.

The macro crates are renamed. cgp-extra-macro and cgp-extra-macro-lib become
cgp-macro-extra and cgp-macro-extra-lib, matching the cgp-macro* family they belong to, and a
new cgp-macro-extra-core sits beside cgp-macro-core. Only a crate that depends on
cgp-extra-macro directly, rather than through cgp, has to change its manifest and its
cgp_extra_macro:: paths.

The generated per-variant computers of #[cgp_auto_dispatch] change shape where a method elides
lifetimes, because each elided argument lifetime now gets its own name (see the lifetime fix
below). Each computer's impl becomes more general rather than less, so code that wires a
Compute{Method} provider by name should keep compiling; the computer_by_name test wires one
this way.

An import that existed only for the old expansion becomes unused. The old expansion named CGP
items without a path, so a module using #[cgp_auto_dispatch] sometimes had to import a matcher or
the prelude just to make the generated code compile. The new expansion qualifies every name, so those
imports now trigger the unused_imports lint. Seven test files in this repository had such imports,
and this PR removes them.

Why the macros were rebuilt

The extra macros were written as single functions that assembled their output with quote! and
emitted other macros for the compiler to expand later. #[cgp_computer] produced a
#[cgp_new_provider] impl and a delegate_components! block, and #[cgp_auto_dispatch] produced a
#[cgp_computer] function per method:

// What #[cgp_computer] fn add(a: u64, b: u64) -> u64 used to emit, after the function:
#[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, /* … */] -> PromoteComputer<Self>,
    }
}

Every name in that output, cgp_new_provider, delegate_components!, Computer, PhantomData,
PromoteComputer, resolved through whatever the caller's module happened to import. So the macros
worked only where cgp::prelude::* was in scope, and a local item with a matching name could take
the place of a CGP one. The core macros avoid both problems by routing every name through
cgp-macro-core's exports markers and building their output from typed AST nodes. The extra macros
already depended on cgp-macro-core, but used it only for a case-conversion helper, so none of that
machinery reached them.

The new structure

The extra macros now mirror the core ones. Each layer has one job:

  • cgp-macro-extra is the proc-macro entry crate. It forwards to the lib crate and now carries
    rustdoc for each macro.
  • cgp-macro-extra-lib holds one thin entry function per macro: parse the input, run the
    stages, and lower the result.
  • cgp-macro-extra-core (new) holds the parsing and code generation. It depends on
    cgp-macro-core and reuses its exports markers, parse_internal!, merge_generics,
    override_item_span, and AST types.

Each macro is a staged pipeline in the core crate's style, Item… → preprocess() →
Preprocessed… → eval(). The difference from the old code is what eval() returns. Instead of
tokens that invoke another macro, it returns the AST nodes that macro would have parsed its input
into: an intermediate representation, or IR. A computer evaluates to a cgp-macro-core
ItemCgpProvider (what #[cgp_new_provider] impl … parses into) and a DelegateTable (what
delegate_components! { … } parses into). The entry function then lowers them with those types'
own methods:

pub fn cgp_computer(attr: TokenStream, body: TokenStream) -> syn::Result<TokenStream> {
    let item_fn: ItemFn = parse2(body)?;
    let ident: Option<Ident> = parse2(attr)?;

    let evaluated = ItemCgpComputer { ident, item_fn }.preprocess()?.eval()?;

    lower_handler_fn(&evaluated) // item_fn, provider.lower()?, delegate_table.eval()?
}

#[cgp_auto_dispatch] goes one level further: its IR holds one ItemCgpComputer per method, which
the entry function runs through the computer pipeline instead of emitting #[cgp_computer]. The
result is that the expansion is the final code in a single pass, every CGP name in it is a full
::cgp::macro_prelude::… path, and the core macros' rules (struct declaration, IsProviderFor
derivation, wiring) apply unchanged because the same types do the work.

Along the way the code adopts the core crate's other conventions:

  • Validation as check_* methods with Error::new_spanned, so an error underlines the whole
    offending parameter list or generic list rather than its first token.
  • Visitors for type rewriting: ElaborateElidedLifetimes (a VisitMut), collect_lifetimes,
    and find_impl_trait.
  • Spans aimed at the user's code: generated provider impls are re-spanned onto the function or
    method name with override_item_span.
  • Commented generic insertions and merge_generics in place of hand-merged generics.

The repository's AGENTS.md and cgp-macro-core/AGENTS.md now describe the extra crates, and the
new crate carries its own AGENTS.md recording the lower-through-IR pattern for future macros.

How the refactor was kept safe

The tests came first. Before any macro code changed, the branch added three snapshot macros,
snapshot_cgp_computer!, snapshot_cgp_producer!, and snapshot_cgp_auto_dispatch!, and pinned
the expansion of every shape each macro distinguishes: sync and async, value and Result<T, E>,
named providers, zero and one parameters, generics, lifetimes, const generics, every receiver form,
generic traits, and raw method names. The rejection tests now pin each error's message, through a new
assert_macro_rejects_with, so an input cannot start failing for some unrelated reason unnoticed.

The refactor then moved one macro per commit. Each commit changed only that macro's snapshots, and
the change was the expected one: the nested #[cgp_new_provider], delegate_components!, or
#[cgp_computer] call replaced by the code it expands to. The #[cgp_auto_dispatch] commit also
removes the seven imports that only the old expansion used. As an independent check, companion
fixtures in cargo-cgp record the fully expanded code, nested macros included, through
cargo cgp expand. Those expansions stayed identical across the whole refactor apart from
PhantomData becoming ::core::marker::PhantomData, which confirms the new pipeline generates the
same final code as the old one.

Defects fixed

Each fix below shows input that failed before, what the compiler said, and what happens now. Every
fix has a regression test.

The expansion depended on the caller's imports

Any of the three macros failed when invoked by path in a module without the prelude:

#[cgp::prelude::cgp_computer]
fn add(a: u64, b: u64) -> u64 {
    a + b
}
error: cannot find attribute `cgp_new_provider` in this scope
error: cannot find macro `delegate_components` in this scope
error[E0405]: cannot find trait `Computer` in this scope
error[E0425]: cannot find type `PhantomData` in this scope

A module that imported the prelude but declared its own Computer failed differently, because the
local struct captured the bare name: error[E0404]: expected trait, found struct `Computer` .

The new expansion qualifies every name, so both programs compile. The macros_without_prelude and
without_prelude tests invoke all three macros by path, beside local items named Computer and
HasExtractor.

#[cgp_auto_dispatch] rejected valid lifetime shapes

The macro copies each method's signature into a matcher bound and a helper function, and both need
every lifetime named. It used to give each top-level &T one shared lifetime, '__a__, and to
quantify only the last lifetime it collected in the bound's for<…>. Several ordinary traits
failed as a result:

#[cgp_auto_dispatch]
pub trait HasSides {
    fn sides<'a>(&'a self) -> u8;             // E0261: use of undeclared lifetime name `'a`
}

#[cgp_auto_dispatch]
pub trait CanLookup {
    fn lookup<'a>(&'a self, key: &str) -> &'a str; // E0261: undeclared lifetime `'__a__`
}

#[cgp_auto_dispatch]
pub trait CanPrefix<'a> {
    fn prefix(&self, prefix: &'a str) -> &'a str;  // E0496: lifetime `'a` shadows one in scope
}

#[cgp_auto_dispatch]
pub trait HasTitle {
    fn title<'a>(&'a self) -> &str;           // E0261, E0207, and E0582 in the generated code
}

#[cgp_auto_dispatch]
pub trait CanFind {
    fn find(&self, key: Option<&str>) -> Option<&str>;
    // E0637: `&` without an explicit lifetime name cannot be used here
}

Sharing one lifetime also changed what a signature meant, even where it compiled. Rust gives each
elided argument its own lifetime and ties an elided return to &self, so this method promises that
the returned borrow comes from the receiver and outlives suffix:

#[cgp_auto_dispatch]
pub trait CanLabel {
    fn label(&self, suffix: &str) -> &str;
}

With self and suffix forced onto one lifetime, the generated blanket impl could not keep that
promise and failed with lifetime may not live long enough.

DispatchMethod now names a method's lifetimes once, by the compiler's elision rules, and both the
bound and the helper read the result:

  • A borrowed receiver keeps its own lifetime, or gets '__a__ when elided.
  • Each elided lifetime in an argument, at any depth, gets its own fresh '__a1__, '__a2__, and so
    on. The ElaborateElidedLifetimes visitor does this, and leaves fn(&T) types and Fn(&T)
    bounds alone, since they bind their own lifetimes.
  • An elided lifetime in the return type takes the receiver's lifetime, or, for a by-value self,
    the one lifetime the arguments use.
  • The bound quantifies the method's own lifetimes plus the introduced ones in a single for<…>, and
    never the trait's lifetimes, which the impl already declares.

For label, the generated helper and bound are now:

fn __compute_label__<'__a__, '__a1__, __Variants__: CanLabel>(
    __Variants__: &'__a__ __Variants__,
    (arg_0): (&'__a1__ str),
) -> &'__a__ str {
    __Variants__.label(arg_0)
}

// in the blanket impl's where clause:
MatchFirstWithValueHandlersRef<ComputeLabel>:
    for<'__a__, '__a1__> Computer<(), (), (&'__a__ __Variants__, (&'__a1__ str)), Output = &'__a__ str>,

The named_lifetimes and elided_lifetimes tests cover every trait above.

#[cgp_auto_dispatch] ignored supertraits

The blanket impl implemented the trait for every __Variants__ but never required the trait's
supertraits, so any dispatch trait with a supertrait failed to compile:

#[cgp_auto_dispatch]
pub trait HasName {
    fn name(&self) -> &'static str;
}

#[cgp_auto_dispatch]
pub trait CanGreet: HasName {
    fn greet(&self) -> String;
}
// error[E0277]: the trait bound `__Variants__: HasName` is not satisfied

The blanket impl now adds __Variants__: HasName to its where clause, so the enum has to provide
the supertrait, here by dispatching it too. The supertrait test pins the expansion.

A typed receiver miscompiled instead of being rejected

#[cgp_auto_dispatch] read self: Box<Self> as a by-value self and passed the box to a matcher
that expects the enum:

#[cgp_auto_dispatch]
pub trait HasArea {
    fn area(self: Box<Self>) -> f64;
}
// error[E0308]: mismatched types
// error[E0599]: no method named `area` found for type parameter `__Variants__`

It now rejects the method, pointing at the receiver:

error: Dispatcher method receiver must be `self`, `&self`, or `&mut self`

Attribute arguments were silently ignored

#[cgp_auto_dispatch(AreaDispatcher)] expanded exactly like #[cgp_auto_dispatch], so a reader
could believe the argument meant something. It is now rejected with
`#[cgp_auto_dispatch]` takes no arguments.

A raw function name crashed the macro

#[cgp_computer] and #[cgp_producer] built the default provider name from the raw spelling of the
function name:

#[cgp_computer]
fn r#type(value: u64) -> u64 {
    value + 1
}
// error: custom attribute panicked
//   = help: message: `"R#type"` is not a valid identifier

The name is now read without its r# prefix, as #[cgp_auto_dispatch] already did for its
computers, so this defines the provider Type. All three macros share one helper for it.

impl Trait produced an error inside generated code

A computer's parameter types become a provider impl's trait arguments, and its return type becomes
the impl's Output, and neither position accepts impl Trait:

#[cgp_computer]
fn describe(value: impl core::fmt::Display) -> String {
    value.to_string()
}
// error[E0562]: `impl Trait` is not allowed in traits

Both macros now reject it at the impl Trait the user wrote:

error: Computer function parameters cannot use `impl Trait`; declare a generic parameter instead
error: Computer functions cannot return `impl Trait`
error: Producer functions cannot return `impl Trait`

A one-argument Result<T> gave a cryptic parse error

#[cgp_computer] treats only a bare Result<T, E> return type as fallible, deciding from the tokens
rather than the type. That rule is deliberate and unchanged. But an alias such as anyhow::Result's
Result<u64> failed with a bare expected `,` , which did not say what was wrong. It now fails
with:

error: A `Result` return type must be written as `Result<T, E>`, naming its error type

Errors pointed at the whole attribute

Two span changes make errors land on the code the user wrote. Rejections now use
Error::new_spanned, so fn magic_number(seed: u64) underlines seed: u64 rather than seed
alone. And generated provider impls are spanned on the name they derive from: when two dispatch
traits in one module share a method name, the conflicting Computer and IsProviderFor impls now
point at the second method's name instead of at the whole #[cgp_auto_dispatch] attribute. The
remaining conflicts, on the promotion wiring, still underline the attribute, but rustc now says they
originate in cgp_auto_dispatch, the macro the user wrote, rather than in an internal
cgp_computer.

Known issues left in place

Four limitations are documented in the knowledge base's Known issues and pinned by a test rather than
fixed, because each fix is larger than this PR's scope or changes a public name:

  • Two dispatch traits in one module cannot share a method name. Both emit
    __compute_area__ and ComputeArea. The fix folds the trait name into those names, but
    Compute{Method} is a public provider that code outside the macro wires by name, so renaming it
    needs its own decision. A cargo-cgp UI fixture pins it.
  • A computer's type parameter used only in its return type is unconstrained.
    fn parse<T: FromStr>(value: String) -> Option<T> fails with E0207, because the provider struct
    would have to carry T. A cargo-cgp UI fixture pins it.
  • A lifetime hidden in a path is not named. Cow<str> in a dispatch method fails with E0726
    or E0106, since a macro cannot tell that Cow carries a lifetime. Writing Cow<'_, str> works. A compile-fail
    fixture in the website's example-code crate pins it.
  • #[cgp_fn] panics on a raw function name, the same defect fixed here for the extra macros.
    It lives in cgp-macro-core, outside this refactor, and a #[should_panic] test now pins it.

soareschen and others added 15 commits October 4, 2026 13:24
…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 <noreply@anthropic.com>
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<T, E>`, 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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<T, E>`, 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<T, E>` 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
`#[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 <noreply@anthropic.com>
`#[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 <noreply@anthropic.com>
…ch]`

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<Self>` 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 <noreply@anthropic.com>
…n 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<T>` alias failed with a bare "expected `,`". It is
  still rejected, since only the bare `Result<T, E>` form selects the
  fallible promotion, but the error now says "A `Result` return type must be
  written as `Result<T, E>`, 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
`#[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 <noreply@anthropic.com>
@soareschen soareschen added the AI label Oct 4, 2026
@soareschen
soareschen merged commit 52bcaf7 into main Oct 4, 2026
3 checks passed
@soareschen
soareschen deleted the macro-extra-refactoring branch October 4, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant