Skip to content

Harden #[use_type] grounding, lift nested delegation tables, and close test gaps - #264

Merged
soareschen merged 10 commits into
mainfrom
ai-updates-20260725-2
Sep 15, 2026
Merged

soareschen merged 10 commits into
mainfrom
ai-updates-20260725-2

Conversation

@soareschen

@soareschen soareschen commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

AI Overview

This branch expands #[use_type] alias resolution, rejects grounding cycles, fixes missing nested table declarations, and exposes cgp-base through cgp-core. It also adds tests for previously untested behaviour. The branch contains ten commits on top of main (add50de, which moved the documentation into the knowledge base), changing 70 files with 4,897 insertions and 751 deletions. Most additions are test code. The macro-core source changes are confined to the use_type attribute implementation, the delegate_components! body grammar, and the cgp_namespace! evaluator.

Behaviour changes

#[use_type] resolves aliases in more positions and rejects grounding cycles

#[use_type] now resolves imported aliases in positions where it previously emitted an unresolved identifier. This resolution, called grounding, replaces an alias with its associated-type projection. The added positions are:

  • Nested types in a pin's right-hand side: HasDbType.Db, HasTransactionType.{Transaction = Tx<Db>} now replaces the nested Db with <Self as HasDbType>::Db. Previously, substitution required whole-type equality and handled only a bare alias such as {Hashed = Password}. The shared substitution visitor now walks the right-hand side. It excludes the pinned alias itself, so {Foo = Foo} remains an unresolved-name error rather than producing a vacuous bound.
  • Expression-path qualifiers: Transaction::begin_from(pool) and Transaction::LABEL inside a #[cgp_fn] or #[cgp_impl] body now use the projected type. For example, the call becomes <<Self as HasTransactionType>::Transaction>::begin_from(pool). Previously, these paths passed through unchanged even when a let annotation in the same body was rewritten. Substitution applies only to paths with at least two segments, so a single-segment Marker in an expression still names the unit struct. The visitor also recurses into the replacement, resolving aliases in later generic arguments such as Transaction::tagged::<Db>(db).
  • Generic arguments on the imported trait: HasDbType.Db, HasPoolType<Db>.Pool now projects against HasPoolType<<Self as HasDbType>::Db> instead of emitting a bare Db. Grounding still stops at the head of the trait path, which must name a trait and cannot be an alias.

Pins on generic traits now produce valid bounds. HasFooType<u8>.{Foo = u32} previously rendered as HasFooType<u8><Foo = u32> and failed inside the macro with a "failed to parse internal tokens" error. Pins now merge into the trait's argument list, producing HasFooType<u8, Foo = u32>. Lifetime arguments retain their leading position, as in HasRefType<'a, Ref = &'a str>.

Grounding cycles now fail during macro expansion with a diagnostic that names the loop and marks the token that closes it. Rejected examples include HasA.A in B, HasB.B in A, the self-reference HasA.A in A, and a cycle through a trait argument, HasPoolType<Pool>.Pool. For example, a diagnostic identifies `B` -> `A` -> `B`. Previously, iterative grounding wrapped the alias one level deeper on each pass and left the compiler to report E0425 four projections down. Circular pins such as {Foo = Bar}, {Bar = Foo} remain the compiler's responsibility because they express a well-defined constraint that a context may satisfy.

Grounding now follows alias dependencies in a single depth-first traversal. ground_specs, in the new use_type/grounding.rs, builds a dependency graph from the positions that support grounding and processes each specification after its dependencies. An edge back to a specification still being processed identifies a cycle. Each substitution uses fully grounded specifications, so replacements cannot contain bare aliases. The traversal visits each specification once and terminates.

Alias resolution is independent of import order, stacked #[use_type] attributes, and imports that share a context. Tests now cover each case. Emitted supertraits and predicates retain source order, so readers see them in the order the author wrote the imports.

Nested tables are emitted from namespace and loop bodies

