Skip to content

Fix expansion defects, reject malformed input, and narrow -> error spans - #281

Merged
soareschen merged 25 commits into
mainfrom
cargo-cgp-refactoring
Nov 6, 2026
Merged

soareschen merged 25 commits into
mainfrom
cargo-cgp-refactoring

Conversation

@soareschen

Copy link
Copy Markdown
Collaborator

Overview

[deepseek-v4.1-flash-1017] CGP macros now compile the programs they should and reject the ones they cannot support, with the error on the token the programmer wrote. This PR fixes the expansion defects that made valid CGP code fail to compile, adds macro-time rejections for malformed inputs, and points a -> wiring entry's errors at the entry. It also extends the test suite so each fix is pinned by a behavioral test or a rejection test, and updates the repository guides to describe the current test layout and the cargo-cgp toolchain.

The behavior changes fall into three groups. The expansion fixes make Self::CONST, the variant derives, the data derives' companion types, Path! segments, #[async_trait] default bodies, and mut parameters work as written. The rejections catch a self-inheriting namespace, #[uses] on a component, a component method parameter without a plain name, a #[cgp_computer] parameter that only the return type uses, and an optional reference to an unsized type. The span change makes a -> entry's missing-key error point at the entry instead of the whole delegate_components! block.

Motivation

Two failure modes make CGP macros costly to use. A macro that rejects valid input forces a workaround or a hand-written impl, and a macro that accepts invalid input hands the compiler a generated type that produces an error with no clear cause. Both modes appeared in the macro suite, and the cargo-cgp error toolchain could not present the resulting diagnostics well because the root cause was either absent or buried under generated code. A CGP macro expands to ordinary Rust, so a mistake in the macro's output surfaces as a compiler error about generated types the programmer never wrote.

This PR closes the defects that a review of the macro suite found. Each fix starts from a program that failed, and each rejection starts from a program whose compiler error named the macro's internals or suggested a change that does not compile. The tests keep both directions pinned: a program that should compile does, and a program that should be rejected gets the macro's own message.

Approach

Each change starts from a program that failed or compiled with a warning, fixes the macro at the point that produced the wrong output, and pins the result with a test. The subsections below give the cause, the fix, and the alternatives where they matter.

A provider can read its own Self::CONST

A provider written with #[cgp_impl] can read a const its block declares as Self::LIMIT, and a component's default method body can read the trait's own const as Self::LIMIT. Both failed before.

A Self::LIMIT naming the block's own const became __Context__::LIMIT, which the context does not have, so the compiler reported E0599. The rewrite turns every Self in a provider block into the context, and it exempted only the associated types the block declares:

error[E0599]: no associated function or constant named `LIMIT` found for type parameter `__Context__` in the current scope
  --> src/main.rs:26:23
   |
26 |         count < Self::LIMIT
   |                       ^^^^^ associated function or constant cannot be called on `__Context__` due to unsatisfied trait bounds

The same rewrite in #[cgp_component] turned a default body's Self::LIMIT into __Context__::LIMIT on an unbounded context.

The fix treats a const the block declares as a local item, as it already treated an associated type. The two sites need different handling:

  • Component default bodies (to_provider_trait.rs): the trait's own consts join the list of local items that ReplaceSelfTypeVisitor skips, so Self::LIMIT stays Self::LIMIT in the provider trait and names the provider's const.
  • #[cgp_impl] bodies (lowered.rs): skipping alone is not enough. With Self left as the provider struct, Self::LIMIT fails with E0034 because the consumer trait's blanket impl gives the provider struct the consumer trait, which declares the same const. A new QualifySelfConstVisitor (visitors/replace_self/qualify_self_const.rs) runs before the other rewrites and replaces Self::LIMIT with <AllowUnderLimit as RateLimiter<__Context__>>::LIMIT, in expressions, patterns, and macro bodies. The visitor also rewrites a macro body's token stream, as the other Self visitors do. The visitor's field skip_assoc_types became skip_assoc_items, since it now holds consts too, and the getter parser's two uses were renamed with it.

The alternative, rewriting Self::LIMIT to the context's const through the consumer trait, would need a consumer-trait bound the provider impl does not carry and would read the wired provider's value rather than this block's.

The expansion now reads:

