Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
128eee6
Describe cargo-cgp's class-based fixture layout in the test guide
soareschen Oct 11, 2026
60aa1ea
Reject an optional reference to an unsized type in field reads
soareschen Oct 12, 2026
e22c2c2
Let a provider read its own associated const as `Self::CONST`
soareschen Oct 13, 2026
b0c83f9
Name the variant derives' associated types through qualified projections
soareschen Oct 13, 2026
3b78226
Keep other derives' helper attributes off the data derives' companions
soareschen Oct 13, 2026
df71fac
Keep only real primitive names as types in `Path!` segments
soareschen Oct 13, 2026
c813d90
Point a `->` entry's errors at the entry
soareschen Oct 13, 2026
25ffa74
Make an `#[async_trait]` default body the returned future
soareschen Oct 13, 2026
70bec20
Reject a namespace that inherits itself
soareschen Oct 13, 2026
6a2e6aa
Reject `#[uses]` on a component trait
soareschen Oct 13, 2026
7b407e9
Reject a component method parameter that is not a plain name
soareschen Oct 13, 2026
d868fd1
Reject a `#[cgp_computer]` parameter only the return type uses
soareschen Oct 13, 2026
50c69c7
Pin which const a provider's `Self::CONST` names
soareschen Oct 26, 2026
876e7a5
Keep only documentation on the data derives' companions
soareschen Oct 26, 2026
048dc0d
Pin every primitive name a `Path!` segment keeps as a type
soareschen Oct 26, 2026
8e8e921
Reject a namespace that inherits another instance of itself
soareschen Oct 26, 2026
166db41
Forward a component method's `mut` parameter by its name
soareschen Oct 26, 2026
291135c
Accept a `#[cgp_computer]` parameter named inside a type macro
soareschen Oct 26, 2026
8ed2287
Pin the `where` and tuple cases of the computer output-only check
soareschen Oct 26, 2026
9bcdc7d
State what cargo-cgp offers and where its fixtures live
soareschen Nov 1, 2026
1616e32
State the sibling list plainly and without its move
soareschen Nov 1, 2026
9f750c8
Reject a namespace that inherits itself through a self:: path
soareschen Nov 6, 2026
6103a06
Name the written type in the unsized-optional rejection
soareschen Nov 6, 2026
b6a6905
Drop the mut of a mut self receiver in the forwarding impls
soareschen Nov 6, 2026
d62a381
Spell arrays and Fn arguments as written in the optional rejection
soareschen Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ versus linking to it. Three of them matter constantly.
There is no `docs/` directory here. The construct reference, the concepts, the guides, the error
catalog, and the macro-implementation notes are all in that repository's
[`cgp/`](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp) directory, and the worked
examples sit at its top level. It is a *consolidated* base — `cargo-cgp`'s documentation is the
neighbouring section — which is why an error class here and the tool's handling of it are now one
directory apart. Treat the knowledge base as part of this repository's surface: **a change to a
examples sit at its top level. It is a *consolidated* base, with `cargo-cgp`'s documentation as the
neighbouring section, so an error class here and the tool's handling of it are one directory apart. Treat the knowledge base as part of this repository's surface: **a change to a
construct's behavior carries its documentation change**, and when the checkout is missing, say plainly
what needs updating there.

**[`cargo-cgp`](https://github.com/contextgeneric/cargo-cgp) is CGP's first-class toolchain**: a cargo
subcommand that rewrites the compiler's diagnostics into readable, root-cause-first CGP errors. It is
the recommended way to build and check CGP code and to read a CGP compile error, so recommend it
the recommended way to check CGP code and to read a CGP compile error, so recommend it
wherever CGP errors are discussed, and prefer it over raw `cargo check` when diagnosing a wiring
failure here. When a construct change alters a diagnostic it has a UI fixture for, update the fixture
in the same change, following that project's own conventions.
Expand All @@ -65,10 +64,9 @@ a doc pointer in a source comment, which names its knowledge-base path
(`cgp-knowledge-base/cgp/implementation/entrypoints/cgp_impl.md`).

**The code dependency stays one-way.** No `cgp` *crate* may depend on a `cargo-cgp` crate:
`cargo-cgp` reads `cgp`, never the reverse. Documentation is the deliberate exception — the knowledge
base and the skill both reference `cargo-cgp` as the recommended toolchain — so it is the *code* graph
that stays acyclic, not the documentation. The post-codegen compile-fail cases this repository used to
keep in `cgp-compile-fail-tests` now live as `cargo-cgp` UI fixtures, and the
`cargo-cgp` reads `cgp`, never the reverse. Documentation is the deliberate exception, since the knowledge
base and the skill both recommend `cargo-cgp`, so the *code* graph stays acyclic and the documentation
need not. Post-codegen compile-fail cases are `cargo-cgp` UI fixtures, and the
[error catalog](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/errors/README.md)
links each error class to the fixture that backs it.

Expand Down Expand Up @@ -120,7 +118,7 @@ do).
- **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`
- **Post-codegen compile failures** (input a macro accepts but whose expansion fails to compile) are
no longer tested here: they are UI fixtures in [`cargo-cgp`](https://github.com/contextgeneric/cargo-cgp),
tested elsewhere: they are UI fixtures in [`cargo-cgp`](https://github.com/contextgeneric/cargo-cgp),
the first-class error toolchain, and cataloged in the [error catalog](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/errors/README.md). See
[crates/tests/AGENTS.md](crates/tests/AGENTS.md) for the workflow.
- Many "tests" are **compile-time wiring checks** (`check_components!` /
Expand Down Expand Up @@ -275,7 +273,7 @@ Walk the shapes the expansion can take across the whole input space and confirm
Rust — no conflicting `impl` blocks from a cartesian expansion, no unbound or doubly-declared generic,
no empty expansion that checks nothing, no clash on a generated identifier. When an expansion fails to
compile, capture it as a [`cargo-cgp` UI fixture](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/README.md)
(post-codegen compile-fail cases live there now, filed by the quality of the output the tool produces)
(post-codegen compile-fail cases live there, filed by their error class)
and catalog the class in the [error catalog](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/errors/README.md); when the failure is a CGP *defect*
the macro should have rejected, also record it under the macro's `## Known issues`, per
[crates/tests/AGENTS.md](crates/tests/AGENTS.md) and [cgp-knowledge-base/cgp/implementation/AGENTS.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/AGENTS.md).
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,13 @@ CGP requires **Rust 1.89+** and runs on the **stable** toolchain — no nightly,
CGP compiles to ordinary Rust, so a wiring mistake surfaces as a compiler error about generated types — often a wall of them with the real cause buried. **When you start with CGP, install [`cargo-cgp`](https://github.com/contextgeneric/cargo-cgp) alongside it.** cargo-cgp is CGP's error toolchain: a drop-in for `cargo check` that rewrites those errors into a compact, root-cause-first form, leading with the field or wiring that actually failed. It is the single biggest quality-of-life improvement for writing CGP, so treat it as part of your getting-started setup.

```bash
cargo install cargo-cgp # the front-end (installs on any toolchain)
cargo cgp setup # provisions the compiler + driver it needs
cargo install --locked cargo-cgp # the front-end (needs Rust 1.96 or newer)
cargo cgp setup # provisions the compiler + driver it needs
```

Then run `cargo cgp check` wherever you would run `cargo check`; it forces the pinned nightly that `cargo cgp setup` installed only for its own check, so your project keeps its toolchain untouched. (Prefer Nix? `nix run github:contextgeneric/cargo-cgp/v0.1.0 -- check` runs it without installing.) cargo-cgp is an early release that already makes the common wiring errors readable and is steadily covering more; see its [README](https://github.com/contextgeneric/cargo-cgp) for the full guide.
Then run `cargo cgp check` wherever you would run `cargo check`; it forces the pinned nightly that `cargo cgp setup` installed only for its own check, so your project keeps its toolchain untouched. (Prefer Nix? `nix run github:contextgeneric/cargo-cgp/v0.1.0 -- check` runs it without installing.) See its [README](https://github.com/contextgeneric/cargo-cgp) for the full guide.

cargo-cgp is optional and adds only a diagnostic `check`: keep building, running, and testing with ordinary cargo — CGP is a plain library needing only stable Rust (1.89+) — and reach for `cargo cgp check` when you want a wiring error made readable. Plain `cargo check` still works too, and is fine when you are not expecting a compile error.
cargo-cgp is optional and adds only the reading commands `check` and `expand`: keep building, running, and testing with ordinary cargo, since CGP is a plain library needing only stable Rust (1.89+), and reach for `cargo cgp check` when you want a wiring error made readable. Plain `cargo check` still works too, and is fine when you are not expecting a compile error.

## When to use CGP — and when not

Expand Down
43 changes: 41 additions & 2 deletions crates/macros/cgp-async-macro/src/impl_async.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use alloc::vec::Vec;

use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{ItemTrait, ReturnType, TraitItem, Type, parse_quote};
use quote::{ToTokens, quote};
use syn::{FnArg, ItemTrait, Pat, ReturnType, Signature, TraitItem, Type, parse_quote};

pub fn impl_async(item: TokenStream) -> TokenStream {
match syn::parse2::<ItemTrait>(item.clone()) {
Expand All @@ -22,6 +24,22 @@ pub fn impl_async(item: TokenStream) -> TokenStream {

trait_fn.sig.output = impl_return;
trait_fn.sig.asyncness = None;

// A default body becomes the future the method returns, as the body of an
// `async fn` is.
if let Some(body) = &mut trait_fn.default {
let rebinds = move_params_into_future(&mut trait_fn.sig);
let stmts = &body.stmts;

*body = parse_quote! {
{
async move {
#(#rebinds)*
#(#stmts)*
}
}
};
}
}
}

Expand All @@ -30,3 +48,24 @@ pub fn impl_async(item: TokenStream) -> TokenStream {
_ => item,
}
}

/// Rebind each by-value identifier parameter inside the future, so the future owns it and drops it
/// when it completes, as an `async fn` does, rather than when the method returns. A `mut` moves
/// from the parameter to the rebinding, where the body mutates it.
fn move_params_into_future(sig: &mut Signature) -> Vec<TokenStream> {
let mut rebinds = Vec::new();

for input in sig.inputs.iter_mut() {
if let FnArg::Typed(arg) = input
&& let Pat::Ident(pat) = arg.pat.as_mut()
&& pat.by_ref.is_none()
&& pat.subpat.is_none()
{
let ident = &pat.ident;
let mutability = pat.mutability.take();
rebinds.push(quote! { let #mutability #ident = #ident; });
}
}

rebinds
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, quote};
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{FnArg, Ident, ImplItemFn, Signature, Type, Visibility};
use syn::{Error, FnArg, Ident, ImplItemFn, Pat, Signature, Type, Visibility};

use crate::functions::parse_internal;

Expand All @@ -28,11 +28,30 @@ pub fn signature_to_delegated_impl_item_fn(
) #await_expr
});

// A `mut` on a parameter or a by-value receiver belongs to the default body that mutates it;
// the forwarding body only passes the argument on, where the `mut` would be unused. A
// `&mut self` receiver also carries `mutability`, as part of its type, so it is kept.
let mut sig = signature.clone();
for input in sig.inputs.iter_mut() {
match input {
FnArg::Typed(arg) => {
if let Pat::Ident(pat) = arg.pat.as_mut() {
pat.mutability = None;
}
}
FnArg::Receiver(receiver) => {
if receiver.reference.is_none() {
receiver.mutability = None;
}
}
}
}

let item = ImplItemFn {
attrs: Vec::new(),
vis: Visibility::Inherited,
defaultness: None,
sig: signature.clone(),
sig,
block: body,
};

Expand All @@ -43,11 +62,20 @@ fn signature_to_idents(sig: &Signature) -> syn::Result<Punctuated<Ident, Comma>>
sig.inputs.iter().map(arg_to_ident).collect()
}

/// The name an argument is forwarded by, a `mut value` parameter's included. A parameter without a
/// name of its own (`_`, `ref value`, a destructuring pattern) has none, so it is rejected with a
/// message saying what to write.
fn arg_to_ident(arg: &FnArg) -> syn::Result<Ident> {
let ident = match arg {
FnArg::Receiver(_) => Ident::new("self", Span::call_site()),
FnArg::Typed(pat) => parse_internal(pat.pat.to_token_stream())?,
};

Ok(ident)
match arg {
FnArg::Receiver(_) => Ok(Ident::new("self", Span::call_site())),
FnArg::Typed(arg) => match arg.pat.as_ref() {
Pat::Ident(pat) if pat.by_ref.is_none() && pat.subpat.is_none() => {
Ok(pat.ident.clone())
}
pat => Err(Error::new_spanned(
pat,
"a component method parameter needs a plain name, since the generated impls forward each argument by name; write a name such as `_value`, and destructure inside a default body",
)),
},
}
}
85 changes: 85 additions & 0 deletions crates/macros/cgp-macro-core/src/functions/field/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub fn parse_field_type(
}
Type::Path(type_path) => {
if let Some(type_ref) = try_parse_option_ref(type_path) {
reject_unsized_option_ref(type_path, type_ref)?;

// `Option<&T>` reads an `Option<T>` 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
Expand Down Expand Up @@ -83,6 +85,89 @@ pub fn parse_field_type(
}
}

/// `Option<&T>` reads an `Option<T>` field, and an `Option` holds only a sized value, so an optional
/// reference to a slice or a trait object has no field to read. Rejected here, naming the sized
/// container to read instead, rather than lowered to a bound over an unsized `Option`. The message
/// quotes the reference as written and names the referent inside each container, so
/// `Option<&[u8]>` points to an `Option<Vec<u8>>` field and `Option<&(dyn Debug + Send)>` to an
/// `Option<Box<dyn Debug + Send>>` field.
fn reject_unsized_option_ref(type_path: &TypePath, type_ref: &TypeReference) -> syn::Result<()> {
let mut referent = type_ref.elem.as_ref();
while let Type::Paren(paren) = referent {
referent = paren.elem.as_ref();
}

let (kind, container) = match referent {
Type::Slice(slice) => ("a slice", format!("Vec<{}>", type_text(&slice.elem))),
Type::TraitObject(_) => ("a trait object", format!("Box<{}>", type_text(referent))),
_ => return Ok(()),
};

let reference = if type_ref.mutability.is_some() {
"&mut "
} else {
"&"
};

Err(Error::new_spanned(
type_path,
format!(
"`Option<{}>` would read an `Option<{}>` field, which cannot exist because {kind} is unsized; read an `Option<{container}>` field as `Option<{reference}{container}>` instead",
type_text(type_ref),
type_text(referent),
),
))
}

/// A type as the reader writes it, from its tokens: the spaces the token printer puts around `::`,
/// `<`, `>`, `,`, `;`, brackets, parentheses, and after `&` removed, so `core :: fmt :: Debug` reads
/// `core::fmt::Debug`, `& mut [u8 ; 4]` reads `&mut [u8; 4]`, and `Fn (u8)` reads `Fn(u8)`.
fn type_text(ty: &impl ToTokens) -> String {
let mut text = ty.to_token_stream().to_string();
for (spaced, tight) in [
(" :: ", "::"),
(" < ", "<"),
("< ", "<"),
(" <", "<"),
(" >", ">"),
(" ,", ","),
(" ;", ";"),
("[ ", "["),
(" ]", "]"),
("( ", "("),
(" )", ")"),
("& ", "&"),
] {
text = text.replace(spaced, tight);
}
join_argument_lists(&text)
}

/// Removes the space before a parenthesized argument list, which follows a name, as in `Fn (u8)`.
/// A parenthesized type keeps its space, so `-> (u8, u16)`, `&'a (dyn Debug)`, and
/// `&mut (dyn Debug)` are unchanged: after a lifetime or the keyword `mut`, `dyn`, or `impl`, the
/// group is not an argument list.
fn join_argument_lists(text: &str) -> String {
let mut joined = String::with_capacity(text.len());
let mut rest = text;
while let Some(at) = rest.find(" (") {
let before = &rest[..at];
let prefix = before.trim_end_matches(|c: char| c.is_alphanumeric() || c == '_');
let word = &before[prefix.len()..];
let is_argument_list =
!word.is_empty() && !prefix.ends_with('\'') && !matches!(word, "mut" | "dyn" | "impl");

joined.push_str(before);
if !is_argument_list {
joined.push(' ');
}
joined.push('(');
rest = &rest[at + 2..];
}
joined.push_str(rest);
joined
}

/// 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`
Expand Down
4 changes: 2 additions & 2 deletions crates/macros/cgp-macro-core/src/functions/getter/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ fn parse_receiver(context_ident: &Ident, arg: &FnArg) -> syn::Result<(ReceiverMo

ReplaceSelfTypeVisitor {
replaced_type: &parse_internal!(#context_ident),
skip_assoc_types: &Vec::new(),
skip_assoc_items: &Vec::new(),
}
.visit_type_mut(&mut receiver);

Expand All @@ -264,7 +264,7 @@ fn parse_return_type(

ReplaceSelfTypeVisitor {
replaced_type: &parse_internal!(#context_type),
skip_assoc_types: &Vec::from_iter(field_assoc_type.clone()),
skip_assoc_items: &Vec::from_iter(field_assoc_type.clone()),
}
.visit_type_mut(&mut replaced_type);

Expand Down
27 changes: 27 additions & 0 deletions crates/macros/cgp-macro-core/src/functions/override_span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,33 @@ where
parse2(trees.into_iter().collect())
}

/// Re-span every *synthesized* top-level token of `body` onto `span`, leaving the user's tokens at
/// their own spans, and re-parse into `T`.
///
/// This is for a fragment whose own span a diagnostic reads, such as a `where` bound or a type,
/// when the fragment mixes user tokens with synthesized ones: the synthesized ends otherwise widen
/// the fragment's span to the whole invocation. `span` must be a user token no resolvable name
/// shares (an operator such as `->`), since a synthesized reference moved onto it becomes, for an
/// editor, that token.
pub fn override_synthesized_span<T>(span: Span, body: &T) -> syn::Result<T>
where
T: Parse + ToTokens,
{
let call_site_text = Span::call_site().source_text();

parse2(
body.to_token_stream()
.into_iter()
.map(|mut tree| {
if is_synthesized(tree.span(), &call_site_text) {
tree.set_span(span);
}
tree
})
.collect(),
)
}

/// Whether `token_span` was stamped by `quote!`/`parse_internal!` rather than
/// carried in from user input. A synthesized token carries the macro's
/// `call_site` span, whose source text is the entire invocation; a user token's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ impl CgpComponentAttributes {
} else if ident == "prefix" {
let namespace_specs = attribute.parse_args_with(PrefixAttribute::parse)?;
parsed_attributes.prefixes.push(namespace_specs);
} else if ident == "uses" {
// `#[uses]` imports a provider's dependencies; on a trait definition it would
// only be left for the compiler, which knows no such attribute.
let bounds = &attribute.meta.require_list()?.tokens;
return Err(syn::Error::new_spanned(
&attribute,
format!(
"`#[uses]` declares the dependencies of a provider, not of a trait; write `#[extend({bounds})]` to make it a supertrait"
),
));
} else if ident == "derive_delegate" {
let derive_delegate_attribute =
attribute.parse_args_with(DeriveDelegateAttribute::parse)?;
Expand Down
Loading
Loading