Repository navigation
Extra macro refactoring - #274
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 reviewof 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 toreject, 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::preludekeepscompiling, 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-macroandcgp-extra-macro-libbecomecgp-macro-extraandcgp-macro-extra-lib, matching thecgp-macro*family they belong to, and anew
cgp-macro-extra-coresits besidecgp-macro-core. Only a crate that depends oncgp-extra-macrodirectly, rather than throughcgp, has to change its manifest and itscgp_extra_macro::paths.The generated per-variant computers of
#[cgp_auto_dispatch]change shape where a method elideslifetimes, 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; thecomputer_by_nametest wires onethis 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 orthe prelude just to make the generated code compile. The new expansion qualifies every name, so those
imports now trigger the
unused_importslint. 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!andemitted other macros for the compiler to expand later.
#[cgp_computer]produced a#[cgp_new_provider]impl and adelegate_components!block, and#[cgp_auto_dispatch]produced a#[cgp_computer]function per method: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 macrosworked only where
cgp::prelude::*was in scope, and a local item with a matching name could takethe place of a CGP one. The core macros avoid both problems by routing every name through
cgp-macro-core'sexportsmarkers and building their output from typed AST nodes. The extra macrosalready depended on
cgp-macro-core, but used it only for a case-conversion helper, so none of thatmachinery reached them.
The new structure
The extra macros now mirror the core ones. Each layer has one job:
cgp-macro-extrais the proc-macro entry crate. It forwards to the lib crate and now carriesrustdoc for each macro.
cgp-macro-extra-libholds one thin entry function per macro: parse the input, run thestages, and lower the result.
cgp-macro-extra-core(new) holds the parsing and code generation. It depends oncgp-macro-coreand reuses itsexportsmarkers,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 whateval()returns. Instead oftokens 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-coreItemCgpProvider(what#[cgp_new_provider] impl …parses into) and aDelegateTable(whatdelegate_components! { … }parses into). The entry function then lowers them with those types'own methods:
#[cgp_auto_dispatch]goes one level further: its IR holds oneItemCgpComputerper method, whichthe entry function runs through the computer pipeline instead of emitting
#[cgp_computer]. Theresult 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,IsProviderForderivation, wiring) apply unchanged because the same types do the work.
Along the way the code adopts the core crate's other conventions:
check_*methods withError::new_spanned, so an error underlines the wholeoffending parameter list or generic list rather than its first token.
ElaborateElidedLifetimes(aVisitMut),collect_lifetimes,and
find_impl_trait.method name with
override_item_span.merge_genericsin place of hand-merged generics.The repository's
AGENTS.mdandcgp-macro-core/AGENTS.mdnow describe the extra crates, and thenew crate carries its own
AGENTS.mdrecording 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!, andsnapshot_cgp_auto_dispatch!, and pinnedthe 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 alsoremoves the seven imports that only the old expansion used. As an independent check, companion
fixtures in
cargo-cgprecord the fully expanded code, nested macros included, throughcargo cgp expand. Those expansions stayed identical across the whole refactor apart fromPhantomDatabecoming::core::marker::PhantomData, which confirms the new pipeline generates thesame 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:
A module that imported the prelude but declared its own
Computerfailed differently, because thelocal 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_preludeandwithout_preludetests invoke all three macros by path, beside local items namedComputerandHasExtractor.#[cgp_auto_dispatch]rejected valid lifetime shapesThe 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
&Tone shared lifetime,'__a__, and toquantify only the last lifetime it collected in the bound's
for<…>. Several ordinary traitsfailed as a result:
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 thatthe returned borrow comes from the receiver and outlives
suffix:With
selfandsuffixforced onto one lifetime, the generated blanket impl could not keep thatpromise and failed with
lifetime may not live long enough.DispatchMethodnow names a method's lifetimes once, by the compiler's elision rules, and both thebound and the helper read the result:
'__a__when elided.'__a1__,'__a2__, and soon. The
ElaborateElidedLifetimesvisitor does this, and leavesfn(&T)types andFn(&T)bounds alone, since they bind their own lifetimes.
self,the one lifetime the arguments use.
for<…>, andnever the trait's lifetimes, which the impl already declares.
For
label, the generated helper and bound are now:The
named_lifetimesandelided_lifetimestests cover every trait above.#[cgp_auto_dispatch]ignored supertraitsThe blanket impl implemented the trait for every
__Variants__but never required the trait'ssupertraits, so any dispatch trait with a supertrait failed to compile:
The blanket impl now adds
__Variants__: HasNameto itswhereclause, so the enum has to providethe supertrait, here by dispatching it too. The
supertraittest pins the expansion.A typed receiver miscompiled instead of being rejected
#[cgp_auto_dispatch]readself: Box<Self>as a by-valueselfand passed the box to a matcherthat expects the enum:
It now rejects the method, pointing at the receiver:
Attribute arguments were silently ignored
#[cgp_auto_dispatch(AreaDispatcher)]expanded exactly like#[cgp_auto_dispatch], so a readercould 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 thefunction name:
The name is now read without its
r#prefix, as#[cgp_auto_dispatch]already did for itscomputers, so this defines the provider
Type. All three macros share one helper for it.impl Traitproduced an error inside generated codeA computer's parameter types become a provider impl's trait arguments, and its return type becomes
the impl's
Output, and neither position acceptsimpl Trait:Both macros now reject it at the
impl Traitthe user wrote:A one-argument
Result<T>gave a cryptic parse error#[cgp_computer]treats only a bareResult<T, E>return type as fallible, deciding from the tokensrather than the type. That rule is deliberate and unchanged. But an alias such as
anyhow::Result'sResult<u64>failed with a bareexpected `,`, which did not say what was wrong. It now failswith:
Errors pointed at the whole attribute
Two span changes make errors land on the code the user wrote. Rejections now use
Error::new_spanned, sofn magic_number(seed: u64)underlinesseed: u64rather thanseedalone. 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
ComputerandIsProviderForimpls nowpoint at the second method's name instead of at the whole
#[cgp_auto_dispatch]attribute. Theremaining 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 internalcgp_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:
__compute_area__andComputeArea. The fix folds the trait name into those names, butCompute{Method}is a public provider that code outside the macro wires by name, so renaming itneeds its own decision. A
cargo-cgpUI fixture pins it.fn parse<T: FromStr>(value: String) -> Option<T>fails withE0207, because the provider structwould have to carry
T. Acargo-cgpUI fixture pins it.Cow<str>in a dispatch method fails withE0726or
E0106, since a macro cannot tell thatCowcarries a lifetime. WritingCow<'_, str>works. A compile-failfixture in the website's
example-codecrate 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.