Nested tables such as Wrapper<new Inner { … }> are now declared when written inside a cgp_namespace! body or a for … in loop body. Both paths previously parsed the inner table and referenced it in the entry's Delegate without declaring its struct. The resulting E0425 pointed to a user-written name and made the missing declaration look like a typo.

Namespace and loop evaluation now extract nested tables for emission. NamespaceTable::eval runs the same ExtractInnerDelegateTables traversal as DelegateTable::eval and renders the extracted structs after the namespace trait. DelegateEntries::extract_inner_tables also walks statements, with ExtractInnerDelegateTables implemented for the statement forms.

Namespace nesting lets every joining context inherit the dispatch table. Loop nesting is supported for correctness, but is not recommended: the loop already yields one provider per key. A nested table in a loop must declare the loop's provider variable as its own generic, written new LoopInner<Provider>, for the generated implementations to be well formed.

cgp-core exposes cgp-base through the facade

cgp-core now re-exports cgp_base as base, making cgp::core::base::traits::StaticFormat and cgp::core::base::types::{Chars, Cons, Nil, PathCons, Symbol} available. The code now matches the paths already documented in the knowledge base and the /cgp skill.

Related export changes affect the base crates:

  • cgp_base: Glob-re-exports cgp_base_types::*, exposing its traits and types modules. This removes the former cgp_base::base_types alias, which neither the workspace nor sibling checkouts referenced.
  • cgp_base_extra::types: Now refers to cgp_base::types, the type-level primitives module, instead of the cgp_type crate. HasType and related items no longer resolve through this path. They remain available at cgp::core::types and in cgp_base_extra::macro_prelude.

Internal refactors

The use_type implementation now shares the rules for identifying alias references and positions that support grounding. bare_alias_ident, in visitors/substitute_abstract_type.rs, identifies aliases in type positions. Both the substitution visitor and the new read-only collect_bare_aliases visitor use it. The latter collects dependencies for cycle detection. UseTypeAttribute::groundable_types and groundable_types_mut define the positions used by grounding, emitted bounds, and cycle detection so those operations agree.

Bound rendering now merges associated-type bindings beside the code that renders the trait path. PathWithTypeArgs::to_bound_tokens adds the bindings to the path's existing argument list. The refactor also removes the iterative algorithm's is_changed field from SubstituteAbstractTypes and simplifies find_type_equality, which no longer needs the current specification or a syn::Result return type.

Tests added

Regression coverage

The new regression tests cover alias grounding, nested table emission, cycle diagnostics, and bound rendering:

  • Alias grounding in abstract_types/: use_type_fn_equality_nested, use_type_fn_expr_path, use_type_fn_trait_arg_alias, use_type_fn_generic_trait_equality, use_type_fn_stacked_attributes, and use_type_fn_shared_context cover the changed resolution rules.
  • Component integration in abstract_types/: use_type_trait_arg_component exercises grounding across a #[cgp_component] and #[cgp_impl] pair, including wiring and runtime dispatch.
  • Nested tables in namespaces/: namespace_nested_table and for_loop_nested_table were both confirmed to fail before the fix.
  • Cycle rejection in cgp-macro-tests: parser_rejections/use_type.rs adds five grounding-cycle rejection cases.
  • Bound rendering in cgp-macro-tests: ident_with_type_params/path_with_type_args.rs tests to_bound_tokens, including whether its output parses as a WherePredicate.

Coverage for previously untested constructs

