Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
119 changes: 119 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,122 @@ inward (core/macros) when changing fundamentals, outward (main) only to adjust t
- See [CHANGELOG.md](CHANGELOG.md) for the evolution of macro syntax — it is the most reliable
record of which macro forms are current vs. removed (e.g. `#[cgp_context]` was removed,
`ProvideType` → `TypeProvider`).

## Macro review workflow

This section defines the standing process for reviewing one CGP macro implementation at a time,
hardening it until no further issue is found. The goal of an iteration is a macro whose
implementation, tests, and documentation are correct, complete, mutually consistent, and as simple
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
[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
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.

Next, study the implementation itself in [crates/macros/](crates/macros). Start from the
`cgp-macro-lib` entry function, follow it into the `cgp-macro-core` `types/<construct>/` AST stack
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
and update it.

### Harden the implementation and its tests

With the macro understood, work through the review in these areas. Each is a distinct concern;
treat correctness as non-negotiable and simplification as a judgment call, and never let a
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).
- **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,
that the corner case it checks makes sense, and that it makes appropriate assertions wherever an
assertion is possible rather than relying on compilation alone.
- **Deduplicate and simplify tests.** Merge or remove tests that check the same or overlapping
behavior, and factor common boilerplate into shared test helpers.
- **Improve the documentation and inline docs.** Update the reference document, the implementation
documents, and any README when they are inconsistent with the code or when something is worth
explaining or clarifying; add a brief `///` to any public struct, trait, or function that lacks
one; and simplify existing inline docs, removing facts that are obvious from reading the code.

### Scrutinize the macro codegen

A CGP macro is only as correct as the code it emits, so review the implementation against the ways
its input can be parsed and its output expanded, not just the happy path its tests exercise. Cover
every one of these concerns, since a gap in any of them is a latent miscompilation waiting for the
right input:

- **Review every supported attribute.** Enumerate the attributes the macro accepts and confirm each
is parsed, validated, mutually constrained, and rejected-when-unknown exactly as documented — an
unrecognized attribute should fail with a spanned error, a mutually exclusive pair should error
when both appear, and a duplicate should not be silently accepted (or silently accepted for one
attribute while rejected for another).
- **Prefer `parse_internal!`/`parse_internal` over `parse2` or `parse_quote!`.** When constructing a
`syn` node from quasi-quoted tokens, build it with `parse_internal!` so a malformed fragment fails
with an error naming the target type and the offending tokens (prelude prefix stripped) rather than
a bare parse error. Reserve `parse2` for re-parsing tokens already known to be valid (a span
override, say), and treat every `parse_quote!` as an assertion that parsing can never fail.
- **Return `syn::Result` wherever parsing can fail.** A function that parses anything should thread
`syn::Result` and propagate the error, rather than `parse_quote!`-ing and risking a panic that
aborts the compiler with no usable diagnostic. Use the panicking `parse_quote!` only when it is
trivially obvious — from the surrounding, fully-controlled tokens — that the parse cannot fail.
- **Enumerate every way the input can be parsed.** For each parser and `parse_internal!` call, think
through the full range of inputs a user could write — path-qualified types, generic and lifetime
parameters, arrays, tuples, empty lists, turbofish, associated-type bindings — and confirm none
reaches a parser that fails with a confusing internal error. Reject malformed or unsupported input
early, at the macro's own parse stage with a clear spanned message, rather than letting the failure
surface deep inside internal fragment parsing or in the expanded code.
- **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.
- **Scrutinize generics with care.** Generic parameters take many forms — lifetimes, types, consts,
and the distinction between *impl* generics (`impl<T>`) and *type* generics (the `<T>` in
`Foo<T>`) — and mixing them produces subtly wrong output. Confirm the macro keeps the kinds
separate, renders each in the right position, merges parameters from different sources without
colliding, and binds every parameter that appears in the generated header so nothing is left free.
- **Fully qualify every CGP construct in the expansion.** Any CGP item the expansion references must
be emitted through the `crate::exports` markers so it resolves as `::cgp::macro_prelude::<Name>`,
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.

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.

### Keep every view in sync and verify

Every change propagates to all four views in the same change, per the synchronization rule. When
you alter the macro's behavior, syntax, expansion, or defaults, update the reference document's
Expansion, the implementation document's Pipeline and Generated items, the affected snapshots, and
the `/cgp` skill. When you move or rename a test, update the implementation document's Tests or
Snapshots section. Then verify the work: run `cargo +nightly fmt --all`, the clippy invocations, and
`cargo nextest run` for the affected crates (and `cargo insta` to review any snapshot diffs) as
described in the Commands section above, confirming a green suite before considering the iteration
done.

### Ask when in doubt