impl<__Context__> RateLimiter<__Context__> for AllowUnderLimit {
    const LIMIT: u32 = 10;
    fn allow(__context__: &__Context__, count: u32) -> bool {
        count < <AllowUnderLimit as RateLimiter<__Context__>>::LIMIT
    }
}

Only a two-segment Self::CONST is qualified. A local const named through a longer or generic path is not a Rust form, so nothing else needs the rewrite.

Variant derives accept the seven reserved names

An enum may now name its variants Value, Remainder, Extractor, ExtractorRef, ExtractorMut, Fields, or FieldsRef and still derive HasFields, FromVariant, ExtractField, CgpVariant, or CgpData. Before, each of those seven names made the derive's expansion fail to compile.

A variant called Value made the generated path ambiguous, because the derives named their own associated types through a bare Self::… path, and inside an impl for an enum such a path can also name a variant. The compiler reported E0221 "ambiguous associated item". #[derive(FromVariant)] reserved Value, #[derive(HasFields)] reserved Fields and FieldsRef, and #[derive(ExtractField)] reserved the other five, so an enum deriving the whole family had seven names it could not use.

Every associated type the enum impls name is now a qualified projection. The changes are in cgp-macro-core/src/types/cgp_data/:

  • derive_extractor/has_extractor_impl.rs: <Self as HasExtractor>::Extractor in to_extractor and from_extractor, and <Self as HasExtractorRef>::ExtractorRef<'__a__> and <Self as HasExtractorMut>::ExtractorMut<'__a__> in the borrowed accessors.
  • derive_extractor/extract_field_impls.rs: extract_field returns Result<<Self as ExtractField<Tag>>::Value, <Self as ExtractField<Tag>>::Remainder>.
  • derive_from_variant.rs: the payload parameter is <Self as FromVariant<Tag>>::Value.
  • derive_has_fields/{to_fields_enum,from_fields_enum,to_fields_ref_enum}.rs: <Self as HasFields>::Fields and <Self as HasFieldsRef>::FieldsRef<'__a>.

The struct impls keep Self::…, since a struct has no variants to collide with. The variant constructors in the bodies (Self::Value(value)) stay as written, because the qualified signature removes the ambiguity. Each trait was already exported through an exports marker, so the paths stay hygienic.

Helper attributes kept off the data derives' companions

A struct or enum can now combine the extensible-data derives with another derive whose helper attributes it uses, such as #[derive(Default)] with #[default] or #[derive(Serialize)] with #[serde(…)]. Before, the helper attribute was copied onto a generated companion type that lacks that derive, and the build failed.

A helper attribute of another derive reached a type that does not run that derive, because the builder's __Partial{Name} struct and the extractor's __Partial{Name} and __PartialRef{Name} enums are clones of the input that cleared the input's own attributes but kept each field's and variant's. The compiler then reported "cannot find attribute default in this scope" and suggested adding the #[derive(Default)] the enum already has.

A new helper, retain_companion_attrs (cgp-macro-core/src/types/cgp_data/companion_attrs.rs), keeps only the documentation of a field or variant. derive_extractor_enum, derive_extractor_enum_ref, and derive_builder_struct call it on each variant or field they clone. The filter first kept #[cfg] as well, so a companion would have the same members as the input, but the compiler evaluates #[cfg] and #[cfg_attr] before a derive sees its input, so no #[cfg] ever reached the filter. The filter now keeps documentation alone, and its comment and the tests' headers say why.

The alternative, dropping every attribute, would lose documentation a reader of the expansion might want. A lint attribute such as #[allow] on a field or variant is no longer copied either. Neither suite showed a new warning, but no test puts such an attribute on a member the companion would warn about.

Path! keeps only real primitive names as types

A path segment is now kept as a type only when it is a real Rust primitive name, so @app.f, @app.u2, and @app.i7 are symbols. Before, any i, u, or f followed by digits or by nothing counted as a primitive, so @app.f named an undeclared type f and failed to compile.

The rule was a shape test that matched non-primitives. Path! reads a lowercase identifier segment as a Symbol unless is_primitive_type accepts it. The compiler reported:

error[E0425]: cannot find type `f` in this scope
  --> src/main.rs:33:34
   |
33 |         GreeterComponent => @app.f,
   |                                  ^ not found in this scope

The fix replaces the shape test with the exact names u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, char, bool, and str. f16 and f128 are left out, since they are unstable on the pinned nightly.