The branch also adds coverage for existing syntax, derives, and provider behaviour:

  • Namespace forms: namespaces/redirect_mapping covers the => operator on a context. Its open equivalent produces the same snapshot except for the context name, testing the equivalence of open C;. namespaces/combined_forms combines every body form in one block. namespaces/default_impls2 tests access to DefaultImpls2 through ordinary #[default_impl] and for … in forms.
  • Product operations: extensible_records/product_ops adds the first tests for AppendProduct, ConcatProduct, and MapFields, expressed as type equalities.
  • Data derives: build_field_derive, extract_field_derive, from_variant_derive, and cgp_record_derive cover the individual derives. has_fields_enum_shapes covers the four variant shapes accepted by #[derive(HasFields)]. record_empty and struct_unit_field cover fieldless shapes.
  • Local associated types: basic_delegation/self_local_assoc_type tests the Self::Assoc exemption in #[cgp_impl].
  • Implicit arguments: implicit_arguments/cgp_fn_mref tests the MRef<'_, T> implicit mode under both receivers.

New snapshot macros support the derive tests: snapshot_derive_build_field!, snapshot_derive_extract_field!, and snapshot_derive_from_variant!. Each adds a 25-line entry point in cgp-macro-test-util-lib and a matching #[proc_macro] wrapper.

Rejection cases

Parser rejection tests cover invalid attribute combinations and body forms whose diagnostics need regression coverage:

  • Provider attributes: parser_rejections/use_provider.rs tests the one-provider-per-attribute rule and its + counterpart.
  • Fused delegation and checks: parser_rejections/delegate_and_check_components.rs tests the check attributes that the fused macro rejects. Cases include #[skip_check] merged with #[check_params] across a list key and its element.
  • Delegation body grammar: parser_rejections/delegate_components.rs adds cases for a statement after a mapping, a braced path group followed by more path, and a bounded generic list on a nested table. Their current messages are misleading, so the tests record those diagnostics.

Tests recording known defects

The branch records existing defects without fixing them. Both are documented under Known issues in the knowledge base:

  • Reserved variant names: invalid_expansion/reserved_variant_names.rs activates the previously empty invalid_expansion target. Variant derives use unqualified Self::… paths for associated types, causing ambiguous-associated-item errors when an enum variant is named Value, Remainder, Extractor, ExtractorRef, ExtractorMut, Fields, or FieldsRef. For #[derive(ExtractField)], the error names an item the user did not write.
  • Lifetime-carrying inner providers: higher_order_providers/lifetime_inner_provider.rs snapshots a higher-order provider whose inner bound lacks an IsProviderFor counterpart. The rewrite treats the bound's first generic argument as the context but encounters a lifetime. The stack compiles and runs, but loses the propagation that lets #[check_providers] identify a broken layer.

The reserved-variant-name defect needs fully qualified projections in code generation. Until that fix, string snapshots record the emitted paths. The test adds cgp-macro-test-util-lib as a dev-dependency of cgp-macro-tests to use pretty_format.

Documentation changes

Repository documentation now separates test navigation, testing conventions, and macro reference material. The changes are:

  • crates/tests/README.md: Maps the suite and delegates conventions to its AGENTS.md.
  • cgp-macro-test-util/README.md: Shrinks from 592 to 112 lines by removing repetitive per-macro sections and a completed migration guide. It points to the knowledge base's snapshot-macros document.
  • crates/main/cgp/README.md: Replaces the retired "new modular programming paradigm" positioning with the current wording and links to the knowledge base on the crates.io front page.
  • Agent instructions: AGENTS.md shortens the knowledge-base link text, and crates/tests/AGENTS.md closes a code span split across lines.
  • Test documentation: Every new test file includes a module comment stating the rule it tests and naming the knowledge-base document that owns it.

The sibling knowledge base and /cgp skill already describe the implementation changes. The knowledge base at ../cgp-knowledge-base covers dependency-ordered ground_specs, cycle rejection, groundable_types, to_bound_tokens, extracted inner_structs in EvaluatedNamespaceTable, both known defects, and the new snapshot macros. It also links the new test files by name. The skill's abstract-types reference states the same #[use_type] rules, and the primer's import table routes StaticFormat through cgp::core::base::traits.

The new cycle diagnostic does not change cargo-cgp UI fixtures because none exercises a #[use_type] cycle.

