Repository navigation
Fix expansion defects, reject malformed input, and narrow -> error spans - #281
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SajznGLmpQzjPd1newVGDK
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>
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.
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 thecargo-cgptoolchain.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, andmutparameters 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 wholedelegate_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-cgperror 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::CONSTA provider written with
#[cgp_impl]can read a const its block declares asSelf::LIMIT, and a component's default method body can read the trait's own const asSelf::LIMIT. Both failed before.A
Self::LIMITnaming the block's own const became__Context__::LIMIT, which the context does not have, so the compiler reportedE0599. The rewrite turns everySelfin a provider block into the context, and it exempted only the associated types the block declares:The same rewrite in
#[cgp_component]turned a default body'sSelf::LIMITinto__Context__::LIMITon 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:
to_provider_trait.rs): the trait's own consts join the list of local items thatReplaceSelfTypeVisitorskips, soSelf::LIMITstaysSelf::LIMITin the provider trait and names the provider's const.#[cgp_impl]bodies (lowered.rs): skipping alone is not enough. WithSelfleft as the provider struct,Self::LIMITfails withE0034because the consumer trait's blanket impl gives the provider struct the consumer trait, which declares the same const. A newQualifySelfConstVisitor(visitors/replace_self/qualify_self_const.rs) runs before the other rewrites and replacesSelf::LIMITwith<AllowUnderLimit as RateLimiter<__Context__>>::LIMIT, in expressions, patterns, and macro bodies. The visitor also rewrites a macro body's token stream, as the otherSelfvisitors do. The visitor's fieldskip_assoc_typesbecameskip_assoc_items, since it now holds consts too, and the getter parser's two uses were renamed with it.The alternative, rewriting
Self::LIMITto 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:
Only a two-segment
Self::CONSTis 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, orFieldsRefand still deriveHasFields,FromVariant,ExtractField,CgpVariant, orCgpData. Before, each of those seven names made the derive's expansion fail to compile.A variant called
Valuemade the generated path ambiguous, because the derives named their own associated types through a bareSelf::…path, and inside an impl for an enum such a path can also name a variant. The compiler reportedE0221"ambiguous associated item".#[derive(FromVariant)]reservedValue,#[derive(HasFields)]reservedFieldsandFieldsRef, 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>::Extractorinto_extractorandfrom_extractor, and<Self as HasExtractorRef>::ExtractorRef<'__a__>and<Self as HasExtractorMut>::ExtractorMut<'__a__>in the borrowed accessors.derive_extractor/extract_field_impls.rs:extract_fieldreturnsResult<<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>::Fieldsand<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 anexportsmarker, 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 attributedefaultin 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, andderive_builder_structcall 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 typesA path segment is now kept as a type only when it is a real Rust primitive name, so
@app.f,@app.u2, and@app.i7are symbols. Before, anyi,u, orffollowed by digits or by nothing counted as a primitive, so@app.fnamed an undeclared typefand failed to compile.The rule was a shape test that matched non-primitives.
Path!reads a lowercase identifier segment as aSymbolunlessis_primitive_typeaccepts it. The compiler reported: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, andstr.f16andf128are left out, since they are unstable on the pinned nightly.The same list replaced the copies in the
cargo-cgprepository'scargo-cgp-error-processing/src/postprocess/resugar_path.rsandcargo-cgp-expand/src/resugar/parts.rs, which must classify a segment as the macro does, or a resugaredPath!(@…)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 barefas 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 aPathConswhose segment is a type named like a primitive, sincePath!has no spelling for it.A
->entry's errors point at the entryWhen 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 wholedelegate_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. ItsDelegateComponentimpl carrieswhere Bundle: DelegateComponent<GreeterComponent>, and itsIsProviderForimpl 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 newoverride_synthesized_spanre-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 carriesdelegate_span: Some(arrow).types/delegate_component/mapping/eval.rs: two newEvaluatedDelegateEntryfields,Nonefor every other entry form.delegate_span(the->) re-spans thetype Delegate = …;item and theIsProviderForimpl's bound on the value.params_span(the-of the arrow alone) re-spans that impl's__Context__and__Params__: ?Sizedparameters.The parameters needed their own span because rustc reports a false global bound at the first
wherepredicate whose span contains it, and an inline parameter bound is such a predicate. Spanning the whole block,__Params__: ?Sizedcontained every bound and claimed all the errors. On the full->, it claimed theIsProviderForimpl'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 asDelegateComponentonto 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:
Only
->entries change. Every other entry form passesNoneand keeps its spans.An
#[async_trait]default body becomes the returned futureAn
asynctrait method with a default body now works under#[async_trait]. The macro wraps the body inasync move { … }, so it becomes the future the rewritten method returns. Before, the body was left as written under animpl Futurereturn type and failed to compile.The body's value was not a future, so the compiler reported
E0271on the pinned nightly orE0277on the stable toolchain.#[async_trait]rewritesasync fn f(&self) -> Ttofn f(&self) -> impl Future<Output = T>so a trait avoids theasync_fn_in_traitlint, but it kept a default body verbatim:impl_asyncincgp-async-macro/src/impl_async.rsnow replaces a rewritten method's default body with{ async move { <rebindings> <body> } }. A newmove_params_into_futurerebinds 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 anasync fndoes. Without the rebinding,async movewould capture only the parameters the body uses and drop the others when the method returns. Amuton a parameter moves to its rebinding, where the body mutates it. Return-positionimpl Traitin a trait captures every lifetime in scope, so a body borrowing&selfor a reference argument needs no extra bound.A component's default
asyncbody 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-valueselfare 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 anasync fnwould drop it. The alternative, rejecting a default body at macro time, would refuse a form the compiler accepts for a plainasync fn.cgp_namespace!rejects a namespace that inherits itselfcgp_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 anE0207about 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) callsreject_self_inheritanceafter 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 aself::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 wholeself::SelfNs.A parent written through any other path, such as
crate::SelfNsorsuper::SelfNs, stays unchecked. The macro cannot tell which item the path resolves to, sincecrate::SelfNsis 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:
#[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 matchesusesand returns an error spanned on the attribute, repeating its arguments in the suggested form: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
mutis forwardedA
#[cgp_component]method parameter written as_or a destructuring pattern now fails with a message on the pattern saying to name it. Amut valueparameter is accepted, and the forwarding impls forward it by name.A
_or destructuring pattern failed with an internal parse error namingproc_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 (noref, no subpattern) and otherwise returns an error spanned on the pattern:A
mut countparameter is a plain name, so it is accepted. The forwarding impls forwardcountand leave themutoff their own signatures, where it would be unused. The default body, which an empty provider inherits, keeps it. Arefbinding 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 usesA
#[cgp_computer]function with a type or const parameter that no argument type names, such asfn 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 aComputerimpl the compiler rejected with twoE0207s 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 fromItemCgpComputer::preprocess, rejects a type or const parameter that no argument type names, unless another bound names it: an inline bound of another parameter, or awherepredicate whose bounded type is not the parameter alone. The exception keeps a parameter an associated-type bound determines, as infn first_item<I: IntoIterator<Item = T>, T>(items: I) -> Option<T>, whereTis constrained throughI. 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)]) -> TcountsTas 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:
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 rejectOption<&[T]>andOption<&dyn Trait>(and their&mutforms) 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 unsizedOption.The generated bound was ill-formed, so the compiler reported
E0277on the attribute and a derivedE0599on the generated.as_ref()call. A getter or implicit argument typedOption<&[T]>combines two shorthands, and the sharedparse_field_typelowered it by theOption<&T>rule, so the bound namedHasField<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 callsreject_unsized_option_refbefore theOption<&T>rule. When the referent of anOption<&T>orOption<&mut T>is a slice or a trait object (looking through parentheses, soOption<&(dyn Debug + Send)>counts), the parser returns a spanned error over the wholeOption<…>type. The message quotes the reference as written and names the referent inside each container, soOption<&[u8]>points to anOption<Vec<u8>>field andOption<&(dyn Debug + Send)>to anOption<Box<dyn Debug + Send>>field.A new private
type_textrenders a type from its tokens and removes the spaces the token printer puts around::,<,>,,,;, brackets, parentheses, and after&, sincecgp-macro-corehas no other type printer and the plain token string readscore :: fmt :: Debug. A new privatejoin_argument_listsremoves the space before a parenthesis that follows a name, soFn (u8)readsFn(u8). A parenthesized type keeps its space, so-> (u8, u16),&'a (dyn Debug), and&mut (dyn Debug)are unchanged.The output now reads:
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, soOption<&Path>still lowers to anOption<Path>field and fails at the compiler. That remainder is an intended deferral, since the macro cannot see the type's definition. The alternative, supportingOption<&[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 selfreceiver'smutA component method whose default body takes
mut selfnow compiles without a warning. The forwarding impls drop the receiver'smut, as they already did for amutparameter, while a&mut selfreceiver keeps its&mut.A correct component compiled with rustc's
unused_mutwarning, whose help to remove themutwould break the default body.signature_to_delegated_impl_item_fn(cgp-macro-core/src/functions/delegated_impls/signature.rs) clearedmutfrom each typed parameter of a forwarding signature but not from amut selfreceiver:The loop over the forwarding signature's inputs now also clears a receiver's
mutability, but only for a by-value receiver.synrecords&mut selfwith the samemutabilityfield, there part of the receiver's type, so clearing it unconditionally would turn&mut selfinto&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::CONSTin 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 aSelf::LIMITnames in each case. The associated-type test's comment now points to the const test.cgp-tests/tests/extensible_variants/reserved_variant_names.rs.#[serde]or#[default]but keep the documentation: the newcgp-macro-tests/tests/companion_attrs/target.DefaultandCgpDatatogether 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.mutparameter 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.self::path:cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs.#[uses]rejection and the plain-name rejection for_,ref, a subpattern, and a destructuring pattern:cgp-macro-tests/tests/parser_rejections/cgp_component.rs.cgp-macro-tests/tests/parser_rejections/cgp_computer.rs.first_itemandlast_item, which the exception accepts through an inline bound and awherepredicate, andfirst_of_pair, which namesTinside aProduct!:cgp-tests/tests/handlers/computer_macro_generics.rs.cgp-macro-tests/tests/parser_rejections/getters.rsandcgp_fn.rs. The tests expect the written types, and the trait-object test checks each form's own message.Consumercomponent whose defaultconsume(mut self, extra)mutatesself, inherited by an empty provider, with a runtime assertion:cgp-tests/tests/basic_delegation/default_method_mut_parameter.rs. Thegetters/mut_slice.rssnapshot 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 theextensible_variantsfiles. Every removed snapshot line is one of the oldSelf::…signatures, replaced by the qualified form.The guides describe the current layout
The repository guides now describe the current test layout and the
cargo-cgptoolchain:cargo-cgp's class-based fixture layout is described incrates/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, orhidden). The guide also drops the migration narration and states the current workflow.cargo-cgpinstalls with--locked, and the front-end needs Rust 1.96 or newer. The tool offers the reading commandscheckandexpand, and the early-release note is gone:README.md.cargo-cgpis 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.sibling-projects.md.companion_attrstarget is listed and theshape_macro_parsingparser 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_mutwarning. The changes are:Self::CONSTthat names a local const now compiles. Code that worked around the defect with the qualified path<AllowUnderLimit as RateLimiter<Self>>::LIMITkeeps working.#[serde(…)]on a#[derive(BuildField)]struct no longer reaches its companion.@app.f,@app.u2, and@app.i7are symbols. A real type named like a primitive in a path segment becomes a symbol, and the resugaring declines aPathConswhose segment is such a type. This is the one case where a program that compiled before can change meaning.->entry's missing-key error points at the entry. The rawE0277s still precede the tool's[CGP-E001]block with rustc's wrongtrivial_boundssuggestion, now on the entry.#[async_trait]default body works. A destructured parameter and a by-valueselfare not rebound, so an unused part drops when the method returns rather than when the future completes.crate::Nsorsuper::Nsparent 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.mutparameter is accepted;refand subpatterns stay rejected.#[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'sE0207.Option<&Path>still reaches the compiler.mut selfreceiver compiles without theunused_mutwarning, and&mut selfkeeps its&mut.The
cargo-cgpUI suite and the knowledge base, website, and skill documents are updated in their own repositories to match. Thecargo-cgpfixtures that pinned the old failures moved took/orpass-through/, and the fixtures whose spans changed were re-blessed with explained diffs. Thecargo-cgpUI suite must be run against this branch before the release, since thecgpchanges 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_boundsreporting a false global bound at the firstwherepredicate whose span contains it, and on the-of the->containing neither the bound nor the parameter predicate. Thedelegate_spanandparams_spanfields areNonefor 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-valueselfare not rebound, so an unused part drops earlier than anasync fnwould 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_textremoves 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 thecrate::form reaches the compiler as anE0207thatcargo-cgppasses through.The
Path!primitive list excludesf16andf128, since they are unstable on the pinned nightly. A program that names a real typef,u2, or the like in a path segment now gets a symbol there instead.The
Self::CONSTqualification handles only a two-segmentSelf::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_attrsfilter 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.