The same list replaced the copies in the cargo-cgp repository's cargo-cgp-error-processing/src/postprocess/resugar_path.rs and cargo-cgp-expand/src/resugar/parts.rs, which must classify a segment as the macro does, or a resugared Path!(@…) would read differently from what the macro builds. The copies had already drifted: the expand copy required a digit after the letter, so it treated a bare f as a symbol while the macro and the diagnostic copy treated it as a type.

A program that names a real type f, u2, or the like in a path segment now gets a symbol there instead. Such a type is unusual in Rust, since types are capitalized by convention, and the expansion shows the change. The resugaring of a diagnostic or an expansion now declines a PathCons whose segment is a type named like a primitive, since Path! has no spelling for it.

A -> entry's errors point at the entry

When a -> wiring entry forwards a key to a table that lacks it, rustc's errors now point at the entry (-> Bundle) instead of underlining the whole delegate_components! block. The same change narrows an unconstrained-parameter help on a -> entry.

The existing boundary re-span did not reach the errors, because a -> entry's errors come from inside its impls, not from their headers. The entry's value is the projection <Bundle as DelegateComponent<GreeterComponent>>::Delegate. Its DelegateComponent impl carries where Bundle: DelegateComponent<GreeterComponent>, and its IsProviderFor impl carries the same bound and a bound on the projection. When the table lacks the key, rustc reports these as false global bounds. Each fragment mixes the user's key and table with synthesized tokens, which widened its span to the whole invocation.

The changes, in cgp-macro-core:

  • functions/override_span.rs: a new override_synthesized_span re-spans every synthesized top-level token of a fragment onto a given span, leaving the user's tokens alone.
  • types/delegate_component/mapping/direct.rs: the bound and the projection get their synthesized tokens re-spanned onto the -> token, and the entry carries delegate_span: Some(arrow).
  • types/delegate_component/mapping/eval.rs: two new EvaluatedDelegateEntry fields, None for every other entry form. delegate_span (the ->) re-spans the type Delegate = …; item and the IsProviderFor impl's bound on the value. params_span (the - of the arrow alone) re-spans that impl's __Context__ and __Params__: ?Sized parameters.

The parameters needed their own span because rustc reports a false global bound at the first where predicate whose span contains it, and an inline parameter bound is such a predicate. Spanning the whole block, __Params__: ?Sized contained every bound and claimed all the errors. On the full ->, it claimed the IsProviderFor impl's copy of the table bound, which rustc then printed a second time at a different span. On the -, it contains neither.

The -> token is the target because no resolvable name shares its range. Re-spanning a synthesized reference such as DelegateComponent onto the key or the table would make the editor offer it on go-to-definition there, which the existing span design avoids. Two alternatives were set aside. Re-spanning only the boundary tokens of each fragment left the whole-block parameter predicate claiming the errors. Using one span for everything printed the bound twice.

The output now reads:

error[E0277]: the trait bound `Bundle: DelegateComponent<GreeterComponent>` is not satisfied
  --> src/main.rs:43:26
   |
43 |         GreeterComponent -> Bundle,
   |                          ^^^^^^^^^ unsatisfied trait bound

Only -> entries change. Every other entry form passes None and keeps its spans.

An #[async_trait] default body becomes the returned future

An async trait method with a default body now works under #[async_trait]. The macro wraps the body in async move { … }, so it becomes the future the rewritten method returns. Before, the body was left as written under an impl Future return type and failed to compile.

The body's value was not a future, so the compiler reported E0271 on the pinned nightly or E0277 on the stable toolchain. #[async_trait] rewrites async fn f(&self) -> T to fn f(&self) -> impl Future<Output = T> so a trait avoids the async_fn_in_trait lint, but it kept a default body verbatim:

error[E0271]: type mismatch resolving `impl Future<Output = u32> == {integer}`
  --> src/main.rs:15:1
   |
15 | #[async_trait]
   | ^^^^^^^^^^^^^^ expected integer, found associated type

impl_async in cgp-async-macro/src/impl_async.rs now replaces a rewritten method's default body with { async move { <rebindings> <body> } }. A new move_params_into_future rebinds each by-value parameter bound to a plain name (let value = value;) at the start of the block, so the future owns it and drops it when it completes, as an async fn does. Without the rebinding, async move would capture only the parameters the body uses and drop the others when the method returns. A mut on a parameter moves to its rebinding, where the body mutates it. Return-position impl Trait in a trait captures every lifetime in scope, so a body borrowing &self or a reference argument needs no extra bound.