soareschen and others added 10 commits July 25, 2026 22:40
`crates/tests/README.md` restated its own `AGENTS.md` at length — the two crates'
jobs, where post-codegen failures live, the target layout, the snapshot ownership
rules. It is now the map of what is here and how to run it, and defers the rules to
`AGENTS.md`, which is where a reader is sent for them anyway.

`cgp-macro-test-util/README.md` duplicated the knowledge base's `snapshot_macros.md`
across fifteen near-identical per-macro sections, plus a guide to a migration that
has long since happened. It keeps the macro table, one canonical invocation, the
`insta` workflow, and the two notes that are genuinely its own, and points at the
implementation document for how the macros are built and at `crates/tests/AGENTS.md`
for when to snapshot at all — 592 lines to 112.

The crate-level `README.md` for `cgp`, which is the crates.io front page, still
called CGP "a new modular programming paradigm in Rust" — the positioning the
knowledge base records as retired. It now opens with the current line and points at
the knowledge base for the exhaustive semantics.

In `AGENTS.md`, the knowledge-base links spelled their full paths as link text,
which read as noise where a name would do; a code span split across two lines in
`crates/tests/AGENTS.md` is closed on one, per the backtick rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two positions where an imported alias was left unresolved, both
inconsistencies rather than boundaries: the alias already resolved in
neighbouring positions, so the substitution was incomplete rather than
deliberately narrow.

An equality pin's right-hand side was matched by whole-type equality, so
`{HashedPassword = Password}` grounded but `{Transaction = Tx<Db>}` emitted a
bare `Db` that resolved to nothing. `find_type_equality` now runs
`SubstituteAbstractTypes` over the right-hand side, which grounds an alias
wherever it occurs inside it — nested in a generic argument or inside a
qualified path — and collapses the two cases into one rule. The pinned alias
is excluded from its own substitution, so a degenerate `{Foo = Foo}` stays an
unresolved-name error rather than becoming a vacuous bound.

An alias qualifying an expression path was also passed through, even though a
`let` annotation in the same body was rewritten. `SubstituteAbstractTypes`
gains `visit_expr_path_mut`, rewriting `Transaction::begin_from(pool)` into
the qualified-type form `<<Self as Trait>::Transaction>::begin_from(pool)`.
The boundary kept is arity: a bare single-segment path in expression position
names a value, which an abstract type can never be, so an alias sharing its
name with a unit struct still resolves to the struct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVFcwx5jehpRRFieG8L8Q1
The expression-path rewrite mutated the node and returned, skipping the recursion,
so an alias in a *later* segment's generic arguments was left unsubstituted:
`Transaction::tagged::<Db>(db)` emitted a bare `Db` and failed with `E0425`.

Falling through to the recursion fixes it and cannot loop, because the rewritten
node now carries a `qself` and both visit guards require `qself: None`.

The asymmetry that hid this is worth noting: `visit_type_mut` uses the same
return-after-mutate shape and is correct there, because its guard requires
`PathArguments::None` so the node it replaces has no children to visit. The
expression case accepts later segments that do carry arguments, so it needs the
recursion the type case does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVFcwx5jehpRRFieG8L8Q1
…, merge pins

Four defects, found by asking of each position whether the construct already
handled the neighbouring one.

**A grounding cycle is now rejected at macro time.** Two imports whose contexts
resolve through each other (`HasA.A in B, HasB.B in A`) have no valid grounding
order, and neither does the one-node `in A`. `forbid_grounding_cycles` builds the
spec dependency graph, searches it depth-first, and reports a back edge on the
token that closes the loop, naming the cycle (`` `B` -> `A` -> `B` ``).

The check has to run *before* grounding, because the obvious after-the-fact test
finds nothing. Grounding does not leave a cyclic alias bare: each pass substitutes
against the previous snapshot, in which the other context is itself ungrounded, so
every pass wraps one more layer and the alias ends up buried four levels down a
projection. Whether a context is "still a bare alias" is therefore the wrong
question; the spec graph states the problem directly.

