diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index b7ed25f..52c921c 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -4,81 +4,44 @@ use darling::util::PathList; use darling::FromAttributes; use darling::FromMeta; use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; use quote::quote; use quote::quote_spanned; use quote::ToTokens; use syn::parse_quote; use syn::spanned::Spanned; -use syn::{parse_macro_input, Data, DeriveInput}; - -#[proc_macro_derive(RandomlyMutable, attributes(randmut))] -pub fn randmut_derive(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - - let (def_ctx, mut ctx_ident) = - create_context_helper(&ast, parse_quote!(RandomlyMutable), parse_quote!(randmut)); - let custom_context = ctx_ident.is_some(); - - let name = ast.ident; - - match ast.data { - Data::Struct(s) => { - let mut inner = Vec::new(); - - for (i, field) in s.fields.into_iter().enumerate() { - let ty = field.ty; - let span = ty.span(); - - if ctx_ident.is_none() { - ctx_ident = Some( - quote_spanned! {span=> <#ty as genetic_rs_common::prelude::RandomlyMutable>::Context }, - ); - } - - if let Some(field_name) = field.ident { - if custom_context { - inner.push(quote_spanned! {span=> - <#ty as genetic_rs_common::prelude::RandomlyMutable>::mutate(&mut self.#field_name, &ctx.#field_name, rate, rng); - }); - } else { - inner.push(quote_spanned! {span=> - <#ty as genetic_rs_common::prelude::RandomlyMutable>::mutate(&mut self.#field_name, ctx, rate, rng); - }); - } - } else if custom_context { - inner.push(quote_spanned! {span=> - <#ty as genetic_rs_common::prelude::RandomlyMutable>::mutate(&mut self.#i, &ctx.#i, rate, rng); - }); - } else { - inner.push(quote_spanned! {span=> - <#ty as genetic_rs_common::prelude::RandomlyMutable>::mutate(&mut self.#i, ctx, rate, rng); - }); - } - } - - let inner: proc_macro2::TokenStream = inner.into_iter().collect(); +use syn::{parse_macro_input, Data, DeriveInput, Fields}; + +/// Determines the context handling strategy for a derive macro. +enum ContextKind { + /// No context attribute found; all fields share a single context passed directly. + Shared, + /// `create_context` or `with_context` was used; context is accessed per field. + /// For `create_context`, a new context struct is generated (see [`ContextInfo::ctx_def`]). + /// For `with_context`, an existing type is referenced with field access. + PerField, +} - quote! { - #[automatically_derived] - impl genetic_rs_common::prelude::RandomlyMutable for #name { - type Context = #ctx_ident; +/// Result of resolving context for a struct derive. +struct ContextInfo { + /// The context type token stream (the `type Context = ...` part). + ctx_type: TokenStream2, + /// Optional token stream defining a new context struct (only for `PerField`). + ctx_def: Option, + /// How to pass the context to each field's method. + kind: ContextKind, +} - fn mutate(&mut self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) { - #inner - } - } +#[derive(FromMeta)] +struct ContextArgs { + with_context: Option, + create_context: Option, +} - #def_ctx - } - .into() - } - Data::Enum(_e) => { - panic!("enums not yet supported"); - } - Data::Union(_u) => { - panic!("unions not yet supported"); - } - } +#[derive(FromMeta)] +struct CreateContext { + name: syn::Ident, + derive: Option, } #[derive(FromAttributes)] @@ -86,8 +49,7 @@ pub fn randmut_derive(input: TokenStream) -> TokenStream { struct MitosisSettings { use_randmut: Option, - // darling is annoyingly restrictive and doesn't - // let me just ignore extra fields + // darling requires all possible fields to be listed to avoid unknown field errors #[darling(rename = "create_context")] _create_context: Option, @@ -95,352 +57,378 @@ struct MitosisSettings { _with_context: Option, } -#[proc_macro_derive(Mitosis, attributes(mitosis))] -pub fn mitosis_derive(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - +/// Resolves the context info from the attribute on an AST node. +/// +/// `trait_name` is the trait whose `Context` associated type is used when +/// generating a per-field context struct (i.e., for `create_context`). +/// `attr_path` is the path of the attribute to look for (e.g. `randmut`). +/// +/// If the attribute is absent, or if it does not contain `create_context` or +/// `with_context`, returns `None` and the caller should fall back to inferring +/// the context type from the first struct field. +fn resolve_context( + ast: &DeriveInput, + trait_name: syn::Ident, + attr_path: syn::Path, + fallback_ctx: TokenStream2, +) -> ContextInfo { let name = &ast.ident; + let vis = ast.vis.to_token_stream(); - let mitosis_settings = MitosisSettings::from_attributes(&ast.attrs).unwrap(); - if mitosis_settings.use_randmut.is_some() && mitosis_settings.use_randmut.unwrap() { - quote! { - #[automatically_derived] - impl genetic_rs_common::prelude::Mitosis for #name { - type Context = ::Context; + let attr = ast.attrs.iter().find(|a| a.path() == &attr_path); - fn divide(&self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) -> Self { - let mut child = self.clone(); - ::mutate(&mut child, ctx, rate, rng); - child - } + if let Some(attr) = attr { + // Try to parse the attribute as ContextArgs; silently ignore if it + // doesn't contain context-related fields (e.g., `use_randmut`). + if let Ok(args) = ContextArgs::from_meta(&attr.meta) { + if args.create_context.is_some() && args.with_context.is_some() { + panic!("cannot have both create_context and with_context"); } - } - .into() - } else { - let (def_ctx, mut ctx_ident) = - create_context_helper(&ast, parse_quote!(RandomlyMutable), parse_quote!(mitosis)); - let custom_context = ctx_ident.is_some(); - - let name = ast.ident; - - match ast.data { - Data::Struct(s) => { - let mut is_tuple_struct = false; - let mut inner = Vec::new(); - - for (i, field) in s.fields.into_iter().enumerate() { - let ty = field.ty; - let span = ty.span(); - - if ctx_ident.is_none() { - ctx_ident = Some( - quote_spanned! {span=> <#ty as genetic_rs_common::prelude::Mitosis>::Context }, - ); - } - if let Some(field_name) = field.ident { - if custom_context { - inner.push(quote_spanned! {span=> - #field_name: <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#field_name, &ctx.#field_name, rate, rng), - }); + if let Some(create_ctx) = args.create_context { + let ident = &create_ctx.name; + let doc = quote! { + #[doc = concat!("Autogenerated context struct for [`", stringify!(#name), "`]")] + }; + let derives = create_ctx.derive.map(|paths| { + quote! { #[derive(#(#paths,)*)] } + }); + + let ctx_def = match &ast.data { + Data::Struct(s) => { + let fields: Vec = s + .fields + .iter() + .map(|field| { + let ty = &field.ty; + let ty_span = ty.span(); + if let Some(field_name) = &field.ident { + quote_spanned! {ty_span=> + #vis #field_name: <#ty as genetic_rs_common::prelude::#trait_name>::Context, + } + } else { + quote_spanned! {ty_span=> + #vis <#ty as genetic_rs_common::prelude::#trait_name>::Context, + } + } + }) + .collect(); + let fields_ts: TokenStream2 = fields.into_iter().collect(); + + let is_tuple = matches!(s.fields, Fields::Unnamed(_)); + if is_tuple { + quote! { #doc #derives #vis struct #ident (#fields_ts); } } else { - inner.push(quote_spanned! {span=> - #field_name: <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#field_name, ctx, rate, rng), - }); - } - } else if custom_context { - is_tuple_struct = true; - inner.push(quote_spanned! {span=> - <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#i, &ctx.#i, rate, rng), - }); - } else { - is_tuple_struct = true; - inner.push(quote_spanned! {span=> - <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#i, ctx, rate, rng), - }); - } - } - - let inner: proc_macro2::TokenStream = inner.into_iter().collect(); - let child = if is_tuple_struct { - quote! { - Self(#inner) - } - } else { - quote! { - Self { - #inner + quote! { #doc #derives #vis struct #ident { #fields_ts } } } } + Data::Enum(_) => panic!("enums not supported"), + Data::Union(_) => panic!("unions not supported"), }; - quote! { - #[automatically_derived] - impl genetic_rs_common::prelude::Mitosis for #name { - type Context = #ctx_ident; - - fn divide(&self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) -> Self { - #child - } - } - - #def_ctx - } - .into() - } - Data::Enum(_e) => { - panic!("enums not yet supported"); + return ContextInfo { + ctx_type: ident.to_token_stream(), + ctx_def: Some(ctx_def), + kind: ContextKind::PerField, + }; } - Data::Union(_u) => { - panic!("unions not yet supported"); + + if let Some(with_ctx) = args.with_context { + return ContextInfo { + ctx_type: with_ctx.to_token_stream(), + ctx_def: None, + kind: ContextKind::PerField, + }; } } } -} -#[derive(FromMeta)] -struct ContextArgs { - with_context: Option, - create_context: Option, -} - -#[derive(FromMeta)] -struct CreateContext { - name: syn::Ident, - derive: Option, + ContextInfo { + ctx_type: fallback_ctx, + ctx_def: None, + kind: ContextKind::Shared, + } } -fn create_context_helper( - ast: &DeriveInput, - trait_name: syn::Ident, - attr_path: syn::Path, -) -> ( - Option, - Option, -) { +#[proc_macro_derive(RandomlyMutable, attributes(randmut))] +pub fn randmut_derive(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident; - let doc = - quote! { #[doc = concat!("Autogenerated context struct for [`", stringify!(#name), "`]")] }; - let vis = ast.vis.to_token_stream(); + let Data::Struct(s) = &ast.data else { + panic!("enums and unions not yet supported"); + }; + + // Determine the fallback context type from the first field (if any). + let fallback_ctx = s.fields.iter().next().map_or_else( + || quote! { () }, + |f| { + let ty = &f.ty; + quote! { <#ty as genetic_rs_common::prelude::RandomlyMutable>::Context } + }, + ); + + let ctx_info = resolve_context( + &ast, + parse_quote!(RandomlyMutable), + parse_quote!(randmut), + fallback_ctx, + ); + + let ctx_type = &ctx_info.ctx_type; + let ctx_def = &ctx_info.ctx_def; + + let inner: TokenStream2 = s + .fields + .iter() + .enumerate() + .map(|(i, field)| { + let ty = &field.ty; + let span = ty.span(); + let idx = syn::Index::from(i); + match (&field.ident, &ctx_info.kind) { + (Some(field_name), ContextKind::PerField) => quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::RandomlyMutable>::mutate(&mut self.#field_name, &ctx.#field_name, rate, rng); + }, + (Some(field_name), _) => quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::RandomlyMutable>::mutate(&mut self.#field_name, ctx, rate, rng); + }, + (None, ContextKind::PerField) => quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::RandomlyMutable>::mutate(&mut self.#idx, &ctx.#idx, rate, rng); + }, + (None, _) => quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::RandomlyMutable>::mutate(&mut self.#idx, ctx, rate, rng); + }, + } + }) + .collect(); - let attr = ast.attrs.iter().find(|a| a.path() == &attr_path); - if attr.is_none() { - return (None, None); + quote! { + #[automatically_derived] + impl genetic_rs_common::prelude::RandomlyMutable for #name { + type Context = #ctx_type; + + fn mutate(&mut self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) { + #inner + } + } + + #ctx_def } + .into() +} - let meta = &attr.unwrap().meta; +#[proc_macro_derive(Mitosis, attributes(mitosis))] +pub fn mitosis_derive(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let name = &ast.ident; - let args = ContextArgs::from_meta(meta).unwrap(); + let mitosis_settings = MitosisSettings::from_attributes(&ast.attrs).unwrap(); + if mitosis_settings.use_randmut.unwrap_or(false) { + return quote! { + #[automatically_derived] + impl genetic_rs_common::prelude::Mitosis for #name { + type Context = ::Context; - if args.create_context.is_some() && args.with_context.is_some() { - panic!("cannot have both create_context and with_context"); + fn divide(&self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) -> Self { + let mut child = self.clone(); + ::mutate(&mut child, ctx, rate, rng); + child + } + } + } + .into(); } - if let Some(create_ctx) = args.create_context { - let ident = &create_ctx.name; - let derives = create_ctx.derive.map(|paths| { - quote! { - #[derive(#(#paths,)*)] + let Data::Struct(s) = &ast.data else { + panic!("enums and unions not yet supported"); + }; + + let fallback_ctx = s.fields.iter().next().map_or_else( + || quote! { () }, + |f| { + let ty = &f.ty; + quote! { <#ty as genetic_rs_common::prelude::Mitosis>::Context } + }, + ); + + let ctx_info = resolve_context( + &ast, + parse_quote!(Mitosis), + parse_quote!(mitosis), + fallback_ctx, + ); + + let ctx_type = &ctx_info.ctx_type; + let ctx_def = &ctx_info.ctx_def; + + let is_tuple_struct = matches!(s.fields, Fields::Unnamed(_)); + + let inner: TokenStream2 = s + .fields + .iter() + .enumerate() + .map(|(i, field)| { + let ty = &field.ty; + let span = ty.span(); + let idx = syn::Index::from(i); + match (&field.ident, &ctx_info.kind) { + (Some(field_name), ContextKind::PerField) => quote_spanned! {span=> + #field_name: <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#field_name, &ctx.#field_name, rate, rng), + }, + (Some(field_name), _) => quote_spanned! {span=> + #field_name: <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#field_name, ctx, rate, rng), + }, + (None, ContextKind::PerField) => quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#idx, &ctx.#idx, rate, rng), + }, + (None, _) => quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#idx, ctx, rate, rng), + }, } - }); - - match &ast.data { - Data::Struct(s) => { - let mut inner = Vec::::new(); - let mut tuple_struct = false; - - for field in &s.fields { - let ty = &field.ty; - let ty_span = ty.span(); - - if let Some(field_name) = &field.ident { - inner.push(quote_spanned! {ty_span=> - #vis #field_name: <#ty as genetic_rs_common::prelude::#trait_name>::Context, - }); - } else { - tuple_struct = true; - inner.push(quote_spanned! {ty_span=> - #vis <#ty as genetic_rs_common::prelude::#trait_name>::Context, - }); - } - } + }) + .collect(); - let inner: proc_macro2::TokenStream = inner.into_iter().collect(); + let child = if is_tuple_struct { + quote! { Self(#inner) } + } else { + quote! { Self { #inner } } + }; - if tuple_struct { - return ( - Some(quote! { #doc #derives #vis struct #ident (#inner);}), - Some(ident.to_token_stream()), - ); - } + quote! { + #[automatically_derived] + impl genetic_rs_common::prelude::Mitosis for #name { + type Context = #ctx_type; - return ( - Some(quote! { #doc #derives #vis struct #ident {#inner};}), - Some(ident.to_token_stream()), - ); + fn divide(&self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) -> Self { + #child } - Data::Enum(_) => panic!("enums not supported"), - Data::Union(_) => panic!("unions not supported"), } - } - if let Some(ident) = args.with_context { - return (None, Some(ident.to_token_stream())); + #ctx_def } - - (None, None) + .into() } #[cfg(feature = "crossover")] #[proc_macro_derive(Crossover, attributes(crossover))] pub fn crossover_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); + let name = &ast.ident; - let (def_ctx, mut context) = - create_context_helper(&ast, parse_quote!(Crossover), parse_quote!(crossover)); - let custom_context = context.is_some(); - - let name = ast.ident; - - match ast.data { - Data::Struct(s) => { - let mut inner = Vec::new(); - let mut tuple_struct = false; - - for (i, field) in s.fields.into_iter().enumerate() { - let ty = field.ty; - let span = ty.span(); - - if context.is_none() { - context = - Some(quote! { <#ty as genetic_rs_common::prelude::Crossover>::Context }); - } - - if let Some(field_name) = field.ident { - if custom_context { - inner.push(quote_spanned! {span=> - #field_name: <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#field_name, &other.#field_name, &ctx.#field_name, rate, rng), - }); - } else { - inner.push(quote_spanned! {span=> - #field_name: <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#field_name, &other.#field_name, ctx, rate, rng), - }); - } - } else { - tuple_struct = true; - - if custom_context { - inner.push(quote_spanned! {span=> - <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#i, &other.#i, &ctx.#i, rate, rng), - }) - } else { - inner.push(quote_spanned! {span=> - <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#i, &other.#i, ctx, rate, rng), - }); - } - } + let Data::Struct(s) = &ast.data else { + panic!("enums and unions not yet supported"); + }; + + let fallback_ctx = s.fields.iter().next().map_or_else( + || quote! { () }, + |f| { + let ty = &f.ty; + quote! { <#ty as genetic_rs_common::prelude::Crossover>::Context } + }, + ); + + let ctx_info = resolve_context( + &ast, + parse_quote!(Crossover), + parse_quote!(crossover), + fallback_ctx, + ); + + let ctx_type = &ctx_info.ctx_type; + let ctx_def = &ctx_info.ctx_def; + + let is_tuple_struct = matches!(s.fields, Fields::Unnamed(_)); + + let inner: TokenStream2 = s + .fields + .iter() + .enumerate() + .map(|(i, field)| { + let ty = &field.ty; + let span = ty.span(); + let idx = syn::Index::from(i); + match (&field.ident, &ctx_info.kind) { + (Some(field_name), ContextKind::PerField) => quote_spanned! {span=> + #field_name: <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#field_name, &other.#field_name, &ctx.#field_name, rate, rng), + }, + (Some(field_name), _) => quote_spanned! {span=> + #field_name: <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#field_name, &other.#field_name, ctx, rate, rng), + }, + (None, ContextKind::PerField) => quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#idx, &other.#idx, &ctx.#idx, rate, rng), + }, + (None, _) => quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#idx, &other.#idx, ctx, rate, rng), + }, } + }) + .collect(); - let inner: proc_macro2::TokenStream = inner.into_iter().collect(); - - if tuple_struct { - quote! { - #def_ctx - - #[automatically_derived] - impl genetic_rs_common::prelude::Crossover for #name { - type Context = #context; + let child = if is_tuple_struct { + quote! { Self(#inner) } + } else { + quote! { Self { #inner } } + }; - fn crossover(&self, other: &Self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) -> Self { - Self(#inner) - } - } - }.into() - } else { - quote! { - #def_ctx + quote! { + #ctx_def - #[automatically_derived] - impl genetic_rs_common::prelude::Crossover for #name { - type Context = #context; + #[automatically_derived] + impl genetic_rs_common::prelude::Crossover for #name { + type Context = #ctx_type; - fn crossover(&self, other: &Self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) -> Self { - Self { - #inner - } - } - } - }.into() + fn crossover(&self, other: &Self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) -> Self { + #child } } - Data::Enum(_e) => { - panic!("enums not yet supported"); - } - Data::Union(_u) => { - panic!("unions not yet supported"); - } } + .into() } #[cfg(feature = "genrand")] #[proc_macro_derive(GenerateRandom)] pub fn genrand_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); + let name = &ast.ident; - let name = ast.ident; - - match ast.data { - Data::Struct(s) => { - let mut inner = Vec::new(); - let mut tuple_struct = false; - - for field in s.fields { - let ty = field.ty; - let span = ty.span(); - - if let Some(field_name) = field.ident { - inner.push(quote_spanned! {span=> - #field_name: <#ty as genetic_rs_common::prelude::GenerateRandom>::gen_random(rng), - }); - } else { - tuple_struct = true; - inner.push(quote_spanned! {span=> - <#ty as genetic_rs_common::prelude::GenerateRandom>::gen_random(rng), - }); + let Data::Struct(s) = &ast.data else { + panic!("enums and unions not yet supported"); + }; + + let is_tuple_struct = matches!(s.fields, Fields::Unnamed(_)); + + let inner: TokenStream2 = s + .fields + .iter() + .map(|field| { + let ty = &field.ty; + let span = ty.span(); + if let Some(field_name) = &field.ident { + quote_spanned! {span=> + #field_name: <#ty as genetic_rs_common::prelude::GenerateRandom>::gen_random(rng), } - } - - let inner: proc_macro2::TokenStream = inner.into_iter().collect(); - if tuple_struct { - quote! { - #[automatically_derived] - impl genetic_rs_common::prelude::GenerateRandom for #name { - fn gen_random(rng: &mut impl rand::Rng) -> Self { - Self(#inner) - } - } - } - .into() } else { - quote! { - #[automatically_derived] - impl genetic_rs_common::prelude::GenerateRandom for #name { - fn gen_random(rng: &mut impl rand::Rng) -> Self { - Self { - #inner - } - } - } + quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::GenerateRandom>::gen_random(rng), } - .into() } - } - Data::Enum(_e) => { - panic!("enums not yet supported"); - } - Data::Union(_u) => { - panic!("unions not yet supported"); + }) + .collect(); + + let body = if is_tuple_struct { + quote! { Self(#inner) } + } else { + quote! { Self { #inner } } + }; + + quote! { + #[automatically_derived] + impl genetic_rs_common::prelude::GenerateRandom for #name { + fn gen_random(rng: &mut impl rand::Rng) -> Self { + #body + } } } + .into() } diff --git a/genetic-rs/Cargo.toml b/genetic-rs/Cargo.toml index 2b211d8..9f08789 100644 --- a/genetic-rs/Cargo.toml +++ b/genetic-rs/Cargo.toml @@ -42,3 +42,7 @@ required-features = ["speciation"] [[example]] name = "derive" required-features = ["derive"] + +[[test]] +name = "derive_macros" +required-features = ["derive", "genrand", "crossover"] diff --git a/genetic-rs/tests/derive_macros.rs b/genetic-rs/tests/derive_macros.rs new file mode 100644 index 0000000..6cadbc9 --- /dev/null +++ b/genetic-rs/tests/derive_macros.rs @@ -0,0 +1,400 @@ +//! Integration tests for the derive macros. +//! +//! These tests guard against macro regressions (see issues #124 and #127). + +#![allow(dead_code)] + +use genetic_rs::prelude::*; + +// ────────────────────────────────────────────────────────────────────────────── +// Helpers shared across multiple tests +// ────────────────────────────────────────────────────────────────────────────── + +/// Newtype around f32 so we can implement traits without orphan-rule violations. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +struct Val(f32); + +impl RandomlyMutable for Val { + type Context = (); + + fn mutate(&mut self, _ctx: &Self::Context, rate: f32, rng: &mut impl Rng) { + self.0 += rng.random_range(-rate..=rate); + } +} + +impl Mitosis for Val { + type Context = (); + + fn divide(&self, _ctx: &Self::Context, _rate: f32, _rng: &mut impl Rng) -> Self { + *self + } +} + +impl GenerateRandom for Val { + fn gen_random(rng: &mut impl rand::Rng) -> Self { + Val(rng.random()) + } +} + +impl Crossover for Val { + type Context = (); + + fn crossover(&self, other: &Self, _ctx: &(), _rate: f32, _rng: &mut impl Rng) -> Self { + Val((self.0 + other.0) / 2.0) + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// RandomlyMutable +// ────────────────────────────────────────────────────────────────────────────── + +/// Plain named struct – context inferred from the first field. +#[derive(Clone, RandomlyMutable)] +struct RandMutNamed { + x: Val, + y: Val, +} + +#[test] +fn randmut_named_struct() { + let mut rng = rand::rng(); + let mut genome = RandMutNamed { + x: Val(0.0), + y: Val(0.0), + }; + genome.mutate(&(), 0.5, &mut rng); +} + +/// Tuple struct. +#[derive(Clone, RandomlyMutable)] +struct RandMutTuple(Val, Val); + +#[test] +fn randmut_tuple_struct() { + let mut rng = rand::rng(); + let mut genome = RandMutTuple(Val(0.0), Val(0.0)); + genome.mutate(&(), 0.5, &mut rng); +} + +/// Empty struct – context should be `()`. +#[derive(Clone, RandomlyMutable)] +struct RandMutEmpty {} + +#[test] +fn randmut_empty_struct() { + let mut rng = rand::rng(); + let mut genome = RandMutEmpty {}; + genome.mutate(&(), 0.0, &mut rng); +} + +/// Per-field contexts – `create_context` generates a context struct. +#[derive(Clone, Debug, Default)] +struct CtxA; + +#[derive(Clone, Debug, Default)] +struct CtxB; + +#[derive(Clone)] +struct FieldA; +impl RandomlyMutable for FieldA { + type Context = CtxA; + fn mutate(&mut self, _ctx: &CtxA, _rate: f32, _rng: &mut impl Rng) {} +} + +#[derive(Clone)] +struct FieldB; +impl RandomlyMutable for FieldB { + type Context = CtxB; + fn mutate(&mut self, _ctx: &CtxB, _rate: f32, _rng: &mut impl Rng) {} +} + +#[derive(Clone, RandomlyMutable)] +#[randmut(create_context(name = CreatedCtx, derive(Clone, Debug, Default)))] +struct RandMutWithCreatedCtx { + a: FieldA, + b: FieldB, +} + +#[test] +fn randmut_create_context() { + let mut rng = rand::rng(); + let ctx = CreatedCtx::default(); + // Verify the generated context has the correct field types. + let _: &CtxA = &ctx.a; + let _: &CtxB = &ctx.b; + let mut genome = RandMutWithCreatedCtx { + a: FieldA, + b: FieldB, + }; + genome.mutate(&ctx, 0.5, &mut rng); +} + +/// `with_context` attribute reuses an existing context type. +#[derive(Clone, RandomlyMutable)] +#[randmut(with_context = CreatedCtx)] +struct RandMutWithProvidedCtx { + a: FieldA, + b: FieldB, +} + +#[test] +fn randmut_with_context() { + let mut rng = rand::rng(); + let ctx = CreatedCtx::default(); + let mut genome = RandMutWithProvidedCtx { + a: FieldA, + b: FieldB, + }; + genome.mutate(&ctx, 0.5, &mut rng); +} + +// ────────────────────────────────────────────────────────────────────────────── +// Mitosis +// ────────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +struct MitFieldA; +impl Mitosis for MitFieldA { + type Context = CtxA; + fn divide(&self, _ctx: &CtxA, _rate: f32, _rng: &mut impl Rng) -> Self { + MitFieldA + } +} + +#[derive(Clone)] +struct MitFieldB; +impl Mitosis for MitFieldB { + type Context = CtxB; + fn divide(&self, _ctx: &CtxB, _rate: f32, _rng: &mut impl Rng) -> Self { + MitFieldB + } +} + +/// Plain named struct – context inferred from the first field. +#[derive(Clone, Mitosis)] +struct MitosisNamed { + a: MitFieldA, +} + +#[test] +fn mitosis_named_struct_shared_ctx() { + let mut rng = rand::rng(); + let genome = MitosisNamed { a: MitFieldA }; + let _child = genome.divide(&CtxA, 0.5, &mut rng); +} + +/// Empty struct – context should be `()`. +#[derive(Clone, Mitosis)] +struct MitosisEmpty {} + +#[test] +fn mitosis_empty_struct() { + let mut rng = rand::rng(); + let genome = MitosisEmpty {}; + let _child = genome.divide(&(), 0.0, &mut rng); +} + +/// `use_randmut = true` – delegates to `RandomlyMutable`. +#[derive(Clone, RandomlyMutable, Mitosis)] +#[mitosis(use_randmut = true)] +struct MitosisUseRandmut(Val); + +#[test] +fn mitosis_use_randmut_true() { + let mut rng = rand::rng(); + let genome = MitosisUseRandmut(Val(1.0)); + let _child = genome.divide(&(), 0.5, &mut rng); +} + +/// `use_randmut = false` previously caused a proc-macro panic (issue #127). +/// Verify it now compiles and behaves like a normal Mitosis derive. +#[derive(Clone, Mitosis)] +#[mitosis(use_randmut = false)] +struct MitosisUseRandmutFalse { + a: MitFieldA, +} + +#[test] +fn mitosis_use_randmut_false_no_panic() { + let mut rng = rand::rng(); + let genome = MitosisUseRandmutFalse { a: MitFieldA }; + // Context is inferred from the first field. + let _child = genome.divide(&CtxA, 0.5, &mut rng); +} + +/// `create_context` with `Mitosis` must generate fields typed as +/// `::Context`, NOT `::Context`. +/// The wrong trait was used in 1.2.0 (issue #127). +#[derive(Clone, Mitosis)] +#[mitosis(create_context(name = MitosisCreatedCtx, derive(Clone, Debug, Default)))] +struct MitosisWithCreatedCtx { + a: MitFieldA, + b: MitFieldB, +} + +#[test] +fn mitosis_create_context_uses_mitosis_context() { + let mut rng = rand::rng(); + let ctx = MitosisCreatedCtx::default(); + // MitosisCreatedCtx.a must be ::Context = CtxA + // MitosisCreatedCtx.b must be ::Context = CtxB + let _: &CtxA = &ctx.a; + let _: &CtxB = &ctx.b; + + let genome = MitosisWithCreatedCtx { + a: MitFieldA, + b: MitFieldB, + }; + let _child = genome.divide(&ctx, 0.5, &mut rng); +} + +/// `with_context` for Mitosis reuses an existing context struct. +#[derive(Clone, Mitosis)] +#[mitosis(with_context = MitosisCreatedCtx)] +struct MitosisWithProvidedCtx { + a: MitFieldA, + b: MitFieldB, +} + +#[test] +fn mitosis_with_context() { + let mut rng = rand::rng(); + let ctx = MitosisCreatedCtx::default(); + let genome = MitosisWithProvidedCtx { + a: MitFieldA, + b: MitFieldB, + }; + let _child = genome.divide(&ctx, 0.5, &mut rng); +} + +/// Tuple struct with Mitosis. +#[derive(Clone, Mitosis)] +struct MitosisTuple(Val); + +#[test] +fn mitosis_tuple_struct() { + let mut rng = rand::rng(); + let genome = MitosisTuple(Val(0.0)); + let _child = genome.divide(&(), 0.5, &mut rng); +} + +// ────────────────────────────────────────────────────────────────────────────── +// GenerateRandom +// ────────────────────────────────────────────────────────────────────────────── + +/// Named struct. +#[derive(Clone, GenerateRandom)] +struct GenRandNamed { + x: Val, + y: Val, +} + +#[test] +fn genrand_named_struct() { + let mut rng = rand::rng(); + let _genome = GenRandNamed::gen_random(&mut rng); +} + +/// Tuple struct. +#[derive(Clone, GenerateRandom)] +struct GenRandTuple(Val, Val); + +#[test] +fn genrand_tuple_struct() { + let mut rng = rand::rng(); + let _genome = GenRandTuple::gen_random(&mut rng); +} + +/// Empty struct. +#[derive(Clone, GenerateRandom)] +struct GenRandEmpty {} + +#[test] +fn genrand_empty_struct() { + let mut rng = rand::rng(); + let _genome = GenRandEmpty::gen_random(&mut rng); +} + +// ────────────────────────────────────────────────────────────────────────────── +// Crossover +// ────────────────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +struct CrossFieldA; +impl Crossover for CrossFieldA { + type Context = CtxA; + fn crossover(&self, _other: &Self, _ctx: &CtxA, _rate: f32, _rng: &mut impl Rng) -> Self { + CrossFieldA + } +} + +#[derive(Clone)] +struct CrossFieldB; +impl Crossover for CrossFieldB { + type Context = CtxB; + fn crossover(&self, _other: &Self, _ctx: &CtxB, _rate: f32, _rng: &mut impl Rng) -> Self { + CrossFieldB + } +} + +/// Named struct – shared context inferred from the first field. +#[derive(Clone, Crossover)] +struct CrossoverNamed { + a: CrossFieldA, +} + +#[test] +fn crossover_named_struct_shared_ctx() { + let mut rng = rand::rng(); + let g1 = CrossoverNamed { a: CrossFieldA }; + let g2 = CrossoverNamed { a: CrossFieldA }; + let _child = g1.crossover(&g2, &CtxA, 0.5, &mut rng); +} + +/// `create_context` with Crossover. +#[derive(Clone, Crossover)] +#[crossover(create_context(name = CrossoverCreatedCtx, derive(Clone, Debug, Default)))] +struct CrossoverWithCreatedCtx { + a: CrossFieldA, + b: CrossFieldB, +} + +#[test] +fn crossover_create_context() { + let mut rng = rand::rng(); + let ctx = CrossoverCreatedCtx::default(); + let g1 = CrossoverWithCreatedCtx { + a: CrossFieldA, + b: CrossFieldB, + }; + let g2 = CrossoverWithCreatedCtx { + a: CrossFieldA, + b: CrossFieldB, + }; + let _child = g1.crossover(&g2, &ctx, 0.5, &mut rng); +} + +/// Tuple struct with Crossover. +#[derive(Clone, Crossover)] +struct CrossoverTuple(Val); + +#[test] +fn crossover_tuple_struct() { + let mut rng = rand::rng(); + let g1 = CrossoverTuple(Val(1.0)); + let g2 = CrossoverTuple(Val(2.0)); + let child = g1.crossover(&g2, &(), 0.5, &mut rng); + assert_eq!(child.0, Val(1.5)); +} + +/// Empty struct with Crossover. +#[derive(Clone, Crossover)] +struct CrossoverEmpty {} + +#[test] +fn crossover_empty_struct() { + let mut rng = rand::rng(); + let g1 = CrossoverEmpty {}; + let g2 = CrossoverEmpty {}; + let _child = g1.crossover(&g2, &(), 0.0, &mut rng); +}