A component's default async body reaches its provider trait through #[cgp_component] and is wrapped there too, so an empty provider inherits it. A destructured parameter ((a, b): (u32, u32)) and a by-value self are not rebound: their parts move into the future only when the body uses them, so an unused part drops when the method returns, earlier than an async fn would drop it. The alternative, rejecting a default body at macro time, would refuse a form the compiler accepts for a plain async fn.

cgp_namespace! rejects a namespace that inherits itself

cgp_namespace! { new SelfNs: SelfNs {} } now fails at expansion with a message naming the cycle and a caret on the parent. Before, the macro lowered it and the compiler rejected the generated impl with an E0207 about an internal __Value__ parameter, underlining the whole block.

A self-parent makes the bound a projection of the very trait the impl defines, which does not determine the impl's __Value__. A namespace inherits its parent through a blanket impl bounded on the parent's lookup trait, and when the parent is the namespace itself, that bound is the projection.

NamespaceTable's parser (cgp-macro-core/src/types/namespace/table.rs) calls reject_self_inheritance after parsing the parent. The check rejects a parent that is a bare name equal to the namespace, a parent that names the namespace with different type arguments (<T> new Ns<T>: Ns<u32> and <T> new Ns<T>: Ns<Vec<T>>), and a self:: path ending in the namespace's name. Every instance of a namespace has the same parent rule, so such a parent always asks the namespace again. The error keeps its caret on the parent path, now the whole self::SelfNs.

A parent written through any other path, such as crate::SelfNs or super::SelfNs, stays unchecked. The macro cannot tell which item the path resolves to, since crate::SelfNs is the namespace only when the macro runs at the crate root. The function's doc, the reference, and the error catalog state that limit. A cycle through two or more namespaces stays the compiler's to report, since one invocation sees only its own parent.

The output now reads:

error: the namespace `SelfNs` cannot inherit from itself; name another namespace as its parent, or omit the parent
  --> src/main.rs:13:17
   |
13 |     new SelfNs: SelfNs {}
   |                 ^^^^^^

#[cgp_component] rejects #[uses]

#[uses] on a #[cgp_component] trait, where #[extend] belongs, now fails at expansion with a message naming the #[extend(...)] form to write. Before, the compiler reported an unknown attribute with a suggestion of the unrelated built-in #[used].

The component's attribute collector forwarded any attribute it did not recognize, so #[uses] reached the compiler. #[uses] declares the dependencies of a provider (a #[cgp_impl] or #[cgp_fn]), while a component declares what every context must implement with #[extend].

CgpComponentAttributes::parse (cgp-macro-core/src/types/attributes/cgp_component_attributes.rs) now matches uses and returns an error spanned on the attribute, repeating its arguments in the suggested form:

`#[uses]` declares the dependencies of a provider, not of a trait; write `#[extend(HasName)]` to make it a supertrait

The same collector serves #[cgp_type], #[cgp_getter], and #[cgp_auto_getter], so each rejects #[uses] the same way, which is why the message speaks of a trait rather than a component. #[extend_where] and #[use_provider] on a component still pass through to the compiler's unknown-attribute error, since neither had a fixture.

A component method parameter needs a plain name, and mut is forwarded

A #[cgp_component] method parameter written as _ or a destructuring pattern now fails with a message on the pattern saying to name it. A mut value parameter is accepted, and the forwarding impls forward it by name.

A _ or destructuring pattern failed with an internal parse error naming proc_macro2::Ident, because the impls a component generates call the delegate with each parameter by name, so the macro read every parameter pattern as an identifier.

arg_to_ident (cgp-macro-core/src/functions/delegated_impls/signature.rs) now reads the name from a plain identifier pattern (no ref, no subpattern) and otherwise returns an error spanned on the pattern:

a component method parameter needs a plain name, since the generated impls forward each argument by name; write a name such as `_value`, and destructure inside a default body

A mut count parameter is a plain name, so it is accepted. The forwarding impls forward count and leave the mut off their own signatures, where it would be unused. The default body, which an empty provider inherits, keeps it. A ref binding and a binding with a subpattern stay rejected, now with tests, as do _ and destructuring patterns.

#[cgp_computer] rejects a parameter only the return type uses

A #[cgp_computer] function with a type or const parameter that no argument type names, such as fn parse<T: FromStr>(value: String) -> Option<T>, now fails at expansion with a message on the parameter saying what to change. Before, the macro emitted a Computer impl the compiler rejected with two E0207s whose suggestions do not compile.

A parameter only the return type uses is constrained by nothing, so the impl fails. The macro moves the function's generics onto the generated impl, whose trait arguments carry only the input types.

A new reject_output_only_params (cgp-macro-extra-core/src/functions/output_only_params.rs), called from ItemCgpComputer::preprocess, rejects a type or const parameter that no argument type names, unless another bound names it: an inline bound of another parameter, or a where predicate whose bounded type is not the parameter alone. The exception keeps a parameter an associated-type bound determines, as in fn first_item<I: IntoIterator<Item = T>, T>(items: I) -> Option<T>, where T is constrained through I. It leaves the compiler the precise rule and rejects only what no bound could constrain. Lifetimes are not checked.

The search reads a macro's tokens too, so fn first_of_pair<T>(pair: Product![(T, T)]) -> T counts T as named by an argument. syn's visitor does not look inside a type macro, so the first version rejected that program, which compiled before the check existed. The search now counts any token of the parameter's name and leaves the doubt to the compiler.

The output now reads:

error: the parameter `T` appears in no argument type, so the generated `Computer` impl cannot determine it; take an argument whose type names `T`, or write the provider by hand as a `Computer` impl on a provider generic over `T`
  --> src/main.rs:14:10
   |