**An alias in the imported trait's own generic arguments is now grounded.** It was
resolved in the context and in a pin's right-hand side but not here, which is two
of the three positions it can occupy — a missing case rather than a boundary. So
`HasDbType.Db, HasPoolType<Db>.Pool` now projects against
`HasPoolType<<Self as HasDbType>::Db>` instead of emitting a bare `Db` that
resolves to nothing.

Grounding deliberately still stops at the trait path's *head*. That is not the
missing third case: the head must name a trait and an alias names a type, so
grounding it would turn a clear `E0405` into an unparseable projection.

**A pin on a generic trait emitted invalid Rust.** `HasFooType<u8>.{Foo = u32}`
built `HasFooType<u8><Foo = u32>` and died inside the macro with a bare "failed to
parse internal tokens to type `syn::generics::WherePredicate`" naming no cause.
Pins now merge into the trait path's existing argument list, so the bound reads
`HasFooType<u8, Foo = u32>` — and `HasRefType<'a, Ref = &'a str>` where a lifetime
must keep leading the list.

A circular *pin* is deliberately left to the compiler, unlike a circular context.
It grounds in one pass and means something well defined — two abstract types are
equal, which a context may satisfy — so only the solver's ability to discharge it
fails, and that is not knowable at expansion time.

`bare_alias_ident` is factored out of the substitution visitor so the new
`CollectBareAliases` reads dependency edges by exactly the rule the substitution
rewrites by; a reference the check cannot see is precisely the input grounding
then fails on. `groundable_types` names the positions once, so the grounding pass,
the emitted bounds, and the cycle check cannot disagree about what grounding
reaches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xGdHGrJEtoZjSmFEhsChw
Grounding used one traversal to detect cycles and a second, a fixpoint, to
resolve. The fixpoint substituted each pass against a *snapshot* of the previous
pass, in which a dependency was still ungrounded — which is the mechanism that
built the spiral the cycle check exists to prevent. Rejecting cycles upstream made
that unreachable, but the robustness then came from a precondition in a
neighbouring function rather than from the code itself.

Both collapse into `ground_specs`, because they answer one question: grounding a
spec means resolving its dependencies first, so the order it needs is a
topological order, and a cycle is exactly the graph having no such order. One
depth-first walk grounds each spec in post-order and reports a back edge as the
cycle. Two properties are now structural instead of resting on an argument about
iteration counts: the walk visits each spec once, so it terminates; and a spec is
substituted only against fully-grounded specs, so a replacement can never carry a
bare alias. The shape is gone rather than guarded — there is no previous pass to
read a half-resolved spec out of.

Resolution keeps source order in its output, so the emitted supertraits and
predicates still read in the order the author wrote the imports.

`resolve` tests `!= Unvisited` rather than `== Done`. An in-progress spec cannot
arrive today, since the call site reports the cycle before recursing, but
descending on one would recurse forever; a future call site that skipped the guard
now fails as an ungrounded spec at the end of the walk — diagnosable — instead of
as a stack overflow inside the compiler.

`PathWithTypeArgs::to_bound_tokens` takes over merging associated-type bindings
into a trait path's own argument list. The caller previously had to know that
`ToTokens` renders those arguments as a *trailing* group, and so that appending
bindings after the whole path yields the invalid `Trait<A><Item = X>`. Keeping the
merge with the rendering it must agree with stops a caller from reintroducing that,
and collapses the pinned and unpinned cases into one path, since an empty binding
list renders the path unchanged.

`is_changed` is removed from the substitution visitor: it existed only to drive the
fixpoint and was left written but never read, which clippy misses on a `pub` field.