During the review, ask the user for clarification whenever something should be settled before the
next step is taken — an ambiguous intended behavior, a corner case whose correct outcome is unclear,
or a design choice with more than one defensible answer. Surface the question rather than guessing,
since a wrong assumption baked into the source, tests, and four documentation views is expensive to
unwind.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ members = [

"crates/tests/cgp-tests",
"crates/tests/cgp-macro-tests",
"crates/tests/cgp-compile-fail-tests",
"crates/tests/cgp-test-crate-a",
"crates/tests/cgp-test-crate-b",
]
Expand Down
2 changes: 1 addition & 1 deletion crates/core/cgp-error/src/traits/can_raise_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::traits::has_error_type::HasErrorType;
#[cgp_component(ErrorRaiser)]
#[prefix(@cgp.core.error in DefaultNamespace)]
#[derive_delegate(UseDelegate<SourceError>)]
#[use_type(HasErrorType::Error)]
#[use_type(HasErrorType.Error)]
pub trait CanRaiseError<SourceError> {
fn raise_error(error: SourceError) -> Error;
}
2 changes: 1 addition & 1 deletion crates/core/cgp-error/src/traits/can_wrap_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::traits::HasErrorType;
#[cgp_component(ErrorWrapper)]
#[prefix(@cgp.core.error in DefaultNamespace)]
#[derive_delegate(UseDelegate<Detail>)]
#[use_type(HasErrorType::Error)]
#[use_type(HasErrorType.Error)]
pub trait CanWrapError<Detail> {
fn wrap_error(error: Error, detail: Detail) -> Error;
}
4 changes: 2 additions & 2 deletions crates/extra/cgp-handler/src/components/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::UseInputDelegate;
#[prefix(@cgp.extra.handler in DefaultNamespace)]
#[derive_delegate(UseDelegate<Code>)]
#[derive_delegate(UseInputDelegate<Input>)]
#[use_type(HasErrorType::Error)]
#[use_type(HasErrorType.Error)]
pub trait CanHandle<Code, Input> {
type Output;

Expand All @@ -22,7 +22,7 @@ pub trait CanHandle<Code, Input> {
#[prefix(@cgp.extra.handler in DefaultNamespace)]
#[derive_delegate(UseDelegate<Code>)]
#[derive_delegate(UseInputDelegate<Input>)]
#[use_type(HasErrorType::Error)]
#[use_type(HasErrorType.Error)]
pub trait CanHandleRef<Code, Input> {
type Output;

Expand Down
4 changes: 2 additions & 2 deletions crates/extra/cgp-handler/src/components/try_compute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::UseInputDelegate;
#[prefix(@cgp.extra.handler in DefaultNamespace)]
#[derive_delegate(UseDelegate<Code>)]
#[derive_delegate(UseInputDelegate<Input>)]
#[use_type(HasErrorType::Error)]
#[use_type(HasErrorType.Error)]
pub trait CanTryCompute<Code, Input> {
type Output;

Expand All @@ -20,7 +20,7 @@ pub trait CanTryCompute<Code, Input> {
#[prefix(@cgp.extra.handler in DefaultNamespace)]
#[derive_delegate(UseDelegate<Code>)]
#[derive_delegate(UseInputDelegate<Input>)]
#[use_type(HasErrorType::Error)]
#[use_type(HasErrorType.Error)]
pub trait CanTryComputeRef<Code, Input> {
type Output;

Expand Down
4 changes: 2 additions & 2 deletions crates/extra/cgp-run/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ use cgp::prelude::*;
#[cgp_component(Runner)]
#[async_trait]
#[derive_delegate(UseDelegate<Code>)]
#[use_type(HasErrorType::Error)]
#[use_type(HasErrorType.Error)]
pub trait CanRun<Code> {
async fn run(&self, _code: PhantomData<Code>) -> Result<(), Error>;
}

#[cgp_component(SendRunner)]
#[async_trait]
#[derive_delegate(UseDelegate<Code>)]
#[use_type(HasErrorType::Error)]
#[use_type(HasErrorType.Error)]
pub trait CanSendRun<Code> {
fn send_run(&self, _code: PhantomData<Code>) -> impl Future<Output = Result<(), Error>> + Send;
}
2 changes: 1 addition & 1 deletion crates/extra/cgp-runtime/src/traits/has_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use cgp::prelude::*;
use crate::HasRuntimeType;

#[cgp_getter]
#[use_type(HasRuntimeType::Runtime)]
#[use_type(HasRuntimeType.Runtime)]
pub trait HasRuntime {
fn runtime(&self) -> &Runtime;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use syn::token::Eq;
use syn::{ImplItemType, TraitItemType, Type, Visibility};

/// Build an associated-type impl item that keeps `trait_type`'s name, generics,
/// and attributes but binds it to `delegated_type`.
pub fn trait_to_impl_item_type(trait_type: &TraitItemType, delegated_type: Type) -> ImplItemType {
ImplItemType {
attrs: trait_type.attrs.clone(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use syn::{FnArg, Ident, ImplItemFn, Signature, Type, Visibility};

use crate::functions::parse_internal;

/// Build an impl method that keeps `signature` and forwards its arguments to the
/// same method on `delegate_type`, adding `.await` when the signature is `async`.
pub fn signature_to_delegated_impl_item_fn(
signature: &Signature,
delegate_type: &Type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub fn trait_items_to_delegated_impl_items(
.collect()
}

/// Forward a single trait item (method, associated type, or const) to
/// `delegate_type`, projecting associated types and consts through
/// `provider_trait_path`. Other trait-item kinds are rejected.
pub fn trait_item_to_delegated_impl_items(
trait_item: &TraitItem,
delegate_type: &Type,
Expand Down
35 changes: 25 additions & 10 deletions crates/macros/cgp-macro-core/src/functions/implicits/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
use syn::{Attribute, FnArg, Meta, Pat, PatIdent, PatType, Receiver, Type};

use crate::functions::parse_field_type;
use crate::types::implicits::{ImplicitArgField, ImplicitArgFields};
Expand All @@ -24,20 +24,28 @@ pub fn extract_and_parse_implicit_args(
));
};

if receiver.mutability.is_some() && implicit_fn_args.len() > 1 {
return Err(syn::Error::new_spanned(
&args,
"Only one mutable implicit argument is allowed when self is mutable",
));
}

let mut implicit_args = Vec::new();

for arg in implicit_fn_args {
let spec = parse_implicit_arg(receiver, &arg)?;
implicit_args.push(spec);
}

// A `&mut` implicit reads through `get_field_mut`, which borrows the whole
// context exclusively for the rest of the body, so it cannot coexist with any
// other implicit read — the emitted impl would borrow the context mutably and
// (im)mutably at once and fail to compile. Purely immutable implicits are all
// shared borrows and combine freely, so the constraint applies only once a
// mutable one is present.
let has_mutable = implicit_args.iter().any(|field| field.field_mut.is_some());

if has_mutable && implicit_args.len() > 1 {
return Err(syn::Error::new_spanned(
&args,
"a `&mut` implicit argument must be the only implicit argument, since its mutable borrow of the context conflicts with reading any other field",
));
}

Ok(ImplicitArgFields::new(implicit_args))
}

Expand All @@ -55,9 +63,16 @@ pub fn parse_implicit_arg(receiver: &Receiver, arg: &PatType) -> syn::Result<Imp

let arg_type = arg.ty.as_ref().clone();

let field_mut = receiver.mutability;
let (field_type, field_mode) = parse_field_type(&arg_type, &receiver.mutability)?;

let (field_type, field_mode) = parse_field_type(&arg_type, &field_mut)?;
// The field is read mutably only when the argument itself is a `&mut`
// reference. The receiver's mutability gates *whether* a `&mut` argument is
// allowed (checked in `parse_field_type`), but a `&mut self` receiver does not
// by itself force a mutable read of an immutably-typed argument.
let field_mut = match &arg_type {
Type::Reference(type_ref) => type_ref.mutability,
_ => None,
};

let spec = ImplicitArgField {
field_name: pat_ident.ident.clone(),
Expand Down
25 changes: 14 additions & 11 deletions crates/macros/cgp-macro-core/src/functions/is_provider_params.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{GenericParam, Generics, Type};
use syn::{Error, GenericParam, Generics, Type};

use crate::exports::Life;
use crate::parse_internal;
use crate::types::generics::TypeGenerics;

/// Convert a trait's generics into the `Params` tuple types of an `IsProviderFor`
/// bound: type params pass through, lifetimes are lifted into `Life<'a>`.
/// bound: type parameters pass through by name and lifetimes are lifted into
/// `Life<'a>`. Bounds and defaults are dropped, since the tuple only names the
/// parameters positionally.
///
/// Panics on a const generic parameter — see the const-generic limitation in
/// docs/implementation/entrypoints/cgp_component.md.
/// Const generic parameters are rejected with a spanned error: the tuple holds
/// *types*, and CGP's type-based wiring cannot key on a const value, so a const
/// parameter has no representation here.
pub fn parse_is_provider_params(generics: &Generics) -> syn::Result<Punctuated<Type, Comma>> {
let params = TypeGenerics::try_from(generics)?.generics.params;

let mut res = Punctuated::new();

for param in params {
for param in &generics.params {
let out = match param {
GenericParam::Type(type_param) => {
let ident = type_param.ident;
let ident = &type_param.ident;
parse_internal! { #ident }
}
GenericParam::Lifetime(life_param) => {
let life = &life_param.lifetime;
parse_internal! { #Life<#life> }
}
GenericParam::Const(_) => {
unimplemented!("const generic parameters are not yet supported in CGP traits")
GenericParam::Const(const_param) => {
return Err(Error::new_spanned(
const_param,
"const generic parameters are not supported on CGP component traits",
));
}
};
res.push(out)
Expand Down
Loading
Loading