14 | fn parse<T: core::str::FromStr>(value: String) -> Option<T> {
   |          ^

A parameter that some bound names but that still ends up unconstrained reaches the compiler's E0207, by design of the exception. #[cgp_auto_dispatch] builds computers through the same path; its generated __Variants__ parameter appears in an input, and its methods reject type and const parameters already.

An optional reference to an unsized type is rejected

cgp's getter macros and #[implicit] arguments now reject Option<&[T]> and Option<&dyn Trait> (and their &mut forms) at expansion, with the caret on the written type and a message naming the sized container to read instead. Before, they lowered the type to a bound over an unsized Option.

The generated bound was ill-formed, so the compiler reported E0277 on the attribute and a derived E0599 on the generated .as_ref() call. A getter or implicit argument typed Option<&[T]> combines two shorthands, and the shared parse_field_type lowered it by the Option<&T> rule, so the bound named HasField<Symbol!("items"), Value = Option<[u8]>>. Neither error named the getter's return type or the shorthand.

parse_field_type (cgp-macro-core/src/functions/field/parse.rs) now calls reject_unsized_option_ref before the Option<&T> rule. When the referent of an Option<&T> or Option<&mut T> is a slice or a trait object (looking through parentheses, so Option<&(dyn Debug + Send)> counts), the parser returns a spanned error over the whole Option<…> type. The message quotes the reference as written and names the referent inside each container, so Option<&[u8]> points to an Option<Vec<u8>> field and Option<&(dyn Debug + Send)> to an Option<Box<dyn Debug + Send>> field.

A new private type_text renders a type from its tokens and removes the spaces the token printer puts around ::, <, >, ,, ;, brackets, parentheses, and after &, since cgp-macro-core has no other type printer and the plain token string reads core :: fmt :: Debug. A new private join_argument_lists removes the space before a parenthesis that follows a name, so Fn (u8) reads Fn(u8). A parenthesized type keeps its space, so -> (u8, u16), &'a (dyn Debug), and &mut (dyn Debug) are unchanged.

The output now reads:

error: `Option<&[u8]>` would read an `Option<[u8]>` field, which cannot exist because a slice is unsized; read an `Option<Vec<u8>>` field as `Option<&Vec<u8>>` instead
  --> src/main.rs:16:24
   |
16 |     fn items(&self) -> Option<&[u8]>;
   |                        ^^^^^^^^^^^^^

The check is syntactic and catches only the shapes that show they are unsized. A type named by a path (Path, OsStr, a program's own dynamically sized struct) gives the macro no such mark, so Option<&Path> still lowers to an Option<Path> field and fails at the compiler. That remainder is an intended deferral, since the macro cannot see the type's definition. The alternative, supporting Option<&[T]> as a new access form, would add a feature with its own generics and tests, while the defect called for a rejection or a reshape.

The forwarding impls drop a mut self receiver's mut

A component method whose default body takes mut self now compiles without a warning. The forwarding impls drop the receiver's mut, as they already did for a mut parameter, while a &mut self receiver keeps its &mut.

A correct component compiled with rustc's unused_mut warning, whose help to remove the mut would break the default body. signature_to_delegated_impl_item_fn (cgp-macro-core/src/functions/delegated_impls/signature.rs) cleared mut from each typed parameter of a forwarding signature but not from a mut self receiver:

warning: variable does not need to be mutable
 --> src/bin/mut_self_receiver.rs:5:16
  |
5 |     fn consume(mut self, extra: u32) -> u32
  |                ----^^^^
  |                |
  |                help: remove this `mut`

The loop over the forwarding signature's inputs now also clears a receiver's mutability, but only for a by-value receiver. syn records &mut self with the same mutability field, there part of the receiver's type, so clearing it unconditionally would turn &mut self into &self. The comment says why.

Tests pin each change

This PR adds behavioral tests and rejection tests for each change, and it extends existing tests where a case was missing:

  • Self::CONST in a provider body, a component default body, and a macro body: cgp-tests/tests/basic_delegation/self_local_assoc_const.rs. A nested item and a const the context also declares through #[uses]: self_local_assoc_const_scope.rs, which pins which const a Self::LIMIT names in each case. The associated-type test's comment now points to the const test.
  • An enum with all seven reserved names builds, extracts (owned, borrowed, and mutable), and round-trips: cgp-tests/tests/extensible_variants/reserved_variant_names.rs.
  • The builder and extractor outputs carry no #[serde] or #[default] but keep the documentation: the new cgp-macro-tests/tests/companion_attrs/ target. Default and CgpData together on an enum with #[default]: cgp-tests/tests/extensible_variants/variant_helper_attribute.rs.
  • Path!(@f.u2.i7.f64) is asserted by type equality, and each of the seventeen primitive names is asserted on its own: cgp-tests/tests/namespaces/path_macro.rs.
  • A plain trait's default bodies, a mut parameter mutated inside, a component's default body inherited by an empty provider, and a by-value guard parameter dropped only when the future completes: cgp-tests/tests/async_and_send/default_body.rs.
  • The self-inheritance message for a plain namespace, a generic namespace, an instance of itself, and a self:: path: cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs.
  • The #[uses] rejection and the plain-name rejection for _, ref, a subpattern, and a destructuring pattern: cgp-macro-tests/tests/parser_rejections/cgp_component.rs.
  • The output-only rejection for a type and a const parameter: cgp-macro-tests/tests/parser_rejections/cgp_computer.rs. first_item and last_item, which the exception accepts through an inline bound and a where predicate, and first_of_pair, which names T inside a Product!: cgp-tests/tests/handlers/computer_macro_generics.rs.
  • The unsized-optional rejection for a slice, a mutable slice, a trait object, a parenthesized trait object, and an implicit argument: cgp-macro-tests/tests/parser_rejections/getters.rs and cgp_fn.rs. The tests expect the written types, and the trait-object test checks each form's own message.
  • A Consumer component whose default consume(mut self, extra) mutates self, inherited by an empty provider, with a runtime assertion: cgp-tests/tests/basic_delegation/default_method_mut_parameter.rs. The getters/mut_slice.rs snapshot pins &mut self.

This PR also removes cgp-macro-tests/tests/invalid_expansion/reserved_variant_names.rs, since the reserved names now compile, and updates the expansion snapshots in the extensible_variants files. Every removed snapshot line is one of the old Self::… signatures, replaced by the qualified form.

The guides describe the current layout

The repository guides now describe the current test layout and the cargo-cgp toolchain:

  • cargo-cgp's class-based fixture layout is described in crates/tests/AGENTS.md. A fixture is filed under its error class, mirroring the knowledge base's error catalog, and the quality of its output is recorded in a //@quality: header (acceptable, usability, or hidden). The guide also drops the migration narration and states the current workflow.
  • cargo-cgp installs with --locked, and the front-end needs Rust 1.96 or newer. The tool offers the reading commands check and expand, and the early-release note is gone: README.md.
  • cargo-cgp is the recommended way to check CGP code and to read a CGP compile error. Fixtures are filed by class, and the one-way code dependency is stated without the migration history: AGENTS.md.
  • The sibling list and the finding rules are stated plainly, without narrating where the documents moved: sibling-projects.md.
  • The new companion_attrs target is listed and the shape_macro_parsing parser target is named: crates/tests/README.md.

Impact

No program that compiled before changes meaning, except for a path segment that names a real type like a primitive. The other changes affect only programs that failed to compile, programs the macros now reject, or a program that compiled with an unused_mut warning. The changes are:

  • A Self::CONST that names a local const now compiles. Code that worked around the defect with the qualified path <AllowUnderLimit as RateLimiter<Self>>::LIMIT keeps working.
  • An enum may use the seven reserved variant names. The qualified path names the same associated type the bare path named whenever the bare path was unambiguous.
  • A data derive's companion no longer carries another derive's helper attributes. A field's #[serde(…)] on a #[derive(BuildField)] struct no longer reaches its companion.
  • @app.f, @app.u2, and @app.i7 are symbols. A real type named like a primitive in a path segment becomes a symbol, and the resugaring declines a PathCons whose segment is such a type. This is the one case where a program that compiled before can change meaning.
  • A -> entry's missing-key error points at the entry. The raw E0277s still precede the tool's [CGP-E001] block with rustc's wrong trivial_bounds suggestion, now on the entry.
  • An #[async_trait] default body works. A destructured parameter and a by-value self are not rebound, so an unused part drops when the method returns rather than when the future completes.
  • A self-inheriting namespace gets the macro's message. A crate::Ns or super::Ns parent stays unchecked and reaches the compiler.
  • #[uses] on a component gets the macro's message. #[extend_where] and #[use_provider] on a component still reach the compiler's unknown-attribute error.
  • A component method parameter without a plain name gets the macro's message. A mut parameter is accepted; ref and subpatterns stay rejected.
  • A #[cgp_computer] parameter that only the return type uses gets the macro's message. A parameter that some bound names but that still ends up unconstrained reaches the compiler's E0207.
  • An optional reference to a slice or a trait object gets the macro's message. Option<&Path> still reaches the compiler.
  • A mut self receiver compiles without the unused_mut warning, and &mut self keeps its &mut.

The cargo-cgp UI suite and the knowledge base, website, and skill documents are updated in their own repositories to match. The cargo-cgp fixtures that pinned the old failures moved to ok/ or pass-through/, and the fixtures whose spans changed were re-blessed with explained diffs. The cargo-cgp UI suite must be run against this branch before the release, since the cgp changes move its snapshots.

Areas needing attention

The span change is the part most worth a careful read. It relies on rustc's check_false_global_bounds reporting a false global bound at the first where predicate whose span contains it, and on the - of the -> containing neither the bound nor the parameter predicate. The delegate_span and params_span fields are None for every other entry form, so a reviewer should check that the other forms keep their spans.

The #[async_trait] rebinding covers only parameters bound to a plain name. A destructured parameter and a by-value self are not rebound, so an unused part drops earlier than an async fn would drop it. The tests cover the plain-name case and the guard parameter, not the destructured case.

The unsized-optional rejection is syntactic. A type named by a path still reaches the compiler, and the message's type_text removes only the spaces it lists. A referent with other token shapes, such as a trait object with a function-call bound, may keep some of the token printer's spacing. The tests cover slices, mutable slices, path-qualified trait objects, and a parenthesized trait object with two bounds.

The namespace self-inheritance check leaves crate::Ns, super::Ns, and other qualified parents unchecked. The documents state the limit, and the crate:: form reaches the compiler as an E0207 that cargo-cgp passes through.

The Path! primitive list excludes f16 and f128, since they are unstable on the pinned nightly. A program that names a real type f, u2, or the like in a path segment now gets a symbol there instead.

The Self::CONST qualification handles only a two-segment Self::CONST. A local const named through a longer or generic path is not a Rust form, so nothing else needs the rewrite.

The retain_companion_attrs filter keeps only documentation. A lint attribute such as #[allow] on a field or variant is no longer copied, and no test puts such an attribute on a member the companion would warn about.

soareschen and others added 25 commits October 11, 2026 19:43
A getter return or #[implicit] argument typed Option<&[T]> or
Option<&dyn Trait> lowered by the Option<&T> rule to a HasField bound
over an unsized Option<[T]>, which the compiler rejected on the macro
attribute. The shared field-type parser now rejects both shapes with a
message naming the sized container to read instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SajznGLmpQzjPd1newVGDK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `#[cgp_impl]` block's `Self::LIMIT` names the block's own const even
when a `#[uses]` trait gives the context a const of the same name, a
nested item's `Self::LIMIT` names that item's own, and a `Self::CEILING`
naming a const only the context has reads the context's, in code and in
a macro body. No test showed the first or the last, so disabling the
name checks of the qualifying visitor passed the suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The companion filter also kept `#[cfg]` so a companion would have the
same members as the input, but the compiler evaluates `#[cfg]` and
`#[cfg_attr]` before a derive sees its input, so no `#[cfg]` ever
reached it. The filter now keeps documentation alone, and its comment
and the tests' headers say why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The segment rule is a list of the seventeen primitive names, and the
test asserted three of them; dropping any of eleven others from the list
passed the suite. Each name is now asserted on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cgp_namespace!` rejected a parent naming the namespace only when its
type arguments matched, so `<T> new Ns<T>: Ns<u32>` and
`<T> new Ns<T>: Ns<Vec<T>>` reached the compiler, which reports an
overflow on stable, once without a location, and accepts both on the
nightly. Every instance of a namespace has the same parent rule, so
such a parent always asks the namespace again. The rejection now
ignores the arguments, and points at the parent's path: the rebuilt
`<…>` carried the macro's span and widened the caret to the whole
invocation for any parent with arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A default method may declare `mut count`, which the macro rejected as a
parameter without a plain name, though the forwarding impls only need
the name. They now forward `count` and leave the `mut` off their own
signatures, where it would be unused; the default body, which an empty
provider inherits, keeps it. A `ref` binding and a binding with a
subpattern stay rejected, now with tests, as do `_` and destructuring
patterns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The output-only check searched the argument types for the parameter
with `syn`'s visitor, which does not look inside a type macro, so
`fn first_of_pair<T>(pair: Product![T, T]) -> T` was rejected as using
`T` only in its return type, though it compiled before the check
existed. The search now reads a macro's tokens too, counting any token
of the parameter's name and leaving the doubt to the compiler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Disabling the `where`-predicate search, its skip of a predicate on the
parameter itself, or the search inside a type macro's groups passed the
suite. The tests now reject an output-only parameter bounded in a
`where` clause, accept one bound through another parameter's `where`
predicate, and accept one named only inside a tuple in a `Product!`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's install command lacked `--locked` and claimed any toolchain
builds the front-end, which needs Rust 1.96, and it described the tool as a
lone `check` in an early release. The agent guides called it a way to build
CGP code, filed fixtures by quality rather than by class, and narrated how
the compile-fail suite moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cgp_namespace!` compared the parent with the namespace only for a
bare name, so `new SelfNs: self::SelfNs` reached the compiler as an
`E0207` on the generated `__Value__`. A `self::` path names the
namespace in the module the macro expands in, so it is now rejected
like the bare form. Any other qualified parent, such as `crate::Ns`,
names an item the macro cannot resolve and stays unchecked, which the
function's doc states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rejection of an optional reference to a slice or a trait object
worded its message from placeholders, so `Option<&[u8]>` read
`Option<&[T]>` and `Option<&dyn Debug>` read `dyn Trait`. It now quotes
the reference as written and names the referent inside each container,
`Option<Vec<u8>>` or `Option<Box<dyn Debug + Send>>`, through a small
renderer that tightens the token printer's spacing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The forwarding impls left the `mut` off each typed parameter of a
default method but kept it on a `mut self` receiver, where nothing
mutates it, so a correct component compiled with rustc's `unused_mut`
warning, whose help to remove the `mut` would break the default body.
The receiver's `mut` is now dropped too, only for a by-value receiver,
since a `&mut self` receiver carries `mutability` as part of its type.
The test of default-method `mut` parameters covers the receiver, and a
test comment's cleft becomes a direct statement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unsized-optional rejection quoted `[u8 ; 4]` and `dyn Fn (u8)`
with the token printer's spaces. The type printer now joins `;` to
its element and a parenthesized argument list to its name, and keeps
the space before a parenthesized type after `->`, a lifetime, `mut`,
`dyn`, or `impl`, so `&mut (dyn Debug)` keeps its form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@soareschen
soareschen merged commit 3e02c29 into main Nov 6, 2026
3 checks passed
@soareschen
soareschen deleted the cargo-cgp-refactoring branch November 6, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant