Skip to content

Conversation

@soareschen
Copy link
Collaborator

@soareschen soareschen commented Oct 17, 2025

Summary

This PR enables CGP context types to use delegate_components! directly, without needing to make use of a separate provider struct for the context. That is, we can now do:

pub struct App { ... }

delegate_components! {
    App {
        FooComponent: FooProvider,
        BarComponent: BarProvider,
        ...
    }
}

instead of the original:

#[cgp_context]
pub struct App { ... }

delegate_components! {
    AppComponents {
        FooComponent: FooProvider,
        BarComponent: BarProvider,
        ...
    }
}

This significantly reduces the ergonomics of wiring for the concrete context, as we no longer need to think of having a separate AppComponents provider to act as a type-level lookup table. Instead, the type-level lookup table is directly stored in App.

This should also slightly improve the compilation performance of compiling CGP code, as there is one less level of indirection for the trait solver to go through.

Direct implementation of consumer traits

A significant advantage that arise is that we can now implement a consumer trait on a concrete context directly. For example:

#[cgp_getter]
pub trait HasName {
    fn name(&self) -> &str;
}

#[cgp_getter]
pub trait HasCount {
    fn count(&self) -> u32;
}

#[derive(HasField)]
pub struct App {
    pub name: String,
    pub count: u32,
}

delegate_components! {
    App {
        NameGetterComponent: UseField<Symbol!("name")>,
    }
}

// Consumer trait can now be implemented directly
impl HasCount for App {
    fn count(&self) -> u32 {
        self.count
    }
}

Previously, we would have to write:

#[cgp_provider]
impl CountGetter<App> for AppComponents {
    fn count(app: &App) -> u32 {
        app.count
    }
}

which makes the code look much worse especially for newcomers.

Removal of HasCgpProvider trait

The consumer traits can be implemented directly, because we have now removed the HasCgpProvider trait from CGP. Instead of using HasCgpProvider, the blanket implementation for the consumer trait now also uses DelegateComponent in the same way as the provider trait's blanket implementation.

For example, the HasName trait earlier now has the following blanket implementation:

impl<Context> HasName for Context
where
    Context: DelegateComponent<NameGetterComponent>,
    Context::Delegate: NameGetter<Context>,
{
    fn name(&self) -> &str {
        Context::Delegate::name(self)
    }
}

While before this, the following was generated:

impl<Context> HasName for Context
where
    Context: HasCgpProvider,
    Context::CgpProvider: NameGetter<Context>,
{
    fn name(&self) -> &str {
        Context::CgpProvider::name(self)
    }
}

When HasCgpProvider was used, the blanket implementation of the consumer trait prevents any type that implements HasCgpProvider to also implement the consumer trait. But with the new derivation, the context can still implement the consumer trait even if it implements DelegateComponent, as long as the trait is not implemented for the key NameGetterComponent. This means that as long as we don't have any generic implementation of DelegateComponent on the context that might cover the component key, we would be able to implement the consumer trait directly.

Backward Compatibility

Since there are already quite some existing code bases that use CGP, completely removing the context provider might cause significant breakage. Fortunately, we can work around the breakage by changing #[cgp_context] to perform a bulk DelegateComponent<Name> for all Name type.

For example, given:

#[cgp_context]
pub struct App { ... }

The macro now generates:

pub struct AppComponents;

impl<Name> DelegateComponent<Name> for App {
    type Delegate = AppComponents;
}

With this, the bulk delegation essentially serves the same role as the use of HasCgpProvider, which was generated before this change:

pub struct AppComponents;

impl HasCgpProvider for App {
    type CgpProvider = AppComponents;
}

Background

I'd like to provide a little bit of background of why HasCgpProvider was initially used in the blanket implementation, as compared to just use DelegateComponent like the design here.

Originally, I had the idea that multiple concrete contexts could share the same provider "table". For example:

pub struct AppComponents;

pub struct AppA { ... }
pub struct AppB { ... }

impl HasCgpProvider for AppA {
    type CgpProvider = AppComponents;
}

impl HasCgpProvider for AppB {
    type CgpProvider = AppComponents;
}

In this design, we could reuse the same component wiring for different applications, without needing to reconfigure them every time.

However, there is always some cases that two concrete context would share almost the same wiring, except for a handful of customization. This eventually lead to the development of the preset feature, which provide the same functionality as what shared context providers are supposed to serve.

As a result, the context providers were largely an artifact from the early design of CGP. However, it lingered for a long time, as I was worried that changing it would cause too much breakage to existing code.

Recently, I thought about this issue again, and I realized that I could mostly preserve the backward compatibility by modifying #[cgp_context] to generate a blanket implementation of DelegateComponent. As a result, the backward compatibility problem is largely resolved, and I am now able to push this change for the next major release.

@soareschen soareschen added the breaking Breaking changes label Oct 17, 2025
@soareschen soareschen merged commit 1a26ff4 into main Oct 17, 2025
5 checks passed
@soareschen soareschen deleted the consumer-delegate branch October 17, 2025 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants