traversable_derive: compiles

This commit is contained in:
nym21
2026-03-06 20:33:49 +01:00
parent 7c80bb0612
commit 8c32ad2483
5 changed files with 324 additions and 94 deletions
@@ -1,8 +1,8 @@
use std::path::Path;
use brk_cohort::{
ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount, ByMaxAge, ByMinAge,
ByClass, BySpendableType, CohortContext, Filter, Term,
ByAgeRange, ByAmountRange, ByClass, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount, ByMaxAge,
ByMinAge, BySpendableType, CohortContext, Filter, Term,
};
use brk_error::Result;
use brk_traversable::Traversable;
@@ -13,41 +13,32 @@ use vecdb::{AnyStoredVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw, Storag
use crate::{blocks, distribution::DynCohortVecs, indexes, prices};
use crate::distribution::metrics::{
AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase,
CoreCohortMetrics, ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
MinimalCohortMetrics, SupplyMetrics,
AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics,
ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig, MinimalCohortMetrics,
SupplyMetrics,
};
use super::{percentiles::PercentileCache, vecs::UTXOCohortVecs};
use crate::distribution::state::{CoreRealizedState, MinimalRealizedState, RealizedState, UTXOCohortState};
use crate::distribution::state::UTXOCohortState;
const VERSION: Version = Version::new(0);
/// All UTXO cohorts organized by filter type.
///
/// Each group uses a concrete metrics type matching its required features:
/// - age_range: extended realized + extended cost basis
/// - epoch/class/amount/type: basic metrics with relative
/// - all: extended + adjusted (no rel_to_all)
/// - sth: extended + adjusted
/// - lth: extended
/// - max_age: adjusted
/// - min_age: basic
#[derive(Traversable)]
pub struct UTXOCohorts<M: StorageMode = Rw> {
pub all: UTXOCohortVecs<AllCohortMetrics<M>, RealizedState>,
pub sth: UTXOCohortVecs<ExtendedAdjustedCohortMetrics<M>, RealizedState>,
pub lth: UTXOCohortVecs<ExtendedCohortMetrics<M>, RealizedState>,
pub age_range: ByAgeRange<UTXOCohortVecs<BasicCohortMetrics<M>, RealizedState>>,
pub max_age: ByMaxAge<UTXOCohortVecs<CoreCohortMetrics<M>, CoreRealizedState>>,
pub min_age: ByMinAge<UTXOCohortVecs<CoreCohortMetrics<M>, CoreRealizedState>>,
pub ge_amount: ByGreatEqualAmount<UTXOCohortVecs<MinimalCohortMetrics<M>, MinimalRealizedState>>,
pub amount_range: ByAmountRange<UTXOCohortVecs<MinimalCohortMetrics<M>, MinimalRealizedState>>,
pub lt_amount: ByLowerThanAmount<UTXOCohortVecs<MinimalCohortMetrics<M>, MinimalRealizedState>>,
pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>, CoreRealizedState>>,
pub class: ByClass<UTXOCohortVecs<CoreCohortMetrics<M>, CoreRealizedState>>,
pub type_: BySpendableType<UTXOCohortVecs<MinimalCohortMetrics<M>, MinimalRealizedState>>,
pub all: UTXOCohortVecs<AllCohortMetrics<M>>,
pub sth: UTXOCohortVecs<ExtendedAdjustedCohortMetrics<M>>,
pub lth: UTXOCohortVecs<ExtendedCohortMetrics<M>>,
pub age_range: ByAgeRange<UTXOCohortVecs<BasicCohortMetrics<M>>>,
pub max_age: ByMaxAge<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub min_age: ByMinAge<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub class: ByClass<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub ge_amount: ByGreatEqualAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub amount_range: ByAmountRange<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub lt_amount: ByLowerThanAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub type_: BySpendableType<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
#[traversable(skip)]
pub(super) percentile_cache: PercentileCache,
/// Cached partition_point positions for tick_tock boundary searches.
@@ -57,7 +48,6 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
pub(super) tick_tock_cached_positions: [usize; 20],
}
impl UTXOCohorts<Rw> {
/// ~71 separate cohorts (21 age + 5 epoch + 18 class + 15 amount + 12 type)
const SEPARATE_COHORT_CAPACITY: usize = 80;
@@ -86,7 +76,7 @@ impl UTXOCohorts<Rw> {
// Helper for separate cohorts with BasicCohortMetrics + full state
let basic_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<BasicCohortMetrics, RealizedState>> {
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<BasicCohortMetrics>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -105,7 +95,7 @@ impl UTXOCohorts<Rw> {
let age_range = ByAgeRange::try_new(&basic_separate)?;
let core_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CoreCohortMetrics, CoreRealizedState>> {
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CoreCohortMetrics>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -126,7 +116,7 @@ impl UTXOCohorts<Rw> {
// Helper for separate cohorts with MinimalCohortMetrics + MinimalRealizedState
let minimal_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics, MinimalRealizedState>> {
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -183,7 +173,7 @@ impl UTXOCohorts<Rw> {
// CoreCohortMetrics without state (no state, for aggregate cohorts)
let core_no_state =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CoreCohortMetrics, CoreRealizedState>> {
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CoreCohortMetrics>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -206,7 +196,7 @@ impl UTXOCohorts<Rw> {
// MinimalCohortMetrics without state (for aggregate amount cohorts)
let minimal_no_state =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics, MinimalRealizedState>> {
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -246,7 +236,12 @@ impl UTXOCohorts<Rw> {
&mut self,
) -> impl ParallelIterator<Item = &mut dyn DynCohortVecs> {
let Self {
age_range, epoch, class, amount_range, type_, ..
age_range,
epoch,
class,
amount_range,
type_,
..
} = self;
age_range
.par_iter_mut()
@@ -278,8 +273,15 @@ impl UTXOCohorts<Rw> {
exit: &Exit,
) -> Result<()> {
let Self {
all, sth, lth, age_range, max_age, min_age,
ge_amount, amount_range, lt_amount,
all,
sth,
lth,
age_range,
max_age,
min_age,
ge_amount,
amount_range,
lt_amount,
..
} = self;
@@ -313,10 +315,14 @@ impl UTXOCohorts<Rw> {
})
}),
Box::new(|| {
ge_amount.par_iter_mut().chain(lt_amount.par_iter_mut()).try_for_each(|vecs| {
let sources = filter_minimal_sources_from(amr.iter(), Some(&vecs.metrics.filter));
vecs.metrics.compute_from_sources(si, &sources, exit)
})
ge_amount
.par_iter_mut()
.chain(lt_amount.par_iter_mut())
.try_for_each(|vecs| {
let sources =
filter_minimal_sources_from(amr.iter(), Some(&vecs.metrics.filter));
vecs.metrics.compute_from_sources(si, &sources, exit)
})
}),
];
@@ -338,7 +344,8 @@ impl UTXOCohorts<Rw> {
) -> Result<()> {
// 1. Compute all metrics except net_sentiment (all cohorts via DynCohortVecs)
{
let mut all: Vec<&mut dyn DynCohortVecs> = Vec::with_capacity(Self::SEPARATE_COHORT_CAPACITY + 3);
let mut all: Vec<&mut dyn DynCohortVecs> =
Vec::with_capacity(Self::SEPARATE_COHORT_CAPACITY + 3);
all.push(&mut self.all);
all.push(&mut self.sth);
all.push(&mut self.lth);
@@ -377,7 +384,10 @@ impl UTXOCohorts<Rw> {
// Note: ge_amount, lt_amount, amount_range are Minimal tier — no net_sentiment.
{
let Self {
all, sth, lth, age_range,
all,
sth,
lth,
age_range,
..
} = self;
@@ -387,15 +397,18 @@ impl UTXOCohorts<Rw> {
let tasks: Vec<Box<dyn FnOnce() -> Result<()> + Send + '_>> = vec![
Box::new(|| {
let sources = filter_sources_from(ar.iter(), None);
all.metrics.compute_net_sentiment_from_others(si, &sources, exit)
all.metrics
.compute_net_sentiment_from_others(si, &sources, exit)
}),
Box::new(|| {
let sources = filter_sources_from(ar.iter(), Some(sth.metrics.filter()));
sth.metrics.compute_net_sentiment_from_others(si, &sources, exit)
sth.metrics
.compute_net_sentiment_from_others(si, &sources, exit)
}),
Box::new(|| {
let sources = filter_sources_from(ar.iter(), Some(lth.metrics.filter()));
lth.metrics.compute_net_sentiment_from_others(si, &sources, exit)
lth.metrics
.compute_net_sentiment_from_others(si, &sources, exit)
}),
];
@@ -454,8 +467,18 @@ impl UTXOCohorts<Rw> {
// Destructure to allow parallel mutable access to independent fields.
let Self {
sth, lth, age_range, max_age, min_age,
ge_amount, amount_range, lt_amount, epoch, class, type_, ..
sth,
lth,
age_range,
max_age,
min_age,
ge_amount,
amount_range,
lt_amount,
epoch,
class,
type_,
..
} = self;
// All remaining groups run in parallel. Each closure owns an exclusive &mut
@@ -465,17 +488,108 @@ impl UTXOCohorts<Rw> {
let ss = &all_supply_sats;
let tasks: Vec<Box<dyn FnOnce() -> Result<()> + Send + '_>> = vec![
Box::new(|| sth.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, vc, vd, ss, exit)),
Box::new(|| lth.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit)),
Box::new(|| age_range.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))),
Box::new(|| max_age.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))),
Box::new(|| min_age.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))),
Box::new(|| ge_amount.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))),
Box::new(|| epoch.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))),
Box::new(|| class.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))),
Box::new(|| amount_range.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))),
Box::new(|| lt_amount.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))),
Box::new(|| type_.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))),
Box::new(|| {
sth.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
height_to_market_cap,
vc,
vd,
ss,
exit,
)
}),
Box::new(|| {
lth.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
height_to_market_cap,
ss,
exit,
)
}),
Box::new(|| {
age_range.par_iter_mut().try_for_each(|v| {
v.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
height_to_market_cap,
ss,
exit,
)
})
}),
Box::new(|| {
max_age.par_iter_mut().try_for_each(|v| {
v.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
height_to_market_cap,
ss,
exit,
)
})
}),
Box::new(|| {
min_age.par_iter_mut().try_for_each(|v| {
v.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
height_to_market_cap,
ss,
exit,
)
})
}),
Box::new(|| {
ge_amount
.par_iter_mut()
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))
}),
Box::new(|| {
epoch.par_iter_mut().try_for_each(|v| {
v.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
height_to_market_cap,
ss,
exit,
)
})
}),
Box::new(|| {
class.par_iter_mut().try_for_each(|v| {
v.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
height_to_market_cap,
ss,
exit,
)
})
}),
Box::new(|| {
amount_range
.par_iter_mut()
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))
}),
Box::new(|| {
lt_amount
.par_iter_mut()
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))
}),
Box::new(|| {
type_
.par_iter_mut()
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))
}),
];
tasks
@@ -589,7 +703,7 @@ impl UTXOCohorts<Rw> {
/// Filter source cohorts by an optional filter.
/// If filter is None, returns all sources (used for "all" aggregate).
fn filter_sources_from<'a, M: CohortMetricsBase + 'a>(
sources: impl Iterator<Item = &'a UTXOCohortVecs<M, RealizedState>>,
sources: impl Iterator<Item = &'a UTXOCohortVecs<M>>,
filter: Option<&Filter>,
) -> Vec<&'a M> {
match filter {
@@ -603,7 +717,7 @@ fn filter_sources_from<'a, M: CohortMetricsBase + 'a>(
/// Filter MinimalCohortMetrics source cohorts by an optional filter.
fn filter_minimal_sources_from<'a>(
sources: impl Iterator<Item = &'a UTXOCohortVecs<MinimalCohortMetrics, MinimalRealizedState>>,
sources: impl Iterator<Item = &'a UTXOCohortVecs<MinimalCohortMetrics>>,
filter: Option<&Filter>,
) -> Vec<&'a MinimalCohortMetrics> {
match filter {
@@ -7,28 +7,27 @@ use vecdb::{Exit, ReadableVec};
use crate::{blocks, distribution::state::UTXOCohortState, prices};
use crate::distribution::metrics::{
CohortMetricsBase, CoreCohortMetrics, MinimalCohortMetrics,
CohortMetricsBase, CohortMetricsState, CoreCohortMetrics, MinimalCohortMetrics,
};
use crate::distribution::state::{CoreRealizedState, MinimalRealizedState, RealizedOps, RealizedState};
use super::super::traits::DynCohortVecs;
#[derive(Traversable)]
pub struct UTXOCohortVecs<Metrics, R: RealizedOps> {
pub struct UTXOCohortVecs<M: CohortMetricsState> {
#[traversable(skip)]
state_starting_height: Option<Height>,
#[traversable(skip)]
pub state: Option<Box<UTXOCohortState<R>>>,
pub state: Option<Box<UTXOCohortState<M::Realized>>>,
#[traversable(flatten)]
pub metrics: Metrics,
pub metrics: M,
}
// --- Shared state helpers (identical across all DynCohortVecs impls) ---
impl<Metrics, R: RealizedOps> UTXOCohortVecs<Metrics, R> {
pub(crate) fn new(state: Option<Box<UTXOCohortState<R>>>, metrics: Metrics) -> Self {
impl<M: CohortMetricsState> UTXOCohortVecs<M> {
pub(crate) fn new(state: Option<Box<UTXOCohortState<M::Realized>>>, metrics: M) -> Self {
Self {
state_starting_height: None,
state,
@@ -66,17 +65,13 @@ impl<Metrics, R: RealizedOps> UTXOCohortVecs<Metrics, R> {
// --- Blanket impl for CohortMetricsBase types (always use full RealizedState) ---
impl<Metrics: CohortMetricsBase + Traversable> Filtered
for UTXOCohortVecs<Metrics, RealizedState>
{
impl<M: CohortMetricsBase + Traversable> Filtered for UTXOCohortVecs<M> {
fn filter(&self) -> &Filter {
self.metrics.filter()
}
}
impl<Metrics: CohortMetricsBase + Traversable> DynCohortVecs
for UTXOCohortVecs<Metrics, RealizedState>
{
impl<M: CohortMetricsBase + Traversable> DynCohortVecs for UTXOCohortVecs<M> {
fn min_stateful_height_len(&self) -> usize {
self.metrics.min_stateful_height_len()
}
@@ -227,13 +222,13 @@ macro_rules! impl_import_state {
// --- MinimalCohortMetrics: uses MinimalRealizedState ---
impl Filtered for UTXOCohortVecs<MinimalCohortMetrics, MinimalRealizedState> {
impl Filtered for UTXOCohortVecs<MinimalCohortMetrics> {
fn filter(&self) -> &Filter {
&self.metrics.filter
}
}
impl DynCohortVecs for UTXOCohortVecs<MinimalCohortMetrics, MinimalRealizedState> {
impl DynCohortVecs for UTXOCohortVecs<MinimalCohortMetrics> {
fn min_stateful_height_len(&self) -> usize {
self.metrics.min_stateful_height_len()
}
@@ -311,13 +306,13 @@ impl DynCohortVecs for UTXOCohortVecs<MinimalCohortMetrics, MinimalRealizedState
// --- CoreCohortMetrics: uses CoreRealizedState ---
impl Filtered for UTXOCohortVecs<CoreCohortMetrics, CoreRealizedState> {
impl Filtered for UTXOCohortVecs<CoreCohortMetrics> {
fn filter(&self) -> &Filter {
&self.metrics.filter
}
}
impl DynCohortVecs for UTXOCohortVecs<CoreCohortMetrics, CoreRealizedState> {
impl DynCohortVecs for UTXOCohortVecs<CoreCohortMetrics> {
fn min_stateful_height_len(&self) -> usize {
self.metrics.min_stateful_height_len()
}
@@ -392,4 +387,3 @@ impl DynCohortVecs for UTXOCohortVecs<CoreCohortMetrics, CoreRealizedState> {
self.reset_iteration_impl();
}
}
@@ -20,8 +20,12 @@ pub(crate) fn compute_overlapping(
) -> Result<()> {
info!("Computing overlapping cohorts...");
utxo_cohorts.compute_overlapping_vecs(starting_indexes, exit)?;
address_cohorts.compute_overlapping_vecs(starting_indexes, exit)?;
let (r1, r2) = rayon::join(
|| utxo_cohorts.compute_overlapping_vecs(starting_indexes, exit),
|| address_cohorts.compute_overlapping_vecs(starting_indexes, exit),
);
r1?;
r2?;
Ok(())
}
@@ -39,8 +43,12 @@ pub(crate) fn compute_rest_part1(
) -> Result<()> {
info!("Computing rest part 1...");
utxo_cohorts.compute_rest_part1(blocks, prices, starting_indexes, exit)?;
address_cohorts.compute_rest_part1(blocks, prices, starting_indexes, exit)?;
let (r1, r2) = rayon::join(
|| utxo_cohorts.compute_rest_part1(blocks, prices, starting_indexes, exit),
|| address_cohorts.compute_rest_part1(blocks, prices, starting_indexes, exit),
);
r1?;
r2?;
Ok(())
}
@@ -164,11 +164,34 @@ pub use unrealized::*;
use brk_cohort::Filter;
use brk_error::Result;
use brk_types::{Cents, Height, Indexes, Version};
use vecdb::{AnyStoredVec, Exit};
use vecdb::{AnyStoredVec, Exit, StorageMode};
use crate::{blocks, distribution::state::{CohortState, RealizedState}, prices};
use crate::{blocks, distribution::state::{CohortState, CoreRealizedState, MinimalRealizedState, RealizedOps, RealizedState}, prices};
pub trait CohortMetricsBase: Send + Sync {
pub trait CohortMetricsState {
type Realized: RealizedOps;
}
impl<M: StorageMode> CohortMetricsState for MinimalCohortMetrics<M> {
type Realized = MinimalRealizedState;
}
impl<M: StorageMode> CohortMetricsState for CoreCohortMetrics<M> {
type Realized = CoreRealizedState;
}
impl<M: StorageMode> CohortMetricsState for BasicCohortMetrics<M> {
type Realized = RealizedState;
}
impl<M: StorageMode> CohortMetricsState for ExtendedCohortMetrics<M> {
type Realized = RealizedState;
}
impl<M: StorageMode> CohortMetricsState for ExtendedAdjustedCohortMetrics<M> {
type Realized = RealizedState;
}
impl<M: StorageMode> CohortMetricsState for AllCohortMetrics<M> {
type Realized = RealizedState;
}
pub trait CohortMetricsBase: CohortMetricsState<Realized = RealizedState> + Send + Sync {
fn filter(&self) -> &Filter;
fn supply(&self) -> &SupplyMetrics;
fn supply_mut(&mut self) -> &mut SupplyMetrics;
+100 -9
View File
@@ -600,6 +600,11 @@ fn type_contains_ident(ty: &Type, ident: &syn::Ident) -> bool {
/// - Types with `M: StorageMode` → maps `Self<Rw>` → `Self<Ro>`.
/// - Types with other generic type params (no M) → propagates `ReadOnlyClone` through each param.
/// - Types with no generic type params → nothing generated (they should `#[derive(Clone)]`).
///
/// Container params (mapped through ReadOnlyClone) are identified by:
/// - Unbounded type params (no inline or where-clause bounds), OR
/// - Bounded params that appear as a bare field type in a non-skipped field
/// (e.g. `metrics: M` where M is the param itself).
fn gen_read_only_clone(input: &DeriveInput) -> proc_macro2::TokenStream {
let generics = &input.generics;
let name = &input.ident;
@@ -627,8 +632,6 @@ fn gen_read_only_clone(input: &DeriveInput) -> proc_macro2::TokenStream {
}
// Determine which type params have bounds (inline or via where clause).
// Bounded params are "leaf" params — kept as-is in the ReadOnly target.
// Unbounded params are "container" params — mapped through ReadOnlyClone.
let where_bounded: Vec<&syn::Ident> = if let Some(where_clause) = &generics.where_clause {
where_clause
.predicates
@@ -651,9 +654,17 @@ fn gen_read_only_clone(input: &DeriveInput) -> proc_macro2::TokenStream {
Vec::new()
};
// Find params that appear as bare (direct) field types in non-skipped fields.
let bare_field_params = find_bare_field_params(data, &type_params);
// Container params: unbounded OR bare-field params.
let container_params: Vec<&syn::Ident> = type_params
.iter()
.filter(|tp| tp.bounds.is_empty() && !where_bounded.contains(&&tp.ident))
.filter(|tp| {
let is_unbounded = tp.bounds.is_empty() && !where_bounded.contains(&&tp.ident);
let is_bare = bare_field_params.contains(&&tp.ident);
is_unbounded || is_bare
})
.map(|tp| &tp.ident)
.collect();
@@ -662,7 +673,37 @@ fn gen_read_only_clone(input: &DeriveInput) -> proc_macro2::TokenStream {
return quote! {};
}
gen_read_only_clone_for_generics(name, generics, data, &container_params)
gen_read_only_clone_for_generics(name, generics, data, &type_params, &container_params)
}
/// Find type params that appear as bare (direct) field types in non-skipped fields.
/// E.g. `metrics: M` where M is a type param → M is a bare field param.
fn find_bare_field_params<'a>(
data: &syn::DataStruct,
type_params: &[&'a syn::TypeParam],
) -> Vec<&'a syn::Ident> {
let fields: &syn::punctuated::Punctuated<syn::Field, _> = match &data.fields {
Fields::Named(named) => &named.named,
Fields::Unnamed(unnamed) => &unnamed.unnamed,
Fields::Unit => return Vec::new(),
};
let mut bare = Vec::new();
for field in fields {
if is_field_skipped(field) {
continue;
}
if let Type::Path(type_path) = &field.ty
&& type_path.path.segments.len() == 1
&& let Some(seg) = type_path.path.segments.first()
&& seg.arguments.is_empty()
{
if let Some(tp) = type_params.iter().find(|tp| tp.ident == seg.ident) {
bare.push(&tp.ident);
}
}
}
bare
}
/// Generate `ReadOnlyClone` for types with `M: StorageMode`.
@@ -798,14 +839,18 @@ fn is_field_skipped(field: &syn::Field) -> bool {
/// Generate `ReadOnlyClone` for types with generic type params but no `M: StorageMode`.
///
/// `container_params` are unbounded type params that get `ReadOnlyClone` bounds and are
/// `container_params` are type params that get `ReadOnlyClone` bounds and are
/// mapped to `T::ReadOnly` in the target type.
/// Bounded type params (leaf params) are kept as-is — they don't change across storage modes.
/// Leaf type params are kept as-is — they don't change across storage modes.
/// Fields containing container params use `.read_only_clone()`, others use `.clone()`.
///
/// For bounded container params, the original bounds are preserved and propagated
/// to the ReadOnly version via where clause (e.g. `M::ReadOnly: CohortMetricsState`).
fn gen_read_only_clone_for_generics(
name: &syn::Ident,
generics: &syn::Generics,
data: &syn::DataStruct,
type_params: &[&syn::TypeParam],
container_params: &[&syn::Ident],
) -> proc_macro2::TokenStream {
// Check if any non-skipped field references a container param (otherwise skip).
@@ -832,6 +877,7 @@ fn gen_read_only_clone_for_generics(
let is_container = |ident: &syn::Ident| container_params.iter().any(|cp| *cp == ident);
// Impl generics: add ReadOnlyClone bound to container params, keep bounds for leaf params.
// For bounded container params, preserve original bounds alongside ReadOnlyClone.
let impl_params: Vec<proc_macro2::TokenStream> = generics
.params
.iter()
@@ -840,7 +886,11 @@ fn gen_read_only_clone_for_generics(
let ident = &tp.ident;
let bounds = &tp.bounds;
if is_container(ident) {
quote! { #ident: vecdb::ReadOnlyClone }
if bounds.is_empty() {
quote! { #ident: vecdb::ReadOnlyClone }
} else {
quote! { #ident: #bounds + vecdb::ReadOnlyClone }
}
} else if bounds.is_empty() {
quote! { #ident }
} else {
@@ -900,7 +950,48 @@ fn gen_read_only_clone_for_generics(
})
.collect();
let where_clause = &generics.where_clause;
// Build where clause: propagate bounds from bounded container params to their ReadOnly.
// E.g. `M: Trait` → add `<M as ReadOnlyClone>::ReadOnly: Trait`.
let mut extra_where: Vec<proc_macro2::TokenStream> = Vec::new();
// Propagate inline bounds.
for tp in type_params {
if is_container(&tp.ident) && !tp.bounds.is_empty() {
let ident = &tp.ident;
let bounds = &tp.bounds;
extra_where.push(quote! {
<#ident as vecdb::ReadOnlyClone>::ReadOnly: #bounds
});
}
}
// Propagate where-clause bounds for container params.
if let Some(wc) = &generics.where_clause {
for pred in &wc.predicates {
if let syn::WherePredicate::Type(pt) = pred
&& let Type::Path(tp) = &pt.bounded_ty
&& let Some(seg) = tp.path.segments.first()
&& container_params.iter().any(|cp| **cp == seg.ident)
{
let ident = &seg.ident;
let bounds = &pt.bounds;
extra_where.push(quote! {
<#ident as vecdb::ReadOnlyClone>::ReadOnly: #bounds
});
}
}
}
let original_predicates = generics
.where_clause
.as_ref()
.map(|w| &w.predicates);
let combined_where = if extra_where.is_empty() && original_predicates.is_none() {
quote! {}
} else {
quote! { where #(#extra_where,)* #original_predicates }
};
// Field-level: if field type contains any container param → read_only_clone, else → clone.
let field_contains_container_param =
@@ -954,7 +1045,7 @@ fn gen_read_only_clone_for_generics(
};
quote! {
impl<#(#impl_params),*> vecdb::ReadOnlyClone for #name<#(#self_ty_args),*> #where_clause {
impl<#(#impl_params),*> vecdb::ReadOnlyClone for #name<#(#self_ty_args),*> #combined_where {
type ReadOnly = #name<#(#ro_ty_args),*>;
fn read_only_clone(&self) -> Self::ReadOnly {