Tests cover what the refactor claims. Order-independence is pinned for a nested pin
whose right-hand side names an alias declared *after* it, and for specs split across
stacked `#[use_type]` attributes in both orders — the latter a documented promise
with nothing behind it, exercised on the case where it could plausibly break
(`{Foo = Vec<Bar>}` above `HasBarType.Bar`). All four arrangements emit the same
bound. `to_bound_tokens` gains unit tests in the file that already covers its type,
including that its output parses as a trait bound at all, which is the property the
two-group form violated and which no unit test pinned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xGdHGrJEtoZjSmFEhsChw
`DelegateTable::eval` collects every nested `Wrapper<new Inner { … }>` value and
emits its struct and `DelegateComponent` impls; two paths that reach the same
value did not, so the table was parsed, named in the entry's `Delegate`, and then
never emitted. Both failed with `E0425` on the dropped struct — a message that
reads like a typo rather than a lost table.

`NamespaceTable::eval` was the first. A namespace body parses into the same
`DelegateEntries` a delegation table does, so it accepts the value either way;
what it lacked was the extraction. Skipping it was an inconsistency rather than a
boundary — the neighbouring case was already handled — which is the test the
implementation documentation prescribes for exactly this judgement. `eval` now
runs the same walk, and `EvaluatedNamespaceTable` grows an `inner_structs` field
rendered after the trait, mirroring `EvaluatedDelegateTable`. The form earns its
keep here rather than merely working: the dispatch table lives in the namespace,
so every context that joins inherits it without restating it.

`DelegateEntries::extract_inner_tables` was the second, and it reaches
`delegate_components!` itself. It walked mappings but not statements, so a value
inside a `for` loop's body was dropped the same way. It now walks both, with
`ExtractInnerDelegateTables` implemented for the statement forms — `for`
delegating to its body's mappings, `namespace` and `open` carrying no value.

Writing such a value well-formed takes one specific shape, and it is worth
knowing before reaching for the form. The loop binds a provider variable the entry
must mention, or the generated impl leaves it unconstrained; but the loop's
variables are not in scope inside the lifted table's own impls, so mentioning it
only there fails on both counts at once. Declaring it as the inner table's own
generic — `new Inner<Provider>` — puts it in both places. The combination stays
redundant in practice, since a loop already yields one provider per key, so the
form is kept correct rather than recommended.

Seven tests close gaps the audit of this grammar turned up, all of them cases
where documentation made a claim nothing pinned.

`redirect_mapping` covers the `=>` operator written on a context, which appeared
nowhere in the suite. Its second snapshot is the `open` spelling of its first
entry, so the two goldens are identical modulo the context name — which pins the
`open C;` ≡ `C => @C,` equivalence by construction rather than asserting it.
`combined_forms` puts every body form in one block, the only place the
composition itself is checked. `namespace_nested_table` and
`for_loop_nested_table` guard the two halves of the fix above; both were confirmed
to fail without it.

Three rejection cases pin body-grammar diagnostics whose wording is misleading
enough to be worth guarding: a statement after a mapping and a braced path group
followed by more path, both reported as `expected ':'`, and a bounded generic list
on a nested table, reported as `expected ','` because the value parser falls back
to reading the whole value as a plain type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four of these cover behaviour that was implemented and untested; the fifth
pins a defect rather than a feature.

`basic_delegation/self_local_assoc_type.rs` covers the exemption in
`#[cgp_impl]`'s `Self` rewrite: a `Self::Assoc` naming an associated type the
block itself declares is left alone, so the emitted provider impl keeps it,
while every other `Self` in the same body still becomes the context.

`implicit_arguments/cgp_fn_mref.rs` covers the `MRef<'_, T>` implicit
argument — the one reference-shaped mode with no mutable mirror, which stays a
shared `HasField` read even under a `&mut self` receiver.

`parser_rejections/use_provider.rs` pins the one-provider-per-attribute rule,
together with the counterpart that stops it being read too broadly: `+`
continues one provider's bound list and is accepted, while a comma is not.

`parser_rejections/delegate_and_check_components.rs` covers the check
attributes the macro refuses, including a `#[skip_check]` merged with a
`#[check_params]` across a list key and its element — a conflict that exists
only after the merge, so neither attribute is wrong on its own.

`higher_order_providers/lifetime_inner_provider.rs` snapshots current, wrong
behaviour: a higher-order provider over a lifetime-carrying component gets no
`IsProviderFor` counterpart on its inner bound, because the rewrite reads the
bound's first generic argument as the context and finds a lifetime there. The
stack compiles and runs; what is lost is the propagation that lets
`#[check_providers]` localize a broken layer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011BfN1sjgaBAA2sEVqVNeig
Add the missing coverage for the extensible-data derives, and capture the
first invalid-expansion case.

Three building-block derives had no test anywhere — `#[derive(BuildField)]`,
`#[derive(ExtractField)]`, and `#[derive(CgpRecord)]` — even though two of
them are the form the concept documentation shows. Give the first two a
snapshot macro of their own, alongside one for `#[derive(FromVariant)]`, so
that each slice's defining claim is checkable rather than asserted: the
snapshot shows the derive emitting its own items and *not* the neighbouring
ones. `CgpRecord` and `CgpVariant` get no macro, since they emit what
`CgpData` emits on the same shape and a snapshot would only duplicate one.

Add seven test files covering those three derives, the four enum variant
shapes `#[derive(HasFields)]` accepts and no other derive does, the unit
struct, and the fieldless record. The variant-shape file is the substantive
one: `HasFields` nests each variant's own fields as a product, so a unit
variant becomes `Nil`, a newtype variant passes its payload through, and the
two multi-field shapes become `Index`- and `Symbol!`-keyed products — none of
which was tested or documented.

Open the `invalid_expansion` target with the case it was reserved for. The
variant derives name their own associated types through an unqualified
`Self::…` path, so seven variant names cannot be used: `Fields` and
`FieldsRef` break `HasFields`, `Value` breaks `FromVariant`, and `Value`,
`Remainder`, `Extractor`, `ExtractorRef`, and `ExtractorMut` break
`ExtractField`. The fix is a fully qualified projection in the codegen; until
then the emitted paths are pinned as string snapshots so the test compiles
even though the code it describes would not. How readable the resulting error
is depends on the derive, which is the part worth recording: the
representation and constructor impls target the user's enum, so a note points
at the real variant, while the extractor's target the generated companions and
point back at the derive, naming nothing.

The three `parser_rejections` files carry `cargo fmt` reflowing only; their
content is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018n2krDG9WF1HD9aA1aMASX
Cover two gaps found by auditing the trait surface while porting the
reference's `traits/` group.

`AppendProduct`, `ConcatProduct`, and `MapFields` had no test anywhere,
despite all three being public and carrying worked examples in the
documentation. They compute types rather than values, so the new tests are
type equalities: each coerces the computed `Output`/`Mapped` to the type the
operation is documented to produce, and compiles only if the two agree. The
file covers both `Nil` identities of concat, the three markers over a product,
and `MapFields` over the `Either`/`Void` sum spine as well as the product one —
the latter being the reason `MapFields` is the only one of the three defined
over both.

`DefaultImpls2` had no user at all: nothing in the library emits or consumes
it, and no test named it, which left it unclear whether the trait was reachable
or merely declared. It is reachable, and needs no new construct to be —
`#[default_impl]` accepts an arbitrary namespace path and appends only the
table parameter, so a two-type key registers and an ordinary `for … in` loop
consumes it. The test establishes both halves and drives the resulting wiring.

Writing them also settled where these traits live, which is now stated on the
public pages: none of the three product operations is in the prelude, nor is
the `IsOptional` marker, nor `DefaultImpls1`/`DefaultImpls2`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018n2krDG9WF1HD9aA1aMASX
@soareschen
soareschen merged commit db07fa9 into main Sep 15, 2026
3 checks passed
@soareschen
soareschen deleted the ai-updates-20260725-2 branch September 15, 2026 12:26
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