From bee9d3b9d98c24f7e6beff60394d0776f0b570c4 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 20:19:09 +0200 Subject: [PATCH 01/17] Rename CLAUDE.md to AGENTS.md --- CLAUDE.md => AGENTS.md | 14 +++++++------- .../macros/cgp-macro-core/{CLAUDE.md => AGENTS.md} | 4 ++-- crates/tests/{CLAUDE.md => AGENTS.md} | 6 +++--- crates/tests/README.md | 4 ++-- .../tests/invalid_expansion_tests.rs | 4 ++-- .../tests/parser_rejections_tests.rs | 2 +- crates/tests/cgp-test-crate-a/src/lib.rs | 2 +- crates/tests/cgp-test-crate-b/src/lib.rs | 2 +- crates/tests/cgp-tests/src/lib.rs | 2 +- docs/{CLAUDE.md => AGENTS.md} | 6 +++--- docs/README.md | 2 +- docs/concepts/README.md | 2 +- docs/examples/README.md | 2 +- docs/implementation/{CLAUDE.md => AGENTS.md} | 12 ++++++------ docs/implementation/README.md | 2 +- docs/implementation/entrypoints/snapshot_macros.md | 4 ++-- docs/implementation/macros/define_keyword.md | 2 +- docs/implementation/macros/export_constructs.md | 2 +- docs/implementation/macros/parse_internal.md | 2 +- docs/reference/README.md | 2 +- docs/skills/README.md | 2 +- 21 files changed, 40 insertions(+), 40 deletions(-) rename CLAUDE.md => AGENTS.md (97%) rename crates/macros/cgp-macro-core/{CLAUDE.md => AGENTS.md} (98%) rename crates/tests/{CLAUDE.md => AGENTS.md} (98%) rename docs/{CLAUDE.md => AGENTS.md} (99%) rename docs/implementation/{CLAUDE.md => AGENTS.md} (95%) diff --git a/CLAUDE.md b/AGENTS.md similarity index 97% rename from CLAUDE.md rename to AGENTS.md index 4f6b7137..1b992fa6 100644 --- a/CLAUDE.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# CLAUDE.md +# AGENTS.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. @@ -103,10 +103,10 @@ core traits are the ground truth the skill describes, so read the two together. Then read the documentation for the macro under review, in [docs/](docs). Read its reference document under [docs/reference/](docs/reference), its implementation documents under [docs/implementation/](docs/implementation) (the `entrypoints/` document, the `asts/` stack it -drives, and any `functions/` helpers it relies on), and the governing `CLAUDE.md` files that define -how those documents stay in sync with the code: [docs/CLAUDE.md](docs/CLAUDE.md), -[docs/implementation/CLAUDE.md](docs/implementation/CLAUDE.md), and -[crates/macros/cgp-macro-core/CLAUDE.md](crates/macros/cgp-macro-core/CLAUDE.md). These establish +drives, and any `functions/` helpers it relies on), and the governing `AGENTS.md` files that define +how those documents stay in sync with the code: [docs/AGENTS.md](docs/AGENTS.md), +[docs/implementation/AGENTS.md](docs/implementation/AGENTS.md), and +[crates/macros/cgp-macro-core/AGENTS.md](crates/macros/cgp-macro-core/AGENTS.md). These establish that the source is the single source of truth and that reference, implementation, snapshot, and skill are four views of it that must never drift. @@ -115,7 +115,7 @@ Next, study the implementation itself in [crates/macros/](crates/macros). Start and the `functions/` helpers it calls, and read closely enough to reason about corner cases, not just the happy path. Finally, study the tests in [crates/tests/](crates/tests) — the behavioral tests in `cgp-tests` and the failure cases and expansion snapshots in `cgp-macro-tests` — and read -[crates/tests/CLAUDE.md](crates/tests/CLAUDE.md) to learn how the suite is organized and how to run +[crates/tests/AGENTS.md](crates/tests/AGENTS.md) to learn how the suite is organized and how to run and update it. ### Harden the implementation and its tests @@ -127,7 +127,7 @@ readability edit introduce a behavioral change. - **Fix bugs and corner cases.** Identify potential bugs and unhandled corner cases in the implementation and fix them. When a corner case cannot be fixed in this iteration, capture it as a failure case in `cgp-macro-tests` and record it under the construct's Known issues, per - [crates/tests/CLAUDE.md](crates/tests/CLAUDE.md). + [crates/tests/AGENTS.md](crates/tests/AGENTS.md). - **Close test gaps.** Add tests for corner cases that are not yet covered, placing each in the concept target that owns the behavior and snapshotting only in the macro's owning target. - **Verify existing tests.** Confirm each existing test really exercises the behavior it claims to, diff --git a/crates/macros/cgp-macro-core/CLAUDE.md b/crates/macros/cgp-macro-core/AGENTS.md similarity index 98% rename from crates/macros/cgp-macro-core/CLAUDE.md rename to crates/macros/cgp-macro-core/AGENTS.md index 6829d6f5..da8d2062 100644 --- a/crates/macros/cgp-macro-core/CLAUDE.md +++ b/crates/macros/cgp-macro-core/AGENTS.md @@ -1,4 +1,4 @@ -# CLAUDE.md +# AGENTS.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. @@ -7,7 +7,7 @@ skill before working here** — every type in this crate exists to generate one the skill describes (`#[cgp_component]`, `#[cgp_impl]`, `#[cgp_fn]`, `delegate_components!`, `check_components!`, `#[derive(HasField)]`, …). You cannot correctly change the *output* of a macro without knowing the *expansion* it is supposed to produce, and that expansion is documented by -`/cgp`. The workspace-level [../../../CLAUDE.md](../../../CLAUDE.md) covers build/test/lint commands +`/cgp`. The workspace-level [../../../AGENTS.md](../../../AGENTS.md) covers build/test/lint commands and the crate hierarchy. ## What this crate is (and isn't) diff --git a/crates/tests/CLAUDE.md b/crates/tests/AGENTS.md similarity index 98% rename from crates/tests/CLAUDE.md rename to crates/tests/AGENTS.md index 73a01a07..df56c405 100644 --- a/crates/tests/CLAUDE.md +++ b/crates/tests/AGENTS.md @@ -1,4 +1,4 @@ -# CLAUDE.md — maintaining the CGP test suite +# AGENTS.md — maintaining the CGP test suite This file governs the test crates under `crates/tests`. Read it before adding, moving, or refactoring any test here. Invoke the `/cgp` skill first — every test @@ -73,7 +73,7 @@ and annotate individual tricky cases inline. Link to the owning **implementation document** — the one under `docs/implementation/` whose Tests and Snapshots sections index this test (for example `// see docs/implementation/entrypoints/cgp_impl.md`); that document is where test pointers live, since a reference document never links -to a test (per `docs/CLAUDE.md`). You may additionally link to a reference +to a test (per `docs/AGENTS.md`). You may additionally link to a reference document when a reader needs the user-facing semantics. Tests link **to** the documentation; the reference documents never link back to a test. @@ -167,7 +167,7 @@ link from the test's comment to the implementation document. This suite is one of the views of CGP's truth, alongside the macro implementation in `cgp-macro-core`, the reference documents in `docs/reference`, the implementation documents in `docs/implementation`, and the `/cgp` skill (see -`docs/CLAUDE.md`). The implementation documents are the ones tightly coupled to +`docs/AGENTS.md`). The implementation documents are the ones tightly coupled to this suite: each macro's implementation document has a `## Tests` section linking every behavioral test and failure case that exercises it, and every entrypoint document a `## Snapshots` section indexing the expansion snapshots and calling out diff --git a/crates/tests/README.md b/crates/tests/README.md index 71e72372..c4daac05 100644 --- a/crates/tests/README.md +++ b/crates/tests/README.md @@ -5,7 +5,7 @@ are organized **by CGP concept** — basic delegation, abstract types, implicit arguments, namespaces, and so on — rather than by the macro that implements each concept, because a single macro (for example `delegate_components!`) serves many concepts at once. If you are maintaining or extending the suite, read -[CLAUDE.md](CLAUDE.md) first; it is the authoritative guide to the conventions. +[AGENTS.md](AGENTS.md) first; it is the authoritative guide to the conventions. This README is the map. ## The crates @@ -55,7 +55,7 @@ from `cgp-macro-test-util`. Each such macro emits the real generated code into t module **and** generates a `#[test]` asserting a pretty-printed inline `insta` snapshot of it. Snapshots are used deliberately: a macro's expansion is snapshotted only in the concept target that owns that macro's feature, and written plainly -everywhere else (see [CLAUDE.md](CLAUDE.md) for the ownership rules). +everywhere else (see [AGENTS.md](AGENTS.md) for the ownership rules). ## Running the tests diff --git a/crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs b/crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs index 27ca7251..4384b986 100644 --- a/crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs +++ b/crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs @@ -13,9 +13,9 @@ //! 2. add a code comment explaining **why** the output is wrong and **what the //! correct output should be**; //! 3. record the limitation in the owning reference document's `## Known issues` -//! section (per docs/CLAUDE.md), and link from the test to that document. +//! section (per docs/AGENTS.md), and link from the test to that document. //! -//! No cases are enumerated yet; see crates/tests/CLAUDE.md ("Migration status"). +//! No cases are enumerated yet; see crates/tests/AGENTS.md ("Migration status"). #![allow(dead_code)] pub mod invalid_expansion; diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections_tests.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections_tests.rs index 5d782c34..1eb2e56b 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections_tests.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections_tests.rs @@ -5,7 +5,7 @@ //! relevant `cgp-macro-core` parser (or a `cgp-macro-lib` entrypoint) returns an //! error rather than silently accepting the input. //! -//! See crates/tests/CLAUDE.md ("Adding a failure case"). +//! See crates/tests/AGENTS.md ("Adding a failure case"). #![allow(dead_code)] pub mod parser_rejections; diff --git a/crates/tests/cgp-test-crate-a/src/lib.rs b/crates/tests/cgp-test-crate-a/src/lib.rs index b4a1eb7c..c7e406d1 100644 --- a/crates/tests/cgp-test-crate-a/src/lib.rs +++ b/crates/tests/cgp-test-crate-a/src/lib.rs @@ -7,7 +7,7 @@ //! namespace — exercising that CGP's two-trait split stays within Rust's //! coherence and orphan rules across crate boundaries. //! -//! See crates/tests/CLAUDE.md and docs/concepts/coherence.md. +//! See crates/tests/AGENTS.md and docs/concepts/coherence.md. use cgp::prelude::*; diff --git a/crates/tests/cgp-test-crate-b/src/lib.rs b/crates/tests/cgp-test-crate-b/src/lib.rs index 9b9fc422..0fb0639d 100644 --- a/crates/tests/cgp-test-crate-b/src/lib.rs +++ b/crates/tests/cgp-test-crate-b/src/lib.rs @@ -9,7 +9,7 @@ //! because the provider struct is local) and wiring a context to it; //! 3. participating in a namespace declared upstream. //! -//! See crates/tests/CLAUDE.md and docs/concepts/coherence.md. +//! See crates/tests/AGENTS.md and docs/concepts/coherence.md. use cgp::prelude::*; use cgp_test_crate_a::{AnnounceLoudly, AnnouncerComponent, GreetHello, Greeter, GreeterComponent}; diff --git a/crates/tests/cgp-tests/src/lib.rs b/crates/tests/cgp-tests/src/lib.rs index 82863e00..6ffc931b 100644 --- a/crates/tests/cgp-tests/src/lib.rs +++ b/crates/tests/cgp-tests/src/lib.rs @@ -3,5 +3,5 @@ //! All tests live in the integration targets under `tests/`, organized by CGP //! concept (one target per concept, one file per unit test). This library crate //! holds only shared support code; today there is none, so it is intentionally -//! empty. See `../README.md` and `../CLAUDE.md` for the organization and the +//! empty. See `../README.md` and `../AGENTS.md` for the organization and the //! rules for maintaining the suite. diff --git a/docs/CLAUDE.md b/docs/AGENTS.md similarity index 99% rename from docs/CLAUDE.md rename to docs/AGENTS.md index a70936e9..382e45d2 100644 --- a/docs/CLAUDE.md +++ b/docs/AGENTS.md @@ -1,6 +1,6 @@ -# CLAUDE.md +# AGENTS.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to LLM agents when working with code in this repository. This directory is the CGP knowledge base — agent-maintained documentation whose job is to record the full semantics of every CGP construct. Read [README.md](README.md) for the background and motivation behind it. The rules below govern how to keep it correct. @@ -48,7 +48,7 @@ Verify the skill against the source the same way you verify a reference document ## The implementation directory -The [implementation/](implementation/README.md) directory documents the *internals* of the CGP macros — how each macro is built, as opposed to what it does for a user. It is the counterpart to this reference: where a reference document explains a construct's syntax and expansion and points only at library source, an implementation document explains the macro's pipeline, the AST types it parses into, the functions that synthesize each generated item, its corner-case handling, and its known limitations, and it is the sole home for every pointer into the [test suite](../crates/tests). An agent asked to review, debug, or extend the macro source reads the implementation document first. The tree is organized into `entrypoints/` (one document per user-facing macro), `asts/` (the `cgp-macro-core` types that implement `Parse`/`ToTokens`, grouped by evaluation stack), `functions/` (standalone helpers, split into `parse/` and `derive/`), and `macros/` (internal `macro_rules!` such as `parse_internal!`). Its own [implementation/CLAUDE.md](implementation/CLAUDE.md) carries the authoring rules and document templates for that tree, and [implementation/README.md](implementation/README.md) is the catalog you register a new implementation document in. +The [implementation/](implementation/README.md) directory documents the *internals* of the CGP macros — how each macro is built, as opposed to what it does for a user. It is the counterpart to this reference: where a reference document explains a construct's syntax and expansion and points only at library source, an implementation document explains the macro's pipeline, the AST types it parses into, the functions that synthesize each generated item, its corner-case handling, and its known limitations, and it is the sole home for every pointer into the [test suite](../crates/tests). An agent asked to review, debug, or extend the macro source reads the implementation document first. The tree is organized into `entrypoints/` (one document per user-facing macro), `asts/` (the `cgp-macro-core` types that implement `Parse`/`ToTokens`, grouped by evaluation stack), `functions/` (standalone helpers, split into `parse/` and `derive/`), and `macros/` (internal `macro_rules!` such as `parse_internal!`). Its own [implementation/AGENTS.md](implementation/AGENTS.md) carries the authoring rules and document templates for that tree, and [implementation/README.md](implementation/README.md) is the catalog you register a new implementation document in. The two trees are bound together and must not drift or duplicate. When you change a macro, update both views in the same change: the reference document's Expansion for the user-facing contract, and the implementation document's Pipeline and Generated items for the mechanics. Every reference document's Source section links to its implementation counterpart, and the implementation document links back to the reference for the user-facing semantics rather than re-deriving them; corner-case behavior a reference document does not want to explain in full is elaborated in the implementation document instead. Because all test and snapshot pointers now live under `implementation/`, moving or renaming a test updates the implementation document's Tests or Snapshots section, never a reference document. diff --git a/docs/README.md b/docs/README.md index 305d7153..5ff84944 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ This directory is a knowledge base about Context-Generic Programming (CGP), writ CGP is implemented almost entirely as procedural macros, and proc-macro source code is a poor place to learn semantics from. An agent reading [crates/macros/cgp-macro-core](../crates/macros/cgp-macro-core) sees token-stream manipulation and AST transforms, not the meaning those transforms produce. The meaning — "`#[cgp_component]` generates a consumer trait, a provider trait, and two blanket impls that connect them" — has to be reconstructed by mentally running the macro. That reconstruction is slow, error-prone, and gets repeated on every visit. This knowledge base captures the reconstruction once, in prose, so the next agent can read the conclusion instead of re-deriving it. -The knowledge base also serves as a contract. When an agent changes how a macro expands, the corresponding reference document is the place where the intended new behavior is stated in plain language. A reviewer can compare the prose against the code and against the expansion snapshots in [crates/tests/cgp-macro-tests](../crates/tests/cgp-macro-tests) to confirm that all three agree. Documentation that drifts out of sync with the code is worse than no documentation, so keeping these documents accurate is a hard requirement of any change — see [CLAUDE.md](CLAUDE.md) for the maintenance rules. +The knowledge base also serves as a contract. When an agent changes how a macro expands, the corresponding reference document is the place where the intended new behavior is stated in plain language. A reviewer can compare the prose against the code and against the expansion snapshots in [crates/tests/cgp-macro-tests](../crates/tests/cgp-macro-tests) to confirm that all three agree. Documentation that drifts out of sync with the code is worse than no documentation, so keeping these documents accurate is a hard requirement of any change — see [AGENTS.md](AGENTS.md) for the maintenance rules. ## How it is organized diff --git a/docs/concepts/README.md b/docs/concepts/README.md index 7629e843..b579ece3 100644 --- a/docs/concepts/README.md +++ b/docs/concepts/README.md @@ -8,7 +8,7 @@ A concept document explains an *idea that spans several constructs*, whereas a [ ## The catalog -The authoring rules for concept documents, including when a cross-cutting idea earns its own page, live in [../CLAUDE.md](../CLAUDE.md). These documents explain the ideas that connect the constructs, each linking down to the per-construct references for the detail. +The authoring rules for concept documents, including when a cross-cutting idea earns its own page, live in [../AGENTS.md](../AGENTS.md). These documents explain the ideas that connect the constructs, each linking down to the per-construct references for the detail. - [Bypassing coherence](coherence.md) — what Rust's coherence rules forbid, and the incoherent-impl-plus-local-wiring strategy CGP uses to work around them. - [Modularity hierarchy](modularity-hierarchy.md) — the ladder from a single blanket impl to per-type-per-provider wiring, and how to pick the lowest rung a use case needs. diff --git a/docs/examples/README.md b/docs/examples/README.md index 84018d39..8a75efbf 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -10,7 +10,7 @@ The audience for these examples is an agent writing expanded documentation — a ## The catalog -The authoring rules for examples, including how to add a new one, live in [../CLAUDE.md](../CLAUDE.md). +The authoring rules for examples, including how to add a new one, live in [../AGENTS.md](../AGENTS.md). - [Area calculation](area-calculation.md) — computing the area of several shapes, progressing from field-driven functions to a wireable area component with composable higher-order providers. - [Shell-scripting DSL](shell-scripting-dsl.md) — a type-level DSL whose programs are types interpreted at compile time, progressing from a fixed CLI program through the handler component and its namespace wiring to a custom context and a language extension. diff --git a/docs/implementation/CLAUDE.md b/docs/implementation/AGENTS.md similarity index 95% rename from docs/implementation/CLAUDE.md rename to docs/implementation/AGENTS.md index a9ecfaa1..2a6d51dc 100644 --- a/docs/implementation/CLAUDE.md +++ b/docs/implementation/AGENTS.md @@ -1,6 +1,6 @@ -# CLAUDE.md — the CGP implementation documentation +# AGENTS.md — the CGP implementation documentation -This directory documents the *internals* of the CGP macros — how each macro is implemented, not how a programmer uses it. Read [README.md](README.md) for the catalog and the map of what lives where. The rules below govern how to keep these documents correct and useful, and they assume you have already read the knowledge-base-wide rules in [../CLAUDE.md](../CLAUDE.md); this file adds the rules specific to the implementation tree. +This directory documents the *internals* of the CGP macros — how each macro is implemented, not how a programmer uses it. Read [README.md](README.md) for the catalog and the map of what lives where. The rules below govern how to keep these documents correct and useful, and they assume you have already read the knowledge-base-wide rules in [../AGENTS.md](../AGENTS.md); this file adds the rules specific to the implementation tree. Invoke the `/cgp` skill before writing or revising any document here, exactly as the reference documents require, and write in the dual-reader prose style (the `/dual-reader-prose` skill). An implementation document is read both by an agent scanning for one detail of a parser and by an agent reading a macro's pipeline start to finish, so every section opens with a self-contained topic sentence and frames any list with a sentence before and after. @@ -14,9 +14,9 @@ These documents are the home for everything about the *test suite* that the refe An implementation document must stay in sync with the code, and keeping it in sync is part of the change, not a follow-up. The source in [crates/macros/cgp-macro-core](../../crates/macros/cgp-macro-core) and [crates/macros/cgp-macro-lib](../../crates/macros/cgp-macro-lib) is the single source of truth, above both this document and the reference. When you change a parser, a pipeline stage, the set of generated items, or an error path, revise the matching implementation document in the same change; when you add or remove a test or a snapshot, update the Tests or Snapshots section that indexes it. A document that describes a pipeline the code no longer has is worse than no document, because the next agent will trust it. Verify every claim against the source before writing it — read the `types//` modules, the `functions/` helpers, and the tests — rather than transcribing the reference or working from memory. -Document the present, not the history, following [../CLAUDE.md](../CLAUDE.md): describe how the code works now, record current limitations under Known issues, and delete superseded wording outright rather than leaving "previously" or "renamed from" traces. +Document the present, not the history, following [../AGENTS.md](../AGENTS.md): describe how the code works now, record current limitations under Known issues, and delete superseded wording outright rather than leaving "previously" or "renamed from" traces. -Improving the code's inline docs is part of writing an implementation document, not a separate task. Reading a macro's source closely enough to document it is exactly when to fix its inline docs, so in the same pass add a brief `///` to any public struct, trait, or function that lacks one, correct a comment that no longer matches the code, clarify genuinely confusing code, and delete comments that only restate the obvious — the convention recorded in [cgp-macro-core/CLAUDE.md](../../crates/macros/cgp-macro-core/CLAUDE.md). Keep the inline docs terse and leave the deep mechanics to the implementation document; a one-line inline doc that links out to the document beats a paragraph inlined in the source. +Improving the code's inline docs is part of writing an implementation document, not a separate task. Reading a macro's source closely enough to document it is exactly when to fix its inline docs, so in the same pass add a brief `///` to any public struct, trait, or function that lacks one, correct a comment that no longer matches the code, clarify genuinely confusing code, and delete comments that only restate the obvious — the convention recorded in [cgp-macro-core/AGENTS.md](../../crates/macros/cgp-macro-core/AGENTS.md). Keep the inline docs terse and leave the deep mechanics to the implementation document; a one-line inline doc that links out to the document beats a paragraph inlined in the source. ## Directory layout @@ -56,11 +56,11 @@ The **Tests** and **Source** sections are always bullet lists, never flowing par Every document links its related tests in a **Tests** section, stating the behavior each test pins in the document's own words. List the behavioral tests in [crates/tests/cgp-tests](../../crates/tests/cgp-tests) and the failure cases in [crates/tests/cgp-macro-tests](../../crates/tests/cgp-macro-tests) that exercise the construct, each as a link to the file with a one-line description of what it verifies. Because the reference documents no longer point at tests, this section is the canonical index of a construct's coverage — write it so an agent can see at a glance what behavior is guarded and, by omission, what is not. -Every entrypoint document additionally carries a **Snapshots** section that indexes the macro-expansion snapshots for that macro and calls out which variants are missing. Macro snapshots are pinned by the `snapshot_*!` macros in [crates/macros/cgp-macro-test-util](../../crates/macros/cgp-macro-test-util) and, per [crates/tests/CLAUDE.md](../../crates/tests/CLAUDE.md), each macro's canonical full-expansion snapshot plus its genuinely distinct variants live only in the concept target that owns the macro's feature. The Snapshots section is the central place to find them: link each snapshot file, describe which expansion variant it captures (the plain case, a supertrait-and-default-method case, a lifetime-and-type-parameter case, a namespace-prefix case, and so on), and then state plainly which variants have *no* snapshot yet, so a gap in coverage is visible rather than silently absent. When you add a snapshot, register it here; when a variant you know matters is untested, record it as a missing snapshot rather than leaving the section to imply full coverage. +Every entrypoint document additionally carries a **Snapshots** section that indexes the macro-expansion snapshots for that macro and calls out which variants are missing. Macro snapshots are pinned by the `snapshot_*!` macros in [crates/macros/cgp-macro-test-util](../../crates/macros/cgp-macro-test-util) and, per [crates/tests/AGENTS.md](../../crates/tests/AGENTS.md), each macro's canonical full-expansion snapshot plus its genuinely distinct variants live only in the concept target that owns the macro's feature. The Snapshots section is the central place to find them: link each snapshot file, describe which expansion variant it captures (the plain case, a supertrait-and-default-method case, a lifetime-and-type-parameter case, a namespace-prefix case, and so on), and then state plainly which variants have *no* snapshot yet, so a gap in coverage is visible rather than silently absent. When you add a snapshot, register it here; when a variant you know matters is untested, record it as a missing snapshot rather than leaving the section to imply full coverage. ## Known issues -Record limitations, surprising behavior, and confirmed bugs in the implementation under a **Known issues** section, following the same rule as the reference documents in [../CLAUDE.md](../CLAUDE.md): describe the behavior as it currently is even when it is wrong, say what the correct behavior would be for a bug, and remove the note in the same change that fixes the code. An implementation document is the right home for a limitation that is about *how the macro is built* — a parser that panics on an input it should reject with a `syn::Error`, a generic-parameter shape the codegen does not yet handle, a stage that drops information — while the reference document's Known issues covers the user-visible consequence. When both exist, cross-link them rather than duplicating the explanation. A failure case captured in `cgp-macro-tests` (per [crates/tests/CLAUDE.md](../../crates/tests/CLAUDE.md)) should be indexed from the relevant Known issue here and from its Tests section. +Record limitations, surprising behavior, and confirmed bugs in the implementation under a **Known issues** section, following the same rule as the reference documents in [../AGENTS.md](../AGENTS.md): describe the behavior as it currently is even when it is wrong, say what the correct behavior would be for a bug, and remove the note in the same change that fixes the code. An implementation document is the right home for a limitation that is about *how the macro is built* — a parser that panics on an input it should reject with a `syn::Error`, a generic-parameter shape the codegen does not yet handle, a stage that drops information — while the reference document's Known issues covers the user-visible consequence. When both exist, cross-link them rather than duplicating the explanation. A failure case captured in `cgp-macro-tests` (per [crates/tests/AGENTS.md](../../crates/tests/AGENTS.md)) should be indexed from the relevant Known issue here and from its Tests section. ## Keeping the reference and implementation documents aligned diff --git a/docs/implementation/README.md b/docs/implementation/README.md index 9976d46a..5151cbee 100644 --- a/docs/implementation/README.md +++ b/docs/implementation/README.md @@ -1,6 +1,6 @@ # CGP Implementation Reference -This directory documents the *internals* of the CGP macros — how each macro is implemented in [crates/macros/cgp-macro-core](../../crates/macros/cgp-macro-core) and [crates/macros/cgp-macro-lib](../../crates/macros/cgp-macro-lib), including corner-case behavior, known limitations and bugs, and the test suite that exercises each construct. It is the documentation an agent reviewing or maintaining the macro source reads first: it records the current state of the code in one place so an agent can pick up a construct's implementation from where the last one left off. The authoring rules, document templates, and the synchronization rule that binds these documents to the code live in [CLAUDE.md](CLAUDE.md). +This directory documents the *internals* of the CGP macros — how each macro is implemented in [crates/macros/cgp-macro-core](../../crates/macros/cgp-macro-core) and [crates/macros/cgp-macro-lib](../../crates/macros/cgp-macro-lib), including corner-case behavior, known limitations and bugs, and the test suite that exercises each construct. It is the documentation an agent reviewing or maintaining the macro source reads first: it records the current state of the code in one place so an agent can pick up a construct's implementation from where the last one left off. The authoring rules, document templates, and the synchronization rule that binds these documents to the code live in [AGENTS.md](AGENTS.md). These documents complement the [construct reference](../reference/README.md) rather than repeating it. The reference explains what each construct does for a *user* — its accepted syntax and the code it expands to — and points only at library source. The implementation documents explain how the macro *produces* that behavior, and they are the sole home for every pointer into the [test suite](../../crates/tests): behavioral tests, failure cases, and macro-expansion snapshots all index from here. A reference document links to its implementation counterpart to elaborate a corner case; an implementation document links back to the reference for the user-facing semantics. diff --git a/docs/implementation/entrypoints/snapshot_macros.md b/docs/implementation/entrypoints/snapshot_macros.md index a22968c0..9ae4481a 100644 --- a/docs/implementation/entrypoints/snapshot_macros.md +++ b/docs/implementation/entrypoints/snapshot_macros.md @@ -59,11 +59,11 @@ The snapshot **runs the production macro**, not a copy: any change to a `cgp-mac The **derive members re-emit the annotated item themselves**. Because a derive macro produces only the *added* code and not the struct or enum it decorates, an entrypoint like `snapshot_derive_has_field` prepends the original item to the output so the generated impls have a type to attach to. -The **assertion is inline by convention**, following [crates/tests/CLAUDE.md](../../../crates/tests/CLAUDE.md): the snapshot string is written as an `insta` inline snapshot (`@"…"`) in the test file, so the golden output lives beside the invocation rather than in a separate `.snap` file. +The **assertion is inline by convention**, following [crates/tests/AGENTS.md](../../../crates/tests/AGENTS.md): the snapshot string is written as an `insta` inline snapshot (`@"…"`) in the test file, so the golden output lives beside the invocation rather than in a separate `.snap` file. ## Tests -The snapshot macros are the test harness rather than the thing under test, so their coverage is the set of snapshot invocations across the suite. Per [crates/tests/CLAUDE.md](../../../crates/tests/CLAUDE.md), each CGP macro's expansion is snapshotted only in the concept target that owns its feature, and each owning target's snapshots are indexed by that macro's own implementation document. Representative usages: +The snapshot macros are the test harness rather than the thing under test, so their coverage is the set of snapshot invocations across the suite. Per [crates/tests/AGENTS.md](../../../crates/tests/AGENTS.md), each CGP macro's expansion is snapshotted only in the concept target that owns its feature, and each owning target's snapshots are indexed by that macro's own implementation document. Representative usages: - [basic_delegation/component_macro.rs](../../../crates/tests/cgp-tests/tests/basic_delegation/component_macro.rs) — the canonical `snapshot_cgp_component!` invocation, using the `AttributeMacroSnapshot` shape. - [async_and_send/cgp_fn_async.rs](../../../crates/tests/cgp-tests/tests/async_and_send/cgp_fn_async.rs) — a `snapshot_cgp_fn!` over a stacked `#[cgp_fn]` / `#[async_trait]`. diff --git a/docs/implementation/macros/define_keyword.md b/docs/implementation/macros/define_keyword.md index c353d88b..9ac2a5d4 100644 --- a/docs/implementation/macros/define_keyword.md +++ b/docs/implementation/macros/define_keyword.md @@ -15,4 +15,4 @@ The keyword string and the struct name are independent, so the marker can be nam ## Source - The macro is defined in [cgp-macro-core/src/macros/keyword.rs](../../../crates/macros/cgp-macro-core/src/macros/keyword.rs); the `IsKeyword` trait it implements lives in `cgp-macro-core/src/traits/`, and the keyword marker types that use it live in `cgp-macro-core/src/types/keyword*.rs`. -- The convention that custom keywords go through this macro is recorded in [cgp-macro-core/CLAUDE.md](../../../crates/macros/cgp-macro-core/CLAUDE.md). +- The convention that custom keywords go through this macro is recorded in [cgp-macro-core/AGENTS.md](../../../crates/macros/cgp-macro-core/AGENTS.md). diff --git a/docs/implementation/macros/export_constructs.md b/docs/implementation/macros/export_constructs.md index 68860f09..5b3a4a65 100644 --- a/docs/implementation/macros/export_constructs.md +++ b/docs/implementation/macros/export_constructs.md @@ -13,4 +13,4 @@ The point of the indirection is hygiene. Generated code interpolates the marker ## Source - The macros are defined in [cgp-macro-core/src/macros/export.rs](../../../crates/macros/cgp-macro-core/src/macros/export.rs); the marker set they generate lives in [cgp-macro-core/src/exports.rs](../../../crates/macros/cgp-macro-core/src/exports.rs), and the `::cgp::macro_prelude` re-export surface is what makes the emitted paths resolve. -- The convention that all CGP items are referenced through these markers is recorded in [cgp-macro-core/CLAUDE.md](../../../crates/macros/cgp-macro-core/CLAUDE.md). +- The convention that all CGP items are referenced through these markers is recorded in [cgp-macro-core/AGENTS.md](../../../crates/macros/cgp-macro-core/AGENTS.md). diff --git a/docs/implementation/macros/parse_internal.md b/docs/implementation/macros/parse_internal.md index 88e3fe18..29a98318 100644 --- a/docs/implementation/macros/parse_internal.md +++ b/docs/implementation/macros/parse_internal.md @@ -20,4 +20,4 @@ A subtle interaction with the `?` expansion is that `parse_internal!` cannot be - The macro is defined in [cgp-macro-core/src/macros/parse.rs](../../../crates/macros/cgp-macro-core/src/macros/parse.rs) and the backing function in [cgp-macro-core/src/functions/parse_internal.rs](../../../crates/macros/cgp-macro-core/src/functions/parse_internal.rs). - The `quote!` re-export it depends on is in `cgp-macro-core/src/vendor.rs`, and the prefix-stripping helper is `strip_macro_prelude` in `cgp-macro-core/src/functions/strip.rs`. -- The convention that all AST nodes are built through this macro is recorded in [cgp-macro-core/CLAUDE.md](../../../crates/macros/cgp-macro-core/CLAUDE.md). +- The convention that all AST nodes are built through this macro is recorded in [cgp-macro-core/AGENTS.md](../../../crates/macros/cgp-macro-core/AGENTS.md). diff --git a/docs/reference/README.md b/docs/reference/README.md index 2586e5bf..ca6911e0 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -1,6 +1,6 @@ # CGP Construct Reference -This directory documents every CGP construct — one self-contained document per construct, each explaining its purpose, syntax or definition, expansion or behavior, examples, related constructs, and source. The documents are written for agents who need precise per-construct semantics, and they point only at library source, never at a test. The high-level conceptual framing that connects the constructs lives in the sibling [concepts/](../concepts/README.md) directory; the internal mechanics of each macro — its pipeline, the functions that synthesize its output, and every pointer into the test suite — live in the sibling [implementation/](../implementation/README.md) directory, to which each reference document's Source section links; the `/cgp` skill remains a complementary teaching aid. The authoring rules, document template, and the requirement to keep these documents in sync with the code live in [../CLAUDE.md](../CLAUDE.md). +This directory documents every CGP construct — one self-contained document per construct, each explaining its purpose, syntax or definition, expansion or behavior, examples, related constructs, and source. The documents are written for agents who need precise per-construct semantics, and they point only at library source, never at a test. The high-level conceptual framing that connects the constructs lives in the sibling [concepts/](../concepts/README.md) directory; the internal mechanics of each macro — its pipeline, the functions that synthesize its output, and every pointer into the test suite — live in the sibling [implementation/](../implementation/README.md) directory, to which each reference document's Source section links; the `/cgp` skill remains a complementary teaching aid. The authoring rules, document template, and the requirement to keep these documents in sync with the code live in [../AGENTS.md](../AGENTS.md). ## Directory layout diff --git a/docs/skills/README.md b/docs/skills/README.md index 7be5e6e8..2b5767e0 100644 --- a/docs/skills/README.md +++ b/docs/skills/README.md @@ -6,7 +6,7 @@ This directory holds the [agent skills](https://agentskills.io/) built from the A skill is a *teaching artifact optimized for an agent's context window*, whereas a reference document is an *exhaustive per-construct record* and a concept document is a *cross-cutting overview*. The skill draws on all three but reproduces none of them wholesale — it carries the mental model, the common constructs, and worked examples in enough depth to act, and points to the online knowledge base for the corner cases it deliberately omits. The relationship is one-directional: the knowledge base is the source of truth, and the skill is a synthesis kept in sync with it. -A skill is also deployed differently. It is copied out of this repository and run on its own, so it cannot rely on any file outside its own directory — every cross-link is either to a sibling sub-skill (a plain relative filename) or to the online knowledge base on GitHub. The authoring and synchronization rules that govern this, including how to keep a skill current when a construct changes, live in [../CLAUDE.md](../CLAUDE.md) under "The skills directory." +A skill is also deployed differently. It is copied out of this repository and run on its own, so it cannot rely on any file outside its own directory — every cross-link is either to a sibling sub-skill (a plain relative filename) or to the online knowledge base on GitHub. The authoring and synchronization rules that govern this, including how to keep a skill current when a construct changes, live in [../AGENTS.md](../AGENTS.md) under "The skills directory." ## The catalog From 114deeffcfdb18e5b30732d2f1b46a2cc7f8b5cd Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 20:45:22 +0200 Subject: [PATCH 02/17] Review `#[implicits]` --- AGENTS.md | 2 +- .../src/functions/field/parse.rs | 35 +++++-- .../src/functions/implicits/parse.rs | 22 +++-- .../src/types/getter/field_mode.rs | 1 + .../types/getter/get_field_with_mode_expr.rs | 11 +++ .../cgp-macro-core/src/types/getter/method.rs | 11 +++ .../tests/parser_rejections/cgp_fn.rs | 41 +++++++- crates/tests/cgp-tests/tests/getters/mod.rs | 1 + .../tests/getters/option_str_auto.rs | 83 +++++++++++++++++ .../implicit_arguments/cgp_fn_option_str.rs | 93 +++++++++++++++++++ .../implicit_arguments/cgp_fn_owned_tuple.rs | 58 ++++++++++++ .../tests/implicit_arguments/cgp_fn_slice.rs | 60 ++++++++++++ .../cgp-tests/tests/implicit_arguments/mod.rs | 3 + docs/implementation/asts/cgp_fn.md | 4 +- docs/implementation/asts/cgp_getter.md | 2 +- .../entrypoints/cgp_auto_getter.md | 1 + docs/implementation/entrypoints/cgp_fn.md | 11 ++- docs/reference/attributes/implicit.md | 12 ++- docs/reference/macros/cgp_auto_getter.md | 2 +- docs/reference/macros/cgp_getter.md | 2 +- .../cgp/references/functions-and-getters.md | 2 +- 21 files changed, 425 insertions(+), 32 deletions(-) create mode 100644 crates/tests/cgp-tests/tests/getters/option_str_auto.rs create mode 100644 crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_str.rs create mode 100644 crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_owned_tuple.rs create mode 100644 crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_slice.rs diff --git a/AGENTS.md b/AGENTS.md index 1b992fa6..df8e8b4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to LLM agents when working with code in this repository. ## Understanding CGP first diff --git a/crates/macros/cgp-macro-core/src/functions/field/parse.rs b/crates/macros/cgp-macro-core/src/functions/field/parse.rs index 50ce7930..02e3903e 100644 --- a/crates/macros/cgp-macro-core/src/functions/field/parse.rs +++ b/crates/macros/cgp-macro-core/src/functions/field/parse.rs @@ -28,7 +28,14 @@ pub fn parse_field_type( let field_type: Type = parse_internal! { String }; Ok((field_type, FieldMode::Str)) - } else if let (Type::Slice(slice), None) = (type_ref.elem.as_ref(), receiver_mut) { + } else if let (Type::Slice(slice), None) = + (type_ref.elem.as_ref(), &type_ref.mutability) + { + // A shared `&[T]` reads any `AsRef<[T]>` field. Whether the read is + // mutable follows the reference's *own* mutability, not the + // receiver's — a `&self`/`&mut self` method may take an immutable + // slice either way, and there is no `AsMut<[T]>` counterpart, so a + // `&mut [T]` falls through to the plain reference case below. let field_type = slice.elem.as_ref().clone(); Ok((field_type, FieldMode::Slice)) @@ -40,20 +47,28 @@ pub fn parse_field_type( } Type::Path(type_path) => { if let Some(field_type) = try_parse_option_ref(type_path) { - Ok(( - parse_internal! { Option< #field_type > }, - FieldMode::OptionRef, - )) - } else if let (Some(field_type), None) = (try_parse_mref(type_path), receiver_mut) { + if field_type == &parse_internal! { str } { + // `Option<&str>` is backed by an `Option` field and read + // with `.as_deref()`, mirroring the `&str`/`String` special case + // for a plain reference. + Ok((parse_internal! { Option< String > }, FieldMode::OptionStr)) + } else { + Ok(( + parse_internal! { Option< #field_type > }, + FieldMode::OptionRef, + )) + } + } else if let Some(field_type) = try_parse_mref(type_path) { + // `MRef` borrows the field as a shared value, so — unlike a `&mut` + // reference — its access mode never depends on the receiver. Ok((field_type.clone(), FieldMode::MRef)) } else { Ok((return_type.clone(), FieldMode::Copy)) } } - _ => Err(Error::new( - return_type.span(), - "return type must be a reference", - )), + // Any other owned type (a tuple, an array, and so on) is read by value and + // cloned, exactly like an owned path type. + _ => Ok((return_type.clone(), FieldMode::Copy)), } } diff --git a/crates/macros/cgp-macro-core/src/functions/implicits/parse.rs b/crates/macros/cgp-macro-core/src/functions/implicits/parse.rs index 6ffaa3fc..69422769 100644 --- a/crates/macros/cgp-macro-core/src/functions/implicits/parse.rs +++ b/crates/macros/cgp-macro-core/src/functions/implicits/parse.rs @@ -11,7 +11,7 @@ use crate::types::implicits::{ImplicitArgField, ImplicitArgFields}; pub fn extract_and_parse_implicit_args( args: &mut Punctuated, ) -> syn::Result { - let implicit_fn_args = extract_implicit_args(args); + let implicit_fn_args = extract_implicit_args(args)?; if implicit_fn_args.is_empty() { return Ok(ImplicitArgFields::default()); @@ -85,14 +85,14 @@ pub fn parse_implicit_arg(receiver: &Receiver, arg: &PatType) -> syn::Result) -> Vec { +pub fn extract_implicit_args(args: &mut Punctuated) -> syn::Result> { let mut implicit_args = Vec::new(); let process_args = mem::take(args); for arg in process_args.into_iter() { if let FnArg::Typed(mut arg) = arg { - if is_implicit_arg(&mut arg) { + if is_implicit_arg(&mut arg)? { implicit_args.push(arg); } else { args.push(FnArg::Typed(arg)); @@ -102,10 +102,10 @@ pub fn extract_implicit_args(args: &mut Punctuated) -> Vec bool { +pub fn is_implicit_arg(arg: &mut PatType) -> syn::Result { let mut res = false; let attrs = mem::take(&mut arg.attrs); @@ -113,12 +113,22 @@ pub fn is_implicit_arg(arg: &mut PatType) -> bool { for attr in attrs { if is_implicit_attr(&attr) { res = true; + } else if attr.path().is_ident("implicit") { + // `#[implicit]` is a bare marker; a list or name-value form such as + // `#[implicit(...)]` or `#[implicit = ...]` is a mistake. Reject it here + // rather than leaving the stray attribute on the parameter, where it + // would surface far downstream as an obscure "cannot find attribute + // `implicit`" error. + return Err(syn::Error::new_spanned( + &attr, + "`#[implicit]` does not take any arguments; write it as a bare `#[implicit]`", + )); } else { arg.attrs.push(attr); } } - res + Ok(res) } pub fn is_implicit_attr(attr: &Attribute) -> bool { diff --git a/crates/macros/cgp-macro-core/src/types/getter/field_mode.rs b/crates/macros/cgp-macro-core/src/types/getter/field_mode.rs index f2fdaa1a..0102740a 100644 --- a/crates/macros/cgp-macro-core/src/types/getter/field_mode.rs +++ b/crates/macros/cgp-macro-core/src/types/getter/field_mode.rs @@ -2,6 +2,7 @@ pub enum FieldMode { Reference, OptionRef, + OptionStr, MRef, Str, Copy, diff --git a/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs b/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs index 3eb02e31..fe39f069 100644 --- a/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs +++ b/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs @@ -27,6 +27,17 @@ impl ToTokens for GetFieldWithModeExpr { } } } + FieldMode::OptionStr => { + if call_expr.field_mut.is_none() { + quote! { + #call_expr .as_deref() + } + } else { + quote! { + #call_expr .as_deref_mut() + } + } + } FieldMode::MRef => { quote! { MRef::Ref( #call_expr ) diff --git a/crates/macros/cgp-macro-core/src/types/getter/method.rs b/crates/macros/cgp-macro-core/src/types/getter/method.rs index 11ee4bfa..91fedeec 100644 --- a/crates/macros/cgp-macro-core/src/types/getter/method.rs +++ b/crates/macros/cgp-macro-core/src/types/getter/method.rs @@ -130,6 +130,17 @@ fn extend_call_expr( } } } + FieldMode::OptionStr => { + if field_mut.is_none() { + quote! { + #call_expr .as_deref() + } + } else { + quote! { + #call_expr .as_deref_mut() + } + } + } FieldMode::MRef => { quote! { MRef::Ref( #call_expr ) diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs index caf38961..4d4532a3 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs @@ -1,9 +1,10 @@ //! `#[cgp_fn]` rejects the implicit-argument shapes it cannot lower: an implicit //! argument on a function with no `self` receiver, a `mut` binding pattern on an -//! implicit argument, and a `&mut` implicit argument that is not the sole implicit +//! implicit argument, a `&mut` implicit argument that is not the sole implicit //! (its exclusive borrow of the context would conflict with reading any other -//! field). Each is a rejection the macro makes during expansion, so it is pinned -//! by driving the entrypoint directly here rather than by a `compile_fail` test. +//! field), and a malformed `#[implicit]` attribute carrying arguments. Each is a +//! rejection the macro makes during expansion, so it is pinned by driving the +//! entrypoint directly here rather than by a `compile_fail` test. //! //! See docs/implementation/entrypoints/cgp_fn.md (Tests) for these failure cases, //! and docs/reference/attributes/implicit.md for the user-facing rules on where @@ -72,3 +73,37 @@ fn rejects_mutable_implicit_with_other_implicit() { }, ); } + +#[test] +fn rejects_implicit_attr_with_arguments() { + // `#[implicit]` is a bare marker attribute; a list form such as + // `#[implicit(foo)]` is a mistake and is rejected with a spanned error, rather + // than left on the parameter to surface downstream as an obscure "cannot find + // attribute `implicit`" error. + assert_macro_rejects("cgp_fn with a `#[implicit(...)]` attribute", || { + cgp_macro_lib::cgp_fn( + quote!(), + quote!( + pub fn greet(&self, #[implicit(foo)] name: &str) { + let _ = name; + } + ), + ) + }); +} + +#[test] +fn rejects_implicit_attr_name_value() { + // The name-value form `#[implicit = ...]` is likewise rejected: `#[implicit]` + // takes no arguments in any form. + assert_macro_rejects("cgp_fn with a `#[implicit = ...]` attribute", || { + cgp_macro_lib::cgp_fn( + quote!(), + quote!( + pub fn greet(&self, #[implicit = 1] name: &str) { + let _ = name; + } + ), + ) + }); +} diff --git a/crates/tests/cgp-tests/tests/getters/mod.rs b/crates/tests/cgp-tests/tests/getters/mod.rs index 782489b2..37ec844a 100644 --- a/crates/tests/cgp-tests/tests/getters/mod.rs +++ b/crates/tests/cgp-tests/tests/getters/mod.rs @@ -11,6 +11,7 @@ pub mod clone_auto; pub mod mref_auto; pub mod non_self_auto; pub mod option_auto; +pub mod option_str_auto; pub mod slice_auto; pub mod string_auto; diff --git a/crates/tests/cgp-tests/tests/getters/option_str_auto.rs b/crates/tests/cgp-tests/tests/getters/option_str_auto.rs new file mode 100644 index 00000000..36a2048f --- /dev/null +++ b/crates/tests/cgp-tests/tests/getters/option_str_auto.rs @@ -0,0 +1,83 @@ +//! `#[cgp_auto_getter]` returning `Option<&str>`: the blanket impl reads an +//! `Option` field named after the method and calls `.as_deref()`, +//! converting `&Option` into `Option<&str>` — the `&str`/`String` special +//! case composed with the option case. +//! +//! See docs/reference/macros/cgp_auto_getter.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_auto_getter; + +snapshot_cgp_auto_getter! { + #[cgp_auto_getter] + pub trait HasNickname { + fn nickname(&self) -> Option<&str>; + } + + expand_has_nickname(output) { + insta::assert_snapshot!(output, @" + pub trait HasNickname { + fn nickname(&self) -> Option<&str>; + } + impl<__Context__> HasNickname for __Context__ + where + __Context__: HasField< + Symbol< + 8, + Chars< + 'n', + Chars< + 'i', + Chars< + 'c', + Chars<'k', Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>>, + >, + >, + >, + >, + Value = Option, + >, + { + fn nickname(&self) -> Option<&str> { + self.get_field( + ::core::marker::PhantomData::< + Symbol< + 8, + Chars< + 'n', + Chars< + 'i', + Chars< + 'c', + Chars< + 'k', + Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + ) + .as_deref() + } + } + ") + } +} + +#[derive(HasField)] +pub struct Profile { + pub nickname: Option, +} + +#[test] +pub fn test_option_str_auto_getter() { + let named = Profile { + nickname: Some("ada".to_owned()), + }; + assert_eq!(named.nickname(), Some("ada")); + + let anon = Profile { nickname: None }; + assert_eq!(anon.nickname(), None); +} diff --git a/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_str.rs b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_str.rs new file mode 100644 index 00000000..2f1deab7 --- /dev/null +++ b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_str.rs @@ -0,0 +1,93 @@ +//! `#[cgp_fn]` with an `Option<&str>` `#[implicit]` argument. +//! +//! `Option<&str>` is backed by an `Option` field and read with +//! `.as_deref()` — the `&str`/`String` special case composed with the +//! `Option<&T>` reference case — so the body receives an `Option<&str>` while the +//! context stores an `Option`. +//! +//! See docs/reference/macros/cgp_fn.md and docs/reference/attributes/implicit.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_fn; + +snapshot_cgp_fn! { + #[cgp_fn] + pub fn describe(&self, #[implicit] nickname: Option<&str>) -> String { + match nickname { + Some(name) => format!("aka {name}"), + None => "anonymous".to_owned(), + } + } + + expand_describe(output) { + insta::assert_snapshot!(output, @r#" + pub trait Describe { + fn describe(&self) -> String; + } + impl<__Context__> Describe for __Context__ + where + Self: HasField< + Symbol< + 8, + Chars< + 'n', + Chars< + 'i', + Chars< + 'c', + Chars<'k', Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>>, + >, + >, + >, + >, + Value = Option, + >, + { + fn describe(&self) -> String { + let nickname: Option<&str> = self + .get_field( + ::core::marker::PhantomData::< + Symbol< + 8, + Chars< + 'n', + Chars< + 'i', + Chars< + 'c', + Chars< + 'k', + Chars<'n', Chars<'a', Chars<'m', Chars<'e', Nil>>>>, + >, + >, + >, + >, + >, + >, + ) + .as_deref(); + match nickname { + Some(name) => format!("aka {name}"), + None => "anonymous".to_owned(), + } + } + } + "#) + } +} + +#[derive(HasField)] +pub struct Profile { + pub nickname: Option, +} + +#[test] +fn test_describe() { + let named = Profile { + nickname: Some("ada".to_owned()), + }; + assert_eq!(named.describe(), "aka ada"); + + let anon = Profile { nickname: None }; + assert_eq!(anon.describe(), "anonymous"); +} diff --git a/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_owned_tuple.rs b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_owned_tuple.rs new file mode 100644 index 00000000..082f66d8 --- /dev/null +++ b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_owned_tuple.rs @@ -0,0 +1,58 @@ +//! `#[cgp_fn]` with an owned, non-path `#[implicit]` argument (a tuple). +//! +//! Owned implicit arguments are read by value and `.clone()`d regardless of their +//! shape — a tuple, an array, or a plain path type all read a `Value = T` field. +//! This pins that a tuple field is accepted rather than rejected. +//! +//! See docs/reference/macros/cgp_fn.md and docs/reference/attributes/implicit.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_fn; + +snapshot_cgp_fn! { + #[cgp_fn] + pub fn origin_distance(&self, #[implicit] point: (f64, f64)) -> f64 { + (point.0 * point.0 + point.1 * point.1).sqrt() + } + + expand_origin_distance(output) { + insta::assert_snapshot!(output, @" + pub trait OriginDistance { + fn origin_distance(&self) -> f64; + } + impl<__Context__> OriginDistance for __Context__ + where + Self: HasField< + Symbol<5, Chars<'p', Chars<'o', Chars<'i', Chars<'n', Chars<'t', Nil>>>>>>, + Value = (f64, f64), + >, + { + fn origin_distance(&self) -> f64 { + let point: (f64, f64) = self + .get_field( + ::core::marker::PhantomData::< + Symbol< + 5, + Chars<'p', Chars<'o', Chars<'i', Chars<'n', Chars<'t', Nil>>>>>, + >, + >, + ) + .clone(); + (point.0 * point.0 + point.1 * point.1).sqrt() + } + } + ") + } +} + +#[derive(HasField)] +pub struct Location { + pub point: (f64, f64), +} + +#[test] +fn test_origin_distance() { + let location = Location { point: (3.0, 4.0) }; + + assert_eq!(location.origin_distance(), 5.0); +} diff --git a/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_slice.rs b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_slice.rs new file mode 100644 index 00000000..91fb1a31 --- /dev/null +++ b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_slice.rs @@ -0,0 +1,60 @@ +//! `#[cgp_fn]` with an immutable `&[T]` `#[implicit]` argument under a `&mut self` +//! receiver. +//! +//! A `&[T]` implicit reads any `AsRef<[T]>` field (e.g. `Vec`) and calls +//! `.as_ref()`. The access mode follows the *argument's* own type, not the +//! receiver's, so the immutable slice reads through `HasField`/`get_field` with an +//! `AsRef<[u8]>` bound even though the receiver is `&mut self` — it is not forced +//! into a plain-reference `Value = [u8]` bound that no context could satisfy. +//! +//! See docs/reference/macros/cgp_fn.md and docs/reference/attributes/implicit.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_fn; + +snapshot_cgp_fn! { + #[cgp_fn] + pub fn count_matches(&mut self, #[implicit] data: &[u8], byte: u8) -> usize { + data.iter().filter(|b| **b == byte).count() + } + + expand_count_matches(output) { + insta::assert_snapshot!(output, @" + pub trait CountMatches { + fn count_matches(&mut self, byte: u8) -> usize; + } + impl<__Context__> CountMatches for __Context__ + where + Self: HasField< + Symbol<4, Chars<'d', Chars<'a', Chars<'t', Chars<'a', Nil>>>>>, + Value: AsRef<[u8]> + 'static, + >, + { + fn count_matches(&mut self, byte: u8) -> usize { + let data: &[u8] = self + .get_field( + ::core::marker::PhantomData::< + Symbol<4, Chars<'d', Chars<'a', Chars<'t', Chars<'a', Nil>>>>>, + >, + ) + .as_ref(); + data.iter().filter(|b| **b == byte).count() + } + } + ") + } +} + +#[derive(HasField)] +pub struct Buffer { + pub data: Vec, +} + +#[test] +fn test_count_matches() { + let mut buffer = Buffer { + data: vec![1, 2, 2, 3, 2], + }; + + assert_eq!(buffer.count_matches(2), 3); +} diff --git a/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs b/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs index 993efd34..857fbd58 100644 --- a/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs +++ b/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs @@ -7,6 +7,9 @@ pub mod cgp_fn_custom_trait_name; pub mod cgp_fn_greet; pub mod cgp_fn_multi_and_use_type; pub mod cgp_fn_mutable; +pub mod cgp_fn_option_str; +pub mod cgp_fn_owned_tuple; +pub mod cgp_fn_slice; // `#[implicit]` arguments inside `#[cgp_impl]` providers, and the implicit // context parameter. diff --git a/docs/implementation/asts/cgp_fn.md b/docs/implementation/asts/cgp_fn.md index 88434790..774a0bb2 100644 --- a/docs/implementation/asts/cgp_fn.md +++ b/docs/implementation/asts/cgp_fn.md @@ -20,7 +20,7 @@ The ordering here is the contract the snapshots pin: the implicit-argument bound The implicit-argument types are shared building blocks that `#[cgp_fn]` and `#[cgp_impl]` both use, so they live under `types/implicits/` rather than in the `cgp_fn` module. An `ImplicitArgField` records one extracted argument — its field name, the field type to require, the field mutability (set from the argument's own type: `Some` only for a `&mut T` argument), the field mode (the conversion to apply), and the original argument type — and `ImplicitArgFields` is the collected list. -The extraction is driven by `extract_and_parse_implicit_args`, which pulls every `#[implicit]`-marked argument out of a signature's inputs, parses each into an `ImplicitArgField`, and rejects the combinations extraction cannot lower (a missing `self` receiver, a `mut` pattern, and a `&mut` implicit sharing the function with any other implicit). `ImplicitArgField` then contributes in two directions: `to_has_field_bound` produces the `HasField`/`HasFieldMut` bound the impl requires, and `to_statement` produces the `let #name: #arg_type = self.get_field(...) ;` binding that `prepend_to_block` splices onto the front of the body. The conversion is chosen by `parse_field_type`, the same field-mode logic the getter macros use; the receiver's mutability enters only to validate that a `&mut T` argument is paired with a `&mut self` receiver. +The extraction is driven by `extract_and_parse_implicit_args`, which pulls every `#[implicit]`-marked argument out of a signature's inputs, parses each into an `ImplicitArgField`, and rejects the inputs extraction cannot lower (a missing `self` receiver, a `mut` pattern, a `&mut` implicit sharing the function with any other implicit, and a malformed `#[implicit(...)]`/`#[implicit = ...]` attribute that is not the bare marker form). `ImplicitArgField` then contributes in two directions: `to_has_field_bound` produces the `HasField`/`HasFieldMut` bound the impl requires, and `to_statement` produces the `let #name: #arg_type = self.get_field(...) ;` binding that `prepend_to_block` splices onto the front of the body. The conversion is chosen by `parse_field_type`, the same field-mode logic the getter macros use; the receiver's mutability enters only to validate that a `&mut T` argument is paired with a `&mut self` receiver. ```rust // for `#[implicit] name: &str` on `&self`, to_statement produces: @@ -32,7 +32,7 @@ let name: &str = self.get_field(PhantomData::).as_str(); ## Tests - The stage transforms are exercised end-to-end by the expansion snapshots indexed in the [entrypoint document's Snapshots section](../entrypoints/cgp_fn.md). -- The rejections enforced during implicit-argument extraction — an implicit argument without a `self` receiver, a `mut` binding pattern on an implicit argument, and a `&mut` implicit combined with any other implicit — are pinned by [parser_rejections/cgp_fn.rs](../../../crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs). +- The rejections enforced during implicit-argument extraction — an implicit argument without a `self` receiver, a `mut` binding pattern on an implicit argument, a `&mut` implicit combined with any other implicit, and a malformed `#[implicit]` attribute carrying arguments — are pinned by [parser_rejections/cgp_fn.rs](../../../crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs). ## Source diff --git a/docs/implementation/asts/cgp_getter.md b/docs/implementation/asts/cgp_getter.md index edcb5f35..89536b66 100644 --- a/docs/implementation/asts/cgp_getter.md +++ b/docs/implementation/asts/cgp_getter.md @@ -6,7 +6,7 @@ This stack covers the two getter macros together, because they share the getter- Both macros turn a getter trait's methods into `GetterField`s through the shared `parse_getter_fields` helper, which is the single source of truth for what a getter signature means. A `GetterField` records the field name (the method name), the field type to require, the return type, the receiver mutability, an optional `PhantomData` phantom-argument type, the field mode, and the receiver mode. -The parser enforces the getter-method contract and resolves the two shorthands a reader most needs to know. A getter method must be a plain (non-const, non-async, non-unsafe, non-generic) method whose first argument is a reference — either `&self` (`ReceiverMode::SelfReceiver`) or a typed `&SomeType` receiver (`ReceiverMode::Type`), the latter letting a getter read a field out of a type other than the context, with `Self` rewritten to the context. The return type then determines the field type and the *field mode* — the conversion the getter body applies — through the shared `parse_field_type`: a `&str` return reads a `String` field (`FieldMode::Str`, `.as_str()`), an `Option<&T>` reads `Option` (`FieldMode::OptionRef`, `.as_ref()`), a `&[T]` reads an `AsRef<[T]>` field (`FieldMode::Slice`, `.as_ref()`), an `MRef<'_, T>` wraps the borrow (`FieldMode::MRef`, `MRef::Ref(...)`), a plain `&T` is a bare `FieldMode::Reference`, and any owned return is `FieldMode::Copy` (`.clone()`). `parse_getter_fields` also extracts an optional single associated return type and checks that, when present, the trait has exactly one method whose return type matches it. +The parser enforces the getter-method contract and resolves the two shorthands a reader most needs to know. A getter method must be a plain (non-const, non-async, non-unsafe, non-generic) method whose first argument is a reference — either `&self` (`ReceiverMode::SelfReceiver`) or a typed `&SomeType` receiver (`ReceiverMode::Type`), the latter letting a getter read a field out of a type other than the context, with `Self` rewritten to the context. The return type then determines the field type and the *field mode* — the conversion the getter body applies — through the shared `parse_field_type`: a `&str` return reads a `String` field (`FieldMode::Str`, `.as_str()`), an `Option<&T>` reads `Option` (`FieldMode::OptionRef`, `.as_ref()`), an `Option<&str>` reads `Option` (`FieldMode::OptionStr`, `.as_deref()`), a `&[T]` reads an `AsRef<[T]>` field (`FieldMode::Slice`, `.as_ref()`), an `MRef<'_, T>` wraps the borrow (`FieldMode::MRef`, `MRef::Ref(...)`), a plain `&T` is a bare `FieldMode::Reference`, and any other owned return — a path type, a tuple, or an array — is `FieldMode::Copy` (`.clone()`). The slice mode is keyed off the reference's own mutability rather than the receiver's, which is what lets the same helper serve the `#[implicit]` case where an argument's mutability is independent of the receiver. `parse_getter_fields` also extracts an optional single associated return type and checks that, when present, the trait has exactly one method whose return type matches it. The getter-method body itself is built by `derive_getter_method` from the `types/getter/` module, which emits `receiver.get_field(PhantomData::) ` for the field's mode; the same `FieldMode` and `GetFieldWithModeExpr` types drive the `#[implicit]` bindings in the [`cgp_fn` stack](cgp_fn.md), which is why the two families convert fields identically. diff --git a/docs/implementation/entrypoints/cgp_auto_getter.md b/docs/implementation/entrypoints/cgp_auto_getter.md index 0c1de9cb..77ba725a 100644 --- a/docs/implementation/entrypoints/cgp_auto_getter.md +++ b/docs/implementation/entrypoints/cgp_auto_getter.md @@ -64,6 +64,7 @@ Every `snapshot_cgp_auto_getter!` invocation across the suite is indexed here, s - [getters/clone_auto.rs](../../../crates/tests/cgp-tests/tests/getters/clone_auto.rs) — an owned return `.clone()`d out by value. - [getters/mref_auto.rs](../../../crates/tests/cgp-tests/tests/getters/mref_auto.rs) — an `MRef<'_, String>` return wrapping the borrow in `MRef::Ref`. - [getters/option_auto.rs](../../../crates/tests/cgp-tests/tests/getters/option_auto.rs) — an `Option<&String>` return reading an `Option` field via `.as_ref()`. +- [getters/option_str_auto.rs](../../../crates/tests/cgp-tests/tests/getters/option_str_auto.rs) — an `Option<&str>` return reading an `Option` field via `.as_deref()`, composing the `&str` and option cases. - [getters/slice_auto.rs](../../../crates/tests/cgp-tests/tests/getters/slice_auto.rs) — a `&[u8]` return reading an `AsRef<[u8]> + 'static` field via `.as_ref()`. - [getters/non_self_auto.rs](../../../crates/tests/cgp-tests/tests/getters/non_self_auto.rs) — a non-`self` getter reading a field out of another type (`&Self::Foo`). - [getters/auto_getter_generic.rs](../../../crates/tests/cgp-tests/tests/getters/auto_getter_generic.rs) — a trait generic over a type parameter, keyed by a `PhantomData` tag. diff --git a/docs/implementation/entrypoints/cgp_fn.md b/docs/implementation/entrypoints/cgp_fn.md index dd29e223..df00982f 100644 --- a/docs/implementation/entrypoints/cgp_fn.md +++ b/docs/implementation/entrypoints/cgp_fn.md @@ -50,7 +50,7 @@ where } ``` -The conversion applied to each binding is chosen by the argument's type, following the same field-mode rules the getter macros use: a `&str` argument reads a `String` field and appends `.as_str()`, an owned value appends `.clone()`, an `Option<&T>` reads an `Option` field and appends `.as_ref()`, an `&[T]` reads an `AsRef<[T]>` field and appends `.as_ref()`, and a plain `&T` is taken by reference with no conversion. A `&mut T` argument reads through `HasFieldMut`/`get_field_mut` and requires a `&mut self` receiver; every immutable argument reads through `HasField`/`get_field` regardless of the receiver. These modes are shared with `#[cgp_auto_getter]` and `#[cgp_getter]` through the [field-parsing helpers](../asts/cgp_getter.md); the difference is only where the read lands — a prepended `let` in the body here, a getter-method body there. +The conversion applied to each binding is chosen by the argument's type, following the same field-mode rules the getter macros use: a `&str` argument reads a `String` field and appends `.as_str()`, an owned value — a path type, tuple, or array — appends `.clone()`, an `Option<&T>` reads an `Option` field and appends `.as_ref()`, an `Option<&str>` reads an `Option` field and appends `.as_deref()`, an `&[T]` reads an `AsRef<[T]>` field and appends `.as_ref()`, and a plain `&T` is taken by reference with no conversion. A `&mut T` argument reads through `HasFieldMut`/`get_field_mut` and requires a `&mut self` receiver; every immutable argument reads through `HasField`/`get_field` regardless of the receiver. The slice and option modes are keyed off the *argument's own* reference, not the receiver's, so an immutable `&[T]` keeps its `AsRef<[T]>` bound even under a `&mut self` receiver rather than collapsing into an unsatisfiable `Value = [T]`. These modes are shared with `#[cgp_auto_getter]` and `#[cgp_getter]` through the [field-parsing helpers](../asts/cgp_getter.md); the difference is only where the read lands — a prepended `let` in the body here, a getter-method body there. ## Behavior and corner cases @@ -60,12 +60,16 @@ The conversion applied to each binding is chosen by the argument's type, followi **The visibility is moved, not copied.** `preprocess` takes the function's visibility off the inner `ItemFn` and re-applies it to the generated trait, so the emitted method inside the trait is always inherited-visibility while the trait itself carries the `pub` the user wrote. +**A malformed `#[implicit]` attribute is rejected at extraction.** `#[implicit]` is recognized only as a bare `Meta::Path`; the extraction pass additionally checks for an `implicit`-named attribute in any other form (`#[implicit(...)]` or `#[implicit = ...]`) and returns a spanned error, rather than leaving the stray attribute on the parameter to surface downstream as an obscure `cannot find attribute 'implicit'` error. + **A `&mut` implicit argument must be the only implicit argument.** The `field_mut` of each argument follows the argument's own type — set from the `&mut` in `&mut T`, not from the receiver — so a `&mut T` argument reads through `get_field_mut` while every immutable argument reads through `get_field`, even under a `&mut self` receiver. Because a `get_field_mut` read borrows the whole context exclusively for the rest of the body, a `&mut` implicit cannot coexist with any other implicit read; extraction rejects the combination (`has_mutable && count > 1`) rather than emit a blanket impl that fails to borrow-check. Any number of purely immutable implicits, by contrast, are shared borrows and combine freely. A `&mut T` argument additionally requires the `&mut self` receiver, and a `mut` *pattern* on any implicit argument is rejected outright. These checks are enforced during implicit-argument extraction. ## Known issues `#[cgp_fn]` does not support generics on the desugared *method* itself — generic parameters are only ever lifted onto the trait and impl. A method-level generic is silently treated as a trait/impl generic rather than rejected, which is the intended limitation rather than a bug: method-level generics are considered an advanced case better written as an explicit blanket impl or a [`#[cgp_component]`](../../reference/macros/cgp_component.md) provider. +The *mutable* variants of the slice and option modes are not supported. A `&mut [T]` implicit falls through the slice case (there is no `AsMut<[T]>` counterpart) into the plain-reference mode, producing an unsatisfiable `HasFieldMut` bound; and an `Option<&mut T>` implicit reads immutably (the argument's outer type is a path, so `field_mut` is `None`), producing an `Option<&T>` value that then fails to coerce to the declared `Option<&mut T>`. Both are exotic shapes rather than a regression — the immutable `&[T]` and `Option<&T>` forms are the supported ones — and the macro accepts them, so the failure lands on the emitted code at the use site rather than as a spanned rejection. + ## Snapshots Every `snapshot_cgp_fn!` invocation across the suite is indexed here, since these snapshots all belong to this entrypoint: @@ -74,6 +78,9 @@ Every `snapshot_cgp_fn!` invocation across the suite is indexed here, since thes - [implicit_arguments/cgp_fn_custom_trait_name.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_custom_trait_name.rs) — `#[cgp_fn(CanCalculateRectangleArea)]` overrides the generated trait name; two owned `f64` implicits each `.clone()`d. - [implicit_arguments/cgp_fn_mutable.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mutable.rs) — `&mut self` with a mutable implicit argument, reading through `HasFieldMut`/`get_field_mut`. - [implicit_arguments/cgp_fn_mut_self_immutable.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mut_self_immutable.rs) — a `&mut self` receiver with two *immutable* implicit arguments, showing they read through `HasField`/`get_field` (the access mode follows the argument, not the receiver) and that several immutable implicits may share a `&mut self` receiver. +- [implicit_arguments/cgp_fn_slice.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_slice.rs) — an immutable `&[u8]` implicit under a `&mut self` receiver, keeping its `Value: AsRef<[u8]> + 'static` bound and `.as_ref()` read because the slice mode follows the argument's own reference, not the receiver's; also mixes an implicit with a plain explicit argument. +- [implicit_arguments/cgp_fn_owned_tuple.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_owned_tuple.rs) — an owned, non-path implicit (a tuple) read by value with `.clone()` and a `Value = (f64, f64)` bound, showing owned types beyond path types are accepted. +- [implicit_arguments/cgp_fn_option_str.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_str.rs) — an `Option<&str>` implicit backed by an `Option` field and read with `.as_deref()`, composing the `&str` and option modes. - [implicit_arguments/cgp_fn_calling_fn.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_calling_fn.rs) — one `#[cgp_fn]` capability depending on another through an explicit `where Self:` bound. - [implicit_arguments/cgp_fn_multi_and_use_type.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_multi_and_use_type.rs) — explicit and implicit arguments mixed, generic type parameters lifted onto the trait, `#[async_trait]` preserved as a raw attribute, and `#[use_type]` importing and renaming abstract types. - [async_and_send/cgp_fn_async.rs](../../../crates/tests/cgp-tests/tests/async_and_send/cgp_fn_async.rs) — the canonical async expansion, an `async fn` combined with `#[async_trait]`. @@ -96,7 +103,7 @@ Because `#[cgp_fn]` emits a blanket impl, its snapshot tests double as behaviora The failure cases pin the inputs `#[cgp_fn]` refuses during expansion, each asserting the entrypoint returns `Err`: -- [parser_rejections/cgp_fn.rs](../../../crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs) covers an implicit argument on a function with no `self` receiver, a `mut` binding pattern on an implicit argument, and a `&mut` implicit argument that is not the sole implicit (which would borrow the context mutably and again at once). +- [parser_rejections/cgp_fn.rs](../../../crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs) covers an implicit argument on a function with no `self` receiver, a `mut` binding pattern on an implicit argument, a `&mut` implicit argument that is not the sole implicit (which would borrow the context mutably and again at once), and a malformed `#[implicit]` attribute carrying arguments (`#[implicit(...)]` or `#[implicit = ...]`). ## Source diff --git a/docs/reference/attributes/implicit.md b/docs/reference/attributes/implicit.md index 6e7a660b..162d96b8 100644 --- a/docs/reference/attributes/implicit.md +++ b/docs/reference/attributes/implicit.md @@ -12,7 +12,7 @@ This is why `#[implicit]` is the recommended starting point for basic CGP. It le ## Syntax -`#[implicit]` is written as a bare attribute on a typed function argument, and the argument must have a plain identifier name: +`#[implicit]` is written as a bare marker attribute on a typed function argument, and the argument must have a plain identifier name. It takes no arguments in any form — a list or name-value spelling such as `#[implicit(foo)]` or `#[implicit = "foo"]` is rejected with a spanned error rather than silently ignored: ```rust fn area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 { @@ -60,7 +60,7 @@ where The two `let` bindings are inserted at the top of the body in argument order, before any of the original statements, so the names are in scope for the rest of the function. The generated context type parameter is literally named `__Context__` in the emitted code; the examples here use `Context` for readability. -The access expression depends on the argument type, following the same rules as [`#[cgp_auto_getter]`](../macros/cgp_auto_getter.md). For an owned type such as `f64` or `String`, the macro reads the field by reference and appends `.clone()`, so the body receives an owned value. The one special case worth knowing is `&str`: an argument typed `&str` is backed by a `String` field, and the access uses `.as_str()` rather than `.clone()`. The mutability of the access follows the *argument's* own type, not the receiver's: only a `&mut T` argument reads through `HasFieldMut`/`get_field_mut`, while every immutable argument reads through `HasField`/`get_field` even on a `&mut self` receiver. Concretely: +The access expression depends on the argument type, following the same rules as [`#[cgp_auto_getter]`](../macros/cgp_auto_getter.md). An owned type — a path type such as `f64` or `String`, or a tuple or array — is read by reference and `.clone()`d, so the body receives an owned value; a plain `&T` is taken by reference with no conversion. Four forms are special: `&str` is backed by a `String` field and read with `.as_str()`; `&[T]` reads any field whose value implements `AsRef<[T]>` and calls `.as_ref()`; `Option<&T>` reads an `Option` field via `.as_ref()`; and `Option<&str>` reads an `Option` field via `.as_deref()`. The mutability of the access follows the *argument's* own type, not the receiver's: only a `&mut T` argument reads through `HasFieldMut`/`get_field_mut`, while every immutable argument — a `&[T]` slice included — reads through `HasField`/`get_field` even on a `&mut self` receiver. Concretely: ```rust #[cgp_fn] @@ -113,9 +113,13 @@ fn print_area(rect: &Rectangle) { `#[implicit]` is most often used inside [`#[cgp_fn]`](../macros/cgp_fn.md), which turns a function into a single-implementation capability, and inside [`#[cgp_impl]`](../macros/cgp_impl.md), which writes a provider for an existing component. It relies on [`#[derive(HasField)]`](../derives/derive_has_field.md) on the context to supply the field accessors that the generated bounds require. Its access rules — `.clone()` for owned values, `.as_str()` for `&str` — are shared with [`#[cgp_auto_getter]`](../macros/cgp_auto_getter.md), which defines a reusable getter *capability* trait; prefer an implicit argument for reading a field as a provider's own input, and reserve `#[cgp_auto_getter]` for the case where the field should be published as a `self.name()` accessor other providers depend on. To bring in other CGP capabilities alongside implicit arguments, combine `#[implicit]` with [`#[uses]`](uses.md). +## Known issues + +The mutable variants of the slice and option forms are not supported. A `&mut [T]` argument has no `AsMut<[T]>` counterpart and falls through to a plain-reference bound (`HasFieldMut`) that no context can satisfy, and an `Option<&mut T>` argument is read immutably and so produces a value that does not match its declared type. The macro accepts both, so the failure lands on the expanded code rather than as a spanned error; the mechanics are described in the [implementation document](../../implementation/entrypoints/cgp_fn.md). Use the immutable `&[T]` and `Option<&T>` forms, and clone into a mutable local inside the body when mutation is needed. + ## Source -- Parsing: implicit-argument parsing lives in [crates/macros/cgp-macro-core/src/functions/implicits/parse.rs](../../../crates/macros/cgp-macro-core/src/functions/implicits/parse.rs), which extracts `#[implicit]`-marked arguments and validates the `self`/`mut` rules. +- Parsing: implicit-argument parsing lives in [crates/macros/cgp-macro-core/src/functions/implicits/parse.rs](../../../crates/macros/cgp-macro-core/src/functions/implicits/parse.rs), which extracts `#[implicit]`-marked arguments, validates the `self`/`mut` rules, and rejects a malformed (non-bare) `#[implicit]` attribute. - Per-argument model: [crates/macros/cgp-macro-core/src/types/implicits/](../../../crates/macros/cgp-macro-core/src/types/implicits/) — `arg_field.rs` builds the `HasField` bound and the `let` binding, and `arg_fields.rs` adds the bounds to the impl generics and prepends the bindings to the body. -- Field-type-to-access-mode mapping (`.clone()`, `.as_str()`, and the reference/option/slice cases): [crates/macros/cgp-macro-core/src/functions/field/parse.rs](../../../crates/macros/cgp-macro-core/src/functions/field/parse.rs) and [crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs](../../../crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs). +- Field-type-to-access-mode mapping (`.clone()`, `.as_str()`, `.as_deref()`, and the reference/option/slice cases): [crates/macros/cgp-macro-core/src/functions/field/parse.rs](../../../crates/macros/cgp-macro-core/src/functions/field/parse.rs) and [crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs](../../../crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs). - Implementation document (how `#[implicit]` arguments are parsed and lowered into `HasField` bounds and `let` bindings, and the index of tests): [implementation/entrypoints/cgp_fn.md](../../implementation/entrypoints/cgp_fn.md). diff --git a/docs/reference/macros/cgp_auto_getter.md b/docs/reference/macros/cgp_auto_getter.md index 4c0c9df9..29bb112b 100644 --- a/docs/reference/macros/cgp_auto_getter.md +++ b/docs/reference/macros/cgp_auto_getter.md @@ -33,7 +33,7 @@ pub trait HasDimensions { } ``` -The return type controls how the field value is read, and several shorthand forms are recognized so the method signature stays ergonomic. A plain reference `&T` reads a field of type `T` directly. The form `&str` is treated specially: it reads a `String` field and calls `.as_str()` on it, so you can return `&str` while the context stores a `String`. Other recognized forms include `Option<&T>` (an `Option` field returned via `.as_ref()`), `&[T]` (a field whose value implements `AsRef<[T]>`), and an owned `T` (a `Copy` field returned by value). A `&mut self` receiver with a `&mut T` return reads the field mutably through `get_field_mut`. +The return type controls how the field value is read, and several shorthand forms are recognized so the method signature stays ergonomic. A plain reference `&T` reads a field of type `T` directly. The form `&str` is treated specially: it reads a `String` field and calls `.as_str()` on it, so you can return `&str` while the context stores a `String`. Other recognized forms include `Option<&T>` (an `Option` field returned via `.as_ref()`), `Option<&str>` (an `Option` field returned via `.as_deref()`, composing the `&str` special case with the option case), `&[T]` (a field whose value implements `AsRef<[T]>`), and any owned type — a path type, a tuple, or an array — read by reference and `.clone()`d. A `&mut self` receiver with a `&mut T` return reads the field mutably through `get_field_mut`. A getter trait may also declare a single associated type and use it as the method's return type, which lets the abstract type be inferred from the field. This is covered under Expansion below. When an associated type is present, the trait must contain exactly one getter method, and that method's return type must be `&Self::AssocType`. diff --git a/docs/reference/macros/cgp_getter.md b/docs/reference/macros/cgp_getter.md index f7fceea5..f707f044 100644 --- a/docs/reference/macros/cgp_getter.md +++ b/docs/reference/macros/cgp_getter.md @@ -12,7 +12,7 @@ The decoupling is delivered through the `UseField` pattern. `#[cgp_getter]` auto ## Syntax -The macro is applied to a getter trait the same way `#[cgp_auto_getter]` is, and accepts the same getter-method forms — `&self`/`&mut self` receivers and the `&str`, `Option<&T>`, `&[T]`, owned, and associated-type return shorthands. The simplest form takes no argument: +The macro is applied to a getter trait the same way `#[cgp_auto_getter]` is, and accepts the same getter-method forms — `&self`/`&mut self` receivers and the `&str`, `Option<&T>`, `Option<&str>`, `&[T]`, owned, and associated-type return shorthands. The simplest form takes no argument: ```rust #[cgp_getter] diff --git a/docs/skills/cgp/references/functions-and-getters.md b/docs/skills/cgp/references/functions-and-getters.md index cb073be2..1ef5f034 100644 --- a/docs/skills/cgp/references/functions-and-getters.md +++ b/docs/skills/cgp/references/functions-and-getters.md @@ -196,7 +196,7 @@ where } ``` -That generated blanket impl is the point of the macro, and it follows the same access rules as `#[implicit]`: a plain `&T` return reads a `T` field directly, while the `&str` shorthand reads a `String` field and appends `.as_str()`. Other shorthands include `Option<&T>` (an `Option` field via `.as_ref()`), `&[T]` (a field implementing `AsRef<[T]>`), an owned `T` (a `Copy` field by value), and `&mut T` with a `&mut self` receiver (mutable access via `get_field_mut`). A trait may declare several methods, each mapping independently to its own field — `fn width(&self) -> &f64; fn height(&self) -> &f64;` produces one `where` predicate and one body per field in the same impl. +That generated blanket impl is the point of the macro, and it follows the same access rules as `#[implicit]`: a plain `&T` return reads a `T` field directly, while the `&str` shorthand reads a `String` field and appends `.as_str()`. Other shorthands include `Option<&T>` (an `Option` field via `.as_ref()`), `Option<&str>` (an `Option` field via `.as_deref()`), `&[T]` (a field implementing `AsRef<[T]>`), an owned type — path, tuple, or array — via `.clone()`, and `&mut T` with a `&mut self` receiver (mutable access via `get_field_mut`). A trait may declare several methods, each mapping independently to its own field — `fn width(&self) -> &f64; fn height(&self) -> &f64;` produces one `where` predicate and one body per field in the same impl. A single getter may also declare a local associated type and use it as its return type, which lets the abstract type be inferred from the field. The trait must then contain exactly one method returning `&Self::AssocType`; the macro lifts the type into a generic parameter on the impl and binds it through the `HasField` `Value`: From 0d083f79235a0137338c05bf1a687f93c5500b32 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 21:07:30 +0200 Subject: [PATCH 03/17] Scaffold compile-fail tests --- AGENTS.md | 15 +- Cargo.lock | 176 ++++++++++++++++++ crates/tests/AGENTS.md | 120 ++++++++---- crates/tests/README.md | 39 +++- .../tests/cgp-compile-fail-tests/Cargo.toml | 3 + .../tests/cgp-compile-fail-tests/src/lib.rs | 80 +++++--- .../acceptable/duplicate_delegate_key.rs | 42 +++++ .../acceptable/duplicate_delegate_key.stderr | 31 +++ .../missing_impl_side_dependency.rs | 51 +++++ .../missing_impl_side_dependency.stderr | 39 ++++ .../overlapping_generic_delegate.rs | 39 ++++ .../overlapping_generic_delegate.stderr | 31 +++ .../tests/compile_fail_tests.rs | 24 +++ .../problematic/cgp_fn_mut_slice_implicit.rs | 41 ++++ .../cgp_fn_mut_slice_implicit.stderr | 23 +++ .../problematic/cgp_fn_option_mut_implicit.rs | 22 +++ .../cgp_fn_option_mut_implicit.stderr | 11 ++ .../delegate_unconstrained_generic.rs | 45 +++++ .../delegate_unconstrained_generic.stderr | 5 + .../tests/parser_rejections/cgp_fn.rs | 6 +- docs/implementation/AGENTS.md | 11 +- docs/implementation/entrypoints/cgp_fn.md | 7 +- .../entrypoints/delegate_components.md | 11 +- 23 files changed, 784 insertions(+), 88 deletions(-) create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.stderr create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.stderr create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.stderr create mode 100644 crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.stderr create mode 100644 crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.stderr create mode 100644 crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.stderr diff --git a/AGENTS.md b/AGENTS.md index df8e8b4f..acf7800f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,9 +27,14 @@ do). `cargo +nightly fmt --all` (check: `cargo +nightly fmt --all -- --check`) - **Lint:** `cargo clippy --all-features --all-targets -- -D warnings` and `cargo clippy --no-default-features --all-targets -- -D warnings` -- **Test** (uses `cargo-nextest`): `cargo nextest run --all-features --no-fail-fast --workspace` +- **Test** (uses `cargo-nextest`): `cargo nextest run --all-features --no-fail-fast --workspace`. + This runs the whole suite, including the `trybuild` compile-fail fixtures in + `cgp-compile-fail-tests` (an ordinary integration test, so nextest picks it up). - **Single test crate / test:** `cargo nextest run -p cgp-tests` or target one file with the standard test harness, e.g. `cargo test -p cgp-tests --test component` +- **Compile-fail fixtures:** `cargo nextest run -p cgp-compile-fail-tests` runs the `trybuild` + cases that check macro *expansions* fail to compile with a pinned `.stderr`; regenerate the + snapshots with `TRYBUILD=overwrite cargo test -p cgp-compile-fail-tests`. - Many "tests" are **compile-time wiring checks** (`check_components!` / `delegate_and_check_components!`) and **macro-expansion snapshots** — for these, a successful `cargo build`/`cargo test` compilation *is* the passing test. A wiring mistake surfaces as a @@ -170,7 +175,13 @@ right input: - **Enumerate every way the output can expand.** Walk the shapes the expansion can take across the whole input space and confirm none can produce invalid Rust — no duplicate or conflicting `impl` blocks from a cartesian expansion, no unbound or doubly-declared generic parameter, no empty - expansion that silently checks nothing, and no clash on a generated identifier. + expansion that silently checks nothing, and no clash on a generated identifier. When you find a + case whose expansion fails to compile, capture it as a `trybuild` fixture in + `cgp-compile-fail-tests` — under `problematic/` when the macro should have rejected it or emitted + wrong code, or `acceptable/` when the failure is one CGP deliberately defers to the compiler — and + document it in the macro's implementation document, per + [crates/tests/AGENTS.md](crates/tests/AGENTS.md) and + [docs/implementation/AGENTS.md](docs/implementation/AGENTS.md). - **Scrutinize generics with care.** Generic parameters take many forms — lifetimes, types, consts, and the distinction between *impl* generics (`impl`) and *type* generics (the `` in `Foo`) — and mixing them produces subtly wrong output. Confirm the macro keeps the kinds diff --git a/Cargo.lock b/Cargo.lock index 2f7beed7..1cdd70bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,7 @@ name = "cgp-compile-fail-tests" version = "0.7.0" dependencies = [ "cgp", + "trybuild", ] [[package]] @@ -337,6 +338,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -463,12 +470,34 @@ dependencies = [ "r-efi", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "indenter" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "insta" version = "1.48.0" @@ -490,6 +519,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "libc" version = "0.2.186" @@ -573,6 +608,57 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "similar" version = "2.7.0" @@ -596,6 +682,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + [[package]] name = "tempfile" version = "3.27.0" @@ -609,12 +701,84 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "trybuild" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0710d4dfbeae4f9c390baa784c49858a7468fa433f3fe5d0ec5ebef651cf59f9" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -629,3 +793,15 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/crates/tests/AGENTS.md b/crates/tests/AGENTS.md index df56c405..d49073b6 100644 --- a/crates/tests/AGENTS.md +++ b/crates/tests/AGENTS.md @@ -15,9 +15,12 @@ The test suite has four jobs, split across crates: functions in `cgp-macro-core` directly (parsers, AST types), and is the home for **inputs a macro rejects** (via `assert_macro_rejects`) and for **pinning the exact invalid tokens** a macro emits (`invalid_expansion` string snapshots). -- **`cgp-compile-fail-tests`** holds the **`compile_fail` doctests**: input a macro - *accepts* but whose *expansion* then fails to compile. It is a library crate - because doctests run only for a library target; see "Adding a failure case" below. +- **`cgp-compile-fail-tests`** holds the **`trybuild` compile-fail tests**: input a + macro *accepts* but whose *expansion* then fails to compile. Each case is a + standalone `.rs` fixture whose expected compiler output is pinned in a committed + `.stderr` file, and the fixtures are split into **acceptable** and **problematic** + failures (see "Adding a failure case" below). Because the driver is an ordinary + integration test, `cargo nextest` runs it like any other test. - **`cgp-test-crate-a` / `cgp-test-crate-b`** are auxiliary packages for **cross-crate** behavior: whether a downstream crate can extend a namespace or provide a provider for a component defined elsewhere, under Rust's coherence and @@ -125,28 +128,56 @@ which is enough to pin a rejection and gives a precise check of the macro's own diagnostic. This is the right tool for a structural error the macro is expected to catch, and such a case does **not** also need a compile-fail test. -**Input a macro accepts whose expansion fails to compile — a `compile_fail` doctest -in `cgp-compile-fail-tests`.** Reserve `compile_fail` tests for input a CGP macro -*accepts* but whose *expansion* then fails to type- or borrow-check — the failure -lands on the emitted Rust, not inside the macro. This is the tool for a documented -bug or known limitation, and for the cases a macro cannot reject because it lacks the -whole-program view the check needs: two separate `delegate_components!` blocks that -delegate the same key, or generic `delegate_components!` entries that expand to -overlapping impls, both of which the macro defers to the Rust compiler. Write each as -a ```` ```rust,compile_fail ```` doctest containing the real macro invocation; -rustdoc compiles it and the test passes only if it fails to compile. Pair it with a -companion ```` ```rust ```` block that compiles once the offending element is -removed, so the probe proves *which* element causes the failure, and comment on why -it must not compile. - -These doctests live in the dedicated **`cgp-compile-fail-tests`** crate, not in -`cgp-tests` — doctests are collected only from a *library* target, never from an -integration (`tests/`) target, so an integration-test file's doctests would silently -never run. Inside that crate, group cases by CGP concept exactly as the main suite -does: one subdirectory per concept under `src/` (`basic_delegation/`, `dispatching/`, -…), one module file per category of case within it, registered with `pub mod`. Run -them with `cargo test --doc -p cgp-compile-fail-tests`; `cargo nextest` does not run -doctests. +**Input a macro accepts whose expansion fails to compile — a `trybuild` fixture in +`cgp-compile-fail-tests`.** Reserve these tests for input a CGP macro *accepts* but +whose *expansion* then fails to type- or borrow-check — the failure lands on the +emitted Rust, not inside the macro. This is the tool for a documented bug or known +limitation, and for the cases a macro cannot reject because it lacks the whole-program +view the check needs. Each case is a standalone `.rs` fixture (a complete program with +`fn main`) under the crate's `tests/` tree; `trybuild` compiles each as its own crate +and compares the compiler's output against a committed sibling `.stderr` file, so the +test passes only when compilation fails *with the pinned diagnostic*. The `.stderr` +snapshot is what proves *which* element causes the failure and *why* — it names the +exact error code and span — so it replaces the companion ```` ```rust ```` block the +old doctest form paired with each probe. Because the toolchain is pinned +([rust-toolchain.toml](../../rust-toolchain.toml)), the pinned diagnostics are stable +across runs. + +**Split every fixture into acceptable and problematic.** A compile failure carries one +of two opposite meanings, and the directory it lives in records which: + +- **`tests/acceptable/`** — the failure is one CGP **intentionally delegates to the + Rust compiler**. CGP is working as designed: it cannot see the whole program, so it + lowers the input faithfully and lets `rustc` reject it. Two separate + `delegate_components!` blocks that delegate the same key, generic + `delegate_components!` entries that expand to overlapping impls, and a lazily-wired + provider whose impl-side dependency the context does not satisfy all belong here. The + pinned `.stderr` documents that the diagnostic a user sees is the compiler doing its + job, and it is the diagnostic they should expect. +- **`tests/problematic/`** — the failure is a **CGP defect**: input a macro should have + rejected with a spanned error, or that a macro expanded into invalid Rust. The pinned + `.stderr` captures the confusing downstream error a user currently hits. Every + problematic fixture must be cross-linked (in its header comment) to the `## Known + issues` section of the owning macro's implementation document, and its `.stderr` + should improve — ideally becoming a clean macro-time rejection — when the defect is + fixed. When it does, regenerate the snapshot and, if the failure moves into the macro + itself, migrate the case to an `assert_macro_rejects` test in `cgp-macro-tests`. + +Within each category directory, write one fixture file per case, named for the CGP +concept and failure mode it probes (`duplicate_delegate_key.rs`, +`cgp_fn_mut_slice_implicit.rs`), and open each with a comment stating what it exercises +and why it must not compile — exactly as the main suite requires. Register each fixture +in the driver [tests/compile_fail_tests.rs](cgp-compile-fail-tests/tests/compile_fail_tests.rs) +through its category glob; the two `t.compile_fail(...)` calls pick up new fixtures +automatically, so no per-file registration is needed. A single `trybuild::TestCases` +runs both globs — do not split them across two `#[test]` functions, which would race on +the shared build directory. + +Run the suite with `cargo test -p cgp-compile-fail-tests` or `cargo nextest run -p +cgp-compile-fail-tests`; both work because the driver is an ordinary integration test. +After adding a fixture, or when an intended change alters a diagnostic, regenerate the +golden output with `TRYBUILD=overwrite cargo test -p cgp-compile-fail-tests` and review +the diff before committing. **Pinning the exact invalid output** is a separate, rarer need: only when you must *inspect* the wrong tokens a macro emits (not merely assert they fail to compile), @@ -155,12 +186,18 @@ capture the expanded code as an `insta` inline string snapshot in the compiles even though the code would not), with a comment explaining **why** the output is wrong and **what the correct output should be**. -Every failure case must also be recorded in the construct's **implementation -document** under `docs/implementation/`, in its `## Known issues` section and -indexed from its `## Tests` section, describing the behavior without referring to -the test. When the failure has a user-visible consequence, note that in the -reference document's `## Known issues` section too and cross-link the two. Put a -link from the test's comment to the implementation document. +Every failure case must also be recorded in the owning construct's **implementation +document** under `docs/implementation/`, and where it is recorded depends on the +category. A **problematic** fixture (and every `invalid_expansion` snapshot) documents +a defect, so describe it in that document's `## Known issues` section and index it from +`## Tests`; when the defect has a user-visible consequence, note it in the reference +document's `## Known issues` section too and cross-link the two. An **acceptable** +fixture documents *intended* behavior — a failure CGP deliberately defers to the +compiler — so it belongs in `## Tests` and in whatever section explains that behavior +(often a Known issues note that already frames the deferral as intended, as +`delegate_components!` does for overlapping impls), never as a fresh bug. In both cases +describe the behavior in the document's own words without referring to the test, and +put a link from the fixture's header comment back to the implementation document. ## Keep the docs in sync @@ -180,19 +217,22 @@ document's Tests or Snapshots section in the same change. ## Running the suite ``` -cargo nextest run -p cgp-tests # the main suite -cargo nextest run -p cgp-macro-tests # macro internals + rejection/invalid-expansion cases -cargo nextest run --workspace # everything +cargo nextest run -p cgp-tests # the main suite +cargo nextest run -p cgp-macro-tests # macro internals + rejection/invalid-expansion cases +cargo nextest run -p cgp-compile-fail-tests # trybuild compile-fail fixtures +cargo nextest run --workspace # everything -cargo test --doc -p cgp-compile-fail-tests # compile_fail doctests — NOT run by nextest +TRYBUILD=overwrite cargo test -p cgp-compile-fail-tests # regenerate .stderr snapshots -cargo insta test -p cgp-tests --review # review snapshot diffs -cargo insta test -p cgp-tests --accept # accept intended snapshot changes +cargo insta test -p cgp-tests --review # review snapshot diffs +cargo insta test -p cgp-tests --accept # accept intended snapshot changes ``` -`cargo nextest` does not run doctests, so the `compile_fail` cases in -`cgp-compile-fail-tests` must be run with `cargo test --doc`; include it when -verifying a change that touches a macro's accepted input or its expansion. +The `cgp-compile-fail-tests` fixtures run under both `cargo test` and `cargo nextest`, +because the `trybuild` driver is an ordinary integration test rather than a doctest. +Include the crate when verifying a change that touches a macro's accepted input or its +expansion, and regenerate its `.stderr` snapshots with `TRYBUILD=overwrite` whenever an +intended change alters a pinned diagnostic — then review the diff before committing. A snapshot test that fails prints a diff of the generated code; accept it with `cargo insta` only after confirming the change is intended. diff --git a/crates/tests/README.md b/crates/tests/README.md index c4daac05..de78030f 100644 --- a/crates/tests/README.md +++ b/crates/tests/README.md @@ -10,7 +10,7 @@ This README is the map. ## The crates -The suite is split into three kinds of crate, each with a distinct job. +The suite is split into four kinds of crate, each with a distinct job. **`cgp-tests`** is the main suite: realistic example code that must compile and run. Because much of CGP is compile-time wiring, a test here often passes simply @@ -18,8 +18,17 @@ by compiling. It is also where the user-facing macros are exercised end-to-end a where the canonical macro-expansion snapshots live. **`cgp-macro-tests`** tests the macro internals directly against `cgp-macro-core` -(the parsers and AST types), and is the home for **failure cases** — inputs CGP -should reject, and cases where a macro currently emits invalid code. +(the parsers and AST types), and is the home for **rejection cases** — inputs CGP +refuses during expansion — and for pinning the invalid tokens a macro currently +emits. + +**`cgp-compile-fail-tests`** holds the **`trybuild` compile-fail tests**: input a +macro *accepts* but whose *expansion* then fails to compile. Each case is a +standalone `.rs` fixture with a committed `.stderr` snapshot of the expected +compiler output, split into two categories — **acceptable** failures that CGP +intentionally delegates to the Rust compiler (overlapping wiring, a lazily-wired +missing dependency) and **problematic** failures that are a CGP defect (input a +macro should have rejected, or an expansion that emits invalid Rust). **`cgp-test-crate-a`** and **`cgp-test-crate-b`** are auxiliary packages for **cross-crate** behavior. Crate A defines components, a provider, and a namespaced @@ -48,6 +57,11 @@ accumulates too many cases to stay coherent, it is split into finer targets. for parser corner cases, and the failure-case targets `parser_rejections` and `invalid_expansion`. +`cgp-compile-fail-tests` is laid out for `trybuild` instead: a single driver +`tests/compile_fail_tests.rs` glob-compiles every fixture under `tests/acceptable/` +and `tests/problematic/`, each a self-contained program paired with a committed +`.stderr` snapshot of the compiler output it must produce. + ## Snapshots Many tests assert the exact code a macro generates, using the `snapshot_*!` macros @@ -60,13 +74,18 @@ everywhere else (see [AGENTS.md](AGENTS.md) for the ownership rules). ## Running the tests ``` -cargo nextest run -p cgp-tests # the main suite -cargo nextest run -p cgp-macro-tests # macro internals + failure cases -cargo nextest run --workspace # everything, including the aux crates +cargo nextest run -p cgp-tests # the main suite +cargo nextest run -p cgp-macro-tests # macro internals + rejection cases +cargo nextest run -p cgp-compile-fail-tests # trybuild compile-fail fixtures +cargo nextest run --workspace # everything, including the aux crates + +cargo insta test -p cgp-tests --review # review snapshot diffs interactively +cargo insta test -p cgp-tests --accept # accept intended snapshot changes -cargo insta test -p cgp-tests --review # review snapshot diffs interactively -cargo insta test -p cgp-tests --accept # accept intended snapshot changes +TRYBUILD=overwrite cargo test -p cgp-compile-fail-tests # regenerate .stderr snapshots ``` -When a snapshot test fails it prints a diff of the generated code; accept the new -output with `cargo insta` only after confirming the change is intended. +When a `snapshot_*!` test fails it prints a diff of the generated code; accept the +new output with `cargo insta` only after confirming the change is intended. When a +`trybuild` fixture fails it prints a diff against the committed `.stderr`; regenerate +it with `TRYBUILD=overwrite` only after confirming the diagnostic change is intended. diff --git a/crates/tests/cgp-compile-fail-tests/Cargo.toml b/crates/tests/cgp-compile-fail-tests/Cargo.toml index 658db067..160c7860 100644 --- a/crates/tests/cgp-compile-fail-tests/Cargo.toml +++ b/crates/tests/cgp-compile-fail-tests/Cargo.toml @@ -10,3 +10,6 @@ keywords = { workspace = true } [dependencies] cgp = { workspace = true } + +[dev-dependencies] +trybuild = { version = "1.0.117" } diff --git a/crates/tests/cgp-compile-fail-tests/src/lib.rs b/crates/tests/cgp-compile-fail-tests/src/lib.rs index ea77fd0a..c073a80b 100644 --- a/crates/tests/cgp-compile-fail-tests/src/lib.rs +++ b/crates/tests/cgp-compile-fail-tests/src/lib.rs @@ -1,39 +1,57 @@ -//! Compile-fail tests for the CGP macros. +//! Compile-fail tests for the CGP macros, driven by [`trybuild`](https://docs.rs/trybuild). //! -//! Every test here is a `compile_fail` doctest: a ```` ```rust,compile_fail ```` -//! block containing a real CGP macro invocation that must **not** compile. -//! rustdoc compiles each block and the test passes only if compilation fails, so -//! this crate exists as a library — doctests are collected only from a library -//! target, never from an integration (`tests/`) target. Run the suite with -//! `cargo test --doc -p cgp-compile-fail-tests` (note that `cargo nextest` does -//! not run doctests). +//! Each test is a standalone `.rs` fixture under `tests/` that a CGP macro +//! **accepts** but whose **expansion** then fails to type- or borrow-check — the +//! failure lands on the emitted Rust, not inside the macro. `trybuild` compiles +//! each fixture as its own throwaway crate and compares the compiler's output +//! against a committed `.stderr` file, so a test passes only when compilation +//! fails *with the pinned diagnostic*. Because the driver is an ordinary +//! integration test (`tests/compile_fail_tests.rs`), `cargo test` and +//! `cargo nextest run` both execute it — unlike the `compile_fail` doctests this +//! crate previously held, which `cargo nextest` silently skipped. //! //! # What belongs here //! -//! A `compile_fail` doctest is reserved for input that a CGP macro **accepts** but -//! whose **expansion** then fails to compile — the failure lands on the emitted -//! Rust, not inside the macro. This is the right tool for a documented bug or -//! known limitation, and for the cases a macro cannot reject because it lacks the -//! whole-program view the borrow/coherence check needs: two separate -//! `delegate_components!` blocks that delegate the same key, or generic -//! `delegate_components!` entries that expand to overlapping impls, both of which -//! the macro defers to the Rust compiler. Pair each probe with a companion -//! ```` ```rust ```` block that compiles once the offending element is removed, so -//! the test proves *which* element causes the failure, and comment on why it must -//! not compile. -//! -//! Input that a macro **rejects** during expansion (it returns `Err`) does not -//! belong here — test it by driving the entrypoint function directly in -//! `cgp-macro-tests` with the `assert_macro_rejects` helper, which is enough to -//! pin a rejection and gives a precise check of the macro's own diagnostic. +//! A `trybuild` fixture is reserved for input that a CGP macro **accepts** but +//! whose **expansion** fails to compile — the case a macro cannot reject because +//! it lacks the whole-program view the borrow/coherence check needs, or a +//! documented bug where the macro emits code it should not. Input that a macro +//! **rejects** during expansion (it returns `Err`) does not belong here; test it +//! by driving the entrypoint directly in `cgp-macro-tests` with the +//! `assert_macro_rejects` helper, which pins the macro's own diagnostic. +//! +//! # Two categories of failure +//! +//! The fixtures are split by *whose fault the failure is*, because the two +//! categories carry opposite messages about CGP's health: +//! +//! - **`tests/acceptable/`** — failures CGP **intentionally delegates to the Rust +//! compiler**. CGP is working as designed: it cannot see the whole program, so +//! it lowers the input faithfully and lets `rustc` reject it (overlapping +//! `delegate_components!` entries becoming conflicting impls, a lazily-wired +//! provider whose impl-side dependency the context does not meet). The pinned +//! `.stderr` documents that the failure is the compiler doing its job, and its +//! diagnostic is the one a user should expect. +//! - **`tests/problematic/`** — failures that are a **CGP defect**: input a macro +//! should have rejected with a spanned error, or that a macro expanded into +//! invalid Rust. The pinned `.stderr` captures the confusing downstream error a +//! user currently hits; each fixture is cross-linked to the `## Known issues` +//! section of the owning macro's implementation document, and its `.stderr` +//! should improve (ideally become a clean macro-time rejection) when the defect +//! is fixed. //! //! # Organization //! -//! Tests are grouped by CGP concept, mirroring the layout of the main `cgp-tests` -//! suite: one subdirectory per concept under `src/` (`basic_delegation/`, -//! `dispatching/`, …), and within each, one module file per category of -//! compile-fail case. Register each concept as a `pub mod` below and each category -//! as a `pub mod` in the concept's `mod.rs`. +//! Within each category directory, one fixture file per case, named for the CGP +//! concept and failure mode it probes (`duplicate_delegate_key.rs`, +//! `cgp_fn_mut_slice_implicit.rs`). Register every fixture in the driver +//! `tests/compile_fail_tests.rs` via its category glob — the two calls to +//! `t.compile_fail(...)` pick up new fixtures automatically. +//! +//! # Regenerating the `.stderr` snapshots //! -//! No cases are enumerated yet; a future agent adds a concept subdirectory and its -//! category modules alongside the first case it captures. +//! A fixture's committed `.stderr` is the golden output. After adding a fixture +//! or when an intended change alters a diagnostic, regenerate the snapshots with +//! `TRYBUILD=overwrite cargo test -p cgp-compile-fail-tests`, then review the diff +//! before committing — an unexpected change to an `acceptable/` diagnostic, or a +//! `problematic/` fixture that stops failing, is a signal worth reading closely. diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.rs new file mode 100644 index 00000000..32f5e6fa --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.rs @@ -0,0 +1,42 @@ +//! Acceptable failure: two `delegate_components!` blocks that map the same +//! component key produce two conflicting `DelegateComponent` +//! impls for `Person`, which the Rust compiler rejects with the coherence error +//! E0119. `delegate_components!` cannot catch this — it lowers each block +//! independently and has no view of the other block — so it correctly defers to +//! the compiler, exactly as two hand-written overlapping impls would. +//! +//! See docs/implementation/entrypoints/delegate_components.md (Known issues). + +use cgp::prelude::*; + +#[cgp_component(Greeter)] +pub trait CanGreet { + fn greet(&self); +} + +#[cgp_impl(new GreetHello)] +impl Greeter { + fn greet(&self) {} +} + +#[cgp_impl(new GreetGoodbye)] +impl Greeter { + fn greet(&self) {} +} + +pub struct Person; + +delegate_components! { + Person { + GreeterComponent: GreetHello, + } +} + +// Re-delegating the same key in a second block emits a conflicting impl. +delegate_components! { + Person { + GreeterComponent: GreetGoodbye, + } +} + +fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.stderr new file mode 100644 index 00000000..bbdd0d0c --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.stderr @@ -0,0 +1,31 @@ +error[E0119]: conflicting implementations of trait `IsProviderFor` for type `Person` + --> tests/acceptable/duplicate_delegate_key.rs:36:1 + | +29 | / delegate_components! { +30 | | Person { +31 | | GreeterComponent: GreetHello, +32 | | } +33 | | } + | |_- first implementation here +... +36 | / delegate_components! { +37 | | Person { +38 | | GreeterComponent: GreetGoodbye, +39 | | } +40 | | } + | |_^ conflicting implementation for `Person` + | + = note: this error originates in the macro `delegate_components` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0119]: conflicting implementations of trait `DelegateComponent` for type `Person` + --> tests/acceptable/duplicate_delegate_key.rs:36:1 + | +29 | / delegate_components! { +30 | | Person { + | |__________- first implementation here +... +36 | / delegate_components! { +37 | | Person { + | |__________^ conflicting implementation for `Person` + | + = note: this error originates in the macro `delegate_components` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.rs new file mode 100644 index 00000000..1c34acb2 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.rs @@ -0,0 +1,51 @@ +//! Acceptable failure: `GreetHello` carries the impl-side dependency +//! `Self: HasName`, but `Person` has no `name` field, so it does not implement +//! `HasName`. CGP wiring is lazy by design — `delegate_components!` accepts the +//! entry without checking the provider's transitive dependencies — so the unmet +//! dependency surfaces only when the consumer trait is finally called, reported +//! by the compiler through `IsProviderFor`. Using `check_components!` (or +//! `delegate_and_check_components!`) would move this same error to the wiring +//! site; deferring it to the use site is the intended lazy behavior, not a +//! macro defect. +//! +//! See docs/implementation/entrypoints/delegate_components.md and +//! docs/implementation/entrypoints/check_components.md. + +use cgp::prelude::*; + +#[cgp_component(Greeter)] +pub trait CanGreet { + fn greet(&self); +} + +#[cgp_auto_getter] +pub trait HasName { + fn name(&self) -> &str; +} + +#[cgp_impl(new GreetHello)] +impl Greeter +where + Self: HasName, +{ + fn greet(&self) { + let _ = self.name(); + } +} + +#[derive(HasField)] +pub struct Person { + pub age: u8, +} + +// Accepted even though `Person` cannot satisfy `GreetHello`'s `Self: HasName`. +delegate_components! { + Person { + GreeterComponent: GreetHello, + } +} + +fn main() { + // The unmet dependency is reported here, at the call site. + Person { age: 0 }.greet(); +} diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.stderr new file mode 100644 index 00000000..be0d90a8 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.stderr @@ -0,0 +1,39 @@ +error[E0599]: the method `greet` exists for struct `Person`, but its trait bounds were not satisfied + --> tests/acceptable/missing_impl_side_dependency.rs:50:23 + | +37 | pub struct Person { + | ----------------- method `greet` not found for this struct because it doesn't satisfy `Person: CanGreet` or `Person: Greeter` +... +50 | Person { age: 0 }.greet(); + | ------------------^^^^^-- + | | | + | | this is an associated function, not a method + | help: use associated function syntax instead: `Person::greet()` + | + = note: found the following associated functions; to be used as methods, functions must have a `self` parameter +note: the candidate is defined in the trait `Greeter` + --> tests/acceptable/missing_impl_side_dependency.rs:18:5 + | +18 | fn greet(&self); + | ^^^^^^^^^^^^^^^^ +note: trait bound `Person: Greeter` was not satisfied + --> tests/acceptable/missing_impl_side_dependency.rs:16:1 + | +16 | #[cgp_component(Greeter)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | pub trait CanGreet { + | ^^^^^^^^ +note: the trait `Greeter` must be implemented + --> tests/acceptable/missing_impl_side_dependency.rs:17:1 + | +17 | / pub trait CanGreet { +18 | | fn greet(&self); +19 | | } + | |_^ + = help: items from traits can only be used if the trait is implemented and in scope +note: `CanGreet` defines an item `greet`, perhaps you need to implement it + --> tests/acceptable/missing_impl_side_dependency.rs:17:1 + | +17 | pub trait CanGreet { + | ^^^^^^^^^^^^^^^^^^ + = note: this error originates in the attribute macro `cgp_component` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.rs new file mode 100644 index 00000000..e18db9c8 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.rs @@ -0,0 +1,39 @@ +//! Acceptable failure: a generic `delegate_components!` entry that wires every +//! `Wrapper` overlaps a second entry that wires the specific `Wrapper`. +//! Stable Rust has no specialization, so the two `DelegateComponent` impls +//! overlap at `Wrapper` and the compiler rejects them with E0119. +//! `delegate_components!` expands each entry to the impl the user asked for and +//! defers the overlap check to the compiler, the same as two overlapping +//! hand-written generic impls. +//! +//! See docs/implementation/entrypoints/delegate_components.md (Known issues). + +use cgp::prelude::*; + +#[cgp_component(Greeter)] +pub trait CanGreet { + fn greet(&self); +} + +#[cgp_impl(new GreetHello)] +impl Greeter { + fn greet(&self) {} +} + +pub struct Wrapper(pub T); + +// Wires the whole `Wrapper` family. +delegate_components! { + Wrapper { + GreeterComponent: GreetHello, + } +} + +// Overlaps the generic entry at `Wrapper`. +delegate_components! { + Wrapper { + GreeterComponent: GreetHello, + } +} + +fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.stderr new file mode 100644 index 00000000..4d8e32dd --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.stderr @@ -0,0 +1,31 @@ +error[E0119]: conflicting implementations of trait `IsProviderFor` for type `Wrapper` + --> tests/acceptable/overlapping_generic_delegate.rs:33:1 + | +26 | / delegate_components! { +27 | | Wrapper { +28 | | GreeterComponent: GreetHello, +29 | | } +30 | | } + | |_- first implementation here +... +33 | / delegate_components! { +34 | | Wrapper { +35 | | GreeterComponent: GreetHello, +36 | | } +37 | | } + | |_^ conflicting implementation for `Wrapper` + | + = note: this error originates in the macro `delegate_components` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0119]: conflicting implementations of trait `DelegateComponent` for type `Wrapper` + --> tests/acceptable/overlapping_generic_delegate.rs:33:1 + | +26 | / delegate_components! { +27 | | Wrapper { + | |__________________- first implementation here +... +33 | / delegate_components! { +34 | | Wrapper { + | |________________^ conflicting implementation for `Wrapper` + | + = note: this error originates in the macro `delegate_components` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs b/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs new file mode 100644 index 00000000..ecd68427 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs @@ -0,0 +1,24 @@ +//! `trybuild` driver for the CGP compile-fail suite. +//! +//! Each fixture under `tests/acceptable/` and `tests/problematic/` is compiled +//! as its own crate and its diagnostics compared against the sibling `.stderr` +//! file. See the crate root docs (`src/lib.rs`) for what belongs in each +//! category and how to regenerate the `.stderr` snapshots. +//! +//! A single `TestCases` runs both globs so `trybuild` invokes `cargo` once; +//! splitting the categories across two `#[test]` functions would race on the +//! shared build directory. + +#[test] +fn compile_fail() { + let t = trybuild::TestCases::new(); + + // Failures CGP intentionally delegates to the Rust compiler — CGP is + // working as designed and `rustc` is the right place for the check. + t.compile_fail("tests/acceptable/*.rs"); + + // Failures that are a CGP defect: input a macro should have rejected, or an + // expansion that emits invalid Rust. Each is cross-linked to a `## Known + // issues` entry in the owning macro's implementation document. + t.compile_fail("tests/problematic/*.rs"); +} diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.rs b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.rs new file mode 100644 index 00000000..32f2b624 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.rs @@ -0,0 +1,41 @@ +//! Problematic failure: `#[cgp_fn]` does not support a mutable-slice implicit +//! argument. With a `&mut self` receiver, a `&mut [u8]` implicit skips the slice +//! case (which only matches a shared `&[T]`, since there is no `AsMut<[T]>` +//! counterpart) and falls through to the plain-reference mode over the field +//! type `[u8]`. The macro then emits a bound on `HasFieldMut` +//! that no context can satisfy, because `[u8]` is unsized and cannot be a field +//! type. The macro *accepts* the input rather than rejecting it, so the failure +//! lands on the emitted code at the call site. +//! +//! (Note the contrast with a `&self` receiver, which the macro *does* reject +//! cleanly with "&mut self is required for mutable field reference" — that +//! rejection is covered by an `assert_macro_rejects` case in `cgp-macro-tests`, +//! not here.) +//! +//! The correct behavior would be either to support the mutable-slice mode or to +//! reject a `&mut [T]` implicit at macro time with a clear message. This fixture +//! pins the current, worse behavior; its `.stderr` should improve when the +//! defect is fixed. +//! +//! See docs/implementation/entrypoints/cgp_fn.md (Known issues). + +use cgp::prelude::*; + +#[cgp_fn] +fn zero_all(&mut self, #[implicit] items: &mut [u8]) { + for x in items.iter_mut() { + *x = 0; + } +} + +#[derive(HasField)] +pub struct Buffer { + pub items: Vec, +} + +fn main() { + let mut buffer = Buffer { + items: vec![1, 2, 3], + }; + buffer.zero_all(); +} diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.stderr b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.stderr new file mode 100644 index 00000000..53644d10 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.stderr @@ -0,0 +1,23 @@ +error[E0599]: the method `zero_all` exists for struct `Buffer`, but its trait bounds were not satisfied + --> tests/problematic/cgp_fn_mut_slice_implicit.rs:40:12 + | +32 | pub struct Buffer { + | ----------------- method `zero_all` not found for this struct because it doesn't satisfy `<_ as HasField>>>>>>>::Value = [u8]` or `Buffer: ZeroAll` +... +40 | buffer.zero_all(); + | ^^^^^^^^ method cannot be called on `Buffer` due to unsatisfied trait bounds + | +note: trait bound `>>>>>>>::Value = [u8]` was not satisfied + --> tests/problematic/cgp_fn_mut_slice_implicit.rs:24:1 + | +24 | #[cgp_fn] + | ^^^^^^^^^ +25 | fn zero_all(&mut self, #[implicit] items: &mut [u8]) { + | ^^^^^^^^ + = help: items from traits can only be used if the trait is implemented and in scope +note: `ZeroAll` defines an item `zero_all`, perhaps you need to implement it + --> tests/problematic/cgp_fn_mut_slice_implicit.rs:24:1 + | +24 | #[cgp_fn] + | ^^^^^^^^^ + = note: this error originates in the attribute macro `cgp_fn` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.rs b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.rs new file mode 100644 index 00000000..c9b23df8 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.rs @@ -0,0 +1,22 @@ +//! Problematic failure: `#[cgp_fn]` mishandles an `Option<&mut T>` implicit. The +//! implicit's outer type is a path (`Option<…>`), so the macro reads it through +//! the immutable option mode and binds an `Option<&T>` value, which then fails to +//! coerce to the `Option<&mut u8>` the signature declares. The failure lands +//! inside the generated impl body, because the macro accepts the shape rather +//! than rejecting it — only the immutable `Option<&T>` form is actually +//! supported. +//! +//! The correct behavior would be to support the mutable-option mode or to reject +//! an `Option<&mut T>` implicit at macro time. This fixture pins the current +//! behavior; its `.stderr` should improve when the defect is fixed. +//! +//! See docs/implementation/entrypoints/cgp_fn.md (Known issues). + +use cgp::prelude::*; + +#[cgp_fn] +fn take_slot(&self, #[implicit] slot: Option<&mut u8>) -> Option<&mut u8> { + slot +} + +fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.stderr b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.stderr new file mode 100644 index 00000000..7c1cf15e --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.stderr @@ -0,0 +1,11 @@ +error[E0308]: mismatched types + --> tests/problematic/cgp_fn_option_mut_implicit.rs:17:1 + | +17 | #[cgp_fn] + | ^^^^^^^^^ types differ in mutability +18 | fn take_slot(&self, #[implicit] slot: Option<&mut u8>) -> Option<&mut u8> { + | --------------- expected due to this + | + = note: expected enum `Option<&mut u8>` + found enum `Option<&u8>` + = note: this error originates in the attribute macro `cgp_fn` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.rs b/crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.rs new file mode 100644 index 00000000..2876dea2 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.rs @@ -0,0 +1,45 @@ +//! Problematic failure: a per-entry generic list on a `delegate_components!` +//! mapping whose parameter appears only in the *provider value* and not in the +//! *key*. The macro faithfully lowers ` GreeterComponent: GreetWith` into +//! `impl DelegateComponent for Person { type Delegate = +//! GreetWith; }`, where `T` is constrained by neither the trait, the self +//! type, nor a predicate — so the compiler rejects it with E0207. +//! +//! A per-entry generic is only well-formed when it appears in the key (as in +//! ` BazKey: BarValue`, where `DelegateComponent>` +//! binds it). The macro does not check that every declared generic reaches the +//! key, so it accepts this nonsensical entry and emits an impl with a free +//! parameter instead of rejecting it with a spanned error at macro time. +//! +//! The correct behavior would be to reject a per-entry generic that does not +//! appear in the key. This fixture pins the current behavior; its `.stderr` +//! should improve when the defect is fixed. +//! +//! See docs/implementation/entrypoints/delegate_components.md (Known issues). + +use core::marker::PhantomData; + +use cgp::prelude::*; + +#[cgp_component(Greeter)] +pub trait CanGreet { + fn greet(&self); +} + +pub struct GreetWith(pub PhantomData); + +#[cgp_provider] +impl Greeter for GreetWith { + fn greet(_context: &Context) {} +} + +pub struct Person; + +// `T` is declared for the entry but only used in the value `GreetWith`. +delegate_components! { + Person { + GreeterComponent: GreetWith, + } +} + +fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.stderr b/crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.stderr new file mode 100644 index 00000000..8f976834 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.stderr @@ -0,0 +1,5 @@ +error[E0207]: the type parameter `T` is not constrained by the impl trait, self type, or predicates + --> tests/problematic/delegate_unconstrained_generic.rs:41:10 + | +41 | GreeterComponent: GreetWith, + | ^ unconstrained type parameter diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs index 4d4532a3..d80375df 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs @@ -3,8 +3,10 @@ //! implicit argument, a `&mut` implicit argument that is not the sole implicit //! (its exclusive borrow of the context would conflict with reading any other //! field), and a malformed `#[implicit]` attribute carrying arguments. Each is a -//! rejection the macro makes during expansion, so it is pinned by driving the -//! entrypoint directly here rather than by a `compile_fail` test. +//! rejection the macro makes during expansion (a returned `Err`), so it is pinned +//! by driving the entrypoint directly here rather than by a `trybuild` +//! compile-fail fixture, which is reserved for input the macro accepts but whose +//! expansion then fails to compile. //! //! See docs/implementation/entrypoints/cgp_fn.md (Tests) for these failure cases, //! and docs/reference/attributes/implicit.md for the user-facing rules on where diff --git a/docs/implementation/AGENTS.md b/docs/implementation/AGENTS.md index 2a6d51dc..a3d030d5 100644 --- a/docs/implementation/AGENTS.md +++ b/docs/implementation/AGENTS.md @@ -54,7 +54,7 @@ The **Tests** and **Source** sections are always bullet lists, never flowing par ## The Tests and Snapshots sections -Every document links its related tests in a **Tests** section, stating the behavior each test pins in the document's own words. List the behavioral tests in [crates/tests/cgp-tests](../../crates/tests/cgp-tests) and the failure cases in [crates/tests/cgp-macro-tests](../../crates/tests/cgp-macro-tests) that exercise the construct, each as a link to the file with a one-line description of what it verifies. Because the reference documents no longer point at tests, this section is the canonical index of a construct's coverage — write it so an agent can see at a glance what behavior is guarded and, by omission, what is not. +Every document links its related tests in a **Tests** section, stating the behavior each test pins in the document's own words. List the behavioral tests in [crates/tests/cgp-tests](../../crates/tests/cgp-tests), the rejection cases in [crates/tests/cgp-macro-tests](../../crates/tests/cgp-macro-tests), and the compile-fail fixtures in [crates/tests/cgp-compile-fail-tests](../../crates/tests/cgp-compile-fail-tests) that exercise the construct, each as a link to the file with a one-line description of what it verifies. Because the reference documents no longer point at tests, this section is the canonical index of a construct's coverage — write it so an agent can see at a glance what behavior is guarded and, by omission, what is not. Every entrypoint document additionally carries a **Snapshots** section that indexes the macro-expansion snapshots for that macro and calls out which variants are missing. Macro snapshots are pinned by the `snapshot_*!` macros in [crates/macros/cgp-macro-test-util](../../crates/macros/cgp-macro-test-util) and, per [crates/tests/AGENTS.md](../../crates/tests/AGENTS.md), each macro's canonical full-expansion snapshot plus its genuinely distinct variants live only in the concept target that owns the macro's feature. The Snapshots section is the central place to find them: link each snapshot file, describe which expansion variant it captures (the plain case, a supertrait-and-default-method case, a lifetime-and-type-parameter case, a namespace-prefix case, and so on), and then state plainly which variants have *no* snapshot yet, so a gap in coverage is visible rather than silently absent. When you add a snapshot, register it here; when a variant you know matters is untested, record it as a missing snapshot rather than leaving the section to imply full coverage. @@ -62,6 +62,15 @@ Every entrypoint document additionally carries a **Snapshots** section that inde Record limitations, surprising behavior, and confirmed bugs in the implementation under a **Known issues** section, following the same rule as the reference documents in [../AGENTS.md](../AGENTS.md): describe the behavior as it currently is even when it is wrong, say what the correct behavior would be for a bug, and remove the note in the same change that fixes the code. An implementation document is the right home for a limitation that is about *how the macro is built* — a parser that panics on an input it should reject with a `syn::Error`, a generic-parameter shape the codegen does not yet handle, a stage that drops information — while the reference document's Known issues covers the user-visible consequence. When both exist, cross-link them rather than duplicating the explanation. A failure case captured in `cgp-macro-tests` (per [crates/tests/AGENTS.md](../../crates/tests/AGENTS.md)) should be indexed from the relevant Known issue here and from its Tests section. +## Documenting how a macro's expansion can fail to compile + +Because a CGP macro expands to ordinary Rust that the compiler checks afterward, a full picture of a macro includes the ways its *accepted* input can still fail to compile downstream — a dimension distinct from the input a macro rejects at parse time. Enumerate these compile-failure modes as part of documenting a macro, and sort each into one of two kinds, mirroring the split in [crates/tests/cgp-compile-fail-tests](../../crates/tests/cgp-compile-fail-tests): + +- An **acceptable** failure is one the macro **intentionally delegates to the Rust compiler** because it lacks the whole-program view the check needs — two `delegate_components!` blocks that map the same key into overlapping impls, or a lazily-wired provider whose impl-side dependency the context does not satisfy. This is correct behavior, not a bug, so document it as intended: describe *what the compiler rejects and why the macro cannot catch it earlier*, in the Behavior and corner cases section or in a Known issues note that frames the deferral as deliberate (as [entrypoints/delegate_components.md](entrypoints/delegate_components.md) does for duplicate keys). Index the covering fixture from the Tests section. +- A **problematic** failure is a **defect**: input the macro should have rejected with a spanned error, or an expansion that emits invalid Rust. Record it under Known issues with the current (wrong) behavior and the correct behavior, note the user-visible consequence in the reference document's Known issues and cross-link, and index the pinning fixture from the Tests section. Remove the note when the defect is fixed, per the synchronization rule. + +Whenever you add, move, or reclassify a compile-fail fixture in `cgp-compile-fail-tests`, update the owning macro's implementation document in the same change so its Known issues and Tests sections stay the canonical index of how that macro can fail to compile. This is the "enumerate every way the output can expand" review concern (see the macro review workflow in [../../AGENTS.md](../../AGENTS.md)) written down as documentation, so a later agent inherits the failure map rather than rediscovering it. + ## Keeping the reference and implementation documents aligned The reference and implementation documents are two views of the same macro and must not drift apart or duplicate each other. The reference owns the user-facing contract — syntax, the expansion shown as before/after Rust, and user-visible corner cases — and points only at library source. The implementation document owns the internal mechanics — the pipeline, the synthesizing functions, the AST types, and all test and snapshot pointers. A reference document may link to an implementation document to elaborate a corner case it does not want to explain in full, and every reference document's Source section links to its implementation counterpart so an agent can drop from "what it does" into "how it is built"; the implementation document links back to the reference for the user-facing semantics rather than re-deriving them. When you change a macro, update both views in the same change: the reference Expansion for the contract, and the implementation Pipeline and Generated items for the mechanics. diff --git a/docs/implementation/entrypoints/cgp_fn.md b/docs/implementation/entrypoints/cgp_fn.md index df00982f..263cd623 100644 --- a/docs/implementation/entrypoints/cgp_fn.md +++ b/docs/implementation/entrypoints/cgp_fn.md @@ -68,7 +68,7 @@ The conversion applied to each binding is chosen by the argument's type, followi `#[cgp_fn]` does not support generics on the desugared *method* itself — generic parameters are only ever lifted onto the trait and impl. A method-level generic is silently treated as a trait/impl generic rather than rejected, which is the intended limitation rather than a bug: method-level generics are considered an advanced case better written as an explicit blanket impl or a [`#[cgp_component]`](../../reference/macros/cgp_component.md) provider. -The *mutable* variants of the slice and option modes are not supported. A `&mut [T]` implicit falls through the slice case (there is no `AsMut<[T]>` counterpart) into the plain-reference mode, producing an unsatisfiable `HasFieldMut` bound; and an `Option<&mut T>` implicit reads immutably (the argument's outer type is a path, so `field_mut` is `None`), producing an `Option<&T>` value that then fails to coerce to the declared `Option<&mut T>`. Both are exotic shapes rather than a regression — the immutable `&[T]` and `Option<&T>` forms are the supported ones — and the macro accepts them, so the failure lands on the emitted code at the use site rather than as a spanned rejection. +The *mutable* variants of the slice and option modes are not supported, and both are **problematic** failures — the macro accepts the shape rather than rejecting it, so the failure lands on the emitted code instead of as a spanned message. Under a `&mut self` receiver, a `&mut [T]` implicit falls through the slice case (which matches only a shared `&[T]`, since there is no `AsMut<[T]>` counterpart) into the plain-reference mode, producing an unsatisfiable `HasFieldMut` bound that surfaces at the call site (`E0599`). Note the contrast with a `&self` receiver, where the same `&mut [T]` is instead rejected cleanly at macro time with "&mut self is required for mutable field reference" — that rejection is a returned `Err` covered in `parser_rejections`, not a compile-fail fixture. An `Option<&mut T>` implicit reads immutably (the argument's outer type is a path, so `field_mut` is `None`), producing an `Option<&T>` value that then fails to coerce to the declared `Option<&mut T>` inside the generated body (`E0308`). Both are exotic shapes rather than a regression — the immutable `&[T]` and `Option<&T>` forms are the supported ones — and the correct behavior would be either to support the mutable modes or to reject them at macro time with a clear message. ## Snapshots @@ -105,6 +105,11 @@ The failure cases pin the inputs `#[cgp_fn]` refuses during expansion, each asse - [parser_rejections/cgp_fn.rs](../../../crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs) covers an implicit argument on a function with no `self` receiver, a `mut` binding pattern on an implicit argument, a `&mut` implicit argument that is not the sole implicit (which would borrow the context mutably and again at once), and a malformed `#[implicit]` attribute carrying arguments (`#[implicit(...)]` or `#[implicit = ...]`). +The compile-fail fixtures in `cgp-compile-fail-tests` pin the problematic mutable-mode expansions the macro accepts but should not: + +- [problematic/cgp_fn_mut_slice_implicit.rs](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.rs) — a `&mut [u8]` implicit under a `&mut self` receiver expands to an unsatisfiable `HasFieldMut` bound (`E0599`). +- [problematic/cgp_fn_option_mut_implicit.rs](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.rs) — an `Option<&mut T>` implicit binds an `Option<&T>` value that fails to coerce to the declared `Option<&mut T>` (`E0308`). + ## Source - Entry point: `cgp_fn` in [cgp-macro-lib/src/cgp_fn.rs](../../../crates/macros/cgp-macro-lib/src/cgp_fn.rs). diff --git a/docs/implementation/entrypoints/delegate_components.md b/docs/implementation/entrypoints/delegate_components.md index cf6a7528..ddce02ac 100644 --- a/docs/implementation/entrypoints/delegate_components.md +++ b/docs/implementation/entrypoints/delegate_components.md @@ -62,7 +62,9 @@ An **`@`-path key** carries a leading `__Wildcard__` generic and lowers the path The macro's parser is permissive about the body shape and surfaces most mistakes as generic `syn` parse errors rather than tailored diagnostics — for example, an `open` header written after a plain mapping fails to parse because statements must lead the block, but the error (`expected `:``) points at the unexpected token rather than explaining the ordering rule. -A duplicate key — the same component mapped twice, whether by two plain entries or by an `open` header colliding with an explicit mapping — is not caught by the macro; it emits two conflicting `DelegateComponent` impls and surfaces as a coherence error (`E0119`) at compile time, the same as two hand-written impls would. +A duplicate key — the same component mapped twice, whether by two plain entries, two separate `delegate_components!` blocks, or an `open` header colliding with an explicit mapping — is not caught by the macro; it emits two conflicting `DelegateComponent` impls and surfaces as a coherence error (`E0119`) at compile time, the same as two hand-written impls would. The same holds for a generic entry that overlaps a more specific one (a ` Wrapper` table and a `Wrapper` table wiring the same key): stable Rust has no specialization, so the two impls conflict at the overlapping type. Both are **acceptable** failures — the macro lowers each block independently and has no whole-program view, so it correctly defers the overlap check to the compiler. The lazy nature of wiring produces a related acceptable failure: a provider whose impl-side dependency the context does not satisfy is wired without complaint, and the unmet bound surfaces only when the consumer trait is used (or earlier, at a `check_components!` site). + +A per-entry generic list whose parameter appears only in the provider **value** and not in the **key** is a **problematic** failure: the macro lowers ` GreeterComponent: GreetWith` into `impl DelegateComponent for Person { type Delegate = GreetWith; }`, where `T` is unconstrained, so the compiler rejects it with `E0207`. A per-entry generic is only well-formed when it reaches the key (as in ` BazKey`, where `DelegateComponent>` binds it); the macro does not check that every declared generic appears in the key, so it accepts the nonsensical entry and emits a free-parameter impl instead of rejecting it with a spanned error. The correct behavior would be to reject a per-entry generic that does not appear in the key. ## Snapshots @@ -102,6 +104,13 @@ The failure cases in `cgp-macro-tests` pin the attribute rejection: - [parser_rejections/delegate_components.rs](../../../crates/tests/cgp-macro-tests/tests/parser_rejections/delegate_components.rs) asserts the macro rejects an attribute on the table, on a key, and on a key nested inside a `UseDelegate` value (the last confirms the validator recurses through mapping values rather than dropping the attribute), and that a braceless `open` header listing more than one component is rejected (the braceless form opens exactly one). +The compile-fail fixtures in `cgp-compile-fail-tests` pin the expansions that fail to compile, split by whether the failure is intended: + +- [acceptable/duplicate_delegate_key.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.rs) — two blocks mapping the same key expand to conflicting `DelegateComponent` impls (`E0119`), a failure the macro deliberately defers to the compiler. +- [acceptable/overlapping_generic_delegate.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.rs) — a generic ` Wrapper` entry overlaps a specific `Wrapper` entry at the same key (`E0119`), the generic form of the same deferred overlap. +- [acceptable/missing_impl_side_dependency.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.rs) — a lazily-wired provider whose `Self: HasName` dependency the context does not satisfy; the unmet bound surfaces at the call site, the intended consequence of lazy wiring. +- [problematic/delegate_unconstrained_generic.rs](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.rs) — a per-entry generic that appears only in the value (` GreeterComponent: GreetWith`) expands to an impl with an unconstrained `T` (`E0207`) instead of being rejected at macro time. + ## Source - Entry point: `delegate_components` in [cgp-macro-lib/src/delegate_components.rs](../../../crates/macros/cgp-macro-lib/src/delegate_components.rs). From 20491fba699a089f327fdf03f50eb999a97dec8e Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 21:22:13 +0200 Subject: [PATCH 04/17] Refine compile-fail docs --- crates/tests/AGENTS.md | 50 +++++++++-------- crates/tests/README.md | 8 +-- .../tests/cgp-compile-fail-tests/src/lib.rs | 21 +++++--- .../duplicate_key.rs} | 2 +- .../duplicate_key.stderr} | 4 +- .../missing_dependency.rs} | 2 +- .../missing_dependency.stderr} | 10 ++-- .../overlapping_generic.rs} | 2 +- .../overlapping_generic.stderr} | 4 +- .../unconstrained_generic.rs} | 16 +++--- .../unconstrained_generic.stderr} | 4 +- .../tests/compile_fail_tests.rs | 19 ++++--- .../mut_slice_implicit.rs} | 0 .../mut_slice_implicit.stderr} | 6 +-- .../option_mut_implicit.rs} | 0 .../option_mut_implicit.stderr} | 2 +- docs/implementation/AGENTS.md | 8 +-- docs/implementation/entrypoints/cgp_fn.md | 22 ++++++-- .../entrypoints/delegate_components.md | 54 +++++++++++++++---- 19 files changed, 152 insertions(+), 82 deletions(-) rename crates/tests/cgp-compile-fail-tests/tests/acceptable/{duplicate_delegate_key.rs => delegate_components/duplicate_key.rs} (98%) rename crates/tests/cgp-compile-fail-tests/tests/acceptable/{duplicate_delegate_key.stderr => delegate_components/duplicate_key.stderr} (88%) rename crates/tests/cgp-compile-fail-tests/tests/acceptable/{missing_impl_side_dependency.rs => delegate_components/missing_dependency.rs} (94%) rename crates/tests/cgp-compile-fail-tests/tests/acceptable/{missing_impl_side_dependency.stderr => delegate_components/missing_dependency.stderr} (80%) rename crates/tests/cgp-compile-fail-tests/tests/acceptable/{overlapping_generic_delegate.rs => delegate_components/overlapping_generic.rs} (98%) rename crates/tests/cgp-compile-fail-tests/tests/acceptable/{overlapping_generic_delegate.stderr => delegate_components/overlapping_generic.stderr} (88%) rename crates/tests/cgp-compile-fail-tests/tests/{problematic/delegate_unconstrained_generic.rs => acceptable/delegate_components/unconstrained_generic.rs} (67%) rename crates/tests/cgp-compile-fail-tests/tests/{problematic/delegate_unconstrained_generic.stderr => acceptable/delegate_components/unconstrained_generic.stderr} (54%) rename crates/tests/cgp-compile-fail-tests/tests/problematic/{cgp_fn_mut_slice_implicit.rs => cgp_fn/mut_slice_implicit.rs} (100%) rename crates/tests/cgp-compile-fail-tests/tests/problematic/{cgp_fn_mut_slice_implicit.stderr => cgp_fn/mut_slice_implicit.stderr} (86%) rename crates/tests/cgp-compile-fail-tests/tests/problematic/{cgp_fn_option_mut_implicit.rs => cgp_fn/option_mut_implicit.rs} (100%) rename crates/tests/cgp-compile-fail-tests/tests/problematic/{cgp_fn_option_mut_implicit.stderr => cgp_fn/option_mut_implicit.stderr} (88%) diff --git a/crates/tests/AGENTS.md b/crates/tests/AGENTS.md index d49073b6..40cf9a8c 100644 --- a/crates/tests/AGENTS.md +++ b/crates/tests/AGENTS.md @@ -163,15 +163,21 @@ of two opposite meanings, and the directory it lives in records which: fixed. When it does, regenerate the snapshot and, if the failure moves into the macro itself, migrate the case to an `assert_macro_rejects` test in `cgp-macro-tests`. -Within each category directory, write one fixture file per case, named for the CGP -concept and failure mode it probes (`duplicate_delegate_key.rs`, -`cgp_fn_mut_slice_implicit.rs`), and open each with a comment stating what it exercises -and why it must not compile — exactly as the main suite requires. Register each fixture -in the driver [tests/compile_fail_tests.rs](cgp-compile-fail-tests/tests/compile_fail_tests.rs) -through its category glob; the two `t.compile_fail(...)` calls pick up new fixtures -automatically, so no per-file registration is needed. A single `trybuild::TestCases` -runs both globs — do not split them across two `#[test]` functions, which would race on -the shared build directory. +Under each category directory, group fixtures into one subdirectory per **owning +macro** — the macro whose expansion produces the failure and whose implementation +document documents it (`acceptable/delegate_components/`, `problematic/cgp_fn/`). This +is the one place the suite groups by construct rather than by concept, and +deliberately so: a compile-fail case is defined by *which macro's expansion* fails, +and each cross-links to that macro's per-entrypoint implementation document, so the +fixture tree mirrors [docs/implementation/entrypoints/](../../docs/implementation/entrypoints). +Within a subdirectory, write one fixture file per case, named for the failure mode it +probes (`duplicate_key.rs`, `mut_slice_implicit.rs`), and open each with a comment +stating what it exercises and why it must not compile — exactly as the main suite +requires. The driver [tests/compile_fail_tests.rs](cgp-compile-fail-tests/tests/compile_fail_tests.rs) +globs both trees with `**`, so the two `t.compile_fail(...)` calls pick up a new +fixture with no per-file registration. A single `trybuild::TestCases` runs both globs +— do not split them across two `#[test]` functions, which would race on the shared +build directory. Run the suite with `cargo test -p cgp-compile-fail-tests` or `cargo nextest run -p cgp-compile-fail-tests`; both work because the driver is an ordinary integration test. @@ -186,18 +192,20 @@ capture the expanded code as an `insta` inline string snapshot in the compiles even though the code would not), with a comment explaining **why** the output is wrong and **what the correct output should be**. -Every failure case must also be recorded in the owning construct's **implementation -document** under `docs/implementation/`, and where it is recorded depends on the -category. A **problematic** fixture (and every `invalid_expansion` snapshot) documents -a defect, so describe it in that document's `## Known issues` section and index it from -`## Tests`; when the defect has a user-visible consequence, note it in the reference -document's `## Known issues` section too and cross-link the two. An **acceptable** -fixture documents *intended* behavior — a failure CGP deliberately defers to the -compiler — so it belongs in `## Tests` and in whatever section explains that behavior -(often a Known issues note that already frames the deferral as intended, as -`delegate_components!` does for overlapping impls), never as a fresh bug. In both cases -describe the behavior in the document's own words without referring to the test, and -put a link from the fixture's header comment back to the implementation document. +Every failure case must also be recorded in the owning macro's **implementation +document** under `docs/implementation/`, and *which section* holds it is what the +acceptable/problematic split decides. An **acceptable** fixture documents *intended* +behavior — a failure CGP deliberately defers to the compiler — so it belongs in that +document's `## Failure modes` section (a dedicated section, kept out of Known issues so +it is not mistaken for a bug), with a short code snippet of the failing input, and is +indexed from `## Tests`. A **problematic** fixture (and every `invalid_expansion` +snapshot) documents a defect, so describe it in `## Known issues` alongside the +construct's other bugs, again with a snippet, and index it from `## Tests`; when the +defect has a user-visible consequence, note it in the reference document's `## Known +issues` section too and cross-link the two. In both cases describe the behavior in the +document's own words without referring to the test, and put a link from the fixture's +header comment back to the implementation document — to its Failure modes section for an +acceptable case, its Known issues section for a problematic one. ## Keep the docs in sync diff --git a/crates/tests/README.md b/crates/tests/README.md index de78030f..d02605aa 100644 --- a/crates/tests/README.md +++ b/crates/tests/README.md @@ -58,9 +58,11 @@ for parser corner cases, and the failure-case targets `parser_rejections` and `invalid_expansion`. `cgp-compile-fail-tests` is laid out for `trybuild` instead: a single driver -`tests/compile_fail_tests.rs` glob-compiles every fixture under `tests/acceptable/` -and `tests/problematic/`, each a self-contained program paired with a committed -`.stderr` snapshot of the compiler output it must produce. +`tests/compile_fail_tests.rs` recursively glob-compiles every fixture under +`tests/acceptable/` and `tests/problematic/`, which are further grouped into one +subdirectory per owning macro (`acceptable/delegate_components/`, +`problematic/cgp_fn/`). Each fixture is a self-contained program paired with a +committed `.stderr` snapshot of the compiler output it must produce. ## Snapshots diff --git a/crates/tests/cgp-compile-fail-tests/src/lib.rs b/crates/tests/cgp-compile-fail-tests/src/lib.rs index c073a80b..8a7f6b77 100644 --- a/crates/tests/cgp-compile-fail-tests/src/lib.rs +++ b/crates/tests/cgp-compile-fail-tests/src/lib.rs @@ -29,9 +29,11 @@ //! compiler**. CGP is working as designed: it cannot see the whole program, so //! it lowers the input faithfully and lets `rustc` reject it (overlapping //! `delegate_components!` entries becoming conflicting impls, a lazily-wired -//! provider whose impl-side dependency the context does not meet). The pinned +//! provider whose impl-side dependency the context does not meet, an ill-formed +//! per-entry generic the compiler rejects as unconstrained). The pinned //! `.stderr` documents that the failure is the compiler doing its job, and its -//! diagnostic is the one a user should expect. +//! diagnostic is the one a user should expect. Each fixture is cross-linked to +//! the `## Failure modes` section of the owning macro's implementation document. //! - **`tests/problematic/`** — failures that are a **CGP defect**: input a macro //! should have rejected with a spanned error, or that a macro expanded into //! invalid Rust. The pinned `.stderr` captures the confusing downstream error a @@ -42,11 +44,16 @@ //! //! # Organization //! -//! Within each category directory, one fixture file per case, named for the CGP -//! concept and failure mode it probes (`duplicate_delegate_key.rs`, -//! `cgp_fn_mut_slice_implicit.rs`). Register every fixture in the driver -//! `tests/compile_fail_tests.rs` via its category glob — the two calls to -//! `t.compile_fail(...)` pick up new fixtures automatically. +//! Under each category directory, fixtures are grouped into one subdirectory per +//! **owning macro** — the macro whose expansion produces the failure and whose +//! implementation document documents it (`acceptable/delegate_components/`, +//! `problematic/cgp_fn/`). This mirrors the per-entrypoint layout of the +//! implementation docs, so a fixture and the document that indexes it share a +//! name. Within a subdirectory, write one fixture file per case, named for the +//! failure mode it probes (`duplicate_key.rs`, `mut_slice_implicit.rs`), and open +//! each with a comment stating what it exercises and why it must not compile. The +//! driver `tests/compile_fail_tests.rs` globs both trees with `**`, so a new +//! fixture is picked up with no registration. //! //! # Regenerating the `.stderr` snapshots //! diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.rs similarity index 98% rename from crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.rs rename to crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.rs index 32f5e6fa..b7ae42fd 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.rs +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.rs @@ -5,7 +5,7 @@ //! independently and has no view of the other block — so it correctly defers to //! the compiler, exactly as two hand-written overlapping impls would. //! -//! See docs/implementation/entrypoints/delegate_components.md (Known issues). +//! See docs/implementation/entrypoints/delegate_components.md (Failure modes). use cgp::prelude::*; diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.stderr similarity index 88% rename from crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.stderr rename to crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.stderr index bbdd0d0c..104a7676 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.stderr +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.stderr @@ -1,5 +1,5 @@ error[E0119]: conflicting implementations of trait `IsProviderFor` for type `Person` - --> tests/acceptable/duplicate_delegate_key.rs:36:1 + --> tests/acceptable/delegate_components/duplicate_key.rs:36:1 | 29 | / delegate_components! { 30 | | Person { @@ -18,7 +18,7 @@ error[E0119]: conflicting implementations of trait `IsProviderFor` for type `Person` - --> tests/acceptable/duplicate_delegate_key.rs:36:1 + --> tests/acceptable/delegate_components/duplicate_key.rs:36:1 | 29 | / delegate_components! { 30 | | Person { diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.rs similarity index 94% rename from crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.rs rename to crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.rs index 1c34acb2..36353cb9 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.rs +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.rs @@ -8,7 +8,7 @@ //! site; deferring it to the use site is the intended lazy behavior, not a //! macro defect. //! -//! See docs/implementation/entrypoints/delegate_components.md and +//! See docs/implementation/entrypoints/delegate_components.md (Failure modes) and //! docs/implementation/entrypoints/check_components.md. use cgp::prelude::*; diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.stderr similarity index 80% rename from crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.stderr rename to crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.stderr index be0d90a8..7a2b8691 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.stderr +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.stderr @@ -1,5 +1,5 @@ error[E0599]: the method `greet` exists for struct `Person`, but its trait bounds were not satisfied - --> tests/acceptable/missing_impl_side_dependency.rs:50:23 + --> tests/acceptable/delegate_components/missing_dependency.rs:50:23 | 37 | pub struct Person { | ----------------- method `greet` not found for this struct because it doesn't satisfy `Person: CanGreet` or `Person: Greeter` @@ -12,19 +12,19 @@ error[E0599]: the method `greet` exists for struct `Person`, but its trait bound | = note: found the following associated functions; to be used as methods, functions must have a `self` parameter note: the candidate is defined in the trait `Greeter` - --> tests/acceptable/missing_impl_side_dependency.rs:18:5 + --> tests/acceptable/delegate_components/missing_dependency.rs:18:5 | 18 | fn greet(&self); | ^^^^^^^^^^^^^^^^ note: trait bound `Person: Greeter` was not satisfied - --> tests/acceptable/missing_impl_side_dependency.rs:16:1 + --> tests/acceptable/delegate_components/missing_dependency.rs:16:1 | 16 | #[cgp_component(Greeter)] | ^^^^^^^^^^^^^^^^^^^^^^^^^ 17 | pub trait CanGreet { | ^^^^^^^^ note: the trait `Greeter` must be implemented - --> tests/acceptable/missing_impl_side_dependency.rs:17:1 + --> tests/acceptable/delegate_components/missing_dependency.rs:17:1 | 17 | / pub trait CanGreet { 18 | | fn greet(&self); @@ -32,7 +32,7 @@ note: the trait `Greeter` must be implemented | |_^ = help: items from traits can only be used if the trait is implemented and in scope note: `CanGreet` defines an item `greet`, perhaps you need to implement it - --> tests/acceptable/missing_impl_side_dependency.rs:17:1 + --> tests/acceptable/delegate_components/missing_dependency.rs:17:1 | 17 | pub trait CanGreet { | ^^^^^^^^^^^^^^^^^^ diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.rs similarity index 98% rename from crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.rs rename to crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.rs index e18db9c8..8268ff36 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.rs +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.rs @@ -6,7 +6,7 @@ //! defers the overlap check to the compiler, the same as two overlapping //! hand-written generic impls. //! -//! See docs/implementation/entrypoints/delegate_components.md (Known issues). +//! See docs/implementation/entrypoints/delegate_components.md (Failure modes). use cgp::prelude::*; diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.stderr similarity index 88% rename from crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.stderr rename to crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.stderr index 4d8e32dd..59fc31fa 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.stderr +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.stderr @@ -1,5 +1,5 @@ error[E0119]: conflicting implementations of trait `IsProviderFor` for type `Wrapper` - --> tests/acceptable/overlapping_generic_delegate.rs:33:1 + --> tests/acceptable/delegate_components/overlapping_generic.rs:33:1 | 26 | / delegate_components! { 27 | | Wrapper { @@ -18,7 +18,7 @@ error[E0119]: conflicting implementations of trait `IsProviderFor` for type `Wrapper` - --> tests/acceptable/overlapping_generic_delegate.rs:33:1 + --> tests/acceptable/delegate_components/overlapping_generic.rs:33:1 | 26 | / delegate_components! { 27 | | Wrapper { diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/unconstrained_generic.rs similarity index 67% rename from crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.rs rename to crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/unconstrained_generic.rs index 2876dea2..81c5514e 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.rs +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/unconstrained_generic.rs @@ -1,4 +1,4 @@ -//! Problematic failure: a per-entry generic list on a `delegate_components!` +//! Acceptable failure: a per-entry generic list on a `delegate_components!` //! mapping whose parameter appears only in the *provider value* and not in the //! *key*. The macro faithfully lowers ` GreeterComponent: GreetWith` into //! `impl DelegateComponent for Person { type Delegate = @@ -7,15 +7,13 @@ //! //! A per-entry generic is only well-formed when it appears in the key (as in //! ` BazKey: BarValue`, where `DelegateComponent>` -//! binds it). The macro does not check that every declared generic reaches the -//! key, so it accepts this nonsensical entry and emits an impl with a free -//! parameter instead of rejecting it with a spanned error at macro time. +//! binds it). Writing one that never reaches the key is ill-formed input, and +//! the macro lowers it faithfully rather than second-guessing it — so `rustc` +//! rejects the unconstrained parameter with exactly the E0207 it would give a +//! hand-written `impl` with an unused parameter. Deferring this to the +//! compiler is the intended behavior, not a macro defect. //! -//! The correct behavior would be to reject a per-entry generic that does not -//! appear in the key. This fixture pins the current behavior; its `.stderr` -//! should improve when the defect is fixed. -//! -//! See docs/implementation/entrypoints/delegate_components.md (Known issues). +//! See docs/implementation/entrypoints/delegate_components.md (Failure modes). use core::marker::PhantomData; diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/unconstrained_generic.stderr similarity index 54% rename from crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.stderr rename to crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/unconstrained_generic.stderr index 8f976834..7e401441 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.stderr +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/unconstrained_generic.stderr @@ -1,5 +1,5 @@ error[E0207]: the type parameter `T` is not constrained by the impl trait, self type, or predicates - --> tests/problematic/delegate_unconstrained_generic.rs:41:10 + --> tests/acceptable/delegate_components/unconstrained_generic.rs:39:10 | -41 | GreeterComponent: GreetWith, +39 | GreeterComponent: GreetWith, | ^ unconstrained type parameter diff --git a/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs b/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs index ecd68427..49a90e71 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs +++ b/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs @@ -1,24 +1,27 @@ //! `trybuild` driver for the CGP compile-fail suite. //! -//! Each fixture under `tests/acceptable/` and `tests/problematic/` is compiled -//! as its own crate and its diagnostics compared against the sibling `.stderr` -//! file. See the crate root docs (`src/lib.rs`) for what belongs in each -//! category and how to regenerate the `.stderr` snapshots. +//! Each fixture under `tests/acceptable//` and +//! `tests/problematic//` is compiled as its own crate and its +//! diagnostics compared against the sibling `.stderr` file. See the crate root +//! docs (`src/lib.rs`) for what belongs in each category and how to regenerate +//! the `.stderr` snapshots. //! //! A single `TestCases` runs both globs so `trybuild` invokes `cargo` once; //! splitting the categories across two `#[test]` functions would race on the -//! shared build directory. +//! shared build directory. The `**` in each glob descends into the per-construct +//! subdirectories, so a new fixture is picked up with no change here. #[test] fn compile_fail() { let t = trybuild::TestCases::new(); // Failures CGP intentionally delegates to the Rust compiler — CGP is - // working as designed and `rustc` is the right place for the check. - t.compile_fail("tests/acceptable/*.rs"); + // working as designed and `rustc` is the right place for the check. These + // are documented under each owning macro's `## Failure modes` section. + t.compile_fail("tests/acceptable/**/*.rs"); // Failures that are a CGP defect: input a macro should have rejected, or an // expansion that emits invalid Rust. Each is cross-linked to a `## Known // issues` entry in the owning macro's implementation document. - t.compile_fail("tests/problematic/*.rs"); + t.compile_fail("tests/problematic/**/*.rs"); } diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.rs b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.rs similarity index 100% rename from crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.rs rename to crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.rs diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.stderr b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.stderr similarity index 86% rename from crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.stderr rename to crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.stderr index 53644d10..f69892f8 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.stderr +++ b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.stderr @@ -1,5 +1,5 @@ error[E0599]: the method `zero_all` exists for struct `Buffer`, but its trait bounds were not satisfied - --> tests/problematic/cgp_fn_mut_slice_implicit.rs:40:12 + --> tests/problematic/cgp_fn/mut_slice_implicit.rs:40:12 | 32 | pub struct Buffer { | ----------------- method `zero_all` not found for this struct because it doesn't satisfy `<_ as HasField>>>>>>>::Value = [u8]` or `Buffer: ZeroAll` @@ -8,7 +8,7 @@ error[E0599]: the method `zero_all` exists for struct `Buffer`, but its trait bo | ^^^^^^^^ method cannot be called on `Buffer` due to unsatisfied trait bounds | note: trait bound `>>>>>>>::Value = [u8]` was not satisfied - --> tests/problematic/cgp_fn_mut_slice_implicit.rs:24:1 + --> tests/problematic/cgp_fn/mut_slice_implicit.rs:24:1 | 24 | #[cgp_fn] | ^^^^^^^^^ @@ -16,7 +16,7 @@ note: trait bound ` tests/problematic/cgp_fn_mut_slice_implicit.rs:24:1 + --> tests/problematic/cgp_fn/mut_slice_implicit.rs:24:1 | 24 | #[cgp_fn] | ^^^^^^^^^ diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.rs b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.rs similarity index 100% rename from crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.rs rename to crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.rs diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.stderr b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.stderr similarity index 88% rename from crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.stderr rename to crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.stderr index 7c1cf15e..00a69c57 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.stderr +++ b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.stderr @@ -1,5 +1,5 @@ error[E0308]: mismatched types - --> tests/problematic/cgp_fn_option_mut_implicit.rs:17:1 + --> tests/problematic/cgp_fn/option_mut_implicit.rs:17:1 | 17 | #[cgp_fn] | ^^^^^^^^^ types differ in mutability diff --git a/docs/implementation/AGENTS.md b/docs/implementation/AGENTS.md index a3d030d5..65f693b2 100644 --- a/docs/implementation/AGENTS.md +++ b/docs/implementation/AGENTS.md @@ -36,7 +36,7 @@ Because documents live in different subdirectories, a cross-link between two of Each kind of document follows a shape so an agent can navigate any of them by habit. All kinds share the same opening — a level-one heading naming the construct and a one-sentence summary — and the same closing pair of a **Tests** section and a **Source** section. The middle sections differ by kind. -An **entrypoint document** (`entrypoints/.md`) proceeds through: **Entry point** — the `cgp-macro-lib` function, what it parses the attribute and item into, and the pipeline call it drives; **Pipeline** — each stage in order (for `cgp_component`, `preprocess → eval → to_items`), what each stage does at a high level, and a link to the [asts/](asts/) document that owns those types; **Generated items** — what the macro emits and in what order, described at a high level and illustrated with a code snippet where a transform is worth showing; **Behavior and corner cases** — how the implementation handles supertraits, default methods, generic parameters, lifetimes, reserved identifiers, and any input the macro treats specially; **Known issues** — limitations and bugs, following the rule below; then **Snapshots**, **Tests**, and **Source**. +An **entrypoint document** (`entrypoints/.md`) proceeds through: **Entry point** — the `cgp-macro-lib` function, what it parses the attribute and item into, and the pipeline call it drives; **Pipeline** — each stage in order (for `cgp_component`, `preprocess → eval → to_items`), what each stage does at a high level, and a link to the [asts/](asts/) document that owns those types; **Generated items** — what the macro emits and in what order, described at a high level and illustrated with a code snippet where a transform is worth showing; **Behavior and corner cases** — how the implementation handles supertraits, default methods, generic parameters, lifetimes, reserved identifiers, and any input the macro treats specially; **Failure modes** — optional; the ways the macro's *accepted* input can still fail to compile downstream that are *intended* (deferred to the compiler), following the rule below; **Known issues** — limitations and bugs, including the *problematic* compile-failure modes, following the rule below; then **Snapshots**, **Tests**, and **Source**. Omit the Failure modes heading when a macro has no acceptable compile-failure mode worth recording. An **AST document** (`asts/.md`) opens with a short overview of the stack as a pipeline, then gives one section per AST type in the order the data flows through them. Each section states the type's role, the shape it parses from or emits into (with a snippet where a grammar or intermediary form is worth showing), and how it hands off to the next stage, and links to the entrypoint document that drives the stack. It then closes with **Tests** and **Source**. @@ -66,10 +66,10 @@ Record limitations, surprising behavior, and confirmed bugs in the implementatio Because a CGP macro expands to ordinary Rust that the compiler checks afterward, a full picture of a macro includes the ways its *accepted* input can still fail to compile downstream — a dimension distinct from the input a macro rejects at parse time. Enumerate these compile-failure modes as part of documenting a macro, and sort each into one of two kinds, mirroring the split in [crates/tests/cgp-compile-fail-tests](../../crates/tests/cgp-compile-fail-tests): -- An **acceptable** failure is one the macro **intentionally delegates to the Rust compiler** because it lacks the whole-program view the check needs — two `delegate_components!` blocks that map the same key into overlapping impls, or a lazily-wired provider whose impl-side dependency the context does not satisfy. This is correct behavior, not a bug, so document it as intended: describe *what the compiler rejects and why the macro cannot catch it earlier*, in the Behavior and corner cases section or in a Known issues note that frames the deferral as deliberate (as [entrypoints/delegate_components.md](entrypoints/delegate_components.md) does for duplicate keys). Index the covering fixture from the Tests section. -- A **problematic** failure is a **defect**: input the macro should have rejected with a spanned error, or an expansion that emits invalid Rust. Record it under Known issues with the current (wrong) behavior and the correct behavior, note the user-visible consequence in the reference document's Known issues and cross-link, and index the pinning fixture from the Tests section. Remove the note when the defect is fixed, per the synchronization rule. +- An **acceptable** failure is one the macro **intentionally delegates to the Rust compiler** because it lacks the whole-program view the check needs — two `delegate_components!` blocks that map the same key into overlapping impls, a lazily-wired provider whose impl-side dependency the context does not satisfy, or an ill-formed per-entry generic the compiler rejects as unconstrained. This is correct behavior, not a bug, so it does *not* belong under Known issues. Record it in a dedicated **`## Failure modes`** section, kept separate precisely so a reader never mistakes it for a defect: describe *what the compiler rejects and why the macro cannot (or should not) catch it earlier*, show a short code snippet of the failing input, and index the covering fixture from the Tests section. [entrypoints/delegate_components.md](entrypoints/delegate_components.md) is the model. +- A **problematic** failure is a **defect**: input the macro should have rejected with a spanned error, or an expansion that emits invalid Rust. Record it under **`## Known issues`**, alongside the construct's other bugs, with the current (wrong) behavior, the correct behavior, and a short code snippet of the offending input; note the user-visible consequence in the reference document's Known issues and cross-link; and index the pinning fixture from the Tests section. Remove the note when the defect is fixed, per the synchronization rule. -Whenever you add, move, or reclassify a compile-fail fixture in `cgp-compile-fail-tests`, update the owning macro's implementation document in the same change so its Known issues and Tests sections stay the canonical index of how that macro can fail to compile. This is the "enumerate every way the output can expand" review concern (see the macro review workflow in [../../AGENTS.md](../../AGENTS.md)) written down as documentation, so a later agent inherits the failure map rather than rediscovering it. +Give every documented case, in either section, a short illustrative snippet of the CGP code that triggers it (and the offending expansion fragment when it clarifies the failure), following the code-snippet guidance above. Whenever you add, move, or reclassify a compile-fail fixture in `cgp-compile-fail-tests`, update the owning macro's implementation document in the same change so its Failure modes, Known issues, and Tests sections stay the canonical index of how that macro can fail to compile. This is the "enumerate every way the output can expand" review concern (see the macro review workflow in [../../AGENTS.md](../../AGENTS.md)) written down as documentation, so a later agent inherits the failure map rather than rediscovering it. ## Keeping the reference and implementation documents aligned diff --git a/docs/implementation/entrypoints/cgp_fn.md b/docs/implementation/entrypoints/cgp_fn.md index 263cd623..96ed2e4f 100644 --- a/docs/implementation/entrypoints/cgp_fn.md +++ b/docs/implementation/entrypoints/cgp_fn.md @@ -68,7 +68,23 @@ The conversion applied to each binding is chosen by the argument's type, followi `#[cgp_fn]` does not support generics on the desugared *method* itself — generic parameters are only ever lifted onto the trait and impl. A method-level generic is silently treated as a trait/impl generic rather than rejected, which is the intended limitation rather than a bug: method-level generics are considered an advanced case better written as an explicit blanket impl or a [`#[cgp_component]`](../../reference/macros/cgp_component.md) provider. -The *mutable* variants of the slice and option modes are not supported, and both are **problematic** failures — the macro accepts the shape rather than rejecting it, so the failure lands on the emitted code instead of as a spanned message. Under a `&mut self` receiver, a `&mut [T]` implicit falls through the slice case (which matches only a shared `&[T]`, since there is no `AsMut<[T]>` counterpart) into the plain-reference mode, producing an unsatisfiable `HasFieldMut` bound that surfaces at the call site (`E0599`). Note the contrast with a `&self` receiver, where the same `&mut [T]` is instead rejected cleanly at macro time with "&mut self is required for mutable field reference" — that rejection is a returned `Err` covered in `parser_rejections`, not a compile-fail fixture. An `Option<&mut T>` implicit reads immutably (the argument's outer type is a path, so `field_mut` is `None`), producing an `Option<&T>` value that then fails to coerce to the declared `Option<&mut T>` inside the generated body (`E0308`). Both are exotic shapes rather than a regression — the immutable `&[T]` and `Option<&T>` forms are the supported ones — and the correct behavior would be either to support the mutable modes or to reject them at macro time with a clear message. +The *mutable* variants of the slice and option modes are not supported, and both are **problematic** failures — the macro accepts the shape rather than rejecting it, so the failure lands on the emitted code instead of as a spanned message. Both are exotic shapes rather than a regression (the immutable `&[T]` and `Option<&T>` forms are the supported ones), and the correct behavior would be either to support the mutable modes or to reject them at macro time with a clear message. Each is pinned by a fixture under [problematic/cgp_fn/](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn) in `cgp-compile-fail-tests`. + +Under a `&mut self` receiver, a `&mut [T]` implicit falls through the slice case (which matches only a shared `&[T]`, since there is no `AsMut<[T]>` counterpart) into the plain-reference mode, producing an unsatisfiable `HasFieldMut` bound that surfaces at the call site (`E0599`): + +```rust +#[cgp_fn] +fn zero_all(&mut self, #[implicit] items: &mut [u8]) { /* … */ } // E0599 when called +``` + +Note the contrast with a `&self` receiver, where the same `&mut [T]` is instead rejected cleanly at macro time with "&mut self is required for mutable field reference" — that rejection is a returned `Err` covered in `parser_rejections`, not a compile-fail fixture. + +An `Option<&mut T>` implicit reads immutably (the argument's outer type is a path, so `field_mut` is `None`), producing an `Option<&T>` value that then fails to coerce to the declared `Option<&mut T>` inside the generated body (`E0308`): + +```rust +#[cgp_fn] +fn take_slot(&self, #[implicit] slot: Option<&mut u8>) -> Option<&mut u8> { slot } // E0308 +``` ## Snapshots @@ -107,8 +123,8 @@ The failure cases pin the inputs `#[cgp_fn]` refuses during expansion, each asse The compile-fail fixtures in `cgp-compile-fail-tests` pin the problematic mutable-mode expansions the macro accepts but should not: -- [problematic/cgp_fn_mut_slice_implicit.rs](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_mut_slice_implicit.rs) — a `&mut [u8]` implicit under a `&mut self` receiver expands to an unsatisfiable `HasFieldMut` bound (`E0599`). -- [problematic/cgp_fn_option_mut_implicit.rs](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn_option_mut_implicit.rs) — an `Option<&mut T>` implicit binds an `Option<&T>` value that fails to coerce to the declared `Option<&mut T>` (`E0308`). +- [problematic/cgp_fn/mut_slice_implicit.rs](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.rs) — a `&mut [u8]` implicit under a `&mut self` receiver expands to an unsatisfiable `HasFieldMut` bound (`E0599`). +- [problematic/cgp_fn/option_mut_implicit.rs](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.rs) — an `Option<&mut T>` implicit binds an `Option<&T>` value that fails to coerce to the declared `Option<&mut T>` (`E0308`). ## Source diff --git a/docs/implementation/entrypoints/delegate_components.md b/docs/implementation/entrypoints/delegate_components.md index ddce02ac..8fd4b1b9 100644 --- a/docs/implementation/entrypoints/delegate_components.md +++ b/docs/implementation/entrypoints/delegate_components.md @@ -58,13 +58,49 @@ An **array key** `[A, B]: Provider` expands to one impl pair per bracketed key, An **`@`-path key** carries a leading `__Wildcard__` generic and lowers the path to a prefix type ending in that wildcard, which is how a dispatch parameter slots in at lookup time. A **brace group on a path segment** (`@Component.{u32, u64}: P`) expands to one key per element, and the `namespace`/`for` statement forms lower through a shared "for-entry" path that builds a `Namespace<…, Delegate = …>` bound rather than a direct `DelegateComponent` impl; these are the namespace machinery and are detailed in the AST document. -## Known issues +## Failure modes -The macro's parser is permissive about the body shape and surfaces most mistakes as generic `syn` parse errors rather than tailored diagnostics — for example, an `open` header written after a plain mapping fails to parse because statements must lead the block, but the error (`expected `:``) points at the unexpected token rather than explaining the ordering rule. +Some `delegate_components!` inputs are accepted by the macro but then fail to compile, and for `delegate_components!` these failures are all ones the macro **intentionally defers to the Rust compiler**: it lowers each block independently, with no whole-program view, so a mistake that only a global check could catch is left to `rustc`. Each is intended behavior rather than a bug — the diagnostic below is the one a user should expect — and each is pinned by a fixture under [acceptable/delegate_components/](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components) in `cgp-compile-fail-tests`. + +A **duplicate key** — the same component mapped twice, whether by two entries in one block, two separate blocks, or an `open` header colliding with an explicit mapping — emits two conflicting `DelegateComponent` impls and fails with the coherence error `E0119`, exactly as two hand-written impls would: + +```rust +delegate_components! { Person { GreeterComponent: GreetHello } } +delegate_components! { Person { GreeterComponent: GreetGoodbye } } // E0119: conflicting impl +``` + +An **overlapping generic entry** is the same failure reached through generics: a ` Wrapper` table and a specific `Wrapper` table wiring the same component overlap at `Wrapper`, and since stable Rust has no specialization the two impls conflict with `E0119`. + +```rust +delegate_components! { Wrapper { GreeterComponent: GreetHello } } +delegate_components! { Wrapper { GreeterComponent: GreetHello } } // E0119 at Wrapper +``` + +A **missing impl-side dependency** follows from wiring being lazy: `delegate_components!` records the entry without checking the provider's transitive requirements, so wiring a provider whose `where` clause the context cannot satisfy is accepted, and the unmet bound surfaces only when the consumer trait is used (an `E0599` naming the missing `Greeter` / `IsProviderFor` bound). A `check_components!` site moves the same error earlier, to the wiring. -A duplicate key — the same component mapped twice, whether by two plain entries, two separate `delegate_components!` blocks, or an `open` header colliding with an explicit mapping — is not caught by the macro; it emits two conflicting `DelegateComponent` impls and surfaces as a coherence error (`E0119`) at compile time, the same as two hand-written impls would. The same holds for a generic entry that overlaps a more specific one (a ` Wrapper` table and a `Wrapper` table wiring the same key): stable Rust has no specialization, so the two impls conflict at the overlapping type. Both are **acceptable** failures — the macro lowers each block independently and has no whole-program view, so it correctly defers the overlap check to the compiler. The lazy nature of wiring produces a related acceptable failure: a provider whose impl-side dependency the context does not satisfy is wired without complaint, and the unmet bound surfaces only when the consumer trait is used (or earlier, at a `check_components!` site). +```rust +// GreetHello requires `Self: HasName`, but `Person` has no `name` field. +delegate_components! { Person { GreeterComponent: GreetHello } } // accepted — wiring is lazy +person.greet(); // E0599: `Person: Greeter` is not satisfied +``` + +An **unconstrained per-entry generic** is accepted when its parameter appears only in the provider value and not in the key. A per-entry generic list is well-formed only when it reaches the key (as in ` BazKey`, where `DelegateComponent>` binds it); writing one that never does is ill-formed input, and the macro lowers it faithfully rather than second-guessing it, so the compiler rejects the free parameter with `E0207` just as it would a hand-written impl with an unused parameter: -A per-entry generic list whose parameter appears only in the provider **value** and not in the **key** is a **problematic** failure: the macro lowers ` GreeterComponent: GreetWith` into `impl DelegateComponent for Person { type Delegate = GreetWith; }`, where `T` is unconstrained, so the compiler rejects it with `E0207`. A per-entry generic is only well-formed when it reaches the key (as in ` BazKey`, where `DelegateComponent>` binds it); the macro does not check that every declared generic appears in the key, so it accepts the nonsensical entry and emits a free-parameter impl instead of rejecting it with a spanned error. The correct behavior would be to reject a per-entry generic that does not appear in the key. +```rust +delegate_components! { + Person { + GreeterComponent: GreetWith, // T never reaches the key + } +} +// lowers to an impl with an unconstrained parameter: +impl DelegateComponent for Person { + type Delegate = GreetWith; // E0207: `T` is not constrained +} +``` + +## Known issues + +The macro's parser is permissive about the body shape and surfaces most mistakes as generic `syn` parse errors rather than tailored diagnostics — for example, an `open` header written after a plain mapping fails to parse because statements must lead the block, but the error (`expected `:``) points at the unexpected token rather than explaining the ordering rule. ## Snapshots @@ -104,12 +140,12 @@ The failure cases in `cgp-macro-tests` pin the attribute rejection: - [parser_rejections/delegate_components.rs](../../../crates/tests/cgp-macro-tests/tests/parser_rejections/delegate_components.rs) asserts the macro rejects an attribute on the table, on a key, and on a key nested inside a `UseDelegate` value (the last confirms the validator recurses through mapping values rather than dropping the attribute), and that a braceless `open` header listing more than one component is rejected (the braceless form opens exactly one). -The compile-fail fixtures in `cgp-compile-fail-tests` pin the expansions that fail to compile, split by whether the failure is intended: +The compile-fail fixtures in `cgp-compile-fail-tests` pin the expansions that fail to compile. All are **acceptable** failures — deferred to the compiler by design — and are described under [Failure modes](#failure-modes) above: -- [acceptable/duplicate_delegate_key.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/duplicate_delegate_key.rs) — two blocks mapping the same key expand to conflicting `DelegateComponent` impls (`E0119`), a failure the macro deliberately defers to the compiler. -- [acceptable/overlapping_generic_delegate.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/overlapping_generic_delegate.rs) — a generic ` Wrapper` entry overlaps a specific `Wrapper` entry at the same key (`E0119`), the generic form of the same deferred overlap. -- [acceptable/missing_impl_side_dependency.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/missing_impl_side_dependency.rs) — a lazily-wired provider whose `Self: HasName` dependency the context does not satisfy; the unmet bound surfaces at the call site, the intended consequence of lazy wiring. -- [problematic/delegate_unconstrained_generic.rs](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/delegate_unconstrained_generic.rs) — a per-entry generic that appears only in the value (` GreeterComponent: GreetWith`) expands to an impl with an unconstrained `T` (`E0207`) instead of being rejected at macro time. +- [acceptable/delegate_components/duplicate_key.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.rs) — two blocks mapping the same key expand to conflicting `DelegateComponent` impls (`E0119`). +- [acceptable/delegate_components/overlapping_generic.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.rs) — a generic ` Wrapper` entry overlaps a specific `Wrapper` entry at the same key (`E0119`). +- [acceptable/delegate_components/missing_dependency.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.rs) — a lazily-wired provider whose `Self: HasName` dependency the context does not satisfy; the unmet bound surfaces at the call site (`E0599`). +- [acceptable/delegate_components/unconstrained_generic.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/unconstrained_generic.rs) — a per-entry generic that appears only in the value (` GreeterComponent: GreetWith`) lowers to an impl with an unconstrained `T` (`E0207`), which the compiler rejects as it would a hand-written impl. ## Source From 75b2192e07f04823d81b4751b11a8fb6abb34a1f Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 21:47:48 +0200 Subject: [PATCH 05/17] Fix implicits for mut ref --- .../src/functions/field/parse.rs | 91 ++++++++++++------- .../src/functions/getter/parse.rs | 5 +- .../src/functions/implicits/parse.rs | 18 ++-- .../src/types/cgp_getter/with_provider.rs | 4 + .../src/types/field/has_field_bound.rs | 7 ++ .../types/getter/get_field_with_mode_expr.rs | 10 +- .../cgp-macro-core/src/types/getter/method.rs | 10 +- crates/tests/AGENTS.md | 12 +-- crates/tests/README.md | 7 +- .../tests/cgp-compile-fail-tests/src/lib.rs | 10 +- .../problematic/cgp_fn/mut_slice_implicit.rs | 41 --------- .../cgp_fn/mut_slice_implicit.stderr | 23 ----- .../problematic/cgp_fn/option_mut_implicit.rs | 22 ----- .../cgp_fn/option_mut_implicit.stderr | 11 --- .../tests/parser_rejections/cgp_fn.rs | 50 +++++++++- .../implicit_arguments/cgp_fn_mut_slice.rs | 66 ++++++++++++++ .../implicit_arguments/cgp_fn_option_mut.rs | 64 +++++++++++++ .../cgp-tests/tests/implicit_arguments/mod.rs | 2 + docs/implementation/asts/cgp_getter.md | 4 +- .../entrypoints/cgp_auto_getter.md | 2 +- docs/implementation/entrypoints/cgp_fn.md | 31 +------ docs/implementation/entrypoints/cgp_getter.md | 2 +- docs/reference/attributes/implicit.md | 8 +- docs/reference/macros/cgp_auto_getter.md | 2 +- .../cgp/references/functions-and-getters.md | 4 +- 25 files changed, 304 insertions(+), 202 deletions(-) delete mode 100644 crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.rs delete mode 100644 crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.stderr delete mode 100644 crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.rs delete mode 100644 crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.stderr create mode 100644 crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mut_slice.rs create mode 100644 crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_mut.rs diff --git a/crates/macros/cgp-macro-core/src/functions/field/parse.rs b/crates/macros/cgp-macro-core/src/functions/field/parse.rs index 02e3903e..fafc9f89 100644 --- a/crates/macros/cgp-macro-core/src/functions/field/parse.rs +++ b/crates/macros/cgp-macro-core/src/functions/field/parse.rs @@ -1,78 +1,107 @@ use quote::ToTokens; use syn::spanned::Spanned; use syn::token::Mut; -use syn::{Error, GenericArgument, PathArguments, PathSegment, Type, TypePath}; +use syn::{Error, GenericArgument, PathArguments, PathSegment, Type, TypePath, TypeReference}; use crate::functions::parse_internal; use crate::types::getter::FieldMode; +/// Determine, from the type a field read must produce, the field type to require, +/// the [`FieldMode`] that shapes the read expression, and whether the read borrows +/// the field mutably. The mutability follows the *reference in the type* — the +/// outer `&mut` of a `&mut T`/`&mut [T]`, or the inner `&mut` of an +/// `Option<&mut T>` — and a mutable read always requires a `&mut self` receiver, +/// which `receiver_mut` reports. pub fn parse_field_type( return_type: &Type, receiver_mut: &Option, -) -> syn::Result<(Type, FieldMode)> { +) -> syn::Result<(Type, FieldMode, Option)> { match &return_type { Type::Reference(type_ref) => { - if type_ref.mutability.is_some() && receiver_mut.is_none() { - return Err(Error::new( - type_ref.span(), - format!( - "&mut self is required for mutable field reference `{}`", - type_ref.to_token_stream() - ), - )); - } + require_mut_receiver(type_ref, receiver_mut)?; + + let field_mut = type_ref.mutability; if type_ref.elem.as_ref() == &parse_internal! { str } { // Special case to handle &str as String field let field_type: Type = parse_internal! { String }; - Ok((field_type, FieldMode::Str)) - } else if let (Type::Slice(slice), None) = - (type_ref.elem.as_ref(), &type_ref.mutability) - { - // A shared `&[T]` reads any `AsRef<[T]>` field. Whether the read is - // mutable follows the reference's *own* mutability, not the - // receiver's — a `&self`/`&mut self` method may take an immutable - // slice either way, and there is no `AsMut<[T]>` counterpart, so a - // `&mut [T]` falls through to the plain reference case below. + Ok((field_type, FieldMode::Str, field_mut)) + } else if let Type::Slice(slice) = type_ref.elem.as_ref() { + // A shared `&[T]` reads any `AsRef<[T]>` field with `.as_ref()`; a + // `&mut [T]` reads any `AsMut<[T]>` field with `.as_mut()`. Both use + // `FieldMode::Slice`; the mutability carried alongside selects the + // bound and the read method. let field_type = slice.elem.as_ref().clone(); - Ok((field_type, FieldMode::Slice)) + Ok((field_type, FieldMode::Slice, field_mut)) } else { let field_type = type_ref.elem.as_ref().clone(); - Ok((field_type, FieldMode::Reference)) + Ok((field_type, FieldMode::Reference, field_mut)) } } Type::Path(type_path) => { - if let Some(field_type) = try_parse_option_ref(type_path) { + if let Some(type_ref) = try_parse_option_ref(type_path) { + // `Option<&T>` reads an `Option` field with `.as_ref()`; + // `Option<&mut T>` reads it with `.as_mut()`. The read borrows the + // field with the *inner* reference's mutability, so a `&mut` inner + // reference likewise requires a `&mut self` receiver. + require_mut_receiver(type_ref, receiver_mut)?; + + let field_mut = type_ref.mutability; + let field_type = type_ref.elem.as_ref(); + if field_type == &parse_internal! { str } { // `Option<&str>` is backed by an `Option` field and read - // with `.as_deref()`, mirroring the `&str`/`String` special case - // for a plain reference. - Ok((parse_internal! { Option< String > }, FieldMode::OptionStr)) + // with `.as_deref()` (or `.as_deref_mut()` for `Option<&mut str>`), + // mirroring the `&str`/`String` special case for a plain reference. + Ok(( + parse_internal! { Option< String > }, + FieldMode::OptionStr, + field_mut, + )) } else { Ok(( parse_internal! { Option< #field_type > }, FieldMode::OptionRef, + field_mut, )) } } else if let Some(field_type) = try_parse_mref(type_path) { // `MRef` borrows the field as a shared value, so — unlike a `&mut` // reference — its access mode never depends on the receiver. - Ok((field_type.clone(), FieldMode::MRef)) + Ok((field_type.clone(), FieldMode::MRef, None)) } else { - Ok((return_type.clone(), FieldMode::Copy)) + Ok((return_type.clone(), FieldMode::Copy, None)) } } // Any other owned type (a tuple, an array, and so on) is read by value and // cloned, exactly like an owned path type. - _ => Ok((return_type.clone(), FieldMode::Copy)), + _ => Ok((return_type.clone(), FieldMode::Copy, None)), } } -fn try_parse_option_ref(type_path: &TypePath) -> Option<&Type> { +/// A mutable field read borrows the whole context exclusively through +/// `get_field_mut`, so a `&mut` reference in the field type — the outer `&mut T` or +/// the inner reference of an `Option<&mut T>` — is only valid under a `&mut self` +/// receiver. +fn require_mut_receiver(type_ref: &TypeReference, receiver_mut: &Option) -> syn::Result<()> { + if type_ref.mutability.is_some() && receiver_mut.is_none() { + return Err(Error::new( + type_ref.span(), + format!( + "&mut self is required for mutable field reference `{}`", + type_ref.to_token_stream() + ), + )); + } + + Ok(()) +} + +fn try_parse_option_ref(type_path: &TypePath) -> Option<&TypeReference> { let segment = parse_single_segment_type_path(type_path).ok()?; if segment.ident == "Option" @@ -81,7 +110,7 @@ fn try_parse_option_ref(type_path: &TypePath) -> Option<&Type> { let [arg] = Vec::from_iter(args.args.iter()).try_into().ok()?; if let GenericArgument::Type(Type::Reference(type_ref)) = arg { - return Some(type_ref.elem.as_ref()); + return Some(type_ref); } } diff --git a/crates/macros/cgp-macro-core/src/functions/getter/parse.rs b/crates/macros/cgp-macro-core/src/functions/getter/parse.rs index 5a422dbf..c443ba44 100644 --- a/crates/macros/cgp-macro-core/src/functions/getter/parse.rs +++ b/crates/macros/cgp-macro-core/src/functions/getter/parse.rs @@ -106,7 +106,10 @@ fn parse_getter_method( let return_type = parse_return_type(context_type, &signature.output, field_assoc_type)?; - let (field_type, field_mode) = parse_field_type(&return_type, &receiver_mut)?; + // A getter keys its access mutability off the receiver (`&self` vs `&mut + // self`), not off the field type, so the mutability `parse_field_type` derives + // from the return type is discarded here in favor of `receiver_mut`. + let (field_type, field_mode, _) = parse_field_type(&return_type, &receiver_mut)?; Ok(GetterField { receiver_mode, diff --git a/crates/macros/cgp-macro-core/src/functions/implicits/parse.rs b/crates/macros/cgp-macro-core/src/functions/implicits/parse.rs index 69422769..b0a6153b 100644 --- a/crates/macros/cgp-macro-core/src/functions/implicits/parse.rs +++ b/crates/macros/cgp-macro-core/src/functions/implicits/parse.rs @@ -3,7 +3,7 @@ use std::mem; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::visit::{self, Visit}; -use syn::{Attribute, FnArg, Meta, Pat, PatIdent, PatType, Receiver, Type}; +use syn::{Attribute, FnArg, Meta, Pat, PatIdent, PatType, Receiver}; use crate::functions::parse_field_type; use crate::types::implicits::{ImplicitArgField, ImplicitArgFields}; @@ -63,16 +63,12 @@ pub fn parse_implicit_arg(receiver: &Receiver, arg: &PatType) -> syn::Result type_ref.mutability, - _ => None, - }; + // `parse_field_type` derives the field-access mutability from the reference in + // the argument type — the outer `&mut` of a `&mut T`/`&mut [T]`, or the inner + // `&mut` of an `Option<&mut T>` — and rejects a mutable read without a `&mut + // self` receiver. The receiver's own mutability never forces a mutable read of + // an immutably-typed argument. + let (field_type, field_mode, field_mut) = parse_field_type(&arg_type, &receiver.mutability)?; let spec = ImplicitArgField { field_name: pat_ident.ident.clone(), diff --git a/crates/macros/cgp-macro-core/src/types/cgp_getter/with_provider.rs b/crates/macros/cgp-macro-core/src/types/cgp_getter/with_provider.rs index 0ed6ec96..1b25fb38 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_getter/with_provider.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_getter/with_provider.rs @@ -89,6 +89,10 @@ impl ItemCgpGetter { #FieldGetter< #receiver_type, #component_name , Value = #field_type > } } + } else if let FieldMode::Slice = field.field_mode { + quote! { + #MutFieldGetter< #receiver_type, #component_name, Value: AsMut< [ #field_type ] > + 'static > + } } else { quote! { #MutFieldGetter< #receiver_type, #component_name, Value = #field_type > diff --git a/crates/macros/cgp-macro-core/src/types/field/has_field_bound.rs b/crates/macros/cgp-macro-core/src/types/field/has_field_bound.rs index 7c62fa4a..796cf881 100644 --- a/crates/macros/cgp-macro-core/src/types/field/has_field_bound.rs +++ b/crates/macros/cgp-macro-core/src/types/field/has_field_bound.rs @@ -32,6 +32,13 @@ impl ToTokens for HasFieldBound { #HasField< #tag_type, Value = #field_type > } } + } else if let FieldMode::Slice = field_mode { + // A mutable slice reads through `.as_mut()`, so the field's value must + // implement `AsMut<[T]>` — the mutable mirror of the shared slice's + // `AsRef<[T]>` bound. + quote! { + #HasFieldMut< #tag_type, Value: AsMut< [ #field_type ] > + 'static > + } } else { quote! { #HasFieldMut< #tag_type, Value = #field_type > diff --git a/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs b/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs index fe39f069..6d3f8d63 100644 --- a/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs +++ b/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs @@ -60,8 +60,14 @@ impl ToTokens for GetFieldWithModeExpr { } } FieldMode::Slice => { - quote! { - #call_expr .as_ref() + if call_expr.field_mut.is_none() { + quote! { + #call_expr .as_ref() + } + } else { + quote! { + #call_expr .as_mut() + } } } }; diff --git a/crates/macros/cgp-macro-core/src/types/getter/method.rs b/crates/macros/cgp-macro-core/src/types/getter/method.rs index 91fedeec..003d1460 100644 --- a/crates/macros/cgp-macro-core/src/types/getter/method.rs +++ b/crates/macros/cgp-macro-core/src/types/getter/method.rs @@ -163,8 +163,14 @@ fn extend_call_expr( } } FieldMode::Slice => { - quote! { - #call_expr .as_ref() + if field_mut.is_none() { + quote! { + #call_expr .as_ref() + } + } else { + quote! { + #call_expr .as_mut() + } } } } diff --git a/crates/tests/AGENTS.md b/crates/tests/AGENTS.md index 40cf9a8c..7ee88f64 100644 --- a/crates/tests/AGENTS.md +++ b/crates/tests/AGENTS.md @@ -165,13 +165,13 @@ of two opposite meanings, and the directory it lives in records which: Under each category directory, group fixtures into one subdirectory per **owning macro** — the macro whose expansion produces the failure and whose implementation -document documents it (`acceptable/delegate_components/`, `problematic/cgp_fn/`). This -is the one place the suite groups by construct rather than by concept, and -deliberately so: a compile-fail case is defined by *which macro's expansion* fails, -and each cross-links to that macro's per-entrypoint implementation document, so the -fixture tree mirrors [docs/implementation/entrypoints/](../../docs/implementation/entrypoints). +document documents it (`acceptable/delegate_components/`, or a `problematic//` +when a defect is pinned). This is the one place the suite groups by construct rather +than by concept, and deliberately so: a compile-fail case is defined by *which +macro's expansion* fails, and each cross-links to that macro's per-entrypoint +implementation document, so the fixture tree mirrors [docs/implementation/entrypoints/](../../docs/implementation/entrypoints). Within a subdirectory, write one fixture file per case, named for the failure mode it -probes (`duplicate_key.rs`, `mut_slice_implicit.rs`), and open each with a comment +probes (`duplicate_key.rs`, `missing_dependency.rs`), and open each with a comment stating what it exercises and why it must not compile — exactly as the main suite requires. The driver [tests/compile_fail_tests.rs](cgp-compile-fail-tests/tests/compile_fail_tests.rs) globs both trees with `**`, so the two `t.compile_fail(...)` calls pick up a new diff --git a/crates/tests/README.md b/crates/tests/README.md index d02605aa..4a3c545f 100644 --- a/crates/tests/README.md +++ b/crates/tests/README.md @@ -60,9 +60,10 @@ for parser corner cases, and the failure-case targets `parser_rejections` and `cgp-compile-fail-tests` is laid out for `trybuild` instead: a single driver `tests/compile_fail_tests.rs` recursively glob-compiles every fixture under `tests/acceptable/` and `tests/problematic/`, which are further grouped into one -subdirectory per owning macro (`acceptable/delegate_components/`, -`problematic/cgp_fn/`). Each fixture is a self-contained program paired with a -committed `.stderr` snapshot of the compiler output it must produce. +subdirectory per owning macro (`acceptable/delegate_components/`, or a +`problematic//` when a defect is pinned). Each fixture is a self-contained +program paired with a committed `.stderr` snapshot of the compiler output it must +produce. ## Snapshots diff --git a/crates/tests/cgp-compile-fail-tests/src/lib.rs b/crates/tests/cgp-compile-fail-tests/src/lib.rs index 8a7f6b77..09da1975 100644 --- a/crates/tests/cgp-compile-fail-tests/src/lib.rs +++ b/crates/tests/cgp-compile-fail-tests/src/lib.rs @@ -46,11 +46,11 @@ //! //! Under each category directory, fixtures are grouped into one subdirectory per //! **owning macro** — the macro whose expansion produces the failure and whose -//! implementation document documents it (`acceptable/delegate_components/`, -//! `problematic/cgp_fn/`). This mirrors the per-entrypoint layout of the -//! implementation docs, so a fixture and the document that indexes it share a -//! name. Within a subdirectory, write one fixture file per case, named for the -//! failure mode it probes (`duplicate_key.rs`, `mut_slice_implicit.rs`), and open +//! implementation document documents it (`acceptable/delegate_components/`, or a +//! `problematic//` when a defect is pinned). This mirrors the per-entrypoint +//! layout of the implementation docs, so a fixture and the document that indexes it +//! share a name. Within a subdirectory, write one fixture file per case, named for +//! the failure mode it probes (`duplicate_key.rs`, `missing_dependency.rs`), and open //! each with a comment stating what it exercises and why it must not compile. The //! driver `tests/compile_fail_tests.rs` globs both trees with `**`, so a new //! fixture is picked up with no registration. diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.rs b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.rs deleted file mode 100644 index 32f2b624..00000000 --- a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Problematic failure: `#[cgp_fn]` does not support a mutable-slice implicit -//! argument. With a `&mut self` receiver, a `&mut [u8]` implicit skips the slice -//! case (which only matches a shared `&[T]`, since there is no `AsMut<[T]>` -//! counterpart) and falls through to the plain-reference mode over the field -//! type `[u8]`. The macro then emits a bound on `HasFieldMut` -//! that no context can satisfy, because `[u8]` is unsized and cannot be a field -//! type. The macro *accepts* the input rather than rejecting it, so the failure -//! lands on the emitted code at the call site. -//! -//! (Note the contrast with a `&self` receiver, which the macro *does* reject -//! cleanly with "&mut self is required for mutable field reference" — that -//! rejection is covered by an `assert_macro_rejects` case in `cgp-macro-tests`, -//! not here.) -//! -//! The correct behavior would be either to support the mutable-slice mode or to -//! reject a `&mut [T]` implicit at macro time with a clear message. This fixture -//! pins the current, worse behavior; its `.stderr` should improve when the -//! defect is fixed. -//! -//! See docs/implementation/entrypoints/cgp_fn.md (Known issues). - -use cgp::prelude::*; - -#[cgp_fn] -fn zero_all(&mut self, #[implicit] items: &mut [u8]) { - for x in items.iter_mut() { - *x = 0; - } -} - -#[derive(HasField)] -pub struct Buffer { - pub items: Vec, -} - -fn main() { - let mut buffer = Buffer { - items: vec![1, 2, 3], - }; - buffer.zero_all(); -} diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.stderr b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.stderr deleted file mode 100644 index f69892f8..00000000 --- a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error[E0599]: the method `zero_all` exists for struct `Buffer`, but its trait bounds were not satisfied - --> tests/problematic/cgp_fn/mut_slice_implicit.rs:40:12 - | -32 | pub struct Buffer { - | ----------------- method `zero_all` not found for this struct because it doesn't satisfy `<_ as HasField>>>>>>>::Value = [u8]` or `Buffer: ZeroAll` -... -40 | buffer.zero_all(); - | ^^^^^^^^ method cannot be called on `Buffer` due to unsatisfied trait bounds - | -note: trait bound `>>>>>>>::Value = [u8]` was not satisfied - --> tests/problematic/cgp_fn/mut_slice_implicit.rs:24:1 - | -24 | #[cgp_fn] - | ^^^^^^^^^ -25 | fn zero_all(&mut self, #[implicit] items: &mut [u8]) { - | ^^^^^^^^ - = help: items from traits can only be used if the trait is implemented and in scope -note: `ZeroAll` defines an item `zero_all`, perhaps you need to implement it - --> tests/problematic/cgp_fn/mut_slice_implicit.rs:24:1 - | -24 | #[cgp_fn] - | ^^^^^^^^^ - = note: this error originates in the attribute macro `cgp_fn` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.rs b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.rs deleted file mode 100644 index c9b23df8..00000000 --- a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Problematic failure: `#[cgp_fn]` mishandles an `Option<&mut T>` implicit. The -//! implicit's outer type is a path (`Option<…>`), so the macro reads it through -//! the immutable option mode and binds an `Option<&T>` value, which then fails to -//! coerce to the `Option<&mut u8>` the signature declares. The failure lands -//! inside the generated impl body, because the macro accepts the shape rather -//! than rejecting it — only the immutable `Option<&T>` form is actually -//! supported. -//! -//! The correct behavior would be to support the mutable-option mode or to reject -//! an `Option<&mut T>` implicit at macro time. This fixture pins the current -//! behavior; its `.stderr` should improve when the defect is fixed. -//! -//! See docs/implementation/entrypoints/cgp_fn.md (Known issues). - -use cgp::prelude::*; - -#[cgp_fn] -fn take_slot(&self, #[implicit] slot: Option<&mut u8>) -> Option<&mut u8> { - slot -} - -fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.stderr b/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.stderr deleted file mode 100644 index 00a69c57..00000000 --- a/crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error[E0308]: mismatched types - --> tests/problematic/cgp_fn/option_mut_implicit.rs:17:1 - | -17 | #[cgp_fn] - | ^^^^^^^^^ types differ in mutability -18 | fn take_slot(&self, #[implicit] slot: Option<&mut u8>) -> Option<&mut u8> { - | --------------- expected due to this - | - = note: expected enum `Option<&mut u8>` - found enum `Option<&u8>` - = note: this error originates in the attribute macro `cgp_fn` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs index d80375df..49da6fd6 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs @@ -2,11 +2,12 @@ //! argument on a function with no `self` receiver, a `mut` binding pattern on an //! implicit argument, a `&mut` implicit argument that is not the sole implicit //! (its exclusive borrow of the context would conflict with reading any other -//! field), and a malformed `#[implicit]` attribute carrying arguments. Each is a -//! rejection the macro makes during expansion (a returned `Err`), so it is pinned -//! by driving the entrypoint directly here rather than by a `trybuild` -//! compile-fail fixture, which is reserved for input the macro accepts but whose -//! expansion then fails to compile. +//! field), a `&mut`-reference implicit under a `&self` receiver (a mutable field +//! read requires `&mut self`), and a malformed `#[implicit]` attribute carrying +//! arguments. Each is a rejection the macro makes during expansion (a returned +//! `Err`), so it is pinned by driving the entrypoint directly here rather than by a +//! `trybuild` compile-fail fixture, which is reserved for input the macro accepts +//! but whose expansion then fails to compile. //! //! See docs/implementation/entrypoints/cgp_fn.md (Tests) for these failure cases, //! and docs/reference/attributes/implicit.md for the user-facing rules on where @@ -76,6 +77,45 @@ fn rejects_mutable_implicit_with_other_implicit() { ); } +#[test] +fn rejects_mut_slice_implicit_without_mut_self() { + // A `&mut [T]` implicit reads the field through `get_field_mut`, which requires a + // `&mut self` receiver. Under a plain `&self` receiver the mutable slice is + // rejected, mirroring the `&mut T` case. (With `&mut self` the same argument is + // accepted and reads through an `AsMut<[T]>` bound.) + assert_macro_rejects("cgp_fn with a `&mut [T]` implicit under `&self`", || { + cgp_macro_lib::cgp_fn( + quote!(), + quote!( + fn zero_all(&self, #[implicit] items: &mut [u8]) { + let _ = items; + } + ), + ) + }); +} + +#[test] +fn rejects_mut_option_implicit_without_mut_self() { + // An `Option<&mut T>` implicit reads the field mutably through `get_field_mut` + // and `.as_mut()`, so — like any mutable field read — it requires a `&mut self` + // receiver and is rejected under a plain `&self` receiver. (With `&mut self` the + // same argument is accepted.) + assert_macro_rejects( + "cgp_fn with an `Option<&mut T>` implicit under `&self`", + || { + cgp_macro_lib::cgp_fn( + quote!(), + quote!( + fn take_slot(&self, #[implicit] slot: Option<&mut u8>) { + let _ = slot; + } + ), + ) + }, + ); +} + #[test] fn rejects_implicit_attr_with_arguments() { // `#[implicit]` is a bare marker attribute; a list form such as diff --git a/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mut_slice.rs b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mut_slice.rs new file mode 100644 index 00000000..7c5d6a7f --- /dev/null +++ b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mut_slice.rs @@ -0,0 +1,66 @@ +//! `#[cgp_fn]` with a mutable `&mut [T]` `#[implicit]` argument under a `&mut self` +//! receiver. +//! +//! A `&mut [T]` implicit reads any `AsMut<[T]>` field (e.g. `Vec`) through +//! `HasFieldMut`/`get_field_mut` and calls `.as_mut()`, the mutable mirror of the +//! shared `&[T]` slice's `AsRef<[T]>`/`.as_ref()` read. The access mode follows the +//! *argument's* own type, so it is the `&mut` in `&mut [T]` — not the receiver — +//! that selects the mutable read. +//! +//! See docs/reference/macros/cgp_fn.md and docs/reference/attributes/implicit.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_fn; + +snapshot_cgp_fn! { + #[cgp_fn] + pub fn zero_all(&mut self, #[implicit] data: &mut [u8]) { + for byte in data.iter_mut() { + *byte = 0; + } + } + + expand_zero_all(output) { + insta::assert_snapshot!(output, @" + pub trait ZeroAll { + fn zero_all(&mut self); + } + impl<__Context__> ZeroAll for __Context__ + where + Self: HasFieldMut< + Symbol<4, Chars<'d', Chars<'a', Chars<'t', Chars<'a', Nil>>>>>, + Value: AsMut<[u8]> + 'static, + >, + { + fn zero_all(&mut self) { + let data: &mut [u8] = self + .get_field_mut( + ::core::marker::PhantomData::< + Symbol<4, Chars<'d', Chars<'a', Chars<'t', Chars<'a', Nil>>>>>, + >, + ) + .as_mut(); + for byte in data.iter_mut() { + *byte = 0; + } + } + } + ") + } +} + +#[derive(HasField)] +pub struct Buffer { + pub data: Vec, +} + +#[test] +fn test_zero_all() { + let mut buffer = Buffer { + data: vec![1, 2, 3], + }; + + buffer.zero_all(); + + assert_eq!(buffer.data, vec![0, 0, 0]); +} diff --git a/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_mut.rs b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_mut.rs new file mode 100644 index 00000000..2296801d --- /dev/null +++ b/crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_mut.rs @@ -0,0 +1,64 @@ +//! `#[cgp_fn]` with a mutable `Option<&mut T>` `#[implicit]` argument under a +//! `&mut self` receiver. +//! +//! An `Option<&mut T>` implicit reads an `Option` field through +//! `HasFieldMut`/`get_field_mut` and calls `.as_mut()`, yielding `Option<&mut T>` — +//! the mutable mirror of the shared `Option<&T>`'s `.as_ref()` read. The read +//! borrows the field with the *inner* reference's mutability, so the `&mut` inside +//! the `Option` selects the mutable read and requires the `&mut self` receiver. +//! +//! See docs/reference/macros/cgp_fn.md and docs/reference/attributes/implicit.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_fn; + +snapshot_cgp_fn! { + #[cgp_fn] + pub fn clear_slot(&mut self, #[implicit] slot: Option<&mut u8>) { + if let Some(value) = slot { + *value = 0; + } + } + + expand_clear_slot(output) { + insta::assert_snapshot!(output, @" + pub trait ClearSlot { + fn clear_slot(&mut self); + } + impl<__Context__> ClearSlot for __Context__ + where + Self: HasFieldMut< + Symbol<4, Chars<'s', Chars<'l', Chars<'o', Chars<'t', Nil>>>>>, + Value = Option, + >, + { + fn clear_slot(&mut self) { + let slot: Option<&mut u8> = self + .get_field_mut( + ::core::marker::PhantomData::< + Symbol<4, Chars<'s', Chars<'l', Chars<'o', Chars<'t', Nil>>>>>, + >, + ) + .as_mut(); + if let Some(value) = slot { + *value = 0; + } + } + } + ") + } +} + +#[derive(HasField)] +pub struct Register { + pub slot: Option, +} + +#[test] +fn test_clear_slot() { + let mut register = Register { slot: Some(42) }; + + register.clear_slot(); + + assert_eq!(register.slot, Some(0)); +} diff --git a/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs b/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs index 857fbd58..f0904f0d 100644 --- a/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs +++ b/crates/tests/cgp-tests/tests/implicit_arguments/mod.rs @@ -6,7 +6,9 @@ pub mod cgp_fn_calling_fn; pub mod cgp_fn_custom_trait_name; pub mod cgp_fn_greet; pub mod cgp_fn_multi_and_use_type; +pub mod cgp_fn_mut_slice; pub mod cgp_fn_mutable; +pub mod cgp_fn_option_mut; pub mod cgp_fn_option_str; pub mod cgp_fn_owned_tuple; pub mod cgp_fn_slice; diff --git a/docs/implementation/asts/cgp_getter.md b/docs/implementation/asts/cgp_getter.md index 89536b66..9c65108b 100644 --- a/docs/implementation/asts/cgp_getter.md +++ b/docs/implementation/asts/cgp_getter.md @@ -6,7 +6,7 @@ This stack covers the two getter macros together, because they share the getter- Both macros turn a getter trait's methods into `GetterField`s through the shared `parse_getter_fields` helper, which is the single source of truth for what a getter signature means. A `GetterField` records the field name (the method name), the field type to require, the return type, the receiver mutability, an optional `PhantomData` phantom-argument type, the field mode, and the receiver mode. -The parser enforces the getter-method contract and resolves the two shorthands a reader most needs to know. A getter method must be a plain (non-const, non-async, non-unsafe, non-generic) method whose first argument is a reference — either `&self` (`ReceiverMode::SelfReceiver`) or a typed `&SomeType` receiver (`ReceiverMode::Type`), the latter letting a getter read a field out of a type other than the context, with `Self` rewritten to the context. The return type then determines the field type and the *field mode* — the conversion the getter body applies — through the shared `parse_field_type`: a `&str` return reads a `String` field (`FieldMode::Str`, `.as_str()`), an `Option<&T>` reads `Option` (`FieldMode::OptionRef`, `.as_ref()`), an `Option<&str>` reads `Option` (`FieldMode::OptionStr`, `.as_deref()`), a `&[T]` reads an `AsRef<[T]>` field (`FieldMode::Slice`, `.as_ref()`), an `MRef<'_, T>` wraps the borrow (`FieldMode::MRef`, `MRef::Ref(...)`), a plain `&T` is a bare `FieldMode::Reference`, and any other owned return — a path type, a tuple, or an array — is `FieldMode::Copy` (`.clone()`). The slice mode is keyed off the reference's own mutability rather than the receiver's, which is what lets the same helper serve the `#[implicit]` case where an argument's mutability is independent of the receiver. `parse_getter_fields` also extracts an optional single associated return type and checks that, when present, the trait has exactly one method whose return type matches it. +The parser enforces the getter-method contract and resolves the two shorthands a reader most needs to know. A getter method must be a plain (non-const, non-async, non-unsafe, non-generic) method whose first argument is a reference — either `&self` (`ReceiverMode::SelfReceiver`) or a typed `&SomeType` receiver (`ReceiverMode::Type`), the latter letting a getter read a field out of a type other than the context, with `Self` rewritten to the context. The return type then determines the field type, the *field mode* — the conversion the getter body applies — and whether the read borrows the field mutably, all through the shared `parse_field_type`, which returns the field type, the `FieldMode`, and an `Option` for the access mutability: a `&str` return reads a `String` field (`FieldMode::Str`, `.as_str()`), an `Option<&T>` reads `Option` (`FieldMode::OptionRef`, `.as_ref()`), an `Option<&str>` reads `Option` (`FieldMode::OptionStr`, `.as_deref()`), a `&[T]` reads an `AsRef<[T]>` field (`FieldMode::Slice`, `.as_ref()`), an `MRef<'_, T>` wraps the borrow (`FieldMode::MRef`, `MRef::Ref(...)`), a plain `&T` is a bare `FieldMode::Reference`, and any other owned return — a path type, a tuple, or an array — is `FieldMode::Copy` (`.clone()`). A `&mut` in the type — the outer reference of `&mut T`/`&mut [T]` or the inner reference of `Option<&mut T>` — sets the access mutability, reusing the same `FieldMode` but selecting the `HasFieldMut`/`get_field_mut` read and the mutable conversion (`.as_mut()`, `.as_deref_mut()`, or an `AsMut<[T]>` bound for a mutable slice); a mutable read requires a `&mut self` receiver. Because the mutability is keyed off the type's own reference rather than the receiver, the same helper serves the `#[implicit]` case where an argument's mutability is independent of the receiver — a getter discards the returned mutability and keys off its receiver instead. `parse_getter_fields` also extracts an optional single associated return type and checks that, when present, the trait has exactly one method whose return type matches it. The getter-method body itself is built by `derive_getter_method` from the `types/getter/` module, which emits `receiver.get_field(PhantomData::) ` for the field's mode; the same `FieldMode` and `GetFieldWithModeExpr` types drive the `#[implicit]` bindings in the [`cgp_fn` stack](cgp_fn.md), which is why the two families convert fields identically. @@ -24,7 +24,7 @@ Its `to_items` emits the trait unchanged plus one blanket impl, built by `to_bla - `to_use_fields_impl` builds the `UseFields` impl, keyed by method name: for each field it emits the getter-method body reading `Symbol!("field_name")` and requires the matching `HasField` bound on the receiver type. Always emitted. - `to_use_field_impl` builds the `UseField<__Tag__>` impl, where `__Tag__` is a *free* generic parameter added to the impl generics, so the getter reads whatever field the wiring supplies. Emitted only for a single-getter trait. -- `to_with_provider_impl` builds the `WithProvider<__Provider__>` impl, which delegates field access to an inner `FieldGetter`/`MutFieldGetter` provider (a slice field uses an `AsRef<[T]>`-valued bound). Emitted only for a single-getter trait. +- `to_with_provider_impl` builds the `WithProvider<__Provider__>` impl, which delegates field access to an inner `FieldGetter`/`MutFieldGetter` provider (a slice field uses an `AsRef<[T]>`-valued bound, or `AsMut<[T]>` for a mutable slice). Emitted only for a single-getter trait. Each of these threads the optional associated type through as an extra generic parameter, exactly as the auto-getter blanket impl does, keeping the three impls consistent with the consumer trait. diff --git a/docs/implementation/entrypoints/cgp_auto_getter.md b/docs/implementation/entrypoints/cgp_auto_getter.md index 77ba725a..05a0530f 100644 --- a/docs/implementation/entrypoints/cgp_auto_getter.md +++ b/docs/implementation/entrypoints/cgp_auto_getter.md @@ -44,7 +44,7 @@ where } ``` -The conversions are exactly those `#[cgp_getter]` uses — `&str` reads a `String` field and appends `.as_str()`, `Option<&T>` reads `Option` and appends `.as_ref()`, `&[T]` reads an `AsRef<[T]>` field, `MRef<'_, T>` wraps the borrow in `MRef::Ref`, an owned return appends `.clone()`, and a plain `&T` is taken by reference — because both macros share the [getter-field parsing](../asts/cgp_getter.md). The difference is only that `#[cgp_auto_getter]` emits a blanket impl on `__Context__` while `#[cgp_getter]` emits provider impls. +The conversions are exactly those `#[cgp_getter]` uses — `&str` reads a `String` field and appends `.as_str()`, `Option<&T>` reads `Option` and appends `.as_ref()`, `&[T]` reads an `AsRef<[T]>` field, `MRef<'_, T>` wraps the borrow in `MRef::Ref`, an owned return appends `.clone()`, and a plain `&T` is taken by reference — plus the mutable mirrors selected by a `&mut self` receiver (`&mut [T]` reads an `AsMut<[T]>` field via `.as_mut()`, `Option<&mut T>` via `.as_mut()`, `Option<&mut str>` via `.as_deref_mut()`) — because both macros share the [getter-field parsing](../asts/cgp_getter.md). The difference is only that `#[cgp_auto_getter]` emits a blanket impl on `__Context__` while `#[cgp_getter]` emits provider impls. ## Behavior and corner cases diff --git a/docs/implementation/entrypoints/cgp_fn.md b/docs/implementation/entrypoints/cgp_fn.md index 96ed2e4f..490cfe3d 100644 --- a/docs/implementation/entrypoints/cgp_fn.md +++ b/docs/implementation/entrypoints/cgp_fn.md @@ -50,7 +50,7 @@ where } ``` -The conversion applied to each binding is chosen by the argument's type, following the same field-mode rules the getter macros use: a `&str` argument reads a `String` field and appends `.as_str()`, an owned value — a path type, tuple, or array — appends `.clone()`, an `Option<&T>` reads an `Option` field and appends `.as_ref()`, an `Option<&str>` reads an `Option` field and appends `.as_deref()`, an `&[T]` reads an `AsRef<[T]>` field and appends `.as_ref()`, and a plain `&T` is taken by reference with no conversion. A `&mut T` argument reads through `HasFieldMut`/`get_field_mut` and requires a `&mut self` receiver; every immutable argument reads through `HasField`/`get_field` regardless of the receiver. The slice and option modes are keyed off the *argument's own* reference, not the receiver's, so an immutable `&[T]` keeps its `AsRef<[T]>` bound even under a `&mut self` receiver rather than collapsing into an unsatisfiable `Value = [T]`. These modes are shared with `#[cgp_auto_getter]` and `#[cgp_getter]` through the [field-parsing helpers](../asts/cgp_getter.md); the difference is only where the read lands — a prepended `let` in the body here, a getter-method body there. +The conversion applied to each binding is chosen by the argument's type, following the same field-mode rules the getter macros use: a `&str` argument reads a `String` field and appends `.as_str()`, an owned value — a path type, tuple, or array — appends `.clone()`, an `Option<&T>` reads an `Option` field and appends `.as_ref()`, an `Option<&str>` reads an `Option` field and appends `.as_deref()`, an `&[T]` reads an `AsRef<[T]>` field and appends `.as_ref()`, and a plain `&T` is taken by reference with no conversion. Each reference mode has a mutable mirror selected by a `&mut` in the argument's type: a `&mut T` reads through `HasFieldMut`/`get_field_mut`; a `&mut [T]` reads an `AsMut<[T]>` field and appends `.as_mut()`; an `Option<&mut T>` reads an `Option` field and appends `.as_mut()`; an `Option<&mut str>` appends `.as_deref_mut()`. Every mutable read requires a `&mut self` receiver, while every immutable argument reads through `HasField`/`get_field` regardless of the receiver. The slice and option modes are keyed off the *argument's own* reference, not the receiver's, so an immutable `&[T]` keeps its shared `AsRef<[T]>` bound even under a `&mut self` receiver rather than being forced mutable. These modes are shared with `#[cgp_auto_getter]` and `#[cgp_getter]` through the [field-parsing helpers](../asts/cgp_getter.md); the difference is only where the read lands — a prepended `let` in the body here, a getter-method body there. ## Behavior and corner cases @@ -62,30 +62,12 @@ The conversion applied to each binding is chosen by the argument's type, followi **A malformed `#[implicit]` attribute is rejected at extraction.** `#[implicit]` is recognized only as a bare `Meta::Path`; the extraction pass additionally checks for an `implicit`-named attribute in any other form (`#[implicit(...)]` or `#[implicit = ...]`) and returns a spanned error, rather than leaving the stray attribute on the parameter to surface downstream as an obscure `cannot find attribute 'implicit'` error. -**A `&mut` implicit argument must be the only implicit argument.** The `field_mut` of each argument follows the argument's own type — set from the `&mut` in `&mut T`, not from the receiver — so a `&mut T` argument reads through `get_field_mut` while every immutable argument reads through `get_field`, even under a `&mut self` receiver. Because a `get_field_mut` read borrows the whole context exclusively for the rest of the body, a `&mut` implicit cannot coexist with any other implicit read; extraction rejects the combination (`has_mutable && count > 1`) rather than emit a blanket impl that fails to borrow-check. Any number of purely immutable implicits, by contrast, are shared borrows and combine freely. A `&mut T` argument additionally requires the `&mut self` receiver, and a `mut` *pattern* on any implicit argument is rejected outright. These checks are enforced during implicit-argument extraction. +**A mutable implicit argument must be the only implicit argument.** The `field_mut` of each argument follows the argument's own type — set from a `&mut` in the type, whether the outer reference of a `&mut T`/`&mut [T]` or the inner reference of an `Option<&mut T>`, not from the receiver — so a mutable argument reads through `get_field_mut` while every immutable argument reads through `get_field`, even under a `&mut self` receiver. Because a `get_field_mut` read borrows the whole context exclusively for the rest of the body, a mutable implicit cannot coexist with any other implicit read; extraction rejects the combination (`has_mutable && count > 1`) rather than emit a blanket impl that fails to borrow-check. Any number of purely immutable implicits, by contrast, are shared borrows and combine freely. A mutable argument additionally requires the `&mut self` receiver (checked in `parse_field_type`), and a `mut` *pattern* on any implicit argument is rejected outright. These checks are enforced during implicit-argument extraction. ## Known issues `#[cgp_fn]` does not support generics on the desugared *method* itself — generic parameters are only ever lifted onto the trait and impl. A method-level generic is silently treated as a trait/impl generic rather than rejected, which is the intended limitation rather than a bug: method-level generics are considered an advanced case better written as an explicit blanket impl or a [`#[cgp_component]`](../../reference/macros/cgp_component.md) provider. -The *mutable* variants of the slice and option modes are not supported, and both are **problematic** failures — the macro accepts the shape rather than rejecting it, so the failure lands on the emitted code instead of as a spanned message. Both are exotic shapes rather than a regression (the immutable `&[T]` and `Option<&T>` forms are the supported ones), and the correct behavior would be either to support the mutable modes or to reject them at macro time with a clear message. Each is pinned by a fixture under [problematic/cgp_fn/](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn) in `cgp-compile-fail-tests`. - -Under a `&mut self` receiver, a `&mut [T]` implicit falls through the slice case (which matches only a shared `&[T]`, since there is no `AsMut<[T]>` counterpart) into the plain-reference mode, producing an unsatisfiable `HasFieldMut` bound that surfaces at the call site (`E0599`): - -```rust -#[cgp_fn] -fn zero_all(&mut self, #[implicit] items: &mut [u8]) { /* … */ } // E0599 when called -``` - -Note the contrast with a `&self` receiver, where the same `&mut [T]` is instead rejected cleanly at macro time with "&mut self is required for mutable field reference" — that rejection is a returned `Err` covered in `parser_rejections`, not a compile-fail fixture. - -An `Option<&mut T>` implicit reads immutably (the argument's outer type is a path, so `field_mut` is `None`), producing an `Option<&T>` value that then fails to coerce to the declared `Option<&mut T>` inside the generated body (`E0308`): - -```rust -#[cgp_fn] -fn take_slot(&self, #[implicit] slot: Option<&mut u8>) -> Option<&mut u8> { slot } // E0308 -``` - ## Snapshots Every `snapshot_cgp_fn!` invocation across the suite is indexed here, since these snapshots all belong to this entrypoint: @@ -95,6 +77,8 @@ Every `snapshot_cgp_fn!` invocation across the suite is indexed here, since thes - [implicit_arguments/cgp_fn_mutable.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mutable.rs) — `&mut self` with a mutable implicit argument, reading through `HasFieldMut`/`get_field_mut`. - [implicit_arguments/cgp_fn_mut_self_immutable.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mut_self_immutable.rs) — a `&mut self` receiver with two *immutable* implicit arguments, showing they read through `HasField`/`get_field` (the access mode follows the argument, not the receiver) and that several immutable implicits may share a `&mut self` receiver. - [implicit_arguments/cgp_fn_slice.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_slice.rs) — an immutable `&[u8]` implicit under a `&mut self` receiver, keeping its `Value: AsRef<[u8]> + 'static` bound and `.as_ref()` read because the slice mode follows the argument's own reference, not the receiver's; also mixes an implicit with a plain explicit argument. +- [implicit_arguments/cgp_fn_mut_slice.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_mut_slice.rs) — a mutable `&mut [u8]` implicit, reading through `HasFieldMut` with a `Value: AsMut<[u8]> + 'static` bound and a `get_field_mut(...).as_mut()` read, the mutable mirror of the shared-slice case. +- [implicit_arguments/cgp_fn_option_mut.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_mut.rs) — a mutable `Option<&mut u8>` implicit, reading an `Option` field through `HasFieldMut` with a `get_field_mut(...).as_mut()` read that yields `Option<&mut u8>`. - [implicit_arguments/cgp_fn_owned_tuple.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_owned_tuple.rs) — an owned, non-path implicit (a tuple) read by value with `.clone()` and a `Value = (f64, f64)` bound, showing owned types beyond path types are accepted. - [implicit_arguments/cgp_fn_option_str.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_option_str.rs) — an `Option<&str>` implicit backed by an `Option` field and read with `.as_deref()`, composing the `&str` and option modes. - [implicit_arguments/cgp_fn_calling_fn.rs](../../../crates/tests/cgp-tests/tests/implicit_arguments/cgp_fn_calling_fn.rs) — one `#[cgp_fn]` capability depending on another through an explicit `where Self:` bound. @@ -119,12 +103,7 @@ Because `#[cgp_fn]` emits a blanket impl, its snapshot tests double as behaviora The failure cases pin the inputs `#[cgp_fn]` refuses during expansion, each asserting the entrypoint returns `Err`: -- [parser_rejections/cgp_fn.rs](../../../crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs) covers an implicit argument on a function with no `self` receiver, a `mut` binding pattern on an implicit argument, a `&mut` implicit argument that is not the sole implicit (which would borrow the context mutably and again at once), and a malformed `#[implicit]` attribute carrying arguments (`#[implicit(...)]` or `#[implicit = ...]`). - -The compile-fail fixtures in `cgp-compile-fail-tests` pin the problematic mutable-mode expansions the macro accepts but should not: - -- [problematic/cgp_fn/mut_slice_implicit.rs](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/mut_slice_implicit.rs) — a `&mut [u8]` implicit under a `&mut self` receiver expands to an unsatisfiable `HasFieldMut` bound (`E0599`). -- [problematic/cgp_fn/option_mut_implicit.rs](../../../crates/tests/cgp-compile-fail-tests/tests/problematic/cgp_fn/option_mut_implicit.rs) — an `Option<&mut T>` implicit binds an `Option<&T>` value that fails to coerce to the declared `Option<&mut T>` (`E0308`). +- [parser_rejections/cgp_fn.rs](../../../crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_fn.rs) covers an implicit argument on a function with no `self` receiver, a `mut` binding pattern on an implicit argument, a `&mut` implicit argument that is not the sole implicit (which would borrow the context mutably and again at once), a malformed `#[implicit]` attribute carrying arguments (`#[implicit(...)]` or `#[implicit = ...]`), and the two mutable-reference forms under a `&self` receiver — a `&mut [T]` slice and an `Option<&mut T>` — each of which reads through `get_field_mut` and so requires `&mut self`. ## Source diff --git a/docs/implementation/entrypoints/cgp_getter.md b/docs/implementation/entrypoints/cgp_getter.md index 1877c53c..884cd030 100644 --- a/docs/implementation/entrypoints/cgp_getter.md +++ b/docs/implementation/entrypoints/cgp_getter.md @@ -54,7 +54,7 @@ Every provider-trait impl is paired with a matching `IsProviderFor` impl carryin **A single associated return type is supported and inferred from the field.** A getter trait may declare one `type Name;` used as the return type; the associated type is added as an extra generic parameter to each provider impl, set to itself via `type Name = Name;`, and any bound on it (for example `Name: Display`) is carried onto the impl with `Self::Name` rewritten to the parameter. More than one associated type, or an associated type alongside more than one method, is rejected during field parsing. -**The return-type shorthands are shared with the auto-getter.** The `&str`-reads-`String`, `Option<&T>`-reads-`Option`, `&[T]`-reads-`AsRef<[T]>`, `MRef<'_, T>`, and owned-`.clone()` conversions all come from the shared field-mode parsing, so `#[cgp_getter]` and [`#[cgp_auto_getter]`](cgp_auto_getter.md) treat a given signature identically — they differ only in the items they emit around the shared getter-method body. +**The return-type shorthands are shared with the auto-getter.** The `&str`-reads-`String`, `Option<&T>`-reads-`Option`, `&[T]`-reads-`AsRef<[T]>`, `MRef<'_, T>`, and owned-`.clone()` conversions, along with their mutable mirrors under a `&mut self` receiver (`&mut [T]`-reads-`AsMut<[T]>`, `Option<&mut T>` via `.as_mut()`), all come from the shared field-mode parsing, so `#[cgp_getter]` and [`#[cgp_auto_getter]`](cgp_auto_getter.md) treat a given signature identically — they differ only in the items they emit around the shared getter-method body. ## Snapshots diff --git a/docs/reference/attributes/implicit.md b/docs/reference/attributes/implicit.md index 162d96b8..a137ef35 100644 --- a/docs/reference/attributes/implicit.md +++ b/docs/reference/attributes/implicit.md @@ -22,7 +22,7 @@ fn area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 { The argument name doubles as the field name. Here `width` and `height` name both the local variables used in the body and the context fields the values are read from, via `Symbol!("width")` and `Symbol!("height")`. The argument type is the type the body sees, and it determines how the field is accessed (described under Expansion). -Three rules constrain where `#[implicit]` may appear. The function must take `self` as its first argument, because the field is read from `self`; a function with implicit arguments but no receiver is rejected. The argument pattern must be a bare identifier, not a destructuring or `mut` pattern — to get a mutable local, clone the injected value explicitly inside the body. And a `&mut`-reference implicit argument (`&mut T`) must be the *only* implicit argument on its function, and requires a `&mut self` receiver: it is read through `get_field_mut`, which borrows the whole context exclusively, so it cannot coexist with any other field read. Immutable implicit arguments carry no such restriction — they are shared borrows and combine freely, in any number, on either a `&self` or a `&mut self` receiver. +Three rules constrain where `#[implicit]` may appear. The function must take `self` as its first argument, because the field is read from `self`; a function with implicit arguments but no receiver is rejected. The argument pattern must be a bare identifier, not a destructuring or `mut` pattern — to get a mutable local, clone the injected value explicitly inside the body. And a *mutable* implicit argument — one whose type carries a `&mut`, whether the outer reference of a `&mut T`/`&mut [T]` or the inner reference of an `Option<&mut T>` — must be the *only* implicit argument on its function, and requires a `&mut self` receiver: it is read through `get_field_mut`, which borrows the whole context exclusively, so it cannot coexist with any other field read. Immutable implicit arguments carry no such restriction — they are shared borrows and combine freely, in any number, on either a `&self` or a `&mut self` receiver. `#[implicit]` is usable wherever CGP rewrites function bodies into providers: inside [`#[cgp_fn]`](../macros/cgp_fn.md) and inside the methods of a [`#[cgp_impl]`](../macros/cgp_impl.md) block. It is not a standalone macro — it is only meaningful as an argument attribute consumed by those macros. @@ -60,7 +60,7 @@ where The two `let` bindings are inserted at the top of the body in argument order, before any of the original statements, so the names are in scope for the rest of the function. The generated context type parameter is literally named `__Context__` in the emitted code; the examples here use `Context` for readability. -The access expression depends on the argument type, following the same rules as [`#[cgp_auto_getter]`](../macros/cgp_auto_getter.md). An owned type — a path type such as `f64` or `String`, or a tuple or array — is read by reference and `.clone()`d, so the body receives an owned value; a plain `&T` is taken by reference with no conversion. Four forms are special: `&str` is backed by a `String` field and read with `.as_str()`; `&[T]` reads any field whose value implements `AsRef<[T]>` and calls `.as_ref()`; `Option<&T>` reads an `Option` field via `.as_ref()`; and `Option<&str>` reads an `Option` field via `.as_deref()`. The mutability of the access follows the *argument's* own type, not the receiver's: only a `&mut T` argument reads through `HasFieldMut`/`get_field_mut`, while every immutable argument — a `&[T]` slice included — reads through `HasField`/`get_field` even on a `&mut self` receiver. Concretely: +The access expression depends on the argument type, following the same rules as [`#[cgp_auto_getter]`](../macros/cgp_auto_getter.md). An owned type — a path type such as `f64` or `String`, or a tuple or array — is read by reference and `.clone()`d, so the body receives an owned value; a plain `&T` is taken by reference with no conversion. Four forms are special: `&str` is backed by a `String` field and read with `.as_str()`; `&[T]` reads any field whose value implements `AsRef<[T]>` and calls `.as_ref()`; `Option<&T>` reads an `Option` field via `.as_ref()`; and `Option<&str>` reads an `Option` field via `.as_deref()`. The mutability of the access follows the *argument's* own type, not the receiver's: an argument carrying a `&mut` reads through `HasFieldMut`/`get_field_mut`, while every immutable argument — a `&[T]` slice included — reads through `HasField`/`get_field` even on a `&mut self` receiver. Each reference form has a mutable mirror: a `&mut [T]` reads a field implementing `AsMut<[T]>` via `.as_mut()`, an `Option<&mut T>` reads an `Option` field via `.as_mut()`, and an `Option<&mut str>` reads an `Option` field via `.as_deref_mut()`. Every mutable form requires a `&mut self` receiver, as described under Syntax. Concretely: ```rust #[cgp_fn] @@ -113,10 +113,6 @@ fn print_area(rect: &Rectangle) { `#[implicit]` is most often used inside [`#[cgp_fn]`](../macros/cgp_fn.md), which turns a function into a single-implementation capability, and inside [`#[cgp_impl]`](../macros/cgp_impl.md), which writes a provider for an existing component. It relies on [`#[derive(HasField)]`](../derives/derive_has_field.md) on the context to supply the field accessors that the generated bounds require. Its access rules — `.clone()` for owned values, `.as_str()` for `&str` — are shared with [`#[cgp_auto_getter]`](../macros/cgp_auto_getter.md), which defines a reusable getter *capability* trait; prefer an implicit argument for reading a field as a provider's own input, and reserve `#[cgp_auto_getter]` for the case where the field should be published as a `self.name()` accessor other providers depend on. To bring in other CGP capabilities alongside implicit arguments, combine `#[implicit]` with [`#[uses]`](uses.md). -## Known issues - -The mutable variants of the slice and option forms are not supported. A `&mut [T]` argument has no `AsMut<[T]>` counterpart and falls through to a plain-reference bound (`HasFieldMut`) that no context can satisfy, and an `Option<&mut T>` argument is read immutably and so produces a value that does not match its declared type. The macro accepts both, so the failure lands on the expanded code rather than as a spanned error; the mechanics are described in the [implementation document](../../implementation/entrypoints/cgp_fn.md). Use the immutable `&[T]` and `Option<&T>` forms, and clone into a mutable local inside the body when mutation is needed. - ## Source - Parsing: implicit-argument parsing lives in [crates/macros/cgp-macro-core/src/functions/implicits/parse.rs](../../../crates/macros/cgp-macro-core/src/functions/implicits/parse.rs), which extracts `#[implicit]`-marked arguments, validates the `self`/`mut` rules, and rejects a malformed (non-bare) `#[implicit]` attribute. diff --git a/docs/reference/macros/cgp_auto_getter.md b/docs/reference/macros/cgp_auto_getter.md index 29bb112b..e09c2d68 100644 --- a/docs/reference/macros/cgp_auto_getter.md +++ b/docs/reference/macros/cgp_auto_getter.md @@ -33,7 +33,7 @@ pub trait HasDimensions { } ``` -The return type controls how the field value is read, and several shorthand forms are recognized so the method signature stays ergonomic. A plain reference `&T` reads a field of type `T` directly. The form `&str` is treated specially: it reads a `String` field and calls `.as_str()` on it, so you can return `&str` while the context stores a `String`. Other recognized forms include `Option<&T>` (an `Option` field returned via `.as_ref()`), `Option<&str>` (an `Option` field returned via `.as_deref()`, composing the `&str` special case with the option case), `&[T]` (a field whose value implements `AsRef<[T]>`), and any owned type — a path type, a tuple, or an array — read by reference and `.clone()`d. A `&mut self` receiver with a `&mut T` return reads the field mutably through `get_field_mut`. +The return type controls how the field value is read, and several shorthand forms are recognized so the method signature stays ergonomic. A plain reference `&T` reads a field of type `T` directly. The form `&str` is treated specially: it reads a `String` field and calls `.as_str()` on it, so you can return `&str` while the context stores a `String`. Other recognized forms include `Option<&T>` (an `Option` field returned via `.as_ref()`), `Option<&str>` (an `Option` field returned via `.as_deref()`, composing the `&str` special case with the option case), `&[T]` (a field whose value implements `AsRef<[T]>`), and any owned type — a path type, a tuple, or an array — read by reference and `.clone()`d. A `&mut self` receiver reads the field mutably through `get_field_mut`, and each reference form has a mutable mirror: a `&mut T` return, a `&mut [T]` return (a field implementing `AsMut<[T]>`, read via `.as_mut()`), an `Option<&mut T>` return (via `.as_mut()`), and an `Option<&mut str>` return (via `.as_deref_mut()`). A getter trait may also declare a single associated type and use it as the method's return type, which lets the abstract type be inferred from the field. This is covered under Expansion below. When an associated type is present, the trait must contain exactly one getter method, and that method's return type must be `&Self::AssocType`. diff --git a/docs/skills/cgp/references/functions-and-getters.md b/docs/skills/cgp/references/functions-and-getters.md index 1ef5f034..dd04a000 100644 --- a/docs/skills/cgp/references/functions-and-getters.md +++ b/docs/skills/cgp/references/functions-and-getters.md @@ -124,7 +124,7 @@ pub fn greet(&self, #[implicit] name: &str) { // binding: let name: &str = self.get_field(PhantomData::).as_str(); ``` -Three rules constrain where `#[implicit]` may appear. The function must take `self` as its first argument, since the field is read from it; the argument pattern must be a bare identifier, not a destructuring or `mut` pattern (clone inside the body for a mutable local); and a `&mut T` implicit argument must be the *only* implicit argument (and needs a `&mut self` receiver), since it reads through `get_field_mut` and borrows the whole context exclusively — immutable implicits are shared borrows and combine freely in any number. The access mode follows the argument's own type, not the receiver's: only a `&mut T` argument reads through `get_field_mut`. `#[implicit]` is usable in both `#[cgp_fn]` and the methods of a `#[cgp_impl]` provider, with the same desugaring in each — inside `#[cgp_impl]` the `HasField` bounds simply join the provider impl's `where` clause. +Three rules constrain where `#[implicit]` may appear. The function must take `self` as its first argument, since the field is read from it; the argument pattern must be a bare identifier, not a destructuring or `mut` pattern (clone inside the body for a mutable local); and a mutable implicit argument — one whose type carries a `&mut` (a `&mut T`, `&mut [T]`, or `Option<&mut T>`) — must be the *only* implicit argument (and needs a `&mut self` receiver), since it reads through `get_field_mut` and borrows the whole context exclusively — immutable implicits are shared borrows and combine freely in any number. The access mode follows the argument's own type, not the receiver's: only an argument carrying a `&mut` reads through `get_field_mut` (a `&mut [T]` via an `AsMut<[T]>` field, an `Option<&mut T>` via `.as_mut()`). `#[implicit]` is usable in both `#[cgp_fn]` and the methods of a `#[cgp_impl]` provider, with the same desugaring in each — inside `#[cgp_impl]` the `HasField` bounds simply join the provider impl's `where` clause. ## Importing capabilities: `#[uses]` @@ -196,7 +196,7 @@ where } ``` -That generated blanket impl is the point of the macro, and it follows the same access rules as `#[implicit]`: a plain `&T` return reads a `T` field directly, while the `&str` shorthand reads a `String` field and appends `.as_str()`. Other shorthands include `Option<&T>` (an `Option` field via `.as_ref()`), `Option<&str>` (an `Option` field via `.as_deref()`), `&[T]` (a field implementing `AsRef<[T]>`), an owned type — path, tuple, or array — via `.clone()`, and `&mut T` with a `&mut self` receiver (mutable access via `get_field_mut`). A trait may declare several methods, each mapping independently to its own field — `fn width(&self) -> &f64; fn height(&self) -> &f64;` produces one `where` predicate and one body per field in the same impl. +That generated blanket impl is the point of the macro, and it follows the same access rules as `#[implicit]`: a plain `&T` return reads a `T` field directly, while the `&str` shorthand reads a `String` field and appends `.as_str()`. Other shorthands include `Option<&T>` (an `Option` field via `.as_ref()`), `Option<&str>` (an `Option` field via `.as_deref()`), `&[T]` (a field implementing `AsRef<[T]>`), an owned type — path, tuple, or array — via `.clone()`, and, with a `&mut self` receiver, the mutable mirrors `&mut T`, `&mut [T]` (a field implementing `AsMut<[T]>`, via `.as_mut()`), and `Option<&mut T>` (via `.as_mut()`) — all through `get_field_mut`. A trait may declare several methods, each mapping independently to its own field — `fn width(&self) -> &f64; fn height(&self) -> &f64;` produces one `where` predicate and one body per field in the same impl. A single getter may also declare a local associated type and use it as its return type, which lets the abstract type be inferred from the field. The trait must then contain exactly one method returning `&Self::AssocType`; the macro lifts the type into a generic parameter on the impl and binds it through the `HasField` `Value`: From 59f771b1ef9bff2dd31c0059f79eb280ce314a01 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 22:09:37 +0200 Subject: [PATCH 06/17] Review `#[cgp_getter]` and `#[cgp_auto_getter]` --- crates/macros/cgp-macro-core/src/exports.rs | 1 + .../src/functions/getter/parse.rs | 7 +- .../src/types/getter/field_mode.rs | 50 +++++ .../types/getter/get_field_with_mode_expr.rs | 67 +------ .../cgp-macro-core/src/types/getter/method.rs | 75 +------- .../cgp-macro-lib/src/cgp_auto_getter.rs | 2 +- crates/main/cgp-core/src/prelude.rs | 2 +- .../tests/parser_rejections/getters.rs | 178 ++++++++++++++++++ .../tests/parser_rejections/mod.rs | 1 + crates/tests/cgp-tests/tests/getters/mod.rs | 4 + crates/tests/cgp-tests/tests/getters/mref.rs | 1 - .../cgp-tests/tests/getters/mref_auto.rs | 1 - .../cgp-tests/tests/getters/mut_getter.rs | 164 ++++++++++++++++ .../tests/getters/mut_option_auto.rs | 55 ++++++ .../cgp-tests/tests/getters/mut_slice_auto.rs | 54 ++++++ .../cgp-tests/tests/getters/mut_str_auto.rs | 56 ++++++ docs/implementation/asts/cgp_getter.md | 4 +- .../entrypoints/cgp_auto_getter.md | 5 +- docs/implementation/entrypoints/cgp_getter.md | 4 + docs/reference/types/mref.md | 2 +- 20 files changed, 591 insertions(+), 142 deletions(-) create mode 100644 crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs create mode 100644 crates/tests/cgp-tests/tests/getters/mut_getter.rs create mode 100644 crates/tests/cgp-tests/tests/getters/mut_option_auto.rs create mode 100644 crates/tests/cgp-tests/tests/getters/mut_slice_auto.rs create mode 100644 crates/tests/cgp-tests/tests/getters/mut_str_auto.rs diff --git a/crates/macros/cgp-macro-core/src/exports.rs b/crates/macros/cgp-macro-core/src/exports.rs index 35a81199..07eb527f 100644 --- a/crates/macros/cgp-macro-core/src/exports.rs +++ b/crates/macros/cgp-macro-core/src/exports.rs @@ -22,6 +22,7 @@ export_constructs! { UseFields, UseField, Life, + MRef, HasField, HasFields, diff --git a/crates/macros/cgp-macro-core/src/functions/getter/parse.rs b/crates/macros/cgp-macro-core/src/functions/getter/parse.rs index c443ba44..095a221e 100644 --- a/crates/macros/cgp-macro-core/src/functions/getter/parse.rs +++ b/crates/macros/cgp-macro-core/src/functions/getter/parse.rs @@ -188,7 +188,12 @@ fn parse_fixed_size_args( args.iter() .collect::>() .try_into() - .map_err(|_| Error::new(args.span(), "expect getter method to contain {I} arguments")) + .map_err(|_| { + Error::new( + args.span(), + format!("expect getter method to contain {I} arguments"), + ) + }) } fn parse_phantom_arg_type(phantom_arg: &FnArg) -> syn::Result { diff --git a/crates/macros/cgp-macro-core/src/types/getter/field_mode.rs b/crates/macros/cgp-macro-core/src/types/getter/field_mode.rs index 0102740a..94167368 100644 --- a/crates/macros/cgp-macro-core/src/types/getter/field_mode.rs +++ b/crates/macros/cgp-macro-core/src/types/getter/field_mode.rs @@ -1,3 +1,11 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::token::Mut; + +use crate::exports::MRef; + +/// The conversion a getter body applies to a raw field read, chosen from the +/// getter method's return type by [`parse_field_type`](crate::functions::parse_field_type). #[derive(Clone, Eq, PartialEq)] pub enum FieldMode { Reference, @@ -8,3 +16,45 @@ pub enum FieldMode { Copy, Slice, } + +impl FieldMode { + /// Wrap a field-read expression in the conversion this mode calls for, picking + /// the shared or mutable variant from `field_mut`. This is the single source of + /// truth shared by the getter-method body and the `#[implicit]` argument + /// binding, so both families convert a given field identically. + pub fn apply(&self, call_expr: TokenStream, field_mut: &Option) -> TokenStream { + match self { + FieldMode::Reference => call_expr, + FieldMode::OptionRef => { + if field_mut.is_none() { + quote! { #call_expr .as_ref() } + } else { + quote! { #call_expr .as_mut() } + } + } + FieldMode::OptionStr => { + if field_mut.is_none() { + quote! { #call_expr .as_deref() } + } else { + quote! { #call_expr .as_deref_mut() } + } + } + FieldMode::MRef => quote! { #MRef::Ref( #call_expr ) }, + FieldMode::Str => { + if field_mut.is_none() { + quote! { #call_expr .as_str() } + } else { + quote! { #call_expr .as_mut_str() } + } + } + FieldMode::Copy => quote! { #call_expr .clone() }, + FieldMode::Slice => { + if field_mut.is_none() { + quote! { #call_expr .as_ref() } + } else { + quote! { #call_expr .as_mut() } + } + } + } + } +} diff --git a/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs b/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs index 6d3f8d63..e5a57b83 100644 --- a/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs +++ b/crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::ToTokens; use crate::types::getter::{FieldMode, GetFieldExpr}; @@ -10,67 +10,10 @@ pub struct GetFieldWithModeExpr { impl ToTokens for GetFieldWithModeExpr { fn to_tokens(&self, tokens: &mut TokenStream) { - let call_expr = &self.get_field; - - let expr = match self.field_mode { - FieldMode::Reference => { - quote!(#call_expr) - } - FieldMode::OptionRef => { - if call_expr.field_mut.is_none() { - quote! { - #call_expr .as_ref() - } - } else { - quote! { - #call_expr .as_mut() - } - } - } - FieldMode::OptionStr => { - if call_expr.field_mut.is_none() { - quote! { - #call_expr .as_deref() - } - } else { - quote! { - #call_expr .as_deref_mut() - } - } - } - FieldMode::MRef => { - quote! { - MRef::Ref( #call_expr ) - } - } - FieldMode::Str => { - if call_expr.field_mut.is_none() { - quote! { - #call_expr .as_str() - } - } else { - quote! { - #call_expr .as_mut_str() - } - } - } - FieldMode::Copy => { - quote! { - #call_expr .clone() - } - } - FieldMode::Slice => { - if call_expr.field_mut.is_none() { - quote! { - #call_expr .as_ref() - } - } else { - quote! { - #call_expr .as_mut() - } - } - } - }; + let field_mut = self.get_field.field_mut; + let expr = self + .field_mode + .apply(self.get_field.to_token_stream(), &field_mut); tokens.extend(expr); } diff --git a/crates/macros/cgp-macro-core/src/types/getter/method.rs b/crates/macros/cgp-macro-core/src/types/getter/method.rs index 003d1460..4ab83504 100644 --- a/crates/macros/cgp-macro-core/src/types/getter/method.rs +++ b/crates/macros/cgp-macro-core/src/types/getter/method.rs @@ -1,11 +1,10 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::token::Mut; use syn::{Ident, ImplItemFn, Type}; use crate::functions::parse_internal; use crate::types::cgp_getter::GetterField; -use crate::types::getter::{ContextArg, FieldMode}; +use crate::types::getter::ContextArg; pub fn derive_getter_method( context_arg: &ContextArg, @@ -94,11 +93,9 @@ impl GetterMethod { } }; - let call_expr = extend_call_expr( - call_expr, - &getter_field.field_mode, - &getter_field.receiver_mut, - ); + let call_expr = getter_field + .field_mode + .apply(call_expr, &getter_field.receiver_mut); let return_type = &getter_field.return_type; @@ -111,67 +108,3 @@ impl GetterMethod { Ok(item_fn) } } - -fn extend_call_expr( - call_expr: TokenStream, - field_mode: &FieldMode, - field_mut: &Option, -) -> TokenStream { - match field_mode { - FieldMode::Reference => call_expr, - FieldMode::OptionRef => { - if field_mut.is_none() { - quote! { - #call_expr .as_ref() - } - } else { - quote! { - #call_expr .as_mut() - } - } - } - FieldMode::OptionStr => { - if field_mut.is_none() { - quote! { - #call_expr .as_deref() - } - } else { - quote! { - #call_expr .as_deref_mut() - } - } - } - FieldMode::MRef => { - quote! { - MRef::Ref( #call_expr ) - } - } - FieldMode::Str => { - if field_mut.is_none() { - quote! { - #call_expr .as_str() - } - } else { - quote! { - #call_expr .as_mut_str() - } - } - } - FieldMode::Copy => { - quote! { - #call_expr .clone() - } - } - FieldMode::Slice => { - if field_mut.is_none() { - quote! { - #call_expr .as_ref() - } - } else { - quote! { - #call_expr .as_mut() - } - } - } - } -} diff --git a/crates/macros/cgp-macro-lib/src/cgp_auto_getter.rs b/crates/macros/cgp-macro-lib/src/cgp_auto_getter.rs index 3a464739..3d9a05d8 100644 --- a/crates/macros/cgp-macro-lib/src/cgp_auto_getter.rs +++ b/crates/macros/cgp-macro-lib/src/cgp_auto_getter.rs @@ -7,7 +7,7 @@ pub fn cgp_auto_getter(attr: TokenStream, body: TokenStream) -> syn::Result &str; + } + ), + ) + }); +} + +#[test] +fn rejects_async_getter_method() { + assert_macro_rejects("cgp_auto_getter with an async getter method", || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasName { + async fn name(&self) -> &str; + } + ), + ) + }); +} + +#[test] +fn rejects_unsafe_getter_method() { + assert_macro_rejects("cgp_auto_getter with an unsafe getter method", || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasName { + unsafe fn name(&self) -> &str; + } + ), + ) + }); +} + +#[test] +fn rejects_generic_getter_method() { + // A getter method must not carry generic parameters: its field read is keyed by + // the method name alone, with nothing to bind a method-level generic to. + assert_macro_rejects("cgp_auto_getter with a generic getter method", || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasName { + fn name(&self) -> &str; + } + ), + ) + }); +} + +#[test] +fn rejects_by_value_self_receiver() { + // The first argument must be a *reference* — a by-value `self` receiver cannot + // back a getter that borrows a field out of the context. + assert_macro_rejects("cgp_auto_getter with a by-value self receiver", || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasName { + fn name(self) -> &str; + } + ), + ) + }); +} + +#[test] +fn rejects_mutable_return_without_mut_self() { + // A mutable field read borrows the context exclusively through `get_field_mut`, + // so a `&mut` return is only valid under a `&mut self` receiver and is rejected + // under a plain `&self` receiver. + assert_macro_rejects("cgp_auto_getter with `&mut str` under `&self`", || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasName { + fn name(&self) -> &mut str; + } + ), + ) + }); +} + +#[test] +fn rejects_multiple_associated_types() { + // A getter trait may declare at most one associated return type. + assert_macro_rejects("cgp_auto_getter with two associated types", || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasName { + type Name; + type Other; + fn name(&self) -> &Self::Name; + } + ), + ) + }); +} + +#[test] +fn rejects_associated_type_with_multiple_methods() { + // An associated return type is inferred from a single field, so a trait that + // declares one must contain exactly one getter method. + assert_macro_rejects( + "cgp_auto_getter with an associated type and two methods", + || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasName { + type Name; + fn name(&self) -> &Self::Name; + fn other(&self) -> &str; + } + ), + ) + }, + ); +} + +#[test] +fn rejects_associated_const_trait_item() { + // A getter trait may contain only getter methods and at most one associated + // type; any other item (here an associated const) is rejected. + assert_macro_rejects("cgp_auto_getter with an associated const", || { + cgp_macro_lib::cgp_auto_getter( + quote!(), + quote!( + pub trait HasName { + const LIMIT: usize; + fn name(&self) -> &str; + } + ), + ) + }); +} + +#[test] +fn rejects_attribute_argument() { + // `#[cgp_auto_getter]` has no provider name, component, or keys to configure, so + // any attribute argument is rejected up front. + assert_macro_rejects("cgp_auto_getter with an attribute argument", || { + cgp_macro_lib::cgp_auto_getter( + quote!(GetName), + quote!( + pub trait HasName { + fn name(&self) -> &str; + } + ), + ) + }); +} diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs index 57213e24..63e2dbad 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs @@ -26,4 +26,5 @@ pub mod cgp_component; pub mod cgp_fn; pub mod check_components; pub mod delegate_components; +pub mod getters; pub mod use_type; diff --git a/crates/tests/cgp-tests/tests/getters/mod.rs b/crates/tests/cgp-tests/tests/getters/mod.rs index 37ec844a..c84d1781 100644 --- a/crates/tests/cgp-tests/tests/getters/mod.rs +++ b/crates/tests/cgp-tests/tests/getters/mod.rs @@ -9,6 +9,9 @@ pub mod assoc_type_self_referential_auto; pub mod auto_getter_generic; pub mod clone_auto; pub mod mref_auto; +pub mod mut_option_auto; +pub mod mut_slice_auto; +pub mod mut_str_auto; pub mod non_self_auto; pub mod option_auto; pub mod option_str_auto; @@ -21,6 +24,7 @@ pub mod assoc_type_getter; pub mod assoc_type_self_referential; pub mod clone; pub mod mref; +pub mod mut_getter; pub mod non_self; pub mod option; pub mod slice; diff --git a/crates/tests/cgp-tests/tests/getters/mref.rs b/crates/tests/cgp-tests/tests/getters/mref.rs index d77c8081..7cecb7eb 100644 --- a/crates/tests/cgp-tests/tests/getters/mref.rs +++ b/crates/tests/cgp-tests/tests/getters/mref.rs @@ -4,7 +4,6 @@ //! //! See docs/reference/macros/cgp_getter.md and docs/reference/providers/use_field.md. -use cgp::core::field::types::MRef; use cgp::prelude::*; use cgp_macro_test_util::snapshot_cgp_getter; diff --git a/crates/tests/cgp-tests/tests/getters/mref_auto.rs b/crates/tests/cgp-tests/tests/getters/mref_auto.rs index 21b30472..c3545a1f 100644 --- a/crates/tests/cgp-tests/tests/getters/mref_auto.rs +++ b/crates/tests/cgp-tests/tests/getters/mref_auto.rs @@ -4,7 +4,6 @@ //! //! See docs/reference/macros/cgp_auto_getter.md. -use cgp::core::field::types::MRef; use cgp::prelude::*; use cgp_macro_test_util::snapshot_cgp_auto_getter; diff --git a/crates/tests/cgp-tests/tests/getters/mut_getter.rs b/crates/tests/cgp-tests/tests/getters/mut_getter.rs new file mode 100644 index 00000000..ded4a03c --- /dev/null +++ b/crates/tests/cgp-tests/tests/getters/mut_getter.rs @@ -0,0 +1,164 @@ +//! `#[cgp_getter]` with a `&mut self` receiver returning `&mut u32`: the three +//! generated provider impls read the field mutably through `get_field_mut`. The +//! `UseFields` and `UseField` impls bound the context by `HasFieldMut`, and the +//! `WithProvider` impl delegates to a `MutFieldGetter` provider rather than a +//! `FieldGetter`. The context binds a differently-named `value` field by wiring +//! to `UseField`. +//! +//! See docs/reference/macros/cgp_getter.md and docs/reference/providers/use_field.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_getter; + +snapshot_cgp_getter! { + #[cgp_getter] + pub trait HasCount { + fn count(&mut self) -> &mut u32; + } + + expand_has_count(output) { + insta::assert_snapshot!(output, @" + pub trait HasCount { + fn count(&mut self) -> &mut u32; + } + impl<__Context__> HasCount for __Context__ + where + __Context__: CountGetter<__Context__>, + { + fn count(&mut self) -> &mut u32 { + __Context__::count(self) + } + } + pub trait CountGetter< + __Context__, + >: IsProviderFor { + fn count(__context__: &mut __Context__) -> &mut u32; + } + impl<__Provider__, __Context__> CountGetter<__Context__> for __Provider__ + where + __Provider__: DelegateComponent + + IsProviderFor, + <__Provider__ as DelegateComponent< + CountGetterComponent, + >>::Delegate: CountGetter<__Context__>, + { + fn count(__context__: &mut __Context__) -> &mut u32 { + <__Provider__ as DelegateComponent< + CountGetterComponent, + >>::Delegate::count(__context__) + } + } + pub struct CountGetterComponent; + impl<__Context__> CountGetter<__Context__> for UseContext + where + __Context__: HasCount, + { + fn count(__context__: &mut __Context__) -> &mut u32 { + __Context__::count(__context__) + } + } + impl<__Context__> IsProviderFor for UseContext + where + __Context__: HasCount, + {} + impl<__Context__, __Components__, __Path__> CountGetter<__Context__> + for RedirectLookup<__Components__, __Path__> + where + __Components__: DelegateComponent<__Path__>, + <__Components__ as DelegateComponent<__Path__>>::Delegate: CountGetter<__Context__>, + { + fn count(__context__: &mut __Context__) -> &mut u32 { + <__Components__ as DelegateComponent<__Path__>>::Delegate::count(__context__) + } + } + impl< + __Context__, + __Components__, + __Path__, + > IsProviderFor + for RedirectLookup<__Components__, __Path__> + where + __Components__: DelegateComponent<__Path__>, + <__Components__ as DelegateComponent< + __Path__, + >>::Delegate: IsProviderFor + + CountGetter<__Context__>, + {} + impl<__Context__> CountGetter<__Context__> for UseFields + where + __Context__: HasFieldMut< + Symbol<5, Chars<'c', Chars<'o', Chars<'u', Chars<'n', Chars<'t', Nil>>>>>>, + Value = u32, + >, + { + fn count(__context__: &mut __Context__) -> &mut u32 { + __context__ + .get_field_mut( + ::core::marker::PhantomData::< + Symbol< + 5, + Chars<'c', Chars<'o', Chars<'u', Chars<'n', Chars<'t', Nil>>>>>, + >, + >, + ) + } + } + impl<__Context__> IsProviderFor for UseFields + where + __Context__: HasFieldMut< + Symbol<5, Chars<'c', Chars<'o', Chars<'u', Chars<'n', Chars<'t', Nil>>>>>>, + Value = u32, + >, + {} + impl<__Context__, __Tag__> CountGetter<__Context__> for UseField<__Tag__> + where + __Context__: HasFieldMut<__Tag__, Value = u32>, + { + fn count(__context__: &mut __Context__) -> &mut u32 { + __context__.get_field_mut(::core::marker::PhantomData::<__Tag__>) + } + } + impl<__Context__, __Tag__> IsProviderFor + for UseField<__Tag__> + where + __Context__: HasFieldMut<__Tag__, Value = u32>, + {} + impl<__Context__, __Provider__> CountGetter<__Context__> for WithProvider<__Provider__> + where + __Provider__: MutFieldGetter<__Context__, CountGetterComponent, Value = u32>, + { + fn count(__context__: &mut __Context__) -> &mut u32 { + __Provider__::get_field_mut( + __context__, + ::core::marker::PhantomData::, + ) + } + } + impl<__Context__, __Provider__> IsProviderFor + for WithProvider<__Provider__> + where + __Provider__: MutFieldGetter<__Context__, CountGetterComponent, Value = u32>, + {} + ") + } +} + +#[derive(HasField)] +pub struct App { + pub value: u32, +} + +delegate_components! { + App { + CountGetterComponent: UseField, + } +} + +#[test] +pub fn test_mut_getter() { + let mut context = App { value: 9 }; + + *context.count() += 1; + + assert_eq!(context.value, 10); +} diff --git a/crates/tests/cgp-tests/tests/getters/mut_option_auto.rs b/crates/tests/cgp-tests/tests/getters/mut_option_auto.rs new file mode 100644 index 00000000..13951cad --- /dev/null +++ b/crates/tests/cgp-tests/tests/getters/mut_option_auto.rs @@ -0,0 +1,55 @@ +//! `#[cgp_auto_getter]` with a `&mut self` receiver returning `Option<&mut u32>`: +//! the blanket impl reads an `Option` field mutably through `get_field_mut` +//! and calls `.as_mut()`, the mutable mirror of the shared `Option<&T>` getter. +//! +//! See docs/reference/macros/cgp_auto_getter.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_auto_getter; + +snapshot_cgp_auto_getter! { + #[cgp_auto_getter] + pub trait HasFoo { + fn foo(&mut self) -> Option<&mut u32>; + } + + expand_has_foo(output) { + insta::assert_snapshot!(output, @" + pub trait HasFoo { + fn foo(&mut self) -> Option<&mut u32>; + } + impl<__Context__> HasFoo for __Context__ + where + __Context__: HasFieldMut< + Symbol<3, Chars<'f', Chars<'o', Chars<'o', Nil>>>>, + Value = Option, + >, + { + fn foo(&mut self) -> Option<&mut u32> { + self.get_field_mut( + ::core::marker::PhantomData::< + Symbol<3, Chars<'f', Chars<'o', Chars<'o', Nil>>>>, + >, + ) + .as_mut() + } + } + ") + } +} + +#[derive(HasField)] +pub struct App { + pub foo: Option, +} + +#[test] +pub fn test_mut_option_auto_getter() { + let mut context = App { foo: Some(1) }; + + if let Some(value) = context.foo() { + *value = 42; + } + + assert_eq!(context.foo, Some(42)); +} diff --git a/crates/tests/cgp-tests/tests/getters/mut_slice_auto.rs b/crates/tests/cgp-tests/tests/getters/mut_slice_auto.rs new file mode 100644 index 00000000..fd7fa2b2 --- /dev/null +++ b/crates/tests/cgp-tests/tests/getters/mut_slice_auto.rs @@ -0,0 +1,54 @@ +//! `#[cgp_auto_getter]` with a `&mut self` receiver returning `&mut [u32]`: the +//! blanket impl reads a field whose type is `AsMut<[u32]> + 'static` (e.g. +//! `Vec`) mutably through `get_field_mut` and calls `.as_mut()`, the mutable +//! mirror of the shared `&[T]` slice getter. +//! +//! See docs/reference/macros/cgp_auto_getter.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_auto_getter; + +snapshot_cgp_auto_getter! { + #[cgp_auto_getter] + pub trait HasFoo { + fn foo(&mut self) -> &mut [u32]; + } + + expand_has_foo(output) { + insta::assert_snapshot!(output, @" + pub trait HasFoo { + fn foo(&mut self) -> &mut [u32]; + } + impl<__Context__> HasFoo for __Context__ + where + __Context__: HasFieldMut< + Symbol<3, Chars<'f', Chars<'o', Chars<'o', Nil>>>>, + Value: AsMut<[u32]> + 'static, + >, + { + fn foo(&mut self) -> &mut [u32] { + self.get_field_mut( + ::core::marker::PhantomData::< + Symbol<3, Chars<'f', Chars<'o', Chars<'o', Nil>>>>, + >, + ) + .as_mut() + } + } + ") + } +} + +#[derive(HasField)] +pub struct App { + pub foo: Vec, +} + +#[test] +pub fn test_mut_slice_auto_getter() { + let mut context = App { foo: vec![1, 2, 3] }; + + context.foo()[0] = 10; + + assert_eq!(context.foo, vec![10, 2, 3]); +} diff --git a/crates/tests/cgp-tests/tests/getters/mut_str_auto.rs b/crates/tests/cgp-tests/tests/getters/mut_str_auto.rs new file mode 100644 index 00000000..82405834 --- /dev/null +++ b/crates/tests/cgp-tests/tests/getters/mut_str_auto.rs @@ -0,0 +1,56 @@ +//! `#[cgp_auto_getter]` with a `&mut self` receiver returning `&mut str`: the +//! blanket impl reads the `String` field mutably through `get_field_mut` (bounded +//! by `HasFieldMut`) and calls `.as_mut_str()`, so a `String` field can be exposed +//! as a mutable `&mut str`. +//! +//! See docs/reference/macros/cgp_auto_getter.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_auto_getter; + +snapshot_cgp_auto_getter! { + #[cgp_auto_getter] + pub trait HasFoo { + fn foo(&mut self) -> &mut str; + } + + expand_has_foo(output) { + insta::assert_snapshot!(output, @" + pub trait HasFoo { + fn foo(&mut self) -> &mut str; + } + impl<__Context__> HasFoo for __Context__ + where + __Context__: HasFieldMut< + Symbol<3, Chars<'f', Chars<'o', Chars<'o', Nil>>>>, + Value = String, + >, + { + fn foo(&mut self) -> &mut str { + self.get_field_mut( + ::core::marker::PhantomData::< + Symbol<3, Chars<'f', Chars<'o', Chars<'o', Nil>>>>, + >, + ) + .as_mut_str() + } + } + ") + } +} + +#[derive(HasField)] +pub struct App { + pub foo: String, +} + +#[test] +pub fn test_mut_str_auto_getter() { + let mut context = App { + foo: "abc".to_owned(), + }; + + context.foo().make_ascii_uppercase(); + + assert_eq!(context.foo, "ABC"); +} diff --git a/docs/implementation/asts/cgp_getter.md b/docs/implementation/asts/cgp_getter.md index 9c65108b..d2fadf78 100644 --- a/docs/implementation/asts/cgp_getter.md +++ b/docs/implementation/asts/cgp_getter.md @@ -8,7 +8,7 @@ Both macros turn a getter trait's methods into `GetterField`s through the shared The parser enforces the getter-method contract and resolves the two shorthands a reader most needs to know. A getter method must be a plain (non-const, non-async, non-unsafe, non-generic) method whose first argument is a reference — either `&self` (`ReceiverMode::SelfReceiver`) or a typed `&SomeType` receiver (`ReceiverMode::Type`), the latter letting a getter read a field out of a type other than the context, with `Self` rewritten to the context. The return type then determines the field type, the *field mode* — the conversion the getter body applies — and whether the read borrows the field mutably, all through the shared `parse_field_type`, which returns the field type, the `FieldMode`, and an `Option` for the access mutability: a `&str` return reads a `String` field (`FieldMode::Str`, `.as_str()`), an `Option<&T>` reads `Option` (`FieldMode::OptionRef`, `.as_ref()`), an `Option<&str>` reads `Option` (`FieldMode::OptionStr`, `.as_deref()`), a `&[T]` reads an `AsRef<[T]>` field (`FieldMode::Slice`, `.as_ref()`), an `MRef<'_, T>` wraps the borrow (`FieldMode::MRef`, `MRef::Ref(...)`), a plain `&T` is a bare `FieldMode::Reference`, and any other owned return — a path type, a tuple, or an array — is `FieldMode::Copy` (`.clone()`). A `&mut` in the type — the outer reference of `&mut T`/`&mut [T]` or the inner reference of `Option<&mut T>` — sets the access mutability, reusing the same `FieldMode` but selecting the `HasFieldMut`/`get_field_mut` read and the mutable conversion (`.as_mut()`, `.as_deref_mut()`, or an `AsMut<[T]>` bound for a mutable slice); a mutable read requires a `&mut self` receiver. Because the mutability is keyed off the type's own reference rather than the receiver, the same helper serves the `#[implicit]` case where an argument's mutability is independent of the receiver — a getter discards the returned mutability and keys off its receiver instead. `parse_getter_fields` also extracts an optional single associated return type and checks that, when present, the trait has exactly one method whose return type matches it. -The getter-method body itself is built by `derive_getter_method` from the `types/getter/` module, which emits `receiver.get_field(PhantomData::) ` for the field's mode; the same `FieldMode` and `GetFieldWithModeExpr` types drive the `#[implicit]` bindings in the [`cgp_fn` stack](cgp_fn.md), which is why the two families convert fields identically. +The getter-method body itself is built by `derive_getter_method` from the `types/getter/` module, which emits `receiver.get_field(PhantomData::) ` for the field's mode. The conversion suffix is chosen by `FieldMode::apply`, the single function that maps a field mode and mutability to the trailing `.as_str()`/`.as_ref()`/`MRef::Ref(...)`/etc.; the `#[implicit]` bindings in the [`cgp_fn` stack](cgp_fn.md) reach the same function through `GetFieldWithModeExpr`, which is why the two families convert fields identically. ## `ItemCgpAutoGetter` @@ -31,7 +31,7 @@ Each of these threads the optional associated type through as an extra generic p ## Tests - The stage transforms are exercised end-to-end by the expansion snapshots indexed in the two entrypoint documents' Snapshots sections — the [`#[cgp_getter]` snapshots](../entrypoints/cgp_getter.md) and the [`#[cgp_auto_getter]` snapshots](../entrypoints/cgp_auto_getter.md). -- There is no separate parser-rejection test file for either macro in `cgp-macro-tests`; the getter-method contract checks in `parse_getter_fields` (rejecting const/async/unsafe/generic getters, a bad receiver, or multiple associated types) are currently unpinned by dedicated failure cases. +- [parser_rejections/getters.rs](../../../crates/tests/cgp-macro-tests/tests/parser_rejections/getters.rs) pins the getter-method contract checks in `parse_getter_fields`, driven through the shared `cgp_auto_getter` entrypoint: it rejects const, async, unsafe, and generic getter methods, a by-value `self` receiver, a `&mut` return under a `&self` receiver, more than one associated type, an associated type alongside a second method, and a non-getter trait item, plus `#[cgp_auto_getter]`'s rejection of any attribute argument. ## Source diff --git a/docs/implementation/entrypoints/cgp_auto_getter.md b/docs/implementation/entrypoints/cgp_auto_getter.md index 05a0530f..1c3e21b5 100644 --- a/docs/implementation/entrypoints/cgp_auto_getter.md +++ b/docs/implementation/entrypoints/cgp_auto_getter.md @@ -66,13 +66,16 @@ Every `snapshot_cgp_auto_getter!` invocation across the suite is indexed here, s - [getters/option_auto.rs](../../../crates/tests/cgp-tests/tests/getters/option_auto.rs) — an `Option<&String>` return reading an `Option` field via `.as_ref()`. - [getters/option_str_auto.rs](../../../crates/tests/cgp-tests/tests/getters/option_str_auto.rs) — an `Option<&str>` return reading an `Option` field via `.as_deref()`, composing the `&str` and option cases. - [getters/slice_auto.rs](../../../crates/tests/cgp-tests/tests/getters/slice_auto.rs) — a `&[u8]` return reading an `AsRef<[u8]> + 'static` field via `.as_ref()`. +- [getters/mut_str_auto.rs](../../../crates/tests/cgp-tests/tests/getters/mut_str_auto.rs) — a `&mut self` getter returning `&mut str`, reading the `String` field through `HasFieldMut`/`get_field_mut` and calling `.as_mut_str()`. +- [getters/mut_slice_auto.rs](../../../crates/tests/cgp-tests/tests/getters/mut_slice_auto.rs) — a `&mut self` getter returning `&mut [u32]`, bounding the field by `AsMut<[u32]> + 'static` and calling `.as_mut()`. +- [getters/mut_option_auto.rs](../../../crates/tests/cgp-tests/tests/getters/mut_option_auto.rs) — a `&mut self` getter returning `Option<&mut u32>`, reading the `Option` field via `.as_mut()`. - [getters/non_self_auto.rs](../../../crates/tests/cgp-tests/tests/getters/non_self_auto.rs) — a non-`self` getter reading a field out of another type (`&Self::Foo`). - [getters/auto_getter_generic.rs](../../../crates/tests/cgp-tests/tests/getters/auto_getter_generic.rs) — a trait generic over a type parameter, keyed by a `PhantomData` tag. - [getters/assoc_type_auto_getter.rs](../../../crates/tests/cgp-tests/tests/getters/assoc_type_auto_getter.rs) — a local associated return type inferred from the field, with a `Display` bound carried onto the impl. - [getters/assoc_type_self_referential_auto.rs](../../../crates/tests/cgp-tests/tests/getters/assoc_type_self_referential_auto.rs) — a self-referential associated-type bound surviving onto the impl with `Self::Scalar` rewritten to the parameter. - [getters/abstract_type_extend.rs](../../../crates/tests/cgp-tests/tests/getters/abstract_type_extend.rs), [getters/abstract_type_use_type.rs](../../../crates/tests/cgp-tests/tests/getters/abstract_type_use_type.rs) — getters whose return type is an abstract type imported via `#[extend]` and `#[use_type]`; each file pins both the auto and the full getter variant. -Coverage is broad; no distinct expansion variant is currently missing a snapshot. +Coverage is broad across return shapes and both receiver mutabilities. The two mutable conversions with no dedicated snapshot yet are a plain `&mut T` return (a bare `get_field_mut` with no conversion) and an `Option<&mut str>` return (read via `.as_deref_mut()`, the mutable mirror of `option_str_auto.rs`). ## Tests diff --git a/docs/implementation/entrypoints/cgp_getter.md b/docs/implementation/entrypoints/cgp_getter.md index 884cd030..b6ac04bf 100644 --- a/docs/implementation/entrypoints/cgp_getter.md +++ b/docs/implementation/entrypoints/cgp_getter.md @@ -65,6 +65,7 @@ Every `snapshot_cgp_getter!` invocation across the suite is indexed here, since - [getters/mref.rs](../../../crates/tests/cgp-tests/tests/getters/mref.rs) — an `MRef<'_, String>` return wrapping the borrow in `MRef::Ref`. - [getters/option.rs](../../../crates/tests/cgp-tests/tests/getters/option.rs) — an `Option<&String>` return reading an `Option` field via `.as_ref()`. - [getters/slice.rs](../../../crates/tests/cgp-tests/tests/getters/slice.rs) — a `&[u8]` return reading any `AsRef<[u8]> + 'static` field via `.as_ref()`. +- [getters/mut_getter.rs](../../../crates/tests/cgp-tests/tests/getters/mut_getter.rs) — a `&mut self` getter returning `&mut u32`: the `UseFields` and `UseField` impls read through `HasFieldMut`/`get_field_mut`, and the `WithProvider` impl delegates to a `MutFieldGetter` rather than a `FieldGetter`. - [getters/non_self.rs](../../../crates/tests/cgp-tests/tests/getters/non_self.rs) — a non-`self` getter reading a field out of another type (`&Self::Foo`). - [getters/string_custom_name.rs](../../../crates/tests/cgp-tests/tests/getters/string_custom_name.rs) — `#[cgp_getter(GetString)]` overriding the provider name (and component). - [getters/string_custom_spec.rs](../../../crates/tests/cgp-tests/tests/getters/string_custom_spec.rs) — the brace-spec form overriding provider and component names independently. @@ -74,12 +75,15 @@ Every `snapshot_cgp_getter!` invocation across the suite is indexed here, since The `UseDelegate`-table dispatch form of a getter is snapshotted separately in [dispatching/use_delegate_getter.rs](../../../crates/tests/cgp-tests/tests/dispatching/use_delegate_getter.rs), which the dispatching concept owns rather than this entrypoint. One variant has no snapshot yet: a getter component carrying its own generic parameter (distinct from the auto-getter's `auto_getter_generic.rs`). +The behavioral tests also assert the mutable full-getter resolves; see the Tests section. + ## Tests The getter snapshot files also wire concrete contexts and assert the getters resolve, so the same files carry the behavioral checks: - [getters/string.rs](../../../crates/tests/cgp-tests/tests/getters/string.rs) and [getters/assoc_type_self_referential.rs](../../../crates/tests/cgp-tests/tests/getters/assoc_type_self_referential.rs) confirm that wiring `UseField` makes the getter read a field whose name differs from the method. - [getters/clone.rs](../../../crates/tests/cgp-tests/tests/getters/clone.rs), [getters/mref.rs](../../../crates/tests/cgp-tests/tests/getters/mref.rs), [getters/option.rs](../../../crates/tests/cgp-tests/tests/getters/option.rs), and [getters/slice.rs](../../../crates/tests/cgp-tests/tests/getters/slice.rs) verify each return-type shorthand at run time. +- [getters/mut_getter.rs](../../../crates/tests/cgp-tests/tests/getters/mut_getter.rs) mutates the context through a `&mut self` getter wired to `UseField`, confirming the mutable provider impls resolve. ## Source diff --git a/docs/reference/types/mref.md b/docs/reference/types/mref.md index 0ac4304c..3e4d1ef6 100644 --- a/docs/reference/types/mref.md +++ b/docs/reference/types/mref.md @@ -58,4 +58,4 @@ Both `borrowed` and `made` have the same type and are consumed the same way; onl ## Source - The type is defined in [crates/core/cgp-field/src/types/mref.rs](../../../crates/core/cgp-field/src/types/mref.rs), including its `Deref`, `AsRef`, the two `From` impls, and `get_or_clone`. -- The macro logic that recognizes an `MRef<'a, T>` getter return type and emits an `MRef::Ref(...)` body is in [crates/macros/cgp-macro-core/src/functions/field/parse.rs](../../../crates/macros/cgp-macro-core/src/functions/field/parse.rs) (the `MRef` field mode) and [crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs](../../../crates/macros/cgp-macro-core/src/types/getter/get_field_with_mode_expr.rs). +- The macro logic that recognizes an `MRef<'a, T>` getter return type is in [crates/macros/cgp-macro-core/src/functions/field/parse.rs](../../../crates/macros/cgp-macro-core/src/functions/field/parse.rs) (the `MRef` field mode), and the `MRef::Ref(...)` body is emitted by the shared field-mode conversion in [crates/macros/cgp-macro-core/src/types/getter/field_mode.rs](../../../crates/macros/cgp-macro-core/src/types/getter/field_mode.rs), which fully-qualifies `MRef` through the `crate::exports` markers. From c2735ccb3d6736a72595b0aa37aafef904f0a012 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 22:14:22 +0200 Subject: [PATCH 07/17] Move out readme for compile-fail tests --- crates/tests/README.md | 8 ++- .../tests/cgp-compile-fail-tests/Cargo.toml | 1 + crates/tests/cgp-compile-fail-tests/README.md | 67 +++++++++++++++++++ .../tests/cgp-compile-fail-tests/src/lib.rs | 65 +----------------- 4 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 crates/tests/cgp-compile-fail-tests/README.md diff --git a/crates/tests/README.md b/crates/tests/README.md index 4a3c545f..ee27d91c 100644 --- a/crates/tests/README.md +++ b/crates/tests/README.md @@ -28,7 +28,10 @@ standalone `.rs` fixture with a committed `.stderr` snapshot of the expected compiler output, split into two categories — **acceptable** failures that CGP intentionally delegates to the Rust compiler (overlapping wiring, a lazily-wired missing dependency) and **problematic** failures that are a CGP defect (input a -macro should have rejected, or an expansion that emits invalid Rust). +macro should have rejected, or an expansion that emits invalid Rust). See +[cgp-compile-fail-tests/README.md](cgp-compile-fail-tests/README.md) for what +belongs in each category, the fixture layout, and how to regenerate the +`.stderr` snapshots. **`cgp-test-crate-a`** and **`cgp-test-crate-b`** are auxiliary packages for **cross-crate** behavior. Crate A defines components, a provider, and a namespaced @@ -63,7 +66,8 @@ for parser corner cases, and the failure-case targets `parser_rejections` and subdirectory per owning macro (`acceptable/delegate_components/`, or a `problematic//` when a defect is pinned). Each fixture is a self-contained program paired with a committed `.stderr` snapshot of the compiler output it must -produce. +produce. Its own [README](cgp-compile-fail-tests/README.md) is the authoritative +guide to this crate. ## Snapshots diff --git a/crates/tests/cgp-compile-fail-tests/Cargo.toml b/crates/tests/cgp-compile-fail-tests/Cargo.toml index 160c7860..589787ac 100644 --- a/crates/tests/cgp-compile-fail-tests/Cargo.toml +++ b/crates/tests/cgp-compile-fail-tests/Cargo.toml @@ -7,6 +7,7 @@ repository = { workspace = true } authors = { workspace = true } rust-version = { workspace = true } keywords = { workspace = true } +readme = "./README.md" [dependencies] cgp = { workspace = true } diff --git a/crates/tests/cgp-compile-fail-tests/README.md b/crates/tests/cgp-compile-fail-tests/README.md new file mode 100644 index 00000000..49025d0d --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/README.md @@ -0,0 +1,67 @@ +# `cgp-compile-fail-tests` + +Compile-fail tests for the CGP macros, driven by +[`trybuild`](https://docs.rs/trybuild). + +Each test is a standalone `.rs` fixture under `tests/` that a CGP macro +**accepts** but whose **expansion** then fails to type- or borrow-check — the +failure lands on the emitted Rust, not inside the macro. `trybuild` compiles +each fixture as its own throwaway crate and compares the compiler's output +against a committed `.stderr` file, so a test passes only when compilation fails +*with the pinned diagnostic*. Because the driver is an ordinary integration test +(`tests/compile_fail_tests.rs`), `cargo test` and `cargo nextest run` both +execute it — unlike the `compile_fail` doctests this crate previously held, which +`cargo nextest` silently skipped. + +## What belongs here + +A `trybuild` fixture is reserved for input that a CGP macro **accepts** but whose +**expansion** fails to compile — the case a macro cannot reject because it lacks +the whole-program view the borrow/coherence check needs, or a documented bug +where the macro emits code it should not. Input that a macro **rejects** during +expansion (it returns `Err`) does not belong here; test it by driving the +entrypoint directly in `cgp-macro-tests` with the `assert_macro_rejects` helper, +which pins the macro's own diagnostic. + +## Two categories of failure + +The fixtures are split by *whose fault the failure is*, because the two +categories carry opposite messages about CGP's health. + +**`tests/acceptable/`** holds failures CGP **intentionally delegates to the Rust +compiler**. CGP is working as designed: it cannot see the whole program, so it +lowers the input faithfully and lets `rustc` reject it (overlapping +`delegate_components!` entries becoming conflicting impls, a lazily-wired +provider whose impl-side dependency the context does not meet, an ill-formed +per-entry generic the compiler rejects as unconstrained). The pinned `.stderr` +documents that the failure is the compiler doing its job, and its diagnostic is +the one a user should expect. Each fixture is cross-linked to the +`## Failure modes` section of the owning macro's implementation document. + +**`tests/problematic/`** holds failures that are a **CGP defect**: input a macro +should have rejected with a spanned error, or that a macro expanded into invalid +Rust. The pinned `.stderr` captures the confusing downstream error a user +currently hits; each fixture is cross-linked to the `## Known issues` section of +the owning macro's implementation document, and its `.stderr` should improve +(ideally become a clean macro-time rejection) when the defect is fixed. + +## Organization + +Under each category directory, fixtures are grouped into one subdirectory per +**owning macro** — the macro whose expansion produces the failure and whose +implementation document documents it (`acceptable/delegate_components/`, or a +`problematic//` when a defect is pinned). This mirrors the per-entrypoint +layout of the implementation docs, so a fixture and the document that indexes it +share a name. Within a subdirectory, write one fixture file per case, named for +the failure mode it probes (`duplicate_key.rs`, `missing_dependency.rs`), and +open each with a comment stating what it exercises and why it must not compile. +The driver `tests/compile_fail_tests.rs` globs both trees with `**`, so a new +fixture is picked up with no registration. + +## Regenerating the `.stderr` snapshots + +A fixture's committed `.stderr` is the golden output. After adding a fixture or +when an intended change alters a diagnostic, regenerate the snapshots with +`TRYBUILD=overwrite cargo test -p cgp-compile-fail-tests`, then review the diff +before committing — an unexpected change to an `acceptable/` diagnostic, or a +`problematic/` fixture that stops failing, is a signal worth reading closely. diff --git a/crates/tests/cgp-compile-fail-tests/src/lib.rs b/crates/tests/cgp-compile-fail-tests/src/lib.rs index 09da1975..8b137891 100644 --- a/crates/tests/cgp-compile-fail-tests/src/lib.rs +++ b/crates/tests/cgp-compile-fail-tests/src/lib.rs @@ -1,64 +1 @@ -//! Compile-fail tests for the CGP macros, driven by [`trybuild`](https://docs.rs/trybuild). -//! -//! Each test is a standalone `.rs` fixture under `tests/` that a CGP macro -//! **accepts** but whose **expansion** then fails to type- or borrow-check — the -//! failure lands on the emitted Rust, not inside the macro. `trybuild` compiles -//! each fixture as its own throwaway crate and compares the compiler's output -//! against a committed `.stderr` file, so a test passes only when compilation -//! fails *with the pinned diagnostic*. Because the driver is an ordinary -//! integration test (`tests/compile_fail_tests.rs`), `cargo test` and -//! `cargo nextest run` both execute it — unlike the `compile_fail` doctests this -//! crate previously held, which `cargo nextest` silently skipped. -//! -//! # What belongs here -//! -//! A `trybuild` fixture is reserved for input that a CGP macro **accepts** but -//! whose **expansion** fails to compile — the case a macro cannot reject because -//! it lacks the whole-program view the borrow/coherence check needs, or a -//! documented bug where the macro emits code it should not. Input that a macro -//! **rejects** during expansion (it returns `Err`) does not belong here; test it -//! by driving the entrypoint directly in `cgp-macro-tests` with the -//! `assert_macro_rejects` helper, which pins the macro's own diagnostic. -//! -//! # Two categories of failure -//! -//! The fixtures are split by *whose fault the failure is*, because the two -//! categories carry opposite messages about CGP's health: -//! -//! - **`tests/acceptable/`** — failures CGP **intentionally delegates to the Rust -//! compiler**. CGP is working as designed: it cannot see the whole program, so -//! it lowers the input faithfully and lets `rustc` reject it (overlapping -//! `delegate_components!` entries becoming conflicting impls, a lazily-wired -//! provider whose impl-side dependency the context does not meet, an ill-formed -//! per-entry generic the compiler rejects as unconstrained). The pinned -//! `.stderr` documents that the failure is the compiler doing its job, and its -//! diagnostic is the one a user should expect. Each fixture is cross-linked to -//! the `## Failure modes` section of the owning macro's implementation document. -//! - **`tests/problematic/`** — failures that are a **CGP defect**: input a macro -//! should have rejected with a spanned error, or that a macro expanded into -//! invalid Rust. The pinned `.stderr` captures the confusing downstream error a -//! user currently hits; each fixture is cross-linked to the `## Known issues` -//! section of the owning macro's implementation document, and its `.stderr` -//! should improve (ideally become a clean macro-time rejection) when the defect -//! is fixed. -//! -//! # Organization -//! -//! Under each category directory, fixtures are grouped into one subdirectory per -//! **owning macro** — the macro whose expansion produces the failure and whose -//! implementation document documents it (`acceptable/delegate_components/`, or a -//! `problematic//` when a defect is pinned). This mirrors the per-entrypoint -//! layout of the implementation docs, so a fixture and the document that indexes it -//! share a name. Within a subdirectory, write one fixture file per case, named for -//! the failure mode it probes (`duplicate_key.rs`, `missing_dependency.rs`), and open -//! each with a comment stating what it exercises and why it must not compile. The -//! driver `tests/compile_fail_tests.rs` globs both trees with `**`, so a new -//! fixture is picked up with no registration. -//! -//! # Regenerating the `.stderr` snapshots -//! -//! A fixture's committed `.stderr` is the golden output. After adding a fixture -//! or when an intended change alters a diagnostic, regenerate the snapshots with -//! `TRYBUILD=overwrite cargo test -p cgp-compile-fail-tests`, then review the diff -//! before committing — an unexpected change to an `acceptable/` diagnostic, or a -//! `problematic/` fixture that stops failing, is a signal worth reading closely. + From 2651cd70eceb0b265471b9ded0e632d5aae2fd4b Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 22:28:44 +0200 Subject: [PATCH 08/17] Fix `delegate_components!` span --- .../cgp-macro-core/src/functions/mod.rs | 2 + .../src/functions/override_span.rs | 33 +++++++++++++++ .../src/types/check_components/table.rs | 20 +-------- .../types/delegate_component/mapping/eval.rs | 22 +++++++++- .../delegate_components/duplicate_key.stderr | 34 +++++---------- .../duplicate_key_same_block.rs | 42 +++++++++++++++++++ .../duplicate_key_same_block.stderr | 15 +++++++ .../overlapping_generic.stderr | 34 +++++---------- .../entrypoints/check_components.md | 2 +- .../entrypoints/delegate_components.md | 11 ++++- 10 files changed, 145 insertions(+), 70 deletions(-) create mode 100644 crates/macros/cgp-macro-core/src/functions/override_span.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.stderr diff --git a/crates/macros/cgp-macro-core/src/functions/mod.rs b/crates/macros/cgp-macro-core/src/functions/mod.rs index cd188782..4208f481 100644 --- a/crates/macros/cgp-macro-core/src/functions/mod.rs +++ b/crates/macros/cgp-macro-core/src/functions/mod.rs @@ -5,6 +5,7 @@ mod generics; mod getter; mod implicits; mod is_provider_params; +mod override_span; mod parse_internal; mod snake_case; mod strip; @@ -16,6 +17,7 @@ pub use generics::*; pub use getter::*; pub use implicits::*; pub use is_provider_params::*; +pub use override_span::*; pub use parse_internal::*; pub use snake_case::*; pub use strip::*; diff --git a/crates/macros/cgp-macro-core/src/functions/override_span.rs b/crates/macros/cgp-macro-core/src/functions/override_span.rs new file mode 100644 index 00000000..65d72f13 --- /dev/null +++ b/crates/macros/cgp-macro-core/src/functions/override_span.rs @@ -0,0 +1,33 @@ +use proc_macro2::Span; +use quote::ToTokens; +use syn::parse::Parse; +use syn::parse2; + +/// Re-span every top-level token of `body` to `span` and re-parse it into `T`. +/// +/// A macro builds its output from quasi-quoted tokens, which carry the whole +/// invocation's `call_site` span; a compiler error on such an item therefore +/// highlights the entire macro block. Re-spanning the tokens onto the specific +/// user-written token that produced the item (a component key, a wiring entry) +/// makes the diagnostic point there instead. `check_components!` uses this to +/// aim an unsatisfied-bound error at the checked component, and +/// `delegate_components!` to aim a coherence conflict at the offending entry. +/// +/// Only top-level tokens are re-spanned; tokens nested inside a delimiter group +/// (an impl body, say) keep their spans, which is why callers that must preserve +/// an inner span — the generic parameters of a generated impl, for instance — +/// restore it after re-spanning rather than relying on this to skip it. +pub fn override_span(span: Span, body: &T) -> syn::Result +where + T: Parse + ToTokens, +{ + parse2( + body.to_token_stream() + .into_iter() + .map(|mut tree| { + tree.set_span(span); + tree + }) + .collect(), + ) +} diff --git a/crates/macros/cgp-macro-core/src/types/check_components/table.rs b/crates/macros/cgp-macro-core/src/types/check_components/table.rs index 184972ad..b9c08c28 100644 --- a/crates/macros/cgp-macro-core/src/types/check_components/table.rs +++ b/crates/macros/cgp-macro-core/src/types/check_components/table.rs @@ -1,4 +1,3 @@ -use proc_macro2::Span; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; @@ -7,7 +6,7 @@ use syn::token::{Comma, Lt, Pound, Where}; use syn::{Attribute, Ident, Item, ItemImpl, ItemTrait, Type, WhereClause, braced, parse2}; use crate::exports::{CanUseComponent, IsProviderFor}; -use crate::functions::merge_generics; +use crate::functions::{merge_generics, override_span}; use crate::parse_internal; use crate::types::check_components::{CheckEntries, EvaluatedCheckEntry, TypeWithGenerics}; use crate::types::generics::ImplGenerics; @@ -65,7 +64,7 @@ impl CheckComponentsTable { } else { // Override the span of the context type so that any unsatisfied constraint // error is highlighted on the component type instead - let context_type = override_span(&span, context_type)?; + let context_type = override_span(span, context_type)?; vec![context_type] }; @@ -193,18 +192,3 @@ pub fn derive_check_trait_ident(context_type: &Type, prefix: &str) -> syn::Resul Ok(Ident::new(&format!("{prefix}{ident}"), ident.span())) } - -fn override_span(span: &Span, body: &T) -> syn::Result -where - T: Parse + ToTokens, -{ - parse2( - body.to_token_stream() - .into_iter() - .map(|mut tree| { - tree.set_span(*span); - tree - }) - .collect(), - ) -} diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs index 96fa9677..cd4cc87f 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs @@ -1,7 +1,8 @@ +use syn::spanned::Spanned; use syn::{Generics, ItemImpl, Type}; use crate::exports::{DelegateComponent, IsProviderFor}; -use crate::functions::{merge_generics, parse_internal}; +use crate::functions::{merge_generics, override_span, parse_internal}; /// The flat form every key, value, and statement collapses to; it renders the /// impl pair for one wiring entry. @@ -48,7 +49,7 @@ impl EvaluatedDelegateEntry { } }; - Ok(item_impl) + self.respan_impl(item_impl) } /// Emit the forwarding `IsProviderFor` impl, @@ -82,6 +83,23 @@ impl EvaluatedDelegateEntry { {} }; + self.respan_impl(item_impl) + } + + /// Re-span a generated impl onto the entry's key so a coherence conflict + /// (`E0119`) between two entries mapping the same key is reported on the + /// offending entry rather than on the whole `delegate_components!` block — + /// the impl's `impl`/trait-ref/self-type tokens otherwise carry the macro's + /// `call_site` span, which spans the entire invocation. + /// + /// The impl's generics keep their original spans, restored after re-spanning, + /// so a diagnostic about a per-entry generic (an unconstrained parameter's + /// `E0207`, say) still points at the generic the user wrote rather than the key. + fn respan_impl(&self, item_impl: ItemImpl) -> syn::Result { + let generics = item_impl.generics.clone(); + let mut item_impl = override_span(self.key.span(), &item_impl)?; + item_impl.generics = generics; + Ok(item_impl) } diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.stderr index 104a7676..63fc344c 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.stderr +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.stderr @@ -1,31 +1,17 @@ error[E0119]: conflicting implementations of trait `IsProviderFor` for type `Person` - --> tests/acceptable/delegate_components/duplicate_key.rs:36:1 + --> tests/acceptable/delegate_components/duplicate_key.rs:38:9 | -29 | / delegate_components! { -30 | | Person { -31 | | GreeterComponent: GreetHello, -32 | | } -33 | | } - | |_- first implementation here +31 | GreeterComponent: GreetHello, + | ---------------- first implementation here ... -36 | / delegate_components! { -37 | | Person { -38 | | GreeterComponent: GreetGoodbye, -39 | | } -40 | | } - | |_^ conflicting implementation for `Person` - | - = note: this error originates in the macro `delegate_components` (in Nightly builds, run with -Z macro-backtrace for more info) +38 | GreeterComponent: GreetGoodbye, + | ^^^^^^^^^^^^^^^^ conflicting implementation for `Person` error[E0119]: conflicting implementations of trait `DelegateComponent` for type `Person` - --> tests/acceptable/delegate_components/duplicate_key.rs:36:1 + --> tests/acceptable/delegate_components/duplicate_key.rs:38:9 | -29 | / delegate_components! { -30 | | Person { - | |__________- first implementation here +31 | GreeterComponent: GreetHello, + | ---------------- first implementation here ... -36 | / delegate_components! { -37 | | Person { - | |__________^ conflicting implementation for `Person` - | - = note: this error originates in the macro `delegate_components` (in Nightly builds, run with -Z macro-backtrace for more info) +38 | GreeterComponent: GreetGoodbye, + | ^^^^^^^^^^^^^^^^ conflicting implementation for `Person` diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.rs new file mode 100644 index 00000000..f8bc7016 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.rs @@ -0,0 +1,42 @@ +//! Acceptable failure: two entries in a *single* `delegate_components!` block +//! that map the same component key produce two conflicting +//! `DelegateComponent` impls for `Person`, rejected with the +//! coherence error E0119 — the same failure as [duplicate_key.rs], reached with +//! one block instead of two. +//! +//! This fixture exists to pin the **error span**: each conflicting entry lowers +//! to an impl re-spanned onto its own key, so E0119 points at the two distinct +//! `GreeterComponent` tokens (the "first implementation here" note lands on the +//! first entry, the conflict caret on the second) rather than at the whole +//! block. If the per-entry re-spanning in `mapping/eval.rs` regresses, both +//! carets snap back to the macro invocation and this `.stderr` changes. +//! +//! See docs/implementation/entrypoints/delegate_components.md (Error spans). + +use cgp::prelude::*; + +#[cgp_component(Greeter)] +pub trait CanGreet { + fn greet(&self); +} + +#[cgp_impl(new GreetHello)] +impl Greeter { + fn greet(&self) {} +} + +#[cgp_impl(new GreetGoodbye)] +impl Greeter { + fn greet(&self) {} +} + +pub struct Person; + +delegate_components! { + Person { + GreeterComponent: GreetHello, + GreeterComponent: GreetGoodbye, + } +} + +fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.stderr new file mode 100644 index 00000000..4cddc5d3 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.stderr @@ -0,0 +1,15 @@ +error[E0119]: conflicting implementations of trait `IsProviderFor` for type `Person` + --> tests/acceptable/delegate_components/duplicate_key_same_block.rs:38:9 + | +37 | GreeterComponent: GreetHello, + | ---------------- first implementation here +38 | GreeterComponent: GreetGoodbye, + | ^^^^^^^^^^^^^^^^ conflicting implementation for `Person` + +error[E0119]: conflicting implementations of trait `DelegateComponent` for type `Person` + --> tests/acceptable/delegate_components/duplicate_key_same_block.rs:38:9 + | +37 | GreeterComponent: GreetHello, + | ---------------- first implementation here +38 | GreeterComponent: GreetGoodbye, + | ^^^^^^^^^^^^^^^^ conflicting implementation for `Person` diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.stderr index 59fc31fa..f728a9f4 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.stderr +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.stderr @@ -1,31 +1,17 @@ error[E0119]: conflicting implementations of trait `IsProviderFor` for type `Wrapper` - --> tests/acceptable/delegate_components/overlapping_generic.rs:33:1 + --> tests/acceptable/delegate_components/overlapping_generic.rs:35:9 | -26 | / delegate_components! { -27 | | Wrapper { -28 | | GreeterComponent: GreetHello, -29 | | } -30 | | } - | |_- first implementation here +28 | GreeterComponent: GreetHello, + | ---------------- first implementation here ... -33 | / delegate_components! { -34 | | Wrapper { -35 | | GreeterComponent: GreetHello, -36 | | } -37 | | } - | |_^ conflicting implementation for `Wrapper` - | - = note: this error originates in the macro `delegate_components` (in Nightly builds, run with -Z macro-backtrace for more info) +35 | GreeterComponent: GreetHello, + | ^^^^^^^^^^^^^^^^ conflicting implementation for `Wrapper` error[E0119]: conflicting implementations of trait `DelegateComponent` for type `Wrapper` - --> tests/acceptable/delegate_components/overlapping_generic.rs:33:1 + --> tests/acceptable/delegate_components/overlapping_generic.rs:35:9 | -26 | / delegate_components! { -27 | | Wrapper { - | |__________________- first implementation here +28 | GreeterComponent: GreetHello, + | ---------------- first implementation here ... -33 | / delegate_components! { -34 | | Wrapper { - | |________________^ conflicting implementation for `Wrapper` - | - = note: this error originates in the macro `delegate_components` (in Nightly builds, run with -Z macro-backtrace for more info) +35 | GreeterComponent: GreetHello, + | ^^^^^^^^^^^^^^^^ conflicting implementation for `Wrapper` diff --git a/docs/implementation/entrypoints/check_components.md b/docs/implementation/entrypoints/check_components.md index fbb5b985..f510c9f7 100644 --- a/docs/implementation/entrypoints/check_components.md +++ b/docs/implementation/entrypoints/check_components.md @@ -68,6 +68,6 @@ The behavioral coverage for `check_components!` is the compile-time assertion it - Entry point: `check_components` in [cgp-macro-lib/src/check_components.rs](../../../crates/macros/cgp-macro-lib/src/check_components.rs). - Tables, entries, keys, and values: [cgp-macro-core/src/types/check_components/](../../../crates/macros/cgp-macro-core/src/types/check_components/), documented together with the `delegate_and_check_components!` stack in [asts/check_components.md](../asts/check_components.md). -- The check trait, the `#[check_trait]`/`#[check_providers]` attributes, the `__Check{Context}` name derivation, the supertrait choice, and the span override are all in `table.rs`; the cartesian-product expansion is in `entry.rs`. +- The check trait, the `#[check_trait]`/`#[check_providers]` attributes, the `__Check{Context}` name derivation, the supertrait choice, and the span override are all in `table.rs`; the cartesian-product expansion is in `entry.rs`. The span override applies the shared [`override_span`](../../../crates/macros/cgp-macro-core/src/functions/override_span.rs) helper (also used by `delegate_components!`). - Fragment construction: [parse_internal!](../macros/parse_internal.md). - The `delegate_and_check_components!` macro reuses this stack; see its [entrypoint document](delegate_and_check_components.md). diff --git a/docs/implementation/entrypoints/delegate_components.md b/docs/implementation/entrypoints/delegate_components.md index 8fd4b1b9..e9d99777 100644 --- a/docs/implementation/entrypoints/delegate_components.md +++ b/docs/implementation/entrypoints/delegate_components.md @@ -58,11 +58,19 @@ An **array key** `[A, B]: Provider` expands to one impl pair per bracketed key, An **`@`-path key** carries a leading `__Wildcard__` generic and lowers the path to a prefix type ending in that wildcard, which is how a dispatch parameter slots in at lookup time. A **brace group on a path segment** (`@Component.{u32, u64}: P`) expands to one key per element, and the `namespace`/`for` statement forms lower through a shared "for-entry" path that builds a `Namespace<…, Delegate = …>` bound rather than a direct `DelegateComponent` impl; these are the namespace machinery and are detailed in the AST document. +## Error spans + +Each generated impl is re-spanned onto the entry that produced it, so a compiler error about that impl points at the entry the user wrote rather than at the whole `delegate_components!` block. The impls are built with `parse_internal!`, which quasi-quotes their tokens; those tokens carry the macro invocation's `call_site` span, and the only tokens with a narrower span are the interpolated key, value, and target type. A coherence conflict (`E0119`) between two entries mapping the same key is reported on the impl header — its `impl` keyword and trait reference — which is exactly the part that starts at `call_site`, so without correction the error underlines the entire block, and two overlapping entries produce two block-wide carets that say nothing about which entry to fix. + +`build_delegate_component_impl` and `build_is_provider_for_impl` fix this through `respan_impl`, which mirrors the `override_span` technique that `check_components!` uses to aim an unsatisfied-bound error at the checked component. It re-spans every token of the finished impl onto the entry's key (`self.key.span()`), then restores the impl's original generics. The re-span moves the header — and therefore the `E0119` caret — onto the `Key` token the user wrote, while restoring the generics keeps a diagnostic about a per-entry generic pointing where the user declared it: an unconstrained parameter's `E0207`, for instance, still lands on the `` rather than being dragged onto the key. The shared helper lives in [functions/override_span.rs](../../../crates/macros/cgp-macro-core/src/functions/override_span.rs). + +The re-span is only as precise as the key's own span. A plain `Key: Provider` mapping carries the user's key token, so the error lands on it; a synthesized key — an `@`-path assembled by the `open`/`namespace` machinery — may still fall back to a coarser span, which is acceptable because these forms cannot produce the same-key coherence conflict that motivated the fix. Because the `.stderr` fixtures record the exact line and column of each caret, they double as regression tests for these spans: reverting the re-span snaps the carets back to the block and the snapshots change (see [Tests](#tests)). + ## Failure modes Some `delegate_components!` inputs are accepted by the macro but then fail to compile, and for `delegate_components!` these failures are all ones the macro **intentionally defers to the Rust compiler**: it lowers each block independently, with no whole-program view, so a mistake that only a global check could catch is left to `rustc`. Each is intended behavior rather than a bug — the diagnostic below is the one a user should expect — and each is pinned by a fixture under [acceptable/delegate_components/](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components) in `cgp-compile-fail-tests`. -A **duplicate key** — the same component mapped twice, whether by two entries in one block, two separate blocks, or an `open` header colliding with an explicit mapping — emits two conflicting `DelegateComponent` impls and fails with the coherence error `E0119`, exactly as two hand-written impls would: +A **duplicate key** — the same component mapped twice, whether by two entries in one block, two separate blocks, or an `open` header colliding with an explicit mapping — emits two conflicting `DelegateComponent` impls and fails with the coherence error `E0119`, exactly as two hand-written impls would. The error points at each offending entry rather than the whole block, per [Error spans](#error-spans): ```rust delegate_components! { Person { GreeterComponent: GreetHello } } @@ -143,6 +151,7 @@ The failure cases in `cgp-macro-tests` pin the attribute rejection: The compile-fail fixtures in `cgp-compile-fail-tests` pin the expansions that fail to compile. All are **acceptable** failures — deferred to the compiler by design — and are described under [Failure modes](#failure-modes) above: - [acceptable/delegate_components/duplicate_key.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.rs) — two blocks mapping the same key expand to conflicting `DelegateComponent` impls (`E0119`). +- [acceptable/delegate_components/duplicate_key_same_block.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.rs) — the same conflict from two entries in one block; its `.stderr` pins the per-entry [error spans](#error-spans), each caret landing on its own `GreeterComponent` key rather than the whole block. - [acceptable/delegate_components/overlapping_generic.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.rs) — a generic ` Wrapper` entry overlaps a specific `Wrapper` entry at the same key (`E0119`). - [acceptable/delegate_components/missing_dependency.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.rs) — a lazily-wired provider whose `Self: HasName` dependency the context does not satisfy; the unmet bound surfaces at the call site (`E0599`). - [acceptable/delegate_components/unconstrained_generic.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/unconstrained_generic.rs) — a per-entry generic that appears only in the value (` GreeterComponent: GreetWith`) lowers to an impl with an unconstrained `T` (`E0207`), which the compiler rejects as it would a hand-written impl. From e2182e6c666f9373dd06c831c8a6c841b96323f2 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 22:45:59 +0200 Subject: [PATCH 09/17] Fix `delegate_component!` span containing namespace paths --- .../src/types/delegate_component/key/eval.rs | 6 ++++ .../src/types/delegate_component/key/multi.rs | 2 ++ .../src/types/delegate_component/key/path.rs | 18 ++++++++++- .../types/delegate_component/key/single.rs | 2 ++ .../delegate_component/mapping/direct.rs | 2 ++ .../types/delegate_component/mapping/eval.rs | 21 ++++++++----- .../delegate_component/mapping/normal.rs | 1 + .../delegate_component/mapping/redirect.rs | 1 + .../delegate_component/statement/eval.rs | 6 ++++ .../delegate_component/statement/for_loop.rs | 1 + .../delegate_component/statement/namespace.rs | 1 + .../delegate_component/statement/open.rs | 2 ++ .../src/types/namespace/inherit.rs | 1 + .../src/types/path/path_element.rs | 16 +++++++++- .../delegate_components/duplicate_path_key.rs | 30 +++++++++++++++++++ .../duplicate_path_key.stderr | 17 +++++++++++ .../implementation/asts/delegate_component.md | 6 ++-- docs/implementation/asts/path.md | 2 +- .../entrypoints/delegate_components.md | 7 +++-- 19 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_path_key.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_path_key.stderr diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/key/eval.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/key/eval.rs index f5755565..455e4bf1 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/key/eval.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/key/eval.rs @@ -1,3 +1,4 @@ +use proc_macro2::Span; use syn::{Generics, Type}; /// A lowered key: the key type plus the generics it introduces. One source key @@ -5,6 +6,11 @@ use syn::{Generics, Type}; pub struct EvaluatedDelegateKey { pub generics: Generics, pub key: Type, + /// The span of the source token the key was written as, carried separately + /// because `key` may be a synthesized type (a `PathCons<..>` nest for an + /// `@`-path key) whose own span points at the macro `call_site` rather than + /// at what the user wrote. It becomes the entry's diagnostic span. + pub span: Span, } /// Lower a key form into its evaluated keys. diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/key/multi.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/key/multi.rs index 251ffb5c..fe909d3e 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/key/multi.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/key/multi.rs @@ -1,5 +1,6 @@ use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; +use syn::spanned::Spanned; use syn::token::Comma; use syn::{Attribute, bracketed}; @@ -32,6 +33,7 @@ impl EvalDelegateKey for MultiDelegateKey { for key in &self.keys { let key = EvaluatedDelegateKey { generics: key.generics.generics.clone(), + span: key.ty.span(), key: key.ty.clone(), }; diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/key/path.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/key/path.rs index 3fedb132..2893f038 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/key/path.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/key/path.rs @@ -1,3 +1,4 @@ +use proc_macro2::Span; use syn::Attribute; use syn::parse::{Parse, ParseStream}; use syn::token::At; @@ -6,7 +7,7 @@ use crate::functions::merge_generics; use crate::parse_internal; use crate::types::delegate_component::{EvalDelegateKey, EvaluatedDelegateKey}; use crate::types::generics::ImplGenerics; -use crate::types::path::PathHead; +use crate::types::path::{PathElement, PathHead}; /// The `@`-prefixed open key. It lowers each path to a prefix type terminated by /// a `__Wildcard__` generic, into which the dispatch parameter slots at lookup @@ -46,12 +47,27 @@ impl EvalDelegateKey for PathDelegateKey { let mut generics = merge_generics(outer_generics, &inner_generics); generics.params.push(parse_internal!(__Wildcard__)); + // Span the entry on the path segments the user wrote, not the + // synthesized `PathCons<..>` key type (whose first token is a + // `call_site`-spanned `PathCons`). `join` widens the span to the whole + // path on toolchains that support it; where it does not (stable), the + // fallback keeps the leaf segment — the component of a namespace path + // or the dispatch key of an `@Component.key` entry — since that is the + // discriminating segment among entries sharing a prefix. + let span = path + .elements + .iter() + .map(PathElement::span) + .reduce(|acc, next| acc.join(next).unwrap_or(next)) + .unwrap_or_else(Span::call_site); + let prefix = path.to_prefix(parse_internal!(__Wildcard__)); let key_type = parse_internal!(#prefix); let key = EvaluatedDelegateKey { generics, key: key_type, + span, }; keys.push(key) diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/key/single.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/key/single.rs index 75c5e3ac..742e2a09 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/key/single.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/key/single.rs @@ -1,4 +1,5 @@ use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; use syn::{Attribute, Type}; use crate::types::delegate_component::{EvalDelegateKey, EvaluatedDelegateKey}; @@ -31,6 +32,7 @@ impl EvalDelegateKey for SingleDelegateKey { fn eval(&self) -> syn::Result> { let key = EvaluatedDelegateKey { generics: self.generics.generics.clone(), + span: self.ty.span(), key: self.ty.clone(), }; diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/direct.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/direct.rs index 48429817..ff2b1d07 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/direct.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/direct.rs @@ -27,6 +27,7 @@ impl EvalDelegateEntries for DirectDelegateMapping { for key in keys { let key_type = key.key; + let span = key.span; let mut generics = key.generics; let where_predicate = parse_internal! { @@ -47,6 +48,7 @@ impl EvalDelegateEntries for DirectDelegateMapping { generics, key: key_type, value: direct_value_type, + span, }); } diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs index cd4cc87f..ec49a8dd 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/eval.rs @@ -1,4 +1,4 @@ -use syn::spanned::Spanned; +use proc_macro2::Span; use syn::{Generics, ItemImpl, Type}; use crate::exports::{DelegateComponent, IsProviderFor}; @@ -11,6 +11,10 @@ pub struct EvaluatedDelegateEntry { pub generics: Generics, pub key: Type, pub value: Type, + /// The span the entry's impls are re-spanned onto, so a coherence conflict is + /// reported on the entry the user wrote. Sourced from the key's original token + /// rather than the (possibly synthesized) `key` type — see `respan_impl`. + pub span: Span, } /// Lower a construct into a single evaluated entry. @@ -86,18 +90,21 @@ impl EvaluatedDelegateEntry { self.respan_impl(item_impl) } - /// Re-span a generated impl onto the entry's key so a coherence conflict - /// (`E0119`) between two entries mapping the same key is reported on the - /// offending entry rather than on the whole `delegate_components!` block — - /// the impl's `impl`/trait-ref/self-type tokens otherwise carry the macro's - /// `call_site` span, which spans the entire invocation. + /// Re-span a generated impl onto the entry's own span (`self.span`, the key's + /// source token) so a coherence conflict (`E0119`) between two entries mapping + /// the same key is reported on the offending entry rather than on the whole + /// `delegate_components!` block — the impl's `impl`/trait-ref/self-type tokens + /// otherwise carry the macro's `call_site` span, which spans the entire + /// invocation. Using the carried span rather than `self.key.span()` keeps a + /// synthesized key (an `@`-path's `PathCons<..>` nest, whose first token is a + /// `call_site`-spanned `PathCons`) pointing at what the user wrote. /// /// The impl's generics keep their original spans, restored after re-spanning, /// so a diagnostic about a per-entry generic (an unconstrained parameter's /// `E0207`, say) still points at the generic the user wrote rather than the key. fn respan_impl(&self, item_impl: ItemImpl) -> syn::Result { let generics = item_impl.generics.clone(); - let mut item_impl = override_span(self.key.span(), &item_impl)?; + let mut item_impl = override_span(self.span, &item_impl)?; item_impl.generics = generics; Ok(item_impl) diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/normal.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/normal.rs index d1cbdf04..a131be04 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/normal.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/normal.rs @@ -39,6 +39,7 @@ impl EvalDelegateEntries for NormalDelegateMapping { generics: key.generics, key: key.key, value: value_type.clone(), + span: key.span, }) } diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/redirect.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/redirect.rs index 3586b367..815fd0bf 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/redirect.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/mapping/redirect.rs @@ -40,6 +40,7 @@ impl EvalDelegateEntries for RedirectDelegateMapping { generics: key.generics, key: key.key, value: value_type.clone(), + span: key.span, }; entries.push(entry); diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs index 3c55aa56..5d4f7e37 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/eval.rs @@ -1,3 +1,4 @@ +use proc_macro2::Span; use syn::{Generics, Ident, Type}; use crate::parse_internal; @@ -25,6 +26,10 @@ pub struct EvaluatedForEntry { pub namespace: PathWithTypeArgs, pub mapping_key: Type, pub mapping_value: Type, + /// The diagnostic span carried through to the entry's impls; sourced from the + /// user token behind the statement (the `namespace` name, or a `for` mapping's + /// key) since `mapping_key` may be a synthesized `__Key__`. + pub span: Span, } /// Lower every for-entry a `namespace`/`for` statement produces into evaluated @@ -82,6 +87,7 @@ impl EvalDelegateEntry for EvaluatedForEntry { generics, key: self.mapping_key.clone(), value: self.mapping_value.clone(), + span: self.span, }; Ok(entry) diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs index 15d66ccc..c77f2d32 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/for_loop.rs @@ -77,6 +77,7 @@ impl EvalForEntries for ForDelegateStatement { namespace: self.namespace.clone(), mapping_key: key.key, mapping_value: value_type.clone(), + span: key.span, }; entries.push(entry); diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/namespace.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/namespace.rs index 62f5d167..3d02f09b 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/namespace.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/namespace.rs @@ -44,6 +44,7 @@ impl EvalForEntry for NamespaceDelegateStatement { mapping_key: parse_internal!(__Key__), mapping_value: parse_internal!(__Value__), namespace: self.ident.clone().into(), + span: self.ident.span(), }; Ok(entry) diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/open.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/open.rs index 6a64e5e3..e38b7fb4 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/statement/open.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/statement/open.rs @@ -1,5 +1,6 @@ use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; +use syn::spanned::Spanned; use syn::token::{Brace, Comma, Semi}; use syn::{Type, braced}; @@ -63,6 +64,7 @@ impl EvalDelegateEntries for OpenDelegateStatement { entries.push(EvaluatedDelegateEntry { table_type: table_type.clone(), generics: Default::default(), + span: component.span(), key, value, }) diff --git a/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs b/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs index b3eaf2f3..fd607c2d 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/inherit.rs @@ -35,6 +35,7 @@ impl EvalForEntry for InheritNamespaceStatement { for_value: parse_internal!(__Value__), mapping_key: parse_internal!(__Key__), mapping_value: parse_internal!(__Value__), + span: self.namespace.ident().span(), namespace: self.namespace.clone(), }; diff --git a/crates/macros/cgp-macro-core/src/types/path/path_element.rs b/crates/macros/cgp-macro-core/src/types/path/path_element.rs index ea1cdc73..76ee53aa 100644 --- a/crates/macros/cgp-macro-core/src/types/path/path_element.rs +++ b/crates/macros/cgp-macro-core/src/types/path/path_element.rs @@ -1,6 +1,7 @@ -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; use syn::{Ident, Type}; use crate::functions::parse_internal; @@ -36,6 +37,19 @@ impl Parse for PathElement { } } +impl PathElement { + /// The element's original source span. A `Symbol` keeps its parse-time span + /// explicitly (its `ToTokens` synthesizes new `Chars`/`Symbol` tokens that + /// would otherwise be `call_site`); a `Type` element carries its span in its + /// own tokens. Used to point a diagnostic at the path segment the user wrote. + pub fn span(&self) -> Span { + match self { + Self::Type(ty) => ty.span(), + Self::Symbol(symbol) => symbol.span, + } + } +} + impl ToType for PathElement { fn to_type(&self) -> Type { match self { diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_path_key.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_path_key.rs new file mode 100644 index 00000000..d9cee975 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_path_key.rs @@ -0,0 +1,30 @@ +//! Acceptable failure: two `@`-path entries in one `delegate_components!` block +//! that map the same namespace path produce two conflicting `DelegateComponent` +//! impls (keyed by the same `PathCons<..>` type) for `App`, rejected with the +//! coherence error E0119 — the `@`-path analogue of [duplicate_key_same_block.rs]. +//! +//! This fixture pins the **error span** for an `@`-path key. The key type is a +//! synthesized `PathCons<..>` nest whose own span points at the macro +//! `call_site`; the entry instead carries the span of the path segments the user +//! wrote, so E0119 lands on the duplicated `ErrorTypeProviderComponent` segment +//! (the path's leaf) rather than on the whole block. If the path-key span +//! threading in `key/path.rs` regresses, the caret snaps back to the block and +//! this `.stderr` changes. +//! +//! See docs/implementation/entrypoints/delegate_components.md (Error spans). + +use cgp::core::error::ErrorTypeProviderComponent; +use cgp::prelude::*; + +pub struct App; + +delegate_components! { + App { + namespace DefaultNamespace; + + @cgp.core.error.ErrorTypeProviderComponent: UseType, + @cgp.core.error.ErrorTypeProviderComponent: UseType, + } +} + +fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_path_key.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_path_key.stderr new file mode 100644 index 00000000..f2f6c109 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_path_key.stderr @@ -0,0 +1,17 @@ +error[E0119]: conflicting implementations of trait `DelegateComponent>>>, PathCons>>>>, PathCons>>>>>, PathCons>>>>` for type `App` + --> tests/acceptable/delegate_components/duplicate_path_key.rs:26:25 + | +25 | @cgp.core.error.ErrorTypeProviderComponent: UseType, + | -------------------------- first implementation here +26 | @cgp.core.error.ErrorTypeProviderComponent: UseType, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `App` + +error[E0119]: conflicting implementations of trait `IsProviderFor>>>, PathCons>>>>, PathCons>>>>>, PathCons>>>, _, _>` for type `App` + --> tests/acceptable/delegate_components/duplicate_path_key.rs:26:25 + | +25 | @cgp.core.error.ErrorTypeProviderComponent: UseType, + | -------------------------- first implementation here +26 | @cgp.core.error.ErrorTypeProviderComponent: UseType, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `App` + | + = note: downstream crates may implement trait `cgp::prelude::IsProviderFor>>>, cgp::prelude::PathCons>>>>, cgp::prelude::PathCons>>>>>, cgp::prelude::PathCons>>>, _, _>` for type `cgp::prelude::UseType` diff --git a/docs/implementation/asts/delegate_component.md b/docs/implementation/asts/delegate_component.md index aaf51d65..7517f491 100644 --- a/docs/implementation/asts/delegate_component.md +++ b/docs/implementation/asts/delegate_component.md @@ -24,7 +24,7 @@ The `delegate_component` stack is the set of AST types that `delegate_components - **`NamespaceDelegateStatement`** (`namespace SomeNamespace;`) — forwards every lookup through a namespace trait. It lowers via the shared "for-entry" path to a blanket `DelegateComponent<__Key__>` impl bounded on `__Key__: SomeNamespace`, so any key the namespace defines is inherited. - **`ForDelegateStatement`** (`for in SomeTable where … { … }`) — a loop that pulls mappings out of another lookup table. Each inner mapping produces a for-entry whose namespace-trait bound is reconstructed with the table type and a `Delegate = value` binding appended to the namespace path's arguments. -The namespace and `for` forms share `EvaluatedForEntry` and `eval_delegate_entries_via_for`, which build the `Namespace<…, Delegate = …>` bound and the `__Key__`/`__Value__` generics before producing the final `EvaluatedDelegateEntry`. +The namespace and `for` forms share `EvaluatedForEntry` and `eval_delegate_entries_via_for`, which build the `Namespace<…, Delegate = …>` bound and the `__Key__`/`__Value__` generics before producing the final `EvaluatedDelegateEntry`. `EvaluatedForEntry` carries its own diagnostic `span` (the namespace name, or a `for` mapping's key) since its `mapping_key` is a synthesized `__Key__`; it passes that span to the entry. ## `DelegateMapping` @@ -38,7 +38,7 @@ Only Normal and Direct mappings can carry a nested inner table (their values are ## A key: `DelegateKey` -`DelegateKey` is the left side of a mapping, an enum over three shapes selected by a lookahead on a fork (after skipping any attributes and generics): a leading `@` gives a `PathDelegateKey`, a leading bracket gives a `MultiDelegateKey`, otherwise a `SingleDelegateKey`. `EvalDelegateKey::eval` returns a `Vec` (a type plus its generics), so a single mapping can expand to several keyed impls. +`DelegateKey` is the left side of a mapping, an enum over three shapes selected by a lookahead on a fork (after skipping any attributes and generics): a leading `@` gives a `PathDelegateKey`, a leading bracket gives a `MultiDelegateKey`, otherwise a `SingleDelegateKey`. `EvalDelegateKey::eval` returns a `Vec` (a type, its generics, and a diagnostic `span`), so a single mapping can expand to several keyed impls. The `span` is the source token the key was written as, carried separately because a `@`-path key lowers to a synthesized `PathCons<..>` type whose own span points at the macro `call_site`; it flows into the entry and drives the [error-span re-spanning](../entrypoints/delegate_components.md#error-spans). - **`SingleDelegateKey`** — attributes, an optional generic list, and a type; evaluates to one key carrying its generics. - **`MultiDelegateKey`** — a bracketed, comma-separated list of `SingleDelegateKey`; the array-key form, evaluating to one key per element so `[A, B]: P` becomes two impls. @@ -52,7 +52,7 @@ Only Normal and Direct mappings can carry a nested inner table (their values are ## `EvaluatedDelegateEntry` -`EvaluatedDelegateEntry` is the lowered form every key/value/statement collapses to: the target type, the merged generics, the key type, and the value (`Delegate`) type. It owns the rendering: `build_delegate_component_impl` emits the `DelegateComponent for TableType { type Delegate = Value; }` impl, and `build_is_provider_for_impl` emits the forwarding `IsProviderFor` impl bounded on `Value: IsProviderFor`, appending the reserved `__Context__` and `__Params__` generics. (`build_namespace_impl`, on the same type, is used by the namespace preset machinery to emit a `Namespace for Key` impl instead.) +`EvaluatedDelegateEntry` is the lowered form every key/value/statement collapses to: the target type, the merged generics, the key type, the value (`Delegate`) type, and the diagnostic `span`. It owns the rendering: `build_delegate_component_impl` emits the `DelegateComponent for TableType { type Delegate = Value; }` impl, and `build_is_provider_for_impl` emits the forwarding `IsProviderFor` impl bounded on `Value: IsProviderFor`, appending the reserved `__Context__` and `__Params__` generics. Both then pass through `respan_impl`, which re-spans the finished impl onto the entry's `span` (restoring the generics) so a coherence conflict points at the entry rather than the whole block — see [Error spans](../entrypoints/delegate_components.md#error-spans). (`build_namespace_impl`, on the same type, is used by the namespace preset machinery to emit a `Namespace for Key` impl instead.) ## Tests diff --git a/docs/implementation/asts/path.md b/docs/implementation/asts/path.md index 4215fe81..b63ec3e0 100644 --- a/docs/implementation/asts/path.md +++ b/docs/implementation/asts/path.md @@ -12,7 +12,7 @@ ErrorRaiserComponent // -> Type (capitalized) u32 // -> Type (lowercase but primitive) ``` -The primitive check recognizes the `iN`/`uN`/`fN` numeric families plus `char`, `bool`, `usize`, `isize`, and `str`. `PathElement` emits itself through `ToTokens` (delegating to the symbol or the type) and also implements the internal `ToType` trait for callers that need a `syn::Type`. +The primitive check recognizes the `iN`/`uN`/`fN` numeric families plus `char`, `bool`, `usize`, `isize`, and `str`. `PathElement` emits itself through `ToTokens` (delegating to the symbol or the type) and also implements the internal `ToType` trait for callers that need a `syn::Type`. Its `span()` accessor returns the segment's source span — the type's own span, or the `Symbol`'s stored parse-time span — which `delegate_components!` joins across a path to point an [error span](../entrypoints/delegate_components.md#error-spans) at an `@`-path key. ## `UniPath` diff --git a/docs/implementation/entrypoints/delegate_components.md b/docs/implementation/entrypoints/delegate_components.md index e9d99777..97d4719b 100644 --- a/docs/implementation/entrypoints/delegate_components.md +++ b/docs/implementation/entrypoints/delegate_components.md @@ -62,9 +62,11 @@ An **`@`-path key** carries a leading `__Wildcard__` generic and lowers the path Each generated impl is re-spanned onto the entry that produced it, so a compiler error about that impl points at the entry the user wrote rather than at the whole `delegate_components!` block. The impls are built with `parse_internal!`, which quasi-quotes their tokens; those tokens carry the macro invocation's `call_site` span, and the only tokens with a narrower span are the interpolated key, value, and target type. A coherence conflict (`E0119`) between two entries mapping the same key is reported on the impl header — its `impl` keyword and trait reference — which is exactly the part that starts at `call_site`, so without correction the error underlines the entire block, and two overlapping entries produce two block-wide carets that say nothing about which entry to fix. -`build_delegate_component_impl` and `build_is_provider_for_impl` fix this through `respan_impl`, which mirrors the `override_span` technique that `check_components!` uses to aim an unsatisfied-bound error at the checked component. It re-spans every token of the finished impl onto the entry's key (`self.key.span()`), then restores the impl's original generics. The re-span moves the header — and therefore the `E0119` caret — onto the `Key` token the user wrote, while restoring the generics keeps a diagnostic about a per-entry generic pointing where the user declared it: an unconstrained parameter's `E0207`, for instance, still lands on the `` rather than being dragged onto the key. The shared helper lives in [functions/override_span.rs](../../../crates/macros/cgp-macro-core/src/functions/override_span.rs). +`build_delegate_component_impl` and `build_is_provider_for_impl` fix this through `respan_impl`, which mirrors the `override_span` technique that `check_components!` uses to aim an unsatisfied-bound error at the checked component. It re-spans every token of the finished impl onto the entry's diagnostic span, then restores the impl's original generics. The re-span moves the header — and therefore the `E0119` caret — onto what the user wrote, while restoring the generics keeps a diagnostic about a per-entry generic pointing where the user declared it: an unconstrained parameter's `E0207`, for instance, still lands on the `` rather than being dragged onto the key. The shared helper lives in [functions/override_span.rs](../../../crates/macros/cgp-macro-core/src/functions/override_span.rs). -The re-span is only as precise as the key's own span. A plain `Key: Provider` mapping carries the user's key token, so the error lands on it; a synthesized key — an `@`-path assembled by the `open`/`namespace` machinery — may still fall back to a coarser span, which is acceptable because these forms cannot produce the same-key coherence conflict that motivated the fix. Because the `.stderr` fixtures record the exact line and column of each caret, they double as regression tests for these spans: reverting the re-span snaps the carets back to the block and the snapshots change (see [Tests](#tests)). +The diagnostic span is carried explicitly rather than read back from the generated key, because a key type may be *synthesized* and so no longer carry a useful span. This follows the [`EvaluatedCheckEntry.span`](check_components.md) pattern: `EvaluatedDelegateKey` and `EvaluatedDelegateEntry` each hold a `span` field, populated at eval time from the token the user actually wrote and threaded through to `respan_impl`. A plain `Key: Provider` mapping sources it from the key type's own span; an `@`-path key sources it from the path segments, since the key it lowers to is a `PathCons<..>` nest whose first token is a `call_site`-spanned `PathCons` (matching `Symbol`, which likewise keeps a parse-time span so its synthesized `Chars`/`Symbol` tokens do not fall back to `call_site`). For an `@`-path the span is the join of the segment spans, which widens to the whole path where the toolchain supports `Span::join` and otherwise keeps the leaf segment — the component of a namespace path, or the dispatch key of an `@Component.key` entry — so the caret lands on the segment that discriminates entries sharing a prefix. + +Because the `.stderr` fixtures record the exact line and column of each caret, they double as regression tests for these spans: reverting the re-span snaps the carets back to the block and the snapshots change (see [Tests](#tests)). ## Failure modes @@ -152,6 +154,7 @@ The compile-fail fixtures in `cgp-compile-fail-tests` pin the expansions that fa - [acceptable/delegate_components/duplicate_key.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.rs) — two blocks mapping the same key expand to conflicting `DelegateComponent` impls (`E0119`). - [acceptable/delegate_components/duplicate_key_same_block.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.rs) — the same conflict from two entries in one block; its `.stderr` pins the per-entry [error spans](#error-spans), each caret landing on its own `GreeterComponent` key rather than the whole block. +- [acceptable/delegate_components/duplicate_path_key.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_path_key.rs) — the `@`-path analogue: two identical `@cgp.core.error.ErrorTypeProviderComponent` entries under a `namespace` header conflict, and its `.stderr` pins the [error span](#error-spans) landing on the duplicated path leaf rather than the whole block, even though the key lowers to a synthesized `PathCons<..>` type. - [acceptable/delegate_components/overlapping_generic.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.rs) — a generic ` Wrapper` entry overlaps a specific `Wrapper` entry at the same key (`E0119`). - [acceptable/delegate_components/missing_dependency.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.rs) — a lazily-wired provider whose `Self: HasName` dependency the context does not satisfy; the unmet bound surfaces at the call site (`E0599`). - [acceptable/delegate_components/unconstrained_generic.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/unconstrained_generic.rs) — a per-entry generic that appears only in the value (` GreeterComponent: GreetWith`) lowers to an impl with an unconstrained `T` (`E0207`), which the compiler rejects as it would a hand-written impl. From 8a343f283005ccf632c3bcb96de83cea3e6c8f32 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 22:58:07 +0200 Subject: [PATCH 10/17] Verified no other span leaks --- crates/extra/cgp-dispatch/src/lib.rs | 1 + .../delegate_components/duplicate_open_key.rs | 39 +++++++++++++++++++ .../duplicate_open_key.stderr | 19 +++++++++ .../entrypoints/delegate_components.md | 5 ++- 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_open_key.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_open_key.stderr diff --git a/crates/extra/cgp-dispatch/src/lib.rs b/crates/extra/cgp-dispatch/src/lib.rs index dea424b9..6568ec82 100644 --- a/crates/extra/cgp-dispatch/src/lib.rs +++ b/crates/extra/cgp-dispatch/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +#![allow(clippy::needless_lifetimes)] mod providers; diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_open_key.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_open_key.rs new file mode 100644 index 00000000..cccecb8b --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_open_key.rs @@ -0,0 +1,39 @@ +//! Acceptable failure: an `open` header and an explicit mapping that both wire +//! the same component. `open GreeterComponent;` emits a `DelegateComponent< +//! GreeterComponent>` redirect impl, and the following `GreeterComponent: +//! GreetHello` emits another, so they conflict with the coherence error E0119 — +//! the third duplicate-key shape named in the Failure modes doc, alongside +//! [duplicate_key.rs] (two blocks) and [duplicate_key_same_block.rs] (two plain +//! entries). +//! +//! This fixture pins the **error span** for the `open`-header entry, whose span +//! is sourced in `statement/open.rs` from the opened component (a distinct source +//! from the plain key path): the "first implementation here" note lands on the +//! `GreeterComponent` inside `open …;`, and the conflict caret on the explicit +//! mapping's key. +//! +//! See docs/implementation/entrypoints/delegate_components.md (Error spans). + +use cgp::prelude::*; + +#[cgp_component(Greeter)] +pub trait CanGreet { + fn greet(&self); +} + +#[cgp_impl(new GreetHello)] +impl Greeter { + fn greet(&self) {} +} + +pub struct Person; + +delegate_components! { + Person { + open GreeterComponent; + + GreeterComponent: GreetHello, + } +} + +fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_open_key.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_open_key.stderr new file mode 100644 index 00000000..bd7f1429 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_open_key.stderr @@ -0,0 +1,19 @@ +error[E0119]: conflicting implementations of trait `IsProviderFor` for type `Person` + --> tests/acceptable/delegate_components/duplicate_open_key.rs:35:9 + | +33 | open GreeterComponent; + | ---------------- first implementation here +34 | +35 | GreeterComponent: GreetHello, + | ^^^^^^^^^^^^^^^^ conflicting implementation for `Person` + | + = note: downstream crates may implement trait `cgp::prelude::IsProviderFor` for type `cgp::prelude::RedirectLookup>` + +error[E0119]: conflicting implementations of trait `DelegateComponent` for type `Person` + --> tests/acceptable/delegate_components/duplicate_open_key.rs:35:9 + | +33 | open GreeterComponent; + | ---------------- first implementation here +34 | +35 | GreeterComponent: GreetHello, + | ^^^^^^^^^^^^^^^^ conflicting implementation for `Person` diff --git a/docs/implementation/entrypoints/delegate_components.md b/docs/implementation/entrypoints/delegate_components.md index 97d4719b..fa6052d9 100644 --- a/docs/implementation/entrypoints/delegate_components.md +++ b/docs/implementation/entrypoints/delegate_components.md @@ -64,7 +64,9 @@ Each generated impl is re-spanned onto the entry that produced it, so a compiler `build_delegate_component_impl` and `build_is_provider_for_impl` fix this through `respan_impl`, which mirrors the `override_span` technique that `check_components!` uses to aim an unsatisfied-bound error at the checked component. It re-spans every token of the finished impl onto the entry's diagnostic span, then restores the impl's original generics. The re-span moves the header — and therefore the `E0119` caret — onto what the user wrote, while restoring the generics keeps a diagnostic about a per-entry generic pointing where the user declared it: an unconstrained parameter's `E0207`, for instance, still lands on the `` rather than being dragged onto the key. The shared helper lives in [functions/override_span.rs](../../../crates/macros/cgp-macro-core/src/functions/override_span.rs). -The diagnostic span is carried explicitly rather than read back from the generated key, because a key type may be *synthesized* and so no longer carry a useful span. This follows the [`EvaluatedCheckEntry.span`](check_components.md) pattern: `EvaluatedDelegateKey` and `EvaluatedDelegateEntry` each hold a `span` field, populated at eval time from the token the user actually wrote and threaded through to `respan_impl`. A plain `Key: Provider` mapping sources it from the key type's own span; an `@`-path key sources it from the path segments, since the key it lowers to is a `PathCons<..>` nest whose first token is a `call_site`-spanned `PathCons` (matching `Symbol`, which likewise keeps a parse-time span so its synthesized `Chars`/`Symbol` tokens do not fall back to `call_site`). For an `@`-path the span is the join of the segment spans, which widens to the whole path where the toolchain supports `Span::join` and otherwise keeps the leaf segment — the component of a namespace path, or the dispatch key of an `@Component.key` entry — so the caret lands on the segment that discriminates entries sharing a prefix. +The diagnostic span is carried explicitly rather than read back from the generated key, because a key type may be *synthesized* and so no longer carry a useful span. This follows the [`EvaluatedCheckEntry.span`](check_components.md) pattern: `EvaluatedDelegateKey`, `EvaluatedDelegateEntry`, and the `namespace`/`for` intermediary `EvaluatedForEntry` each hold a `span` field, populated at eval time from the token the user actually wrote and threaded through to `respan_impl`. Every entry form sources its span this way, so none falls back to `call_site`: a plain `Key: Provider`, `Key -> Value`, or `Key => @path` mapping (and each element of an array key) takes the key type's own span; the `open` header takes the opened component's span; a `namespace`/`for` statement takes the namespace name or the loop mapping's key; and a nested `UseDelegate` table lowers through the same per-entry path, so its inner keys are spanned too. + +An `@`-path key needs more than its lowered type, since that type is a `PathCons<..>` nest whose first token is a `call_site`-spanned `PathCons` (the same problem `Symbol` avoids by keeping a parse-time span so its synthesized `Chars`/`Symbol` tokens do not fall back to `call_site`). So an `@`-path sources its span from the path segments — the join of the segment spans, which widens to the whole path where the toolchain supports `Span::join` and otherwise keeps the leaf segment (the component of a namespace path, or the dispatch key of an `@Component.key` entry), so the caret lands on the segment that discriminates entries sharing a prefix. Because the `.stderr` fixtures record the exact line and column of each caret, they double as regression tests for these spans: reverting the re-span snaps the carets back to the block and the snapshots change (see [Tests](#tests)). @@ -155,6 +157,7 @@ The compile-fail fixtures in `cgp-compile-fail-tests` pin the expansions that fa - [acceptable/delegate_components/duplicate_key.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key.rs) — two blocks mapping the same key expand to conflicting `DelegateComponent` impls (`E0119`). - [acceptable/delegate_components/duplicate_key_same_block.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_key_same_block.rs) — the same conflict from two entries in one block; its `.stderr` pins the per-entry [error spans](#error-spans), each caret landing on its own `GreeterComponent` key rather than the whole block. - [acceptable/delegate_components/duplicate_path_key.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_path_key.rs) — the `@`-path analogue: two identical `@cgp.core.error.ErrorTypeProviderComponent` entries under a `namespace` header conflict, and its `.stderr` pins the [error span](#error-spans) landing on the duplicated path leaf rather than the whole block, even though the key lowers to a synthesized `PathCons<..>` type. +- [acceptable/delegate_components/duplicate_open_key.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/duplicate_open_key.rs) — an `open` header colliding with an explicit mapping for the same component; its `.stderr` pins the [error span](#error-spans) of the `open`-header entry, whose span comes from the opened component (a source distinct from the plain key path). - [acceptable/delegate_components/overlapping_generic.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/overlapping_generic.rs) — a generic ` Wrapper` entry overlaps a specific `Wrapper` entry at the same key (`E0119`). - [acceptable/delegate_components/missing_dependency.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.rs) — a lazily-wired provider whose `Self: HasName` dependency the context does not satisfy; the unmet bound surfaces at the call site (`E0599`). - [acceptable/delegate_components/unconstrained_generic.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/unconstrained_generic.rs) — a per-entry generic that appears only in the value (` GreeterComponent: GreetWith`) lowers to an impl with an unconstrained `T` (`E0207`), which the compiler rejects as it would a hand-written impl. From 40eb530ee604ceb674c72a2eeccbf927d78fd96c Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 23:02:22 +0200 Subject: [PATCH 11/17] Codify span leaks rule --- AGENTS.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index acf7800f..ec495e43 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -191,11 +191,30 @@ right input: be emitted through the `crate::exports` markers so it resolves as `::cgp::macro_prelude::`, never as a bare or hand-written path — this is what lets a user with only `cgp` in scope compile the output. Grep the codegen for any CGP name that is not interpolated from an `exports` marker. +- **Aim every generated item's span at the token the user wrote (watch for `call_site` leaks).** A + macro builds its output with `parse_internal!`/`quote!`, which stamp the structural tokens — the + `impl` keyword, the trait reference, the self type — with the macro's `call_site` span (the whole + invocation), while only the interpolated user fragments keep a narrower span. A compiler error that + reports on an item's header then underlines the *entire macro block* instead of the entry, + attribute, or impl the user actually wrote: a coherence conflict (`E0119`) between two generated + impls, an unsatisfied bound, or a name-resolution failure all read as "somewhere in this macro." + Re-span each generated item onto its originating token, following the + `delegate_components!`/`check_components!` pattern — the shared + [`override_span`](crates/macros/cgp-macro-core/src/functions/override_span.rs) helper re-spans an + item's tokens (restore the generics afterward so a per-entry generic's `E0207` keeps pointing at the + `` the user wrote), and where the originating token is *synthesized* and has lost its span (a + `PathCons<..>` nest, a `Symbol`'s `Chars` encoding), carry an explicit span field through the + evaluated form as `EvaluatedCheckEntry.span` and `EvaluatedDelegateEntry.span` do — mirroring + `Symbol`, which keeps its parse-time span and stamps its output with `quote_spanned!`. The same leak + lurks in the provider macros (`#[cgp_impl]`, `#[cgp_provider]`, `#[cgp_new_provider]`) and every + other expansion, so confirm a duplicated or conflicting generated item points at the impl or + attribute the user wrote, not the macro name. These spans are testable: a `trybuild` `.stderr` + fixture records the exact line and column of each caret, so a span regression changes the snapshot + (see the `acceptable/delegate_components/duplicate_*` fixtures). Beyond these, weigh the concerns that recur across the macro suite: the hygiene of the reserved -identifiers the expansion introduces (`__Component__`, `__Context__`, and the like), the span -attached to each generated item so a downstream type error points at the token the user actually -wrote, and the idempotency of the expansion when the same entry is listed more than once. +identifiers the expansion introduces (`__Component__`, `__Context__`, and the like) and the +idempotency of the expansion when the same entry is listed more than once. ### Keep every view in sync and verify From 52062395344d756f215ca10c3e65cd61f032dd4a Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 23:28:20 +0200 Subject: [PATCH 12/17] Review and fix span leaks in `#[cgp_impl]` --- .../attributes/default_impl/attribute.rs | 14 ++++++- .../src/types/cgp_impl/lowered.rs | 11 ++++- .../cgp-macro-core/src/types/empty_struct.rs | 26 ++++++++++-- .../cgp-macro-core/src/types/provider_impl.rs | 10 +++-- .../cgp_impl/duplicate_default_impl.rs | 42 +++++++++++++++++++ .../cgp_impl/duplicate_default_impl.stderr | 8 ++++ .../cgp_impl/duplicate_provider_name.rs | 35 ++++++++++++++++ .../cgp_impl/duplicate_provider_name.stderr | 36 ++++++++++++++++ .../use_provider_impl.rs | 2 +- docs/implementation/asts/cgp_provider.md | 6 +-- docs/implementation/entrypoints/cgp_impl.md | 14 +++++++ .../entrypoints/cgp_provider.md | 2 +- docs/reference/macros/cgp_new_provider.md | 2 +- 13 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_default_impl.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_default_impl.stderr create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_provider_name.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_provider_name.stderr diff --git a/crates/macros/cgp-macro-core/src/types/attributes/default_impl/attribute.rs b/crates/macros/cgp-macro-core/src/types/attributes/default_impl/attribute.rs index b9ad4e7b..ddb6ce94 100644 --- a/crates/macros/cgp-macro-core/src/types/attributes/default_impl/attribute.rs +++ b/crates/macros/cgp-macro-core/src/types/attributes/default_impl/attribute.rs @@ -1,7 +1,9 @@ use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; use syn::token::In; use syn::{Generics, ItemImpl, Type}; +use crate::functions::override_span; use crate::parse_internal; use crate::types::ident::PathWithTypeArgs; use crate::types::path::UniPathOrType; @@ -31,7 +33,7 @@ impl DefaultImplAttribute { let (impl_generics, _, where_clause) = generics.split_for_impl(); - let item_impl = parse_internal! { + let item_impl: ItemImpl = parse_internal! { impl #impl_generics #namespace_trait_path for #key_type #where_clause { @@ -39,6 +41,16 @@ impl DefaultImplAttribute { } }; + // The impl is built wholly from quasi-quoted tokens, so its `impl`/`for` + // structural tokens carry the macro `call_site` span. Re-span it onto the + // user-written key token so a coherence conflict (`E0119`) between two + // default impls for the same key is reported on that key rather than on + // the whole `#[cgp_impl]` attribute. Generics keep their own spans, + // restored afterward, mirroring `EvaluatedDelegateEntry::respan_impl`. + let generics = item_impl.generics.clone(); + let mut item_impl = override_span(key_type.span(), &item_impl)?; + item_impl.generics = generics; + Ok(item_impl) } } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs b/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs index 2f3bbdc7..77f599e1 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_impl/lowered.rs @@ -93,10 +93,19 @@ impl LoweredCgpImpl { .args .insert(0, parse_internal!(#context_type)); + // Reuse the user's `for` token span when the block was written in the + // explicit `impl Trait for Context` form; otherwise (the bare `impl Trait` + // form) aim the synthesized `for` at the provider trait the user wrote, so + // no structural token of the generated impl carries the macro `call_site`. + let for_token = match &item_impl.trait_ { + Some((_, _, for_token)) => *for_token, + None => For(self.provider_trait_path.span()), + }; + out_impl.trait_ = Some(( None, parse_internal(provider_trait_path.to_token_stream())?, - For(Span::call_site()), + for_token, )); ReplaceSelfTypeVisitor { diff --git a/crates/macros/cgp-macro-core/src/types/empty_struct.rs b/crates/macros/cgp-macro-core/src/types/empty_struct.rs index caaf6aa1..53c33a11 100644 --- a/crates/macros/cgp-macro-core/src/types/empty_struct.rs +++ b/crates/macros/cgp-macro-core/src/types/empty_struct.rs @@ -1,4 +1,4 @@ -use quote::{ToTokens, quote}; +use quote::{ToTokens, quote_spanned}; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{GenericParam, Generics, Ident, ItemStruct, Type, parse_quote}; @@ -21,8 +21,15 @@ impl ToTokens for EmptyStruct { let struct_ident = &self.ident; let struct_generics = &self.generics; + // Stamp the synthesized `pub struct … ;` tokens with the struct ident's + // span rather than the macro `call_site`, so a redefinition error + // (`E0428`) when two providers share a name points at the offending name + // (e.g. the `Foo` in `#[cgp_impl(new Foo)]`) instead of the whole macro + // invocation. + let span = struct_ident.span(); + if struct_generics.params.is_empty() { - tokens.extend(quote! { + tokens.extend(quote_spanned! { span => pub struct #struct_ident; }); } else { @@ -49,9 +56,20 @@ impl ToTokens for EmptyStruct { } } - tokens.extend(quote! { + // Emit `PhantomData` for a single parameter and `PhantomData<()>` + // when none survive (all consts), reserving the tuple form for two or + // more. A single-element `PhantomData<(T)>` is a parenthesized type, + // not a tuple, and now that the struct carries the user's span (above) + // it would trip the `unused_parens` lint in the caller's crate. + let phantom_type: Type = if phantom_params.len() == 1 { + phantom_params.into_iter().next().unwrap() + } else { + parse_quote!( ( #phantom_params ) ) + }; + + tokens.extend(quote_spanned! { span => pub struct #struct_ident < #generic_params > ( - pub ::core::marker::PhantomData<( #phantom_params )> + pub ::core::marker::PhantomData< #phantom_type > ); }) } diff --git a/crates/macros/cgp-macro-core/src/types/provider_impl.rs b/crates/macros/cgp-macro-core/src/types/provider_impl.rs index 81b81051..ab8bf171 100644 --- a/crates/macros/cgp-macro-core/src/types/provider_impl.rs +++ b/crates/macros/cgp-macro-core/src/types/provider_impl.rs @@ -1,9 +1,7 @@ use std::collections::BTreeMap; -use proc_macro2::Span; use quote::ToTokens; use syn::spanned::Spanned; -use syn::token::For; use syn::{Error, ItemImpl, Path, Type}; use crate::exports::IsProviderFor; @@ -55,7 +53,7 @@ impl ItemProviderImpl { let component_type = &self.component_type; let item_impl = &self.item_impl; - let (_, provider_path, _) = item_impl.trait_.as_ref().ok_or_else(|| { + let (_, provider_path, for_token) = item_impl.trait_.as_ref().ok_or_else(|| { Error::new(item_impl.span(), "provider impl should contain trait path") })?; @@ -76,7 +74,11 @@ impl ItemProviderImpl { is_provider_impl.defaultness = None; is_provider_impl.unsafety = None; - is_provider_impl.trait_ = Some((None, is_provider_path, For(Span::call_site()))); + // Reuse the provider impl's own `for` token span rather than a fresh + // `call_site` one, so this derived impl's structural tokens point at the + // provider impl the user wrote (or, for `#[cgp_impl]`, at the trait the + // consumer-style block named) instead of the whole macro invocation. + is_provider_impl.trait_ = Some((None, is_provider_path, *for_token)); let provider_map = BTreeMap::from([(provider_ident.clone(), component_type.clone())]); replace_provider_in_generics(&provider_map, &mut is_provider_impl.generics); diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_default_impl.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_default_impl.rs new file mode 100644 index 00000000..b971a70f --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_default_impl.rs @@ -0,0 +1,42 @@ +//! Acceptable failure: two `#[cgp_impl]` blocks each carrying a +//! `#[default_impl(String in DefaultImpls1<..>)]` for the same key emit two +//! conflicting `DefaultImpls1` impls for +//! `String`, which the Rust compiler rejects with the coherence error E0119. +//! `#[cgp_impl]` lowers each block independently and has no view of the other, +//! so it correctly defers to the compiler, exactly as two hand-written +//! overlapping impls would. +//! +//! The carets fall on the `String` key inside `#[default_impl(...)]` rather than +//! on the whole `#[cgp_impl]` attribute, because the synthesized default-impl is +//! re-spanned onto that key token (see +//! cgp-macro-core/src/types/attributes/default_impl/attribute.rs). A regression +//! that dropped the re-span would move the carets back onto the macro attribute. +//! +//! See docs/implementation/entrypoints/cgp_impl.md (Failure modes). + +use cgp::core::component::DefaultImpls1; +use cgp::prelude::*; + +#[cgp_component(ShowImpl)] +pub trait Show { + fn show(&self, value: &T) -> String; +} + +#[cgp_impl(new ShowStringA)] +#[default_impl(String in DefaultImpls1)] +impl ShowImpl { + fn show(&self, value: &String) -> String { + value.clone() + } +} + +// Re-registering the same key as a per-type default emits a conflicting impl. +#[cgp_impl(new ShowStringB)] +#[default_impl(String in DefaultImpls1)] +impl ShowImpl { + fn show(&self, value: &String) -> String { + value.clone() + } +} + +fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_default_impl.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_default_impl.stderr new file mode 100644 index 00000000..3423df8b --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_default_impl.stderr @@ -0,0 +1,8 @@ +error[E0119]: conflicting implementations of trait `DefaultImpls1` for type `String` + --> tests/acceptable/cgp_impl/duplicate_default_impl.rs:35:16 + | +26 | #[default_impl(String in DefaultImpls1)] + | ------ first implementation here +... +35 | #[default_impl(String in DefaultImpls1)] + | ^^^^^^ conflicting implementation for `String` diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_provider_name.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_provider_name.rs new file mode 100644 index 00000000..477d37e8 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_provider_name.rs @@ -0,0 +1,35 @@ +//! Acceptable failure: two `#[cgp_impl(new GreetHello)]` blocks each declare a +//! `pub struct GreetHello;`, so the name is defined twice (E0428) and the two +//! provider impls also conflict (E0119). `#[cgp_impl]` lowers each block +//! independently and has no view of the other, so it correctly defers both to +//! the compiler, exactly as two hand-written definitions would. +//! +//! The E0428 carets fall on the `GreetHello` name inside `#[cgp_impl(new …)]` +//! rather than on the whole attribute, because the synthesized provider struct is +//! emitted with the struct ident's span (see +//! cgp-macro-core/src/types/empty_struct.rs). A regression that stamped the +//! struct with `call_site` would move the carets back onto the macro attribute. +//! The E0119 carets fall on each provider `impl` block, since those impls are the +//! user's own `#[cgp_impl]` blocks rewritten in place. +//! +//! See docs/implementation/entrypoints/cgp_impl.md (Failure modes). + +use cgp::prelude::*; + +#[cgp_component(Greeter)] +pub trait CanGreet { + fn greet(&self); +} + +#[cgp_impl(new GreetHello)] +impl Greeter { + fn greet(&self) {} +} + +// Re-declaring the same provider name emits a conflicting struct + impls. +#[cgp_impl(new GreetHello)] +impl Greeter { + fn greet(&self) {} +} + +fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_provider_name.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_provider_name.stderr new file mode 100644 index 00000000..9bf8417c --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_provider_name.stderr @@ -0,0 +1,36 @@ +error[E0428]: the name `GreetHello` is defined multiple times + --> tests/acceptable/cgp_impl/duplicate_provider_name.rs:30:16 + | +24 | #[cgp_impl(new GreetHello)] + | ---------- previous definition of the type `GreetHello` here +... +30 | #[cgp_impl(new GreetHello)] + | ^^^^^^^^^^ `GreetHello` redefined here + | + = note: `GreetHello` must be defined only once in the type namespace of this module + +error[E0119]: conflicting implementations of trait `Greeter<_>` for type `GreetHello` + --> tests/acceptable/cgp_impl/duplicate_provider_name.rs:31:1 + | +25 | / impl Greeter { +26 | | fn greet(&self) {} +27 | | } + | |_- first implementation here +... +31 | / impl Greeter { +32 | | fn greet(&self) {} +33 | | } + | |_^ conflicting implementation for `GreetHello` + +error[E0119]: conflicting implementations of trait `IsProviderFor` for type `GreetHello` + --> tests/acceptable/cgp_impl/duplicate_provider_name.rs:31:1 + | +25 | / impl Greeter { +26 | | fn greet(&self) {} +27 | | } + | |_- first implementation here +... +31 | / impl Greeter { +32 | | fn greet(&self) {} +33 | | } + | |_^ conflicting implementation for `GreetHello` diff --git a/crates/tests/cgp-tests/tests/higher_order_providers/use_provider_impl.rs b/crates/tests/cgp-tests/tests/higher_order_providers/use_provider_impl.rs index 9a15018a..44523ac7 100644 --- a/crates/tests/cgp-tests/tests/higher_order_providers/use_provider_impl.rs +++ b/crates/tests/cgp-tests/tests/higher_order_providers/use_provider_impl.rs @@ -150,7 +150,7 @@ snapshot_cgp_impl! { Inner: IsProviderFor + AreaCalculator<__Context__>, {} - pub struct ScaledArea(pub ::core::marker::PhantomData<(Inner)>); + pub struct ScaledArea(pub ::core::marker::PhantomData); ") } } diff --git a/docs/implementation/asts/cgp_provider.md b/docs/implementation/asts/cgp_provider.md index 6e92c28d..006affb7 100644 --- a/docs/implementation/asts/cgp_provider.md +++ b/docs/implementation/asts/cgp_provider.md @@ -27,7 +27,7 @@ It packages the original `item_impl` (emitted verbatim), the derived `IsProvider ## `ItemProviderImpl` and the `IsProviderFor` derivation -`ItemProviderImpl` pairs a component type with a provider impl and derives the `IsProviderFor` marker impl from it. `to_is_provider_for_impl` clones the provider impl, clears its body, associated types, attributes, `defaultness`, and `unsafety`, and swaps the trait for `IsProviderFor` — keeping the original generic parameters and `where` clause so the marker holds under exactly the same conditions: +`ItemProviderImpl` pairs a component type with a provider impl and derives the `IsProviderFor` marker impl from it. `to_is_provider_for_impl` clones the provider impl, clears its body, associated types, attributes, `defaultness`, and `unsafety`, and swaps the trait for `IsProviderFor` — keeping the original generic parameters, `where` clause, and the provider impl's own `for` token so the marker holds under exactly the same conditions and none of its structural tokens fall back to the macro `call_site` span (the cloned `impl` keyword, generics, and self type already carry the user's spans; reusing the `for` token keeps the middle of the header from leaking too): ```rust // from impl ComputerRef for FirstNameToString where … @@ -47,11 +47,11 @@ The trait arguments come from `ProviderImplArgs::from_generic_args`, and the der ## `EmptyStruct` -`EmptyStruct` is the provider struct, emitted only when `new` is set. `to_provider_struct` reads the shape from the impl's `Self` type: a plain name yields a unit `pub struct Name;`, while a generic provider yields a struct whose single `PhantomData` field binds every parameter — a lifetime parameter is bound as `Life<'a>` inside the `PhantomData` tuple so the struct stays covariant and `'static`-friendly. +`EmptyStruct` is the provider struct, emitted only when `new` is set. `to_provider_struct` reads the shape from the impl's `Self` type: a plain name yields a unit `pub struct Name;`, while a generic provider yields a struct whose single `PhantomData` field binds every parameter — a lifetime parameter is bound as `Life<'a>` so the struct stays covariant and `'static`-friendly. Two or more parameters are grouped into a `PhantomData` tuple; a lone parameter is bound directly (`PhantomData`, not `PhantomData<(InCode)>`) to avoid a single-element parenthesized type, and a const-only provider gets `PhantomData<()>`. ```rust // generic provider Self type SpawnAndRun -pub struct SpawnAndRun(pub ::core::marker::PhantomData<(InCode)>); +pub struct SpawnAndRun(pub ::core::marker::PhantomData); ``` ## Tests diff --git a/docs/implementation/entrypoints/cgp_impl.md b/docs/implementation/entrypoints/cgp_impl.md index 178d975e..6afa28b9 100644 --- a/docs/implementation/entrypoints/cgp_impl.md +++ b/docs/implementation/entrypoints/cgp_impl.md @@ -56,6 +56,20 @@ The **companion attributes** are applied in `ItemCgpImpl::lower` before the prov The **`#[cgp_impl(Self)]` passthrough** bypasses the whole rewrite: when the provider type is the literal `Self`, `LoweredCgpImpl::lower` returns the original `impl` block unchanged as an ordinary consumer-trait impl, so companion attributes still apply while the body keeps its `self` receiver. This form requires the `for Context` clause; omitting it is rejected with a spanned "Expected context type to be specified" error. +## Error spans + +Because the macro starts from the user's own `syn::ItemImpl` and rewrites it in place, the provider impl and its derived `IsProviderFor` impl keep the user's spans on their `impl` keyword, generics, self type, and body — so a coherence conflict (`E0119`) between two providers already underlines the offending `#[cgp_impl]` block rather than the macro name, exactly as two hand-written impls would. Two tokens are synthesized rather than cloned, and both are aimed at what the user wrote so they cannot leak the `call_site` span: `to_raw_item_impl` gives the provider impl's `for` token the original `for` token's span (explicit `impl Trait for Context` form) or the provider trait path's span (bare `impl Trait` form), and the [`IsProviderFor` derivation](../asts/cgp_provider.md#itemproviderimpl-and-the-isproviderfor-derivation) reuses that same `for` token for its own header. + +The one wholly-synthesized item is each `#[default_impl]` delegation impl, built entirely from quasi-quoted tokens whose `impl`/`for` keywords carry `call_site`. `DefaultImplAttribute::to_item_impl` re-spans it onto the user-written key token — mirroring the `override_span` technique that [`delegate_components!`](delegate_components.md#error-spans) uses — while restoring the impl's generics afterward, so a conflict between two default impls for the same key points at the key inside `#[default_impl(Key in …)]` rather than at the whole `#[cgp_impl]` attribute. + +## Failure modes + +Both failures below are ones `#[cgp_impl]` **intentionally defers to the Rust compiler**: each block lowers independently with no view of any other, so a collision only a whole-program view could catch is left to `rustc`, exactly as two hand-written definitions would be. Each is pinned by a fixture under [acceptable/cgp_impl/](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl) in `cgp-compile-fail-tests`. + +A **duplicate provider name** — two `#[cgp_impl(new Foo)]` blocks — declares `pub struct Foo;` twice (`E0428`) and emits two conflicting provider impls (`E0119`). The `E0428` carets land on the `Foo` inside each `#[cgp_impl(new …)]` and the `E0119` carets on each provider `impl` block, per [Error spans](#error-spans). Pinned by [acceptable/cgp_impl/duplicate_provider_name.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_provider_name.rs). + +A **duplicate `#[default_impl]` key** — two `#[cgp_impl]` blocks each registering the same key as a per-type default in the same namespace — emits two conflicting delegation impls and fails with `E0119`. The carets land on the `Key` inside each `#[default_impl(Key in …)]` per [Error spans](#error-spans). Pinned by [acceptable/cgp_impl/duplicate_default_impl.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_impl/duplicate_default_impl.rs). + ## Known issues The `#[cgp_impl(Self)]` form requires an explicit `for Context` clause and errors cleanly when it is missing. In this form the `new` keyword and the `: ComponentType` override are silently ignored rather than rejected: the `Bare` branch returns the `impl` block untouched and never consults `args.new` or `args.component_type`, so `#[cgp_impl(new Self)]` and `#[cgp_impl(Self: SomeComponent)]` parse and compile but have the same effect as a plain `#[cgp_impl(Self)]`. This is harmless — neither option is meaningful for a direct consumer-trait impl — but a stricter parser would reject them. Beyond this, the macro has no known limitations specific to it apart from those inherited from [`#[cgp_provider]`](cgp_provider.md) (see its Known issues). diff --git a/docs/implementation/entrypoints/cgp_provider.md b/docs/implementation/entrypoints/cgp_provider.md index f5de8889..39f4ee4e 100644 --- a/docs/implementation/entrypoints/cgp_provider.md +++ b/docs/implementation/entrypoints/cgp_provider.md @@ -50,7 +50,7 @@ The **`new` keyword controls the struct**, whose shape is read from the impl's ` ```rust // #[cgp_new_provider] impl Runner for SpawnAndRun -pub struct SpawnAndRun(pub ::core::marker::PhantomData<(InCode)>); +pub struct SpawnAndRun(pub ::core::marker::PhantomData); ``` The **provider-name rewrite in the generics** keeps the `IsProviderFor` bounds honest: `replace_provider_in_generics` rewrites any `Provider: SomeTrait` bound in the `where` clause into an `IsProviderFor` bound, so a higher-order provider's inner-provider dependency surfaces in error messages as an `IsProviderFor` obligation rather than a raw provider-trait bound. diff --git a/docs/reference/macros/cgp_new_provider.md b/docs/reference/macros/cgp_new_provider.md index 6e310d72..fd161717 100644 --- a/docs/reference/macros/cgp_new_provider.md +++ b/docs/reference/macros/cgp_new_provider.md @@ -75,7 +75,7 @@ where emits, in addition to the provider and `IsProviderFor` impls: ```rust -pub struct SpawnAndRun(pub ::core::marker::PhantomData<(InCode)>); +pub struct SpawnAndRun(pub ::core::marker::PhantomData); ``` ## Examples From a77ad46767efc06b8855115a319e3f61615007c6 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 23:42:31 +0200 Subject: [PATCH 13/17] Fix clippy --- .../tests/basic_delegation/delegate_generic_table.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/tests/cgp-tests/tests/basic_delegation/delegate_generic_table.rs b/crates/tests/cgp-tests/tests/basic_delegation/delegate_generic_table.rs index 29d9d5d6..fde79971 100644 --- a/crates/tests/cgp-tests/tests/basic_delegation/delegate_generic_table.rs +++ b/crates/tests/cgp-tests/tests/basic_delegation/delegate_generic_table.rs @@ -7,6 +7,11 @@ //! //! See docs/reference/macros/delegate_components.md. +// The header lifts `'a` onto every generated impl, but only the `BarKey<'a, T1>` +// entry uses it, so the `FooKey`/`BazKey` impls carry an intentionally-unused +// lifetime. +#![allow(clippy::extra_unused_lifetimes)] + use core::marker::PhantomData; use cgp::prelude::DelegateComponent; From cf6b04c70ea25d3fdd0cd520c19ceec759af7848 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 23:50:23 +0200 Subject: [PATCH 14/17] Update AGENTS.md --- AGENTS.md | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ec495e43..bcf4ac97 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,14 +2,29 @@ This file provides guidance to LLM agents when working with code in this repository. -## Understanding CGP first - -This repository **is** the implementation of Context-Generic Programming (CGP). Before reading or -writing any CGP code here, **always invoke the `/cgp` skill** to load the fundamentals (consumer vs. -provider traits, `#[cgp_component]`/`#[cgp_impl]`/`#[cgp_fn]`, `delegate_components!`, `HasField`, -`UseDelegate`, check traits, etc.). Re-invoke it whenever you navigate into unfamiliar parts of the -codebase — the macros and core traits here are the ground truth that the skill describes, so the two -should always be read together. +## Orient before any task + +This repository **is** the implementation of Context-Generic Programming (CGP), and its behavior is +recorded as much in the knowledge base under [docs/](docs) as in the code. Before starting any task +here — reading, writing, reviewing, debugging, or answering a question — load the CGP mental model +and the documentation that covers what you are about to touch. The following steps are standing +requirements: they apply to every task regardless of how small it looks, not just to the macro +review workflow below. + +- **Always invoke the `/cgp` skill** to load the fundamentals (consumer vs. provider traits, + `#[cgp_component]`/`#[cgp_impl]`/`#[cgp_fn]`, `delegate_components!`, `HasField`, `UseDelegate`, + check traits, and so on). Re-invoke it whenever you move into an unfamiliar construct — the macros + and core traits here are the ground truth the skill describes, so read the two together. +- **Always read [docs/README.md](docs/README.md)** to orient in the knowledge base, then follow it + into the README of whichever section covers your task. +- **Read [docs/reference/README.md](docs/reference/README.md) and the relevant reference documents + whenever the task requires understanding a CGP construct** — what it means, what syntax it accepts, + and what code it expands to. +- **Read [docs/implementation/README.md](docs/implementation/README.md) and the relevant + implementation documents whenever the task involves reading or modifying the CGP source code** — + they map each macro to its `cgp-macro-core`/`cgp-macro-lib` internals, corner cases, and tests. +- **Load the `/dual-reader-prose` skill whenever the task involves editing markdown documentation or + inline code comments**, and follow its writing convention for any prose you add. The canonical export surface for users is `cgp::prelude` — see [crates/main/cgp/src/prelude.rs](crates/main/cgp/src/prelude.rs), which re-exports @@ -100,13 +115,9 @@ as the behavior allows. ### Orient before touching anything -Load the fundamentals first, every iteration. Invoke the `/cgp` skill to reload CGP's mental model -and vocabulary, and the `/dual-reader-prose` skill to reload the writing convention the docs must -follow. Re-invoke `/cgp` whenever the review moves into an unfamiliar construct — the macros and -core traits are the ground truth the skill describes, so read the two together. - -Then read the documentation for the macro under review, in [docs/](docs). Read its reference -document under [docs/reference/](docs/reference), its implementation documents under +Perform the standing steps in [Orient before any task](#orient-before-any-task) first, every +iteration. Then read the documentation specific to the macro under review, in [docs/](docs): its +reference document under [docs/reference/](docs/reference), its implementation documents under [docs/implementation/](docs/implementation) (the `entrypoints/` document, the `asts/` stack it drives, and any `functions/` helpers it relies on), and the governing `AGENTS.md` files that define how those documents stay in sync with the code: [docs/AGENTS.md](docs/AGENTS.md), From fab29d316c4bb831a4eb32fa113d6b17780d6e48 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Fri, 3 Jul 2026 23:51:28 +0200 Subject: [PATCH 15/17] Check in skills --- .claude/settings.local.json | 9 + .claude/skills/cgp | 1 + .claude/skills/dual-reader-prose/SKILL.md | 207 ++++++++++++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 .claude/settings.local.json create mode 120000 .claude/skills/cgp create mode 100644 .claude/skills/dual-reader-prose/SKILL.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..189de724 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(ls skills/cgp/references/)", + "Bash(ls -la /home/soares/development/context-generic/.claude/skills/)", + "Bash(diff -q skills/cgp/SKILL.md /home/soares/development/context-generic/.claude/skills/cgp/SKILL.md)" + ] + } +} diff --git a/.claude/skills/cgp b/.claude/skills/cgp new file mode 120000 index 00000000..bf705a63 --- /dev/null +++ b/.claude/skills/cgp @@ -0,0 +1 @@ +../../docs/skills/cgp \ No newline at end of file diff --git a/.claude/skills/dual-reader-prose/SKILL.md b/.claude/skills/dual-reader-prose/SKILL.md new file mode 100644 index 00000000..dbd0242d --- /dev/null +++ b/.claude/skills/dual-reader-prose/SKILL.md @@ -0,0 +1,207 @@ +--- +name: dual-reader-prose +description: > + Apply this skill whenever you are writing explanatory content, documentation, + instructional text, analysis, reports, or any substantive prose that will be + read by a mixed audience. Use it when the user asks you to "write clearly", + "make this readable", "explain this well", or whenever the content is long + enough that different readers might approach it differently. Also trigger when + the user explicitly asks for a style that works for both skimmers and deep + readers, or when converting bullet-heavy AI output into more readable form. + Do not use for short conversational replies, single-sentence answers, or + creative fiction. +--- + +# Dual-Reader Prose + +Good writing serves two kinds of readers at once. Some people scan first and +read only what they need. Others read every word in order and want the full +picture. Most formats help one group and hurt the other. Bullet points help +scanners but frustrate deep readers. Dense prose helps deep readers but loses +scanners. This skill shows you how to write for both. Use prose by default, and +use bullets only when the content is truly a list. + +--- + +## Write in Plain English + +Plain words and short sentences help both readers. A scanner grasps the point +faster. A deep reader follows the argument with less effort. So the first rule +is simple: say things plainly. + +**Prefer simple words.** Choose the common word over the fancy one. Write "use" +instead of "utilize", "show" instead of "demonstrate", and "help" instead of +"facilitate". A plain word is easier to read and never sounds thin. + +**Write short sentences.** One sentence should carry one idea. When a sentence +runs long or holds two or three ideas, split it. Short sentences are easier to +scan and easier to follow. + +**Cut words you do not need.** Remove filler like "in order to", "it is +important to note that", and "the fact that". If a word adds nothing, delete it. + +**Say it directly.** Prefer the active voice and a clear subject. Write "the +reader stops here" instead of "it is at this point that the reader may cease". + +Plain English is not the same as shallow writing. You keep every idea. You just +say each one in the clearest way you can. + +--- + +## The Two Reader Types + +Readers behave differently because they process information differently. The +difference is not random. It reflects how each person reads and remembers. + +**Scanner-first readers** come to a text with a goal. They want to find what +they need, skip what they know, and move on. They read the first sentence of a +paragraph to decide whether to read the rest. Lists and headers are their map. +When a text is all prose with no visual anchors, they feel the writer is wasting +their time and making them dig. Bullet points feel fast and respectful. These +readers often work at speed, read a lot each day, and have learned to scan on +purpose. + +**Sequential-depth readers** read a text as one argument that builds step by +step. They read prose the way others listen to music. The flow carries meaning +that single points cannot. When they hit bullet points, they feel the reasoning +has been cut out. They see claims without evidence and steps without reasons. +They also find bullets harder to remember, because prose forms a story the brain +holds on to, while loose bullets feel like a random list. These readers distrust +writing that feels thin, and they will re-read a hard passage rather than skip +it. + +The two groups do not disagree about the *quality* of the information. They +disagree about the *form* that makes it easy to use. Good writing finds a form +that works for both. + +--- + +## The Core Technique: Topic Sentence + Elaboration + +The best bridge between the two readers is the topic-sentence paragraph. Start +every paragraph with one sentence that states the main point in full. The +sentences after it explain, expand, and give evidence. + +Scanners read only the opening sentences and still get the whole structure of +your argument. Deep readers read everything and get the full reasoning. Neither +group loses out. This is not new. It is the standard method of good journalism, +science writing, and legal prose, and it has worked for a long time because both +reader types have always existed. + +The rule this demands is simple: **the opening sentence must stand on its own.** +If a scanner reads only your first sentences and comes away with the wrong or +partial picture, your topic sentences are not doing their job. Test them by +reading only the first line of each paragraph in order. If that reading makes +sense on its own, you have succeeded. + +--- + +## Supporting Techniques + +A few more techniques back up the core method without turning the text into a +pile of bullets. + +**Bold key terms, but rarely.** When a paragraph introduces a term that matters, +bold it on first use. Scanners use the bold as a landmark. Deep readers read the +sentence around it. Keep it to one or two bold phrases per section. Too much bold +teaches readers to ignore it, which defeats the point. + +**Lead with the main point in each section.** State the conclusion first, then +add support in order of importance. A reader who stops partway through should +still leave with the most important idea, not a half-argument that makes no +sense. + +**Keep paragraphs short and focused.** One paragraph should make one point. If a +paragraph makes three points, you have three paragraphs. Short paragraphs give +scanners clean stopping points and give deep readers one idea to hold at a time. + +**Use headers only for real shifts.** Headers help scanners navigate, but they +break the flow for deep readers. Use them to mark a true change of topic, not as +decoration and not as a fix for weak transitions. A header every two paragraphs +means you have given up on prose. + +**Write transitions that also summarize.** The sentence that ends one section and +opens the next should restate what you just showed and point to what comes next. +Scanners use these as checkpoints. Deep readers feel them as the glue of the +argument. + +--- + +## When Bullets Are the Right Choice + +Prose is the default, but lists are not banned. Use bullets when you are truly +listing a set of loosely related items. These are items whose only link is that +they belong to the same group, not a chain of reasoning where each point leads +to the next. Good cases include a list of independent options, a set of +unordered settings, discrete steps a reader will do one at a time, or numbered +items a reader may want to point to on their own. + +The test is whether the list form is *clearly* better than a paragraph, not just +okay. If the items flow into each other, depend on each other, or share one line +of argument, prose serves both readers better, so write prose. But if forcing +them into sentences would give you a stiff "first, second, third" paragraph that +is really a list in disguise, use the list. When you are unsure, take that doubt +as a sign: the content probably does not need bullets, so use prose. + +Even when you use a list, frame it. Write a sentence before it that says what the +items share and why they matter. Add a sentence after it, when needed, that says +what to do with them. A framed list serves both readers. An orphaned one serves +only the scanner. + +--- + +## What to Avoid + +Some habits help one reader and hurt the other. Naming them makes them easier to +catch. + +**Avoid orphaned bullets.** This is a list with no lead-in sentence and no +follow-up. It is the most common failure in AI writing. Scanners can read it, +but deep readers have no idea why the items matter or what to do with them. The +problem is not the list. It is the missing frame. When a list is right, keep it +and frame it with a sentence before and, when useful, a sentence after. + +**Avoid reflexive bullets.** The opposite mistake is breaking prose into a list +out of habit. If the items share one line of reasoning or flow into each other, +bullets cut the thread that deep readers need and hand scanners a set of +fragments with no argument. Use bullets only when the list form is clearly +better than the paragraph. + +**Avoid burying the point.** Prose that saves its conclusion for the last +sentence fails scanners, because they never reach it. Put the point at the top +and let the detail follow. + +**Avoid too much bold.** Bolding more than a few phrases per page is like +shouting every third word. When everything is bold, nothing stands out. Scanners +stop tracking it, and deep readers find it noisy. + +**Avoid fake structure.** Three levels of nested bullets, headers under headers +under headers, and lists of lists add visual noise without clarity. Real +structure lives in the logic of the argument, not in the indentation. + +--- + +## A Quick Self-Check + +Before you send any substantial piece of writing, run these checks. + +Read only the first sentence of each paragraph in order. If that reading tells a +clear story, your topic sentences work. + +Read only the bold phrases in the document. If there are more than a handful, cut +most of them. + +Find your longest paragraph. Ask whether it makes one point or several. If +several, split it. + +Find the first place where you state your main conclusion. If it is not in the +first or second paragraph, move it there. + +Find every list. Ask whether its items are truly loosely related or an argument +in disguise. If the items flow into each other, turn the list back into prose. + +Read your longest sentences aloud. If one is hard to say in one breath or holds +more than one idea, split it or cut it down. + +A document that passes these checks reads well for both audiences. It is not a +compromise. It is simply better structured than pure bullets or unbroken prose. From 369b92b3f3539d4ec27eaf469ee1f4690318ca10 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 4 Jul 2026 00:10:12 +0200 Subject: [PATCH 16/17] AI revision --- .../tests/cgp-compile-fail-tests/src/lib.rs | 7 +- .../cgp_auto_getter/option_slice.rs | 22 +++ .../cgp_auto_getter/option_slice.stderr | 30 +++ .../tests/compile_fail_tests.rs | 6 +- crates/tests/cgp-tests/tests/getters/mod.rs | 1 + .../cgp-tests/tests/getters/mut_slice.rs | 178 ++++++++++++++++++ .../entrypoints/cgp_auto_getter.md | 2 + docs/implementation/entrypoints/cgp_getter.md | 3 +- 8 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.rs create mode 100644 crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.stderr create mode 100644 crates/tests/cgp-tests/tests/getters/mut_slice.rs diff --git a/crates/tests/cgp-compile-fail-tests/src/lib.rs b/crates/tests/cgp-compile-fail-tests/src/lib.rs index 8b137891..32127762 100644 --- a/crates/tests/cgp-compile-fail-tests/src/lib.rs +++ b/crates/tests/cgp-compile-fail-tests/src/lib.rs @@ -1 +1,6 @@ - +//! Compile-fail tests for the CGP macros, driven by `trybuild`. +//! +//! This crate carries no library code — the suite lives in the `trybuild` +//! fixtures under `tests/` and their driver `tests/compile_fail_tests.rs`. See +//! the crate [README](../README.md) for what belongs in each category and how to +//! regenerate the `.stderr` snapshots. diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.rs b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.rs new file mode 100644 index 00000000..9e6ae5cd --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.rs @@ -0,0 +1,22 @@ +//! Acceptable failure: a getter (or `#[implicit]` argument) typed `Option<&[T]>` +//! combines the `Option<&T>` and `&[T]` shorthands, a combination CGP does not +//! provide magic for. The shared `parse_field_type` lowers it literally by the +//! `Option<&T>` rule — reading an `Option` field where `T` is the slice `[u8]` +//! — so the generated `HasField` bound names the unsized `Option<[u8]>`, which +//! `rustc` rejects (`[u8]` has no statically known size). CGP is working as +//! designed: the `Option` and slice shorthands ease the common single-shape +//! cases, and an unsupported combination is deferred to the compiler rather than +//! given a bespoke rule. The same boundary holds for `#[cgp_getter]` and for a +//! `#[cgp_fn]` `#[implicit]` argument, since all three share `parse_field_type`. +//! +//! See docs/implementation/entrypoints/cgp_auto_getter.md (Behavior and corner +//! cases). + +use cgp::prelude::*; + +#[cgp_auto_getter] +pub trait HasItems { + fn items(&self) -> Option<&[u8]>; +} + +fn main() {} diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.stderr new file mode 100644 index 00000000..31e9aba3 --- /dev/null +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.stderr @@ -0,0 +1,30 @@ +error[E0277]: the size for values of type `[u8]` cannot be known at compilation time + --> tests/acceptable/cgp_auto_getter/option_slice.rs:17:1 + | +17 | #[cgp_auto_getter] + | ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time + | + = help: the trait `Sized` is not implemented for `[u8]` +note: required by an implicit `Sized` bound in `Option` + --> $RUST/core/src/option.rs + | + | pub enum Option { + | ^ required by the implicit `Sized` requirement on this type parameter in `Option` + = note: this error originates in the attribute macro `cgp_auto_getter` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0599]: the method `as_ref` exists for reference `&Option<[u8]>`, but its trait bounds were not satisfied + --> tests/acceptable/cgp_auto_getter/option_slice.rs:17:1 + | +17 | #[cgp_auto_getter] + | ^^^^^^^^^^^^^^^^^^ method cannot be called on `&Option<[u8]>` due to unsatisfied trait bounds + | + ::: $RUST/core/src/option.rs + | + | pub enum Option { + | ------------------ doesn't satisfy `Option<[u8]>: AsRef<_>` + | + = note: the following trait bounds were not satisfied: + `[u8]: Sized` + `Option<[u8]>: AsRef<_>` + which is required by `&Option<[u8]>: AsRef<_>` + = note: this error originates in the attribute macro `cgp_auto_getter` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs b/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs index 49a90e71..f6e49e59 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs +++ b/crates/tests/cgp-compile-fail-tests/tests/compile_fail_tests.rs @@ -2,9 +2,9 @@ //! //! Each fixture under `tests/acceptable//` and //! `tests/problematic//` is compiled as its own crate and its -//! diagnostics compared against the sibling `.stderr` file. See the crate root -//! docs (`src/lib.rs`) for what belongs in each category and how to regenerate -//! the `.stderr` snapshots. +//! diagnostics compared against the sibling `.stderr` file. See the crate +//! [README](../README.md) for what belongs in each category and how to +//! regenerate the `.stderr` snapshots. //! //! A single `TestCases` runs both globs so `trybuild` invokes `cargo` once; //! splitting the categories across two `#[test]` functions would race on the diff --git a/crates/tests/cgp-tests/tests/getters/mod.rs b/crates/tests/cgp-tests/tests/getters/mod.rs index c84d1781..5893d164 100644 --- a/crates/tests/cgp-tests/tests/getters/mod.rs +++ b/crates/tests/cgp-tests/tests/getters/mod.rs @@ -25,6 +25,7 @@ pub mod assoc_type_self_referential; pub mod clone; pub mod mref; pub mod mut_getter; +pub mod mut_slice; pub mod non_self; pub mod option; pub mod slice; diff --git a/crates/tests/cgp-tests/tests/getters/mut_slice.rs b/crates/tests/cgp-tests/tests/getters/mut_slice.rs new file mode 100644 index 00000000..26fd9b4a --- /dev/null +++ b/crates/tests/cgp-tests/tests/getters/mut_slice.rs @@ -0,0 +1,178 @@ +//! `#[cgp_getter]` with a `&mut self` receiver returning `&mut [u32]`: the +//! generated provider impls read a field whose value implements `AsMut<[u32]>` +//! (e.g. `Vec`) mutably through `get_field_mut` and call `.as_mut()`. The +//! `UseFields`/`UseField` impls bound the context by `HasFieldMut<..., Value: +//! AsMut<[u32]>>` and the `WithProvider` impl delegates to a `MutFieldGetter` +//! with the same `AsMut<[u32]>` bound — the mutable mirror of the shared `&[T]` +//! getter's `FieldGetter`/`AsRef<[u32]>`. This pins the non-auto getter's +//! mutable-slice path, which the `#[cgp_auto_getter]` snapshots do not exercise. +//! +//! See docs/reference/macros/cgp_getter.md and docs/reference/providers/use_field.md. + +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_getter; + +snapshot_cgp_getter! { + #[cgp_getter] + pub trait HasItems { + fn items(&mut self) -> &mut [u32]; + } + + expand_has_items(output) { + insta::assert_snapshot!(output, @" + pub trait HasItems { + fn items(&mut self) -> &mut [u32]; + } + impl<__Context__> HasItems for __Context__ + where + __Context__: ItemsGetter<__Context__>, + { + fn items(&mut self) -> &mut [u32] { + __Context__::items(self) + } + } + pub trait ItemsGetter< + __Context__, + >: IsProviderFor { + fn items(__context__: &mut __Context__) -> &mut [u32]; + } + impl<__Provider__, __Context__> ItemsGetter<__Context__> for __Provider__ + where + __Provider__: DelegateComponent + + IsProviderFor, + <__Provider__ as DelegateComponent< + ItemsGetterComponent, + >>::Delegate: ItemsGetter<__Context__>, + { + fn items(__context__: &mut __Context__) -> &mut [u32] { + <__Provider__ as DelegateComponent< + ItemsGetterComponent, + >>::Delegate::items(__context__) + } + } + pub struct ItemsGetterComponent; + impl<__Context__> ItemsGetter<__Context__> for UseContext + where + __Context__: HasItems, + { + fn items(__context__: &mut __Context__) -> &mut [u32] { + __Context__::items(__context__) + } + } + impl<__Context__> IsProviderFor for UseContext + where + __Context__: HasItems, + {} + impl<__Context__, __Components__, __Path__> ItemsGetter<__Context__> + for RedirectLookup<__Components__, __Path__> + where + __Components__: DelegateComponent<__Path__>, + <__Components__ as DelegateComponent<__Path__>>::Delegate: ItemsGetter<__Context__>, + { + fn items(__context__: &mut __Context__) -> &mut [u32] { + <__Components__ as DelegateComponent<__Path__>>::Delegate::items(__context__) + } + } + impl< + __Context__, + __Components__, + __Path__, + > IsProviderFor + for RedirectLookup<__Components__, __Path__> + where + __Components__: DelegateComponent<__Path__>, + <__Components__ as DelegateComponent< + __Path__, + >>::Delegate: IsProviderFor + + ItemsGetter<__Context__>, + {} + impl<__Context__> ItemsGetter<__Context__> for UseFields + where + __Context__: HasFieldMut< + Symbol<5, Chars<'i', Chars<'t', Chars<'e', Chars<'m', Chars<'s', Nil>>>>>>, + Value: AsMut<[u32]> + 'static, + >, + { + fn items(__context__: &mut __Context__) -> &mut [u32] { + __context__ + .get_field_mut( + ::core::marker::PhantomData::< + Symbol< + 5, + Chars<'i', Chars<'t', Chars<'e', Chars<'m', Chars<'s', Nil>>>>>, + >, + >, + ) + .as_mut() + } + } + impl<__Context__> IsProviderFor for UseFields + where + __Context__: HasFieldMut< + Symbol<5, Chars<'i', Chars<'t', Chars<'e', Chars<'m', Chars<'s', Nil>>>>>>, + Value: AsMut<[u32]> + 'static, + >, + {} + impl<__Context__, __Tag__> ItemsGetter<__Context__> for UseField<__Tag__> + where + __Context__: HasFieldMut<__Tag__, Value: AsMut<[u32]> + 'static>, + { + fn items(__context__: &mut __Context__) -> &mut [u32] { + __context__.get_field_mut(::core::marker::PhantomData::<__Tag__>).as_mut() + } + } + impl<__Context__, __Tag__> IsProviderFor + for UseField<__Tag__> + where + __Context__: HasFieldMut<__Tag__, Value: AsMut<[u32]> + 'static>, + {} + impl<__Context__, __Provider__> ItemsGetter<__Context__> for WithProvider<__Provider__> + where + __Provider__: MutFieldGetter< + __Context__, + ItemsGetterComponent, + Value: AsMut<[u32]> + 'static, + >, + { + fn items(__context__: &mut __Context__) -> &mut [u32] { + __Provider__::get_field_mut( + __context__, + ::core::marker::PhantomData::, + ) + .as_mut() + } + } + impl<__Context__, __Provider__> IsProviderFor + for WithProvider<__Provider__> + where + __Provider__: MutFieldGetter< + __Context__, + ItemsGetterComponent, + Value: AsMut<[u32]> + 'static, + >, + {} + ") + } +} + +#[derive(HasField)] +pub struct App { + pub values: Vec, +} + +delegate_components! { + App { + ItemsGetterComponent: UseField, + } +} + +#[test] +pub fn test_mut_slice_getter() { + let mut context = App { + values: vec![1, 2, 3], + }; + + context.items()[0] = 10; + + assert_eq!(context.values, vec![10, 2, 3]); +} diff --git a/docs/implementation/entrypoints/cgp_auto_getter.md b/docs/implementation/entrypoints/cgp_auto_getter.md index 1c3e21b5..ec8f56e8 100644 --- a/docs/implementation/entrypoints/cgp_auto_getter.md +++ b/docs/implementation/entrypoints/cgp_auto_getter.md @@ -56,6 +56,8 @@ The conversions are exactly those `#[cgp_getter]` uses — `&str` reads a `Strin **A getter can read a field of another type.** A method with a typed receiver (`fn foo_bar(foo: &Self::Foo) -> &Self::Bar`) reads the field out of that receiver type; `Self` in the receiver is rewritten to the context, and the `HasField` bound lands on the receiver type rather than the context. +**Unsupported field-type combinations are deferred to the compiler.** The `Option` and slice shorthands each cover a single field shape; a combination CGP provides no rule for — most notably `Option<&[T]>` — is lowered literally rather than given a bespoke rule, so it reaches the compiler as invalid Rust rather than a macro-time rejection. `parse_field_type` applies the `Option<&T>` rule to `Option<&[T]>` and emits a `HasField` bound over the unsized `Option<[T]>`, which `rustc` rejects. This is a deliberate boundary shared by `#[cgp_getter]` and `#[cgp_fn]` implicits (all three call `parse_field_type`), pinned as an acceptable failure by [acceptable/cgp_auto_getter/option_slice.rs](../../../crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.rs). + ## Snapshots Every `snapshot_cgp_auto_getter!` invocation across the suite is indexed here, since these snapshots all belong to this entrypoint: diff --git a/docs/implementation/entrypoints/cgp_getter.md b/docs/implementation/entrypoints/cgp_getter.md index b6ac04bf..f0c7fa30 100644 --- a/docs/implementation/entrypoints/cgp_getter.md +++ b/docs/implementation/entrypoints/cgp_getter.md @@ -66,6 +66,7 @@ Every `snapshot_cgp_getter!` invocation across the suite is indexed here, since - [getters/option.rs](../../../crates/tests/cgp-tests/tests/getters/option.rs) — an `Option<&String>` return reading an `Option` field via `.as_ref()`. - [getters/slice.rs](../../../crates/tests/cgp-tests/tests/getters/slice.rs) — a `&[u8]` return reading any `AsRef<[u8]> + 'static` field via `.as_ref()`. - [getters/mut_getter.rs](../../../crates/tests/cgp-tests/tests/getters/mut_getter.rs) — a `&mut self` getter returning `&mut u32`: the `UseFields` and `UseField` impls read through `HasFieldMut`/`get_field_mut`, and the `WithProvider` impl delegates to a `MutFieldGetter` rather than a `FieldGetter`. +- [getters/mut_slice.rs](../../../crates/tests/cgp-tests/tests/getters/mut_slice.rs) — a `&mut self` getter returning `&mut [u32]`: the mutable-slice path, where the `UseField` and `MutFieldGetter` bounds are the `AsMut<[u32]> + 'static` mirror of `slice.rs`'s `AsRef<[u32]>`. - [getters/non_self.rs](../../../crates/tests/cgp-tests/tests/getters/non_self.rs) — a non-`self` getter reading a field out of another type (`&Self::Foo`). - [getters/string_custom_name.rs](../../../crates/tests/cgp-tests/tests/getters/string_custom_name.rs) — `#[cgp_getter(GetString)]` overriding the provider name (and component). - [getters/string_custom_spec.rs](../../../crates/tests/cgp-tests/tests/getters/string_custom_spec.rs) — the brace-spec form overriding provider and component names independently. @@ -83,7 +84,7 @@ The getter snapshot files also wire concrete contexts and assert the getters res - [getters/string.rs](../../../crates/tests/cgp-tests/tests/getters/string.rs) and [getters/assoc_type_self_referential.rs](../../../crates/tests/cgp-tests/tests/getters/assoc_type_self_referential.rs) confirm that wiring `UseField` makes the getter read a field whose name differs from the method. - [getters/clone.rs](../../../crates/tests/cgp-tests/tests/getters/clone.rs), [getters/mref.rs](../../../crates/tests/cgp-tests/tests/getters/mref.rs), [getters/option.rs](../../../crates/tests/cgp-tests/tests/getters/option.rs), and [getters/slice.rs](../../../crates/tests/cgp-tests/tests/getters/slice.rs) verify each return-type shorthand at run time. -- [getters/mut_getter.rs](../../../crates/tests/cgp-tests/tests/getters/mut_getter.rs) mutates the context through a `&mut self` getter wired to `UseField`, confirming the mutable provider impls resolve. +- [getters/mut_getter.rs](../../../crates/tests/cgp-tests/tests/getters/mut_getter.rs) mutates the context through a `&mut self` getter wired to `UseField`, confirming the mutable provider impls resolve; [getters/mut_slice.rs](../../../crates/tests/cgp-tests/tests/getters/mut_slice.rs) does the same for a `&mut [u32]` getter over a `Vec` field, exercising the `AsMut<[T]>` bound. ## Source From ab7b039d1a5e02131a6208d338b01f3d3c548388 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sat, 4 Jul 2026 00:22:49 +0200 Subject: [PATCH 17/17] Update Rust toolchain and CI --- .github/workflows/tests.yml | 73 ++++++++----------- AGENTS.md | 2 +- .../cgp_auto_getter/option_slice.stderr | 8 -- .../missing_dependency.stderr | 10 ++- rust-toolchain.toml | 2 +- 5 files changed, 38 insertions(+), 57 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eb746bea..3a1c8564 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,75 +1,62 @@ -name: Rust Unit Tests +name: Tests + on: pull_request: {} push: - branches: main + branches: [main] env: CARGO_INCREMENTAL: 0 - CARGO_PROFILE_DEV_DEBUG: 1 - CARGO_PROFILE_RELEASE_DEBUG: 1 + CARGO_TERM_COLOR: always RUST_BACKTRACE: short CARGO_NET_RETRY: 10 RUSTUP_MAX_RETRIES: 10 -# Cancel previous runs of this workflow when a new commit is added to the PR, branch or tag +# Cancel in-progress runs when a new commit is pushed to the same PR, branch, or tag. concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +permissions: + contents: read + +# To minimize supply-chain exposure this workflow uses only GitHub's own +# first-party checkout action; every toolchain and tool is installed with the +# rustup/cargo already present on the runner. jobs: fmt: + name: Rustfmt (nightly) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - override: true - components: rustfmt - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + - name: Install nightly rustfmt + run: rustup toolchain install nightly --profile minimal --component rustfmt + - run: cargo +nightly fmt --all -- --check clippy: + name: Clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: clippy - override: true - - uses: Swatinem/rust-cache@v2 - - uses: actions-rs/clippy-check@v1 - with: - name: clippy-all-features - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features --all-targets -- -D warnings - - uses: actions-rs/clippy-check@v1 - with: - name: clippy-no-default-features - token: ${{ secrets.GITHUB_TOKEN }} - args: --no-default-features --all-targets -- -D warnings + - name: Install pinned toolchain + run: rustup toolchain install + - name: Clippy (all features) + run: cargo clippy --all-features --all-targets -- -D warnings + - name: Clippy (no default features) + run: cargo clippy --no-default-features --all-targets -- -D warnings test: + name: Test (nextest) runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - uses: Swatinem/rust-cache@v2 + - name: Install pinned toolchain + run: rustup toolchain install + # Prebuilt binary from the official nextest distribution. - name: Install cargo-nextest run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin - - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features --no-fail-fast --no-run - - uses: actions-rs/cargo@v1 - with: - command: nextest - args: run --all-features --no-fail-fast --workspace --no-capture + # Runs the whole workspace, including the trybuild compile-fail fixtures in + # cgp-compile-fail-tests (an ordinary integration test nextest picks up). + - name: Run tests + run: cargo nextest run --all-features --no-fail-fast --workspace diff --git a/AGENTS.md b/AGENTS.md index bcf4ac97..cbaa6d72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,7 @@ prelude re-exports in [crates/main/cgp-core/src/prelude.rs](crates/main/cgp-core ## Commands -This is a Cargo workspace (edition 2024, resolver 3). Toolchain is pinned to **1.93** via +This is a Cargo workspace (edition 2024, resolver 3). Toolchain is pinned to **1.96** via [rust-toolchain.toml](rust-toolchain.toml). Nearly every crate is `#![no_std]` — keep new code `no_std`-compatible (use `core`/`alloc`, gate `std`/`alloc` usage behind features as existing crates do). diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.stderr index 31e9aba3..92398d5d 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.stderr +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/cgp_auto_getter/option_slice.stderr @@ -7,9 +7,6 @@ error[E0277]: the size for values of type `[u8]` cannot be known at compilation = help: the trait `Sized` is not implemented for `[u8]` note: required by an implicit `Sized` bound in `Option` --> $RUST/core/src/option.rs - | - | pub enum Option { - | ^ required by the implicit `Sized` requirement on this type parameter in `Option` = note: this error originates in the attribute macro `cgp_auto_getter` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0599]: the method `as_ref` exists for reference `&Option<[u8]>`, but its trait bounds were not satisfied @@ -18,11 +15,6 @@ error[E0599]: the method `as_ref` exists for reference `&Option<[u8]>`, but its 17 | #[cgp_auto_getter] | ^^^^^^^^^^^^^^^^^^ method cannot be called on `&Option<[u8]>` due to unsatisfied trait bounds | - ::: $RUST/core/src/option.rs - | - | pub enum Option { - | ------------------ doesn't satisfy `Option<[u8]>: AsRef<_>` - | = note: the following trait bounds were not satisfied: `[u8]: Sized` `Option<[u8]>: AsRef<_>` diff --git a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.stderr b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.stderr index 7a2b8691..9ed6ef49 100644 --- a/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.stderr +++ b/crates/tests/cgp-compile-fail-tests/tests/acceptable/delegate_components/missing_dependency.stderr @@ -5,10 +5,7 @@ error[E0599]: the method `greet` exists for struct `Person`, but its trait bound | ----------------- method `greet` not found for this struct because it doesn't satisfy `Person: CanGreet` or `Person: Greeter` ... 50 | Person { age: 0 }.greet(); - | ------------------^^^^^-- - | | | - | | this is an associated function, not a method - | help: use associated function syntax instead: `Person::greet()` + | ^^^^^ this is an associated function, not a method | = note: found the following associated functions; to be used as methods, functions must have a `self` parameter note: the candidate is defined in the trait `Greeter` @@ -37,3 +34,8 @@ note: `CanGreet` defines an item `greet`, perhaps you need to implement it 17 | pub trait CanGreet { | ^^^^^^^^^^^^^^^^^^ = note: this error originates in the attribute macro `cgp_component` (in Nightly builds, run with -Z macro-backtrace for more info) +help: use associated function syntax instead + | +50 - Person { age: 0 }.greet(); +50 + Person::greet(); + | diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9c144613..5c97b7a6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.93" +channel = "1.96" profile = "default"