use proc_macro::TokenStream; use quote::quote; use syn::{ DeriveInput, ItemStruct, parse_macro_input, }; /// Sync strategy types #[derive(Debug, Clone, PartialEq)] enum SyncStrategy { LastWriteWins, Set, Sequence, Custom, } impl SyncStrategy { fn from_str(s: &str) -> Result { match s { | "LastWriteWins" => Ok(SyncStrategy::LastWriteWins), | "Set" => Ok(SyncStrategy::Set), | "Sequence" => Ok(SyncStrategy::Sequence), | "Custom" => Ok(SyncStrategy::Custom), | _ => Err(format!( "Unknown strategy '{}'. Choose one of: \"LastWriteWins\", \"Set\", \"Sequence\", \"Custom\"", s )), } } fn to_tokens(&self) -> proc_macro2::TokenStream { match self { | SyncStrategy::LastWriteWins => { quote! { libmarathon::networking::SyncStrategy::LastWriteWins } }, | SyncStrategy::Set => quote! { libmarathon::networking::SyncStrategy::Set }, | SyncStrategy::Sequence => quote! { libmarathon::networking::SyncStrategy::Sequence }, | SyncStrategy::Custom => quote! { libmarathon::networking::SyncStrategy::Custom }, } } } /// Parsed sync attributes struct SyncAttributes { version: u32, strategy: SyncStrategy, } impl SyncAttributes { fn parse(input: &DeriveInput) -> Result { let mut version: Option = None; let mut strategy: Option = None; // Find the #[sync(...)] attribute for attr in &input.attrs { if !attr.path().is_ident("sync") { continue; } attr.parse_nested_meta(|meta| { if meta.path.is_ident("version") { let value: syn::LitInt = meta.value()?.parse()?; version = Some(value.base10_parse()?); Ok(()) } else if meta.path.is_ident("strategy") { let value: syn::LitStr = meta.value()?.parse()?; let strategy_str = value.value(); strategy = Some( SyncStrategy::from_str(&strategy_str) .map_err(|e| syn::Error::new_spanned(&value, e))?, ); Ok(()) } else { Err(meta.error("unrecognized sync attribute")) } })?; } // Require version and strategy let version = version.ok_or_else(|| { syn::Error::new( proc_macro2::Span::call_site(), "Missing required attribute `version`\n\n \n\n = help: Add #[sync(version = 1, strategy = \"...\")] to your struct\n\n = note: See documentation: https://docs.rs/lonni/sync/strategies.html", ) })?; let strategy = strategy.ok_or_else(|| { syn::Error::new( proc_macro2::Span::call_site(), "Missing required attribute `strategy`\n\n \n\n = help: Choose one of: \"LastWriteWins\", \"Set\", \"Sequence\", \"Custom\"\n\n = help: Add #[sync(version = 1, strategy = \"LastWriteWins\")] to your struct\n\n = note: See documentation: https://docs.rs/lonni/sync/strategies.html", ) })?; Ok(SyncAttributes { version, strategy, }) } } /// RFC 0003 macro: Generate SyncComponent trait implementation /// /// # Example /// ```ignore /// use bevy::prelude::*; /// use libmarathon::networking::Synced; /// use sync_macros::Synced as SyncedDerive; /// /// #[derive(Component, Clone)] /// #[derive(Synced)] /// #[sync(version = 1, strategy = "LastWriteWins")] /// struct Health(f32); /// /// // In a Bevy system: /// fn spawn_health(mut commands: Commands) { /// commands.spawn((Health(100.0), Synced)); /// } /// ``` #[proc_macro_derive(Synced, attributes(sync))] pub fn derive_synced(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); // Parse attributes let attrs = match SyncAttributes::parse(&input) { | Ok(attrs) => attrs, | Err(e) => return TokenStream::from(e.to_compile_error()), }; let name = &input.ident; let name_str = name.to_string(); let version = attrs.version; let strategy_tokens = attrs.strategy.to_tokens(); // Generate serialization method based on type let serialize_impl = generate_serialize(&input); let deserialize_impl = generate_deserialize(&input, name); // Generate merge method based on strategy let merge_impl = generate_merge(&input, &attrs.strategy); // Extract struct attributes and visibility for re-emission let vis = &input.vis; let attrs_without_sync: Vec<_> = input .attrs .iter() .filter(|attr| !attr.path().is_ident("sync")) .collect(); let struct_token = match &input.data { | syn::Data::Struct(_) => quote! { struct }, | _ => quote! {}, }; // Re-emit the struct with rkyv derives added let rkyv_struct = match &input.data { | syn::Data::Struct(data_struct) => { let fields = &data_struct.fields; quote! { #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] #(#attrs_without_sync)* #vis #struct_token #name #fields } }, | _ => quote! {}, }; let expanded = quote! { // Re-emit struct with rkyv derives #rkyv_struct // Register component with inventory for type registry // Build type path at compile time using concat! and module_path! // since std::any::type_name() is not yet const const _: () = { const TYPE_PATH: &str = concat!(module_path!(), "::", stringify!(#name)); inventory::submit! { libmarathon::persistence::ComponentMeta { type_name: #name_str, type_path: TYPE_PATH, type_id: std::any::TypeId::of::<#name>(), deserialize_fn: |bytes: &[u8]| -> anyhow::Result> { let component: #name = rkyv::from_bytes::<#name, rkyv::rancor::Failure>(bytes)?; Ok(Box::new(component)) }, serialize_fn: |world: &bevy::ecs::world::World, entity: bevy::ecs::entity::Entity| -> Option> { world.get::<#name>(entity).and_then(|component| { rkyv::to_bytes::(component) .map(|bytes| bytes.to_vec()) .ok() }) }, insert_fn: |entity_mut: &mut bevy::ecs::world::EntityWorldMut, boxed: Box| { if let Ok(component) = boxed.downcast::<#name>() { entity_mut.insert(*component); } }, } }; }; impl libmarathon::networking::SyncComponent for #name { const VERSION: u32 = #version; const STRATEGY: libmarathon::networking::SyncStrategy = #strategy_tokens; #[inline] fn serialize_sync(&self) -> anyhow::Result> { #serialize_impl } #[inline] fn deserialize_sync(data: &[u8]) -> anyhow::Result { #deserialize_impl } #[inline] fn merge(&mut self, remote: Self, clock_cmp: libmarathon::networking::ClockComparison) -> libmarathon::networking::ComponentMergeDecision { #merge_impl } } }; TokenStream::from(expanded) } /// Generate specialized serialization code fn generate_serialize(_input: &DeriveInput) -> proc_macro2::TokenStream { // Use rkyv for zero-copy serialization // Later we can optimize for specific types (e.g., f32 -> to_le_bytes) quote! { rkyv::to_bytes::(self).map(|bytes| bytes.to_vec()).map_err(|e| anyhow::anyhow!("Serialization failed: {}", e)) } } /// Generate specialized deserialization code fn generate_deserialize(_input: &DeriveInput, _name: &syn::Ident) -> proc_macro2::TokenStream { quote! { rkyv::from_bytes::(data).map_err(|e| anyhow::anyhow!("Deserialization failed: {}", e)) } } /// Generate merge logic based on strategy fn generate_merge(input: &DeriveInput, strategy: &SyncStrategy) -> proc_macro2::TokenStream { match strategy { | SyncStrategy::LastWriteWins => generate_lww_merge(input), | SyncStrategy::Set => generate_set_merge(input), | SyncStrategy::Sequence => generate_sequence_merge(input), | SyncStrategy::Custom => generate_custom_merge(input), } } /// Generate hash calculation code for tiebreaking in concurrent merges /// /// Returns a TokenStream that computes hashes for both local and remote values /// and compares them for deterministic conflict resolution. fn generate_hash_tiebreaker() -> proc_macro2::TokenStream { quote! { let local_hash = { let bytes = rkyv::to_bytes::(self).map(|b| b.to_vec()).unwrap_or_default(); bytes.iter().fold(0u64, |acc, &b| acc.wrapping_mul(31).wrapping_add(b as u64)) }; let remote_hash = { let bytes = rkyv::to_bytes::(&remote).map(|b| b.to_vec()).unwrap_or_default(); bytes.iter().fold(0u64, |acc, &b| acc.wrapping_mul(31).wrapping_add(b as u64)) }; } } /// Generate Last-Write-Wins merge logic fn generate_lww_merge(_input: &DeriveInput) -> proc_macro2::TokenStream { let hash_tiebreaker = generate_hash_tiebreaker(); quote! { use tracing::info; match clock_cmp { libmarathon::networking::ClockComparison::RemoteNewer => { info!( component = std::any::type_name::(), ?clock_cmp, "Taking remote (newer)" ); *self = remote; libmarathon::networking::ComponentMergeDecision::TookRemote } libmarathon::networking::ClockComparison::LocalNewer => { libmarathon::networking::ComponentMergeDecision::KeptLocal } libmarathon::networking::ClockComparison::Concurrent => { // Tiebreaker: Compare serialized representations for deterministic choice // In a real implementation, we'd use node_id, but for now use a simple hash #hash_tiebreaker if remote_hash > local_hash { info!( component = std::any::type_name::(), ?clock_cmp, "Taking remote (concurrent, tiebreaker)" ); *self = remote; libmarathon::networking::ComponentMergeDecision::TookRemote } else { libmarathon::networking::ComponentMergeDecision::KeptLocal } } } } } /// Generate OR-Set merge logic /// /// For OR-Set strategy, the component must contain an OrSet field. /// We merge by calling the OrSet's merge method which implements add-wins /// semantics. fn generate_set_merge(_input: &DeriveInput) -> proc_macro2::TokenStream { let hash_tiebreaker = generate_hash_tiebreaker(); quote! { use tracing::info; // For Set strategy, we always merge the sets // The OrSet CRDT handles the conflict resolution with add-wins semantics info!( component = std::any::type_name::(), "Merging OR-Set (add-wins semantics)" ); // Assuming the component wraps an OrSet or has a field with merge() // For now, we'll do a structural merge by replacing the whole value // This is a simplified implementation - full implementation would require // the component to expose merge() method or implement it directly match clock_cmp { libmarathon::networking::ClockComparison::RemoteNewer => { *self = remote; libmarathon::networking::ComponentMergeDecision::TookRemote } libmarathon::networking::ClockComparison::LocalNewer => { libmarathon::networking::ComponentMergeDecision::KeptLocal } libmarathon::networking::ClockComparison::Concurrent => { // In a full implementation, we would merge the OrSet here // For now, use LWW with tiebreaker as fallback #hash_tiebreaker if remote_hash > local_hash { *self = remote; libmarathon::networking::ComponentMergeDecision::TookRemote } else { libmarathon::networking::ComponentMergeDecision::KeptLocal } } } } } /// Generate RGA/Sequence merge logic /// /// For Sequence strategy, the component must contain an Rga field. /// We merge by calling the Rga's merge method which maintains causal ordering. fn generate_sequence_merge(_input: &DeriveInput) -> proc_macro2::TokenStream { let hash_tiebreaker = generate_hash_tiebreaker(); quote! { use tracing::info; // For Sequence strategy, we always merge the sequences // The RGA CRDT handles the conflict resolution with causal ordering info!( component = std::any::type_name::(), "Merging RGA sequence (causal ordering)" ); // Assuming the component wraps an Rga or has a field with merge() // For now, we'll do a structural merge by replacing the whole value // This is a simplified implementation - full implementation would require // the component to expose merge() method or implement it directly match clock_cmp { libmarathon::networking::ClockComparison::RemoteNewer => { *self = remote; libmarathon::networking::ComponentMergeDecision::TookRemote } libmarathon::networking::ClockComparison::LocalNewer => { libmarathon::networking::ComponentMergeDecision::KeptLocal } libmarathon::networking::ClockComparison::Concurrent => { // In a full implementation, we would merge the Rga here // For now, use LWW with tiebreaker as fallback #hash_tiebreaker if remote_hash > local_hash { *self = remote; libmarathon::networking::ComponentMergeDecision::TookRemote } else { libmarathon::networking::ComponentMergeDecision::KeptLocal } } } } } /// Generate custom merge logic placeholder fn generate_custom_merge(input: &DeriveInput) -> proc_macro2::TokenStream { let name = &input.ident; quote! { compile_error!( concat!( "Custom strategy requires implementing ConflictResolver trait for ", stringify!(#name) ) ); libmarathon::networking::ComponentMergeDecision::KeptLocal } } /// Attribute macro for synced components /// /// This is an alternative to the derive macro that automatically adds rkyv derives. /// /// # Example /// ```ignore /// #[synced(version = 1, strategy = "LastWriteWins")] /// struct Health(f32); /// ``` #[proc_macro_attribute] pub fn synced(attr: TokenStream, item: TokenStream) -> TokenStream { let input_struct = match syn::parse::(item.clone()) { Ok(s) => s, Err(e) => { return syn::Error::new_spanned( proc_macro2::TokenStream::from(item), format!("synced attribute can only be applied to structs: {}", e), ) .to_compile_error() .into(); } }; // Parse the attribute arguments manually let attr_str = attr.to_string(); let (version, strategy) = parse_attr_string(&attr_str); // Generate the same implementations as the derive macro let name = &input_struct.ident; let name_str = name.to_string(); let strategy_tokens = strategy.to_tokens(); let vis = &input_struct.vis; let attrs = &input_struct.attrs; let generics = &input_struct.generics; let fields = &input_struct.fields; // Convert ItemStruct to DeriveInput for compatibility with existing functions // Build it manually to avoid parse_quote issues with tuple structs let derive_input = DeriveInput { attrs: attrs.clone(), vis: vis.clone(), ident: name.clone(), generics: generics.clone(), data: syn::Data::Struct(syn::DataStruct { struct_token: syn::token::Struct::default(), fields: fields.clone(), semi_token: if matches!(fields, syn::Fields::Unit) { Some(syn::token::Semi::default()) } else { None }, }), }; let serialize_impl = generate_serialize(&derive_input); let deserialize_impl = generate_deserialize(&derive_input, name); let merge_impl = generate_merge(&derive_input, &strategy); // Add semicolon for tuple/unit structs let semi = if matches!(fields, syn::Fields::Named(_)) { quote! {} } else { quote! { ; } }; let expanded = quote! { // Output the struct with rkyv derives added #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] #(#attrs)* #vis struct #name #generics #fields #semi // Register component with inventory for type registry const _: () = { const TYPE_PATH: &str = concat!(module_path!(), "::", stringify!(#name)); inventory::submit! { libmarathon::persistence::ComponentMeta { type_name: #name_str, type_path: TYPE_PATH, type_id: std::any::TypeId::of::<#name>(), deserialize_fn: |bytes: &[u8]| -> anyhow::Result> { let component: #name = rkyv::from_bytes::<#name, rkyv::rancor::Failure>(bytes)?; Ok(Box::new(component)) }, serialize_fn: |world: &bevy::ecs::world::World, entity: bevy::ecs::entity::Entity| -> Option> { world.get::<#name>(entity).and_then(|component| { rkyv::to_bytes::(component) .map(|bytes| bytes.to_vec()) .ok() }) }, insert_fn: |entity_mut: &mut bevy::ecs::world::EntityWorldMut, boxed: Box| { if let Ok(component) = boxed.downcast::<#name>() { entity_mut.insert(*component); } }, } }; }; impl libmarathon::networking::SyncComponent for #name { const VERSION: u32 = #version; const STRATEGY: libmarathon::networking::SyncStrategy = #strategy_tokens; #[inline] fn serialize_sync(&self) -> anyhow::Result> { #serialize_impl } #[inline] fn deserialize_sync(data: &[u8]) -> anyhow::Result { #deserialize_impl } #[inline] fn merge(&mut self, remote: Self, clock_cmp: libmarathon::networking::ClockComparison) -> libmarathon::networking::ComponentMergeDecision { #merge_impl } } }; TokenStream::from(expanded) } /// Parse attribute string (simple parser for version and strategy) fn parse_attr_string(attr: &str) -> (u32, SyncStrategy) { let mut version = 1; let mut strategy = SyncStrategy::LastWriteWins; // Simple parsing - look for version = N and strategy = "..." if let Some(v_pos) = attr.find("version") { if let Some(eq_pos) = attr[v_pos..].find('=') { let start = v_pos + eq_pos + 1; let rest = &attr[start..].trim(); if let Some(comma_pos) = rest.find(',') { if let Ok(v) = rest[..comma_pos].trim().parse() { version = v; } } else if let Ok(v) = rest.trim().parse() { version = v; } } } if let Some(s_pos) = attr.find("strategy") { if let Some(eq_pos) = attr[s_pos..].find('=') { let start = s_pos + eq_pos + 1; let rest = &attr[start..].trim(); if let Some(quote_start) = rest.find('"') { if let Some(quote_end) = rest[quote_start + 1..].find('"') { let strategy_str = &rest[quote_start + 1..quote_start + 1 + quote_end]; if let Ok(s) = SyncStrategy::from_str(strategy_str) { strategy = s; } } } } } (version, strategy) }