From 654128f6cdc509f1fb9298327b053c629fb8ff22 Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 22 Aug 2025 01:16:53 +0100 Subject: [PATCH 1/2] feat: add ZeroCopyMut enum support --- program-libs/zero-copy-derive/README.md | 2 +- program-libs/zero-copy-derive/src/lib.rs | 2 +- .../zero-copy-derive/src/shared/z_enum.rs | 122 +++++++-- .../src/shared/zero_copy_new.rs | 129 ++++++++++ .../zero-copy-derive/src/zero_copy.rs | 8 +- .../zero-copy-derive/src/zero_copy_mut.rs | 242 +++++++++++------- .../tests/ui/pass/09_enum_unit_variants.rs | 22 +- .../tests/ui/pass/10_enum_mixed_variants.rs | 36 ++- .../tests/ui/pass/21_enum_single_variant.rs | 20 +- .../tests/ui/pass/39_enum_with_array.rs | 29 ++- .../ui/pass/40_enum_discriminant_order.rs | 20 +- .../ui/pass/68_enum_containing_option.rs | 22 +- .../tests/ui/pass/basic_enum.rs | 34 ++- .../tests/ui/pass/complex_enum.rs | 34 ++- 14 files changed, 575 insertions(+), 147 deletions(-) diff --git a/program-libs/zero-copy-derive/README.md b/program-libs/zero-copy-derive/README.md index fe7b4eb03d..ead9045828 100644 --- a/program-libs/zero-copy-derive/README.md +++ b/program-libs/zero-copy-derive/README.md @@ -48,7 +48,7 @@ Procedural macros for borsh compatible zero copy serialization. - **Empty structs**: Not supported - structs must have at least one field for zero-copy serialization - **Enum support**: - `ZeroCopy` supports enums with unit variants or single unnamed field variants - - `ZeroCopyMut` does NOT support enums (structs only) + - `ZeroCopyMut` supports enums with unit variants or single unnamed field variants - `ZeroCopyEq` does NOT support enums (structs only) ### Special Type Handling diff --git a/program-libs/zero-copy-derive/src/lib.rs b/program-libs/zero-copy-derive/src/lib.rs index 1183f077e5..6ee88772a4 100644 --- a/program-libs/zero-copy-derive/src/lib.rs +++ b/program-libs/zero-copy-derive/src/lib.rs @@ -48,7 +48,7 @@ //! - **Empty structs**: Not supported - structs must have at least one field for zero-copy serialization //! - **Enum support**: //! - `ZeroCopy` supports enums with unit variants or single unnamed field variants -//! - `ZeroCopyMut` does NOT support enums +//! - `ZeroCopyMut` supports enums with unit variants or single unnamed field variants //! - `ZeroCopyEq` does NOT support enums //! - `ZeroCopyEq` does NOT support enums, vectors, arrays) //! diff --git a/program-libs/zero-copy-derive/src/shared/z_enum.rs b/program-libs/zero-copy-derive/src/shared/z_enum.rs index d329fd071f..109b07b736 100644 --- a/program-libs/zero-copy-derive/src/shared/z_enum.rs +++ b/program-libs/zero-copy-derive/src/shared/z_enum.rs @@ -3,7 +3,18 @@ use quote::{format_ident, quote}; use syn::{DataEnum, Fields, Ident}; /// Generate the zero-copy enum definition with type aliases for pattern matching -pub fn generate_z_enum(z_enum_name: &Ident, enum_data: &DataEnum) -> syn::Result { +/// The `MUT` parameter controls whether to generate mutable or immutable variants +pub fn generate_z_enum( + z_enum_name: &Ident, + enum_data: &DataEnum, +) -> syn::Result { + // Add Mut suffix when MUT is true + let z_enum_name = if MUT { + format_ident!("{}Mut", z_enum_name) + } else { + z_enum_name.clone() + }; + // Collect type aliases for complex variants let mut type_aliases = Vec::new(); let mut has_lifetime_dependent_variants = false; @@ -28,9 +39,21 @@ pub fn generate_z_enum(z_enum_name: &Ident, enum_data: &DataEnum) -> syn::Result has_lifetime_dependent_variants = true; // Create a type alias for this variant to enable pattern matching - let alias_name = format_ident!("{}Type", variant_name); - type_aliases.push(quote! { - pub type #alias_name<'a> = <#field_type as ::light_zero_copy::traits::ZeroCopyAt<'a>>::ZeroCopyAt; + let alias_name = if MUT { + format_ident!("{}TypeMut", variant_name) + } else { + format_ident!("{}Type", variant_name) + }; + + // Generate appropriate type based on MUT + type_aliases.push(if MUT { + quote! { + pub type #alias_name<'a> = <#field_type as ::light_zero_copy::traits::ZeroCopyAtMut<'a>>::ZeroCopyAtMut; + } + } else { + quote! { + pub type #alias_name<'a> = <#field_type as ::light_zero_copy::traits::ZeroCopyAt<'a>>::ZeroCopyAt; + } }); Ok(quote! { #variant_name(#alias_name<'a>) }) @@ -58,17 +81,24 @@ pub fn generate_z_enum(z_enum_name: &Ident, enum_data: &DataEnum) -> syn::Result } }).collect::, _>>()?; + // For mutable enums, we don't derive Clone (can't clone mutable references) + let derive_attrs = if MUT { + quote! { #[derive(Debug, PartialEq)] } + } else { + quote! { #[derive(Debug, Clone, PartialEq)] } + }; + // Conditionally add lifetime parameter only if needed let enum_declaration = if has_lifetime_dependent_variants { quote! { - #[derive(Debug, Clone, PartialEq)] + #derive_attrs pub enum #z_enum_name<'a> { #(#variants,)* } } } else { quote! { - #[derive(Debug, Clone, PartialEq)] + #derive_attrs pub enum #z_enum_name { #(#variants,)* } @@ -84,11 +114,36 @@ pub fn generate_z_enum(z_enum_name: &Ident, enum_data: &DataEnum) -> syn::Result } /// Generate the deserialize implementation for the enum -pub fn generate_enum_deserialize_impl( +/// The `MUT` parameter controls whether to generate mutable or immutable deserialization +pub fn generate_enum_deserialize_impl( original_name: &Ident, z_enum_name: &Ident, enum_data: &DataEnum, ) -> syn::Result { + // Add Mut suffix when MUT is true + let z_enum_name = if MUT { + format_ident!("{}Mut", z_enum_name) + } else { + z_enum_name.clone() + }; + + // Choose trait and method based on MUT + let (trait_name, mutability, method_name, associated_type) = if MUT { + ( + quote!(::light_zero_copy::traits::ZeroCopyAtMut), + quote!(mut), + quote!(zero_copy_at_mut), + quote!(ZeroCopyAtMut), + ) + } else { + ( + quote!(::light_zero_copy::traits::ZeroCopyAt), + quote!(), + quote!(zero_copy_at), + quote!(ZeroCopyAt), + ) + }; + // Check if any variants need lifetime parameters let mut has_lifetime_dependent_variants = false; @@ -120,10 +175,21 @@ pub fn generate_enum_deserialize_impl( "Internal error: expected exactly one unnamed field but found none" ))? .ty; + + // Use appropriate trait method based on MUT + let deserialize_call = if MUT { + quote! { + <#field_type as ::light_zero_copy::traits::ZeroCopyAtMut>::zero_copy_at_mut(remaining_data)? + } + } else { + quote! { + <#field_type as ::light_zero_copy::traits::ZeroCopyAt>::zero_copy_at(remaining_data)? + } + }; + Ok(quote! { #discriminant => { - let (value, remaining_bytes) = - <#field_type as ::light_zero_copy::traits::ZeroCopyAt>::zero_copy_at(remaining_data)?; + let (value, remaining_bytes) = #deserialize_call; Ok((#z_enum_name::#variant_name(value), remaining_bytes)) } }) @@ -148,13 +214,14 @@ pub fn generate_enum_deserialize_impl( }; Ok(quote! { - impl<'a> ::light_zero_copy::traits::ZeroCopyAt<'a> for #original_name { - type ZeroCopyAt = #type_annotation; + impl<'a> #trait_name<'a> for #original_name { + type #associated_type = #type_annotation; - fn zero_copy_at( - data: &'a [u8], - ) -> Result<(Self::ZeroCopyAt, &'a [u8]), ::light_zero_copy::errors::ZeroCopyError> { + fn #method_name( + data: &'a #mutability [u8], + ) -> Result<(Self::#associated_type, &'a #mutability [u8]), ::light_zero_copy::errors::ZeroCopyError> { // Read discriminant (first 1 byte for borsh enum) + // Note: Discriminant is ALWAYS immutable for safety, even in mutable deserialization if data.is_empty() { return Err(::light_zero_copy::errors::ZeroCopyError::ArraySize( 1, @@ -163,7 +230,7 @@ pub fn generate_enum_deserialize_impl( } let discriminant = data[0]; - let remaining_data = &data[1..]; + let remaining_data = &#mutability data[1..]; match discriminant { #(#match_arms)* @@ -175,11 +242,19 @@ pub fn generate_enum_deserialize_impl( } /// Generate the ZeroCopyStructInner implementation for the enum -pub fn generate_enum_zero_copy_struct_inner( +/// The `MUT` parameter controls whether to generate mutable or immutable struct inner trait +pub fn generate_enum_zero_copy_struct_inner( original_name: &Ident, z_enum_name: &Ident, enum_data: &DataEnum, ) -> syn::Result { + // Add Mut suffix when MUT is true + let z_enum_name = if MUT { + format_ident!("{}Mut", z_enum_name) + } else { + z_enum_name.clone() + }; + // Check if any variants need lifetime parameters let has_lifetime_dependent_variants = enum_data.variants.iter().any( |variant| matches!(&variant.fields, Fields::Unnamed(fields) if fields.unnamed.len() == 1), @@ -192,9 +267,18 @@ pub fn generate_enum_zero_copy_struct_inner( quote! { #z_enum_name } }; - Ok(quote! { - impl ::light_zero_copy::traits::ZeroCopyStructInner for #original_name { - type ZeroCopyInner = #type_annotation; + // Generate appropriate trait impl based on MUT + Ok(if MUT { + quote! { + impl ::light_zero_copy::traits::ZeroCopyStructInnerMut for #original_name { + type ZeroCopyInnerMut = #type_annotation; + } + } + } else { + quote! { + impl ::light_zero_copy::traits::ZeroCopyStructInner for #original_name { + type ZeroCopyInner = #type_annotation; + } } }) } diff --git a/program-libs/zero-copy-derive/src/shared/zero_copy_new.rs b/program-libs/zero-copy-derive/src/shared/zero_copy_new.rs index b845aa37dd..e2c5766134 100644 --- a/program-libs/zero-copy-derive/src/shared/zero_copy_new.rs +++ b/program-libs/zero-copy-derive/src/shared/zero_copy_new.rs @@ -477,3 +477,132 @@ pub fn generate_byte_len_calculation(field_type: &FieldType) -> syn::Result syn::Result { + let config_name = quote::format_ident!("{}Config", enum_name); + let z_enum_mut_name = quote::format_ident!("Z{}Mut", enum_name); + + // Generate config enum that mirrors the original enum structure + let config_variants = enum_data.variants.iter().map(|variant| { + let variant_name = &variant.ident; + match &variant.fields { + syn::Fields::Unit => Ok(quote! { #variant_name }), + syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + let field_type = &fields.unnamed.first() + .ok_or_else(|| syn::Error::new_spanned( + fields, + "Internal error: expected exactly one unnamed field but found none" + ))? + .ty; + // Get the config type for the inner field + Ok(quote! { + #variant_name(<#field_type as ::light_zero_copy::traits::ZeroCopyNew<'static>>::ZeroCopyConfig) + }) + } + _ => Err(syn::Error::new_spanned(variant, "Unsupported enum variant format for ZeroCopyMut")), + } + }).collect::, _>>()?; + + // Generate byte_len match arms + let byte_len_arms = enum_data.variants.iter().map(|variant| { + let variant_name = &variant.ident; + let discriminant_size = 1usize; // Always 1 byte for borsh + + match &variant.fields { + syn::Fields::Unit => { + Ok(quote! { + #config_name::#variant_name => Ok(#discriminant_size) + }) + } + syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + let field_type = &fields.unnamed.first() + .ok_or_else(|| syn::Error::new_spanned( + fields, + "Internal error: expected exactly one unnamed field but found none" + ))? + .ty; + Ok(quote! { + #config_name::#variant_name(ref config) => { + <#field_type as ::light_zero_copy::traits::ZeroCopyNew>::byte_len(config) + .map(|len| #discriminant_size + len) + } + }) + } + _ => Err(syn::Error::new_spanned(variant, "Unsupported enum variant format for ZeroCopyMut")), + } + }).collect::, _>>()?; + + // Generate new_zero_copy match arms + let new_arms = enum_data.variants.iter().enumerate().map(|(idx, variant)| { + let variant_name = &variant.ident; + let discriminant = idx as u8; + + match &variant.fields { + syn::Fields::Unit => { + Ok(quote! { + #config_name::#variant_name => { + bytes[0] = #discriminant; + let remaining = &mut bytes[1..]; + Ok((#z_enum_mut_name::#variant_name, remaining)) + } + }) + } + syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + let field_type = &fields.unnamed.first() + .ok_or_else(|| syn::Error::new_spanned( + fields, + "Internal error: expected exactly one unnamed field but found none" + ))? + .ty; + Ok(quote! { + #config_name::#variant_name(config) => { + bytes[0] = #discriminant; + let remaining = &mut bytes[1..]; + let (value, remaining) = + <#field_type as ::light_zero_copy::traits::ZeroCopyNew>::new_zero_copy( + remaining, config + )?; + Ok((#z_enum_mut_name::#variant_name(value), remaining)) + } + }) + } + _ => Err(syn::Error::new_spanned(variant, "Unsupported enum variant format for ZeroCopyMut")), + } + }).collect::, _>>()?; + + Ok(quote! { + // Config enum that specifies which variant to create + #[derive(Debug, Clone)] + pub enum #config_name { + #(#config_variants,)* + } + + impl<'a> ::light_zero_copy::traits::ZeroCopyNew<'a> for #enum_name { + type ZeroCopyConfig = #config_name; + type Output = >::ZeroCopyAtMut; + + fn byte_len(config: &Self::ZeroCopyConfig) -> Result { + match config { + #(#byte_len_arms,)* + } + } + + fn new_zero_copy( + bytes: &'a mut [u8], + config: Self::ZeroCopyConfig, + ) -> Result<(Self::Output, &'a mut [u8]), ::light_zero_copy::errors::ZeroCopyError> { + if bytes.is_empty() { + return Err(::light_zero_copy::errors::ZeroCopyError::ArraySize(1, bytes.len())); + } + + match config { + #(#new_arms,)* + } + } + } + }) +} diff --git a/program-libs/zero-copy-derive/src/zero_copy.rs b/program-libs/zero-copy-derive/src/zero_copy.rs index 059c9e8778..126cf51bcf 100644 --- a/program-libs/zero-copy-derive/src/zero_copy.rs +++ b/program-libs/zero-copy-derive/src/zero_copy.rs @@ -303,10 +303,12 @@ pub fn derive_zero_copy_impl(input: ProcTokenStream) -> syn::Result { let z_enum_name = z_name; - let z_enum_def = generate_z_enum(&z_enum_name, enum_data)?; - let deserialize_impl = generate_enum_deserialize_impl(name, &z_enum_name, enum_data)?; + // Use refactored const generic functions with MUT=false + let z_enum_def = generate_z_enum::(&z_enum_name, enum_data)?; + let deserialize_impl = + generate_enum_deserialize_impl::(name, &z_enum_name, enum_data)?; let zero_copy_struct_inner_impl = - generate_enum_zero_copy_struct_inner(name, &z_enum_name, enum_data)?; + generate_enum_zero_copy_struct_inner::(name, &z_enum_name, enum_data)?; Ok(quote! { #z_enum_def diff --git a/program-libs/zero-copy-derive/src/zero_copy_mut.rs b/program-libs/zero-copy-derive/src/zero_copy_mut.rs index ee7e901b90..347abd2153 100644 --- a/program-libs/zero-copy-derive/src/zero_copy_mut.rs +++ b/program-libs/zero-copy-derive/src/zero_copy_mut.rs @@ -5,6 +5,9 @@ use syn::DeriveInput; use crate::{ shared::{ meta_struct, utils, + z_enum::{ + generate_enum_deserialize_impl, generate_enum_zero_copy_struct_inner, generate_z_enum, + }, z_struct::{self, analyze_struct_fields}, zero_copy_new::{generate_config_struct, generate_init_mut_impl}, }, @@ -15,109 +18,156 @@ pub fn derive_zero_copy_mut_impl(fn_input: TokenStream) -> syn::Result(&z_struct_meta_name, &meta_fields, hasher)? - } else { - quote! {} - }; - - let z_struct_def_mut = z_struct::generate_z_struct::( - &z_struct_name, - &z_struct_meta_name, - &struct_fields, - &meta_fields, - hasher, - )?; - - let zero_copy_struct_inner_impl_mut = zero_copy::generate_zero_copy_struct_inner::( - name, - &format_ident!("{}Mut", z_struct_name), - )?; - - let deserialize_impl_mut = zero_copy::generate_deserialize_impl::( - name, - &z_struct_name, - &z_struct_meta_name, - &struct_fields, - meta_fields.is_empty(), - quote! {}, - )?; - - // Analyze only struct fields for ZeroCopyNew (meta fields are always fixed-size) - let struct_field_types = analyze_struct_fields(&struct_fields)?; - - // Always generate ZeroCopyNew, but use unit config for fixed-size types - // This follows zerocopy-derive pattern of always implementing traits - let field_strategies: Vec<_> = struct_field_types - .iter() - .map(crate::shared::zero_copy_new::analyze_field_strategy) - .collect(); - - let has_dynamic_fields = !field_strategies.iter().all(|strategy| { - matches!( - strategy, - crate::shared::zero_copy_new::FieldStrategy::FixedSize - ) - }); - - let (config_struct, init_mut_impl) = if has_dynamic_fields { - // Generate complex config struct for dynamic fields - let config = generate_config_struct(name, &struct_field_types)?; - let init_impl = generate_init_mut_impl(name, &meta_fields, &struct_fields)?; - (config, Some(init_impl)) - } else { - // Generate unit type alias for fixed-size fields - let config_name = quote::format_ident!("{}Config", name); - let unit_config = Some(quote! { - pub type #config_name = (); - }); - let init_impl = generate_init_mut_impl(name, &meta_fields, &struct_fields)?; - (unit_config, Some(init_impl)) - }; - - // Combine all mutable implementations with selective hygiene isolation - // Types must be public for trait associated types, but implementations are isolated - let expanded = quote! { - // Public types that need to be accessible from trait implementations - #config_struct - #meta_struct_def_mut - #z_struct_def_mut - - // Isolate only the implementations to prevent pollution while keeping types accessible - const _: () = { - // Import all necessary items within the isolated scope - #[allow(unused_imports)] - use ::core::{mem::size_of, ops::Deref}; - - #[allow(unused_imports)] - use ::light_zero_copy::{ - errors::ZeroCopyError, - slice_mut::ZeroCopySliceMutBorsh, + // Use generic input processing that handles both structs and enums + let (name, z_name, input_type) = utils::process_input_generic(&input)?; + + match input_type { + utils::InputType::Struct(fields) => { + // EXISTING STRUCT LOGIC + let z_struct_name = z_name; + let z_struct_meta_name = format_ident!("Z{}Meta", name); + + // Process the fields to separate meta fields and struct fields + let (meta_fields, struct_fields) = utils::process_fields(fields); + + let meta_struct_def_mut = if !meta_fields.is_empty() { + meta_struct::generate_meta_struct::( + &z_struct_meta_name, + &meta_fields, + hasher, + )? + } else { + quote! {} }; - #[allow(unused_imports)] - use ::zerocopy::{ - little_endian::{U16, U32, U64}, - FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned, + let z_struct_def_mut = z_struct::generate_z_struct::( + &z_struct_name, + &z_struct_meta_name, + &struct_fields, + &meta_fields, + hasher, + )?; + + let zero_copy_struct_inner_impl_mut = zero_copy::generate_zero_copy_struct_inner::( + name, + &format_ident!("{}Mut", z_struct_name), + )?; + + let deserialize_impl_mut = zero_copy::generate_deserialize_impl::( + name, + &z_struct_name, + &z_struct_meta_name, + &struct_fields, + meta_fields.is_empty(), + quote! {}, + )?; + + // Analyze only struct fields for ZeroCopyNew (meta fields are always fixed-size) + let struct_field_types = analyze_struct_fields(&struct_fields)?; + + // Always generate ZeroCopyNew, but use unit config for fixed-size types + // This follows zerocopy-derive pattern of always implementing traits + let field_strategies: Vec<_> = struct_field_types + .iter() + .map(crate::shared::zero_copy_new::analyze_field_strategy) + .collect(); + + let has_dynamic_fields = !field_strategies.iter().all(|strategy| { + matches!( + strategy, + crate::shared::zero_copy_new::FieldStrategy::FixedSize + ) + }); + + let (config_struct, init_mut_impl) = if has_dynamic_fields { + // Generate complex config struct for dynamic fields + let config = generate_config_struct(name, &struct_field_types)?; + let init_impl = generate_init_mut_impl(name, &meta_fields, &struct_fields)?; + (config, Some(init_impl)) + } else { + // Generate unit type alias for fixed-size fields + let config_name = quote::format_ident!("{}Config", name); + let unit_config = Some(quote! { + pub type #config_name = (); + }); + let init_impl = generate_init_mut_impl(name, &meta_fields, &struct_fields)?; + (unit_config, Some(init_impl)) }; - // Implementations are isolated to prevent helper function pollution - #zero_copy_struct_inner_impl_mut - #deserialize_impl_mut - #init_mut_impl - }; - }; + // Combine all mutable implementations with selective hygiene isolation + // Types must be public for trait associated types, but implementations are isolated + let expanded = quote! { + // Public types that need to be accessible from trait implementations + #config_struct + #meta_struct_def_mut + #z_struct_def_mut + + // Isolate only the implementations to prevent pollution while keeping types accessible + const _: () = { + // Import all necessary items within the isolated scope + #[allow(unused_imports)] + use ::core::{mem::size_of, ops::Deref}; + + #[allow(unused_imports)] + use ::light_zero_copy::{ + errors::ZeroCopyError, + slice_mut::ZeroCopySliceMutBorsh, + }; + + #[allow(unused_imports)] + use ::zerocopy::{ + little_endian::{U16, U32, U64}, + FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned, + }; + + // Implementations are isolated to prevent helper function pollution + #zero_copy_struct_inner_impl_mut + #deserialize_impl_mut + #init_mut_impl + }; + }; - Ok(expanded) + Ok(expanded) + } + utils::InputType::Enum(enum_data) => { + // NEW ENUM LOGIC - reusing const generic functions + let z_enum_name = z_name; + + // Reuse existing functions with MUT=true + let z_enum_def = generate_z_enum::(&z_enum_name, enum_data)?; + let deserialize_impl = + generate_enum_deserialize_impl::(name, &z_enum_name, enum_data)?; + let zero_copy_struct_inner = + generate_enum_zero_copy_struct_inner::(name, &z_enum_name, enum_data)?; + + // Generate ZeroCopyNew for enums + let zero_copy_new_full = + crate::shared::zero_copy_new::generate_enum_zero_copy_new(name, enum_data)?; + + Ok(quote! { + // Public types + #z_enum_def + + // ZeroCopyNew includes both config and impl - config needs to be public + #zero_copy_new_full + + // Isolated implementations + const _: () = { + #[allow(unused_imports)] + use ::light_zero_copy::{ + errors::ZeroCopyError, + traits::{ZeroCopyAtMut, ZeroCopyNew}, + }; + + #deserialize_impl + #zero_copy_struct_inner + }; + }) + } + } } diff --git a/program-libs/zero-copy-derive/tests/ui/pass/09_enum_unit_variants.rs b/program-libs/zero-copy-derive/tests/ui/pass/09_enum_unit_variants.rs index 5645d1fe7f..ec619a4da6 100644 --- a/program-libs/zero-copy-derive/tests/ui/pass/09_enum_unit_variants.rs +++ b/program-libs/zero-copy-derive/tests/ui/pass/09_enum_unit_variants.rs @@ -1,9 +1,10 @@ // Edge case: Enum with only unit variants +#![cfg(feature = "mut")] use borsh::{BorshDeserialize, BorshSerialize}; -use light_zero_copy::traits::ZeroCopyAt; -use light_zero_copy_derive::ZeroCopy; +use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut, ZeroCopyNew}; +use light_zero_copy_derive::{ZeroCopy, ZeroCopyMut}; -#[derive(Debug, ZeroCopy, BorshSerialize, BorshDeserialize)] +#[derive(Debug, ZeroCopy, ZeroCopyMut, BorshSerialize, BorshDeserialize)] #[repr(C)] pub enum UnitEnum { First, @@ -22,5 +23,18 @@ fn main() { // Note: ZeroCopyEq not supported for enums assert!(remaining.is_empty()); - // Note: ZeroCopyMut not supported for enums + // Test ZeroCopyMut - mutable deserialization + let mut bytes_mut = bytes.clone(); + let (_enum_mut, remaining) = UnitEnum::zero_copy_at_mut(&mut bytes_mut).unwrap(); + assert!(remaining.is_empty()); + + // Test ZeroCopyNew - initialization with config + let config = UnitEnumConfig::Third; + let byte_len = UnitEnum::byte_len(&config).unwrap(); + assert_eq!(bytes.len(), byte_len); + + let mut new_bytes = vec![0u8; byte_len]; + let (_enum_new, remaining) = UnitEnum::new_zero_copy(&mut new_bytes, config).unwrap(); + assert!(remaining.is_empty()); + assert_eq!(new_bytes, bytes); } diff --git a/program-libs/zero-copy-derive/tests/ui/pass/10_enum_mixed_variants.rs b/program-libs/zero-copy-derive/tests/ui/pass/10_enum_mixed_variants.rs index 2fd5b64b8f..db90c22373 100644 --- a/program-libs/zero-copy-derive/tests/ui/pass/10_enum_mixed_variants.rs +++ b/program-libs/zero-copy-derive/tests/ui/pass/10_enum_mixed_variants.rs @@ -1,9 +1,10 @@ // Edge case: Enum with mixed variant types -use light_zero_copy_derive::ZeroCopy; +#![cfg(feature = "mut")] +use light_zero_copy_derive::{ZeroCopy, ZeroCopyMut}; use borsh::{BorshSerialize, BorshDeserialize}; -use light_zero_copy::traits::ZeroCopyAt; +use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut, ZeroCopyNew}; -#[derive(Debug, ZeroCopy, BorshSerialize, BorshDeserialize)] +#[derive(Debug, ZeroCopy, ZeroCopyMut, BorshSerialize, BorshDeserialize)] #[repr(C)] pub enum MixedEnum { Empty, @@ -20,5 +21,32 @@ fn main() { // Note: ZeroCopyEq not supported for enums assert!(remaining.is_empty()); - // Note: ZeroCopyMut not supported for enums + // Test ZeroCopyMut - mutable deserialization + let mut bytes_mut = bytes.clone(); + let (mut enum_mut, remaining) = MixedEnum::zero_copy_at_mut(&mut bytes_mut).unwrap(); + assert!(remaining.is_empty()); + + // Can mutate data within existing variant (discriminant remains immutable) + match &mut enum_mut { + ZMixedEnumMut::WithData(ref mut data) => { + **data = 100u32.into(); // Modify the u32 value + } + _ => panic!("Expected WithData variant"), + } + + // Test ZeroCopyNew - initialization with config + let config = MixedEnumConfig::WithData(()); + let byte_len = MixedEnum::byte_len(&config).unwrap(); + + let mut new_bytes = vec![0u8; byte_len]; + let (mut enum_new, remaining) = MixedEnum::new_zero_copy(&mut new_bytes, config).unwrap(); + assert!(remaining.is_empty()); + + // Initialize the data field + match &mut enum_new { + ZMixedEnumMut::WithData(ref mut data) => { + **data = 42u32.into(); + } + _ => panic!("Expected WithData variant"), + } } \ No newline at end of file diff --git a/program-libs/zero-copy-derive/tests/ui/pass/21_enum_single_variant.rs b/program-libs/zero-copy-derive/tests/ui/pass/21_enum_single_variant.rs index 2d19168634..84a69a3b55 100644 --- a/program-libs/zero-copy-derive/tests/ui/pass/21_enum_single_variant.rs +++ b/program-libs/zero-copy-derive/tests/ui/pass/21_enum_single_variant.rs @@ -1,10 +1,10 @@ // Edge case: Enum with single variant #![cfg(feature = "mut")] use borsh::{BorshDeserialize, BorshSerialize}; -use light_zero_copy::traits::ZeroCopyAt; -use light_zero_copy_derive::ZeroCopy; +use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut, ZeroCopyNew}; +use light_zero_copy_derive::{ZeroCopy, ZeroCopyMut}; -#[derive(Debug, ZeroCopy, BorshSerialize, BorshDeserialize)] +#[derive(Debug, ZeroCopy, ZeroCopyMut, BorshSerialize, BorshDeserialize)] // Note: ZeroCopyEq typically not used with enums #[repr(C)] pub enum SingleVariant { @@ -19,4 +19,18 @@ fn main() { let (_enum_copy, remaining) = SingleVariant::zero_copy_at(&bytes).unwrap(); assert!(remaining.is_empty()); + // Test ZeroCopyMut - mutable deserialization + let mut bytes_mut = bytes.clone(); + let (_enum_mut, remaining) = SingleVariant::zero_copy_at_mut(&mut bytes_mut).unwrap(); + assert!(remaining.is_empty()); + + // Test ZeroCopyNew - initialization with config + let config = SingleVariantConfig::Only; + let byte_len = SingleVariant::byte_len(&config).unwrap(); + assert_eq!(bytes.len(), byte_len); + + let mut new_bytes = vec![0u8; byte_len]; + let (_enum_new, remaining) = SingleVariant::new_zero_copy(&mut new_bytes, config).unwrap(); + assert!(remaining.is_empty()); + assert_eq!(new_bytes, bytes); } diff --git a/program-libs/zero-copy-derive/tests/ui/pass/39_enum_with_array.rs b/program-libs/zero-copy-derive/tests/ui/pass/39_enum_with_array.rs index ecd2306be2..d553f71c84 100644 --- a/program-libs/zero-copy-derive/tests/ui/pass/39_enum_with_array.rs +++ b/program-libs/zero-copy-derive/tests/ui/pass/39_enum_with_array.rs @@ -1,10 +1,10 @@ // Edge case: Enum variant with array #![cfg(feature = "mut")] use borsh::{BorshDeserialize, BorshSerialize}; -use light_zero_copy::traits::ZeroCopyAt; -use light_zero_copy_derive::ZeroCopy; +use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut, ZeroCopyNew}; +use light_zero_copy_derive::{ZeroCopy, ZeroCopyMut}; -#[derive(Debug, ZeroCopy, BorshSerialize, BorshDeserialize)] +#[derive(Debug, ZeroCopy, ZeroCopyMut, BorshSerialize, BorshDeserialize)] // Note: ZeroCopyEq not supported for enums #[repr(C)] pub enum EnumWithArray { @@ -21,4 +21,27 @@ fn main() { // Note: Can't use assert_eq! due to ZeroCopyEq limitation for enums assert!(remaining.is_empty()); + // Test ZeroCopyMut - mutable deserialization + let mut bytes_mut = bytes.clone(); + let (_enum_mut, remaining) = EnumWithArray::zero_copy_at_mut(&mut bytes_mut).unwrap(); + assert!(remaining.is_empty()); + + // Test ZeroCopyNew - initialization with config + let config = EnumWithArrayConfig::WithArray(()); + let byte_len = EnumWithArray::byte_len(&config).unwrap(); + + let mut new_bytes = vec![0u8; byte_len]; + let (mut enum_new, remaining) = EnumWithArray::new_zero_copy(&mut new_bytes, config).unwrap(); + assert!(remaining.is_empty()); + + // Initialize the array data + match &mut enum_new { + ZEnumWithArrayMut::WithArray(ref mut array_data) => { + // Set array values + for i in 0..32 { + array_data[i] = 42; + } + } + _ => panic!("Expected WithArray variant"), + } } diff --git a/program-libs/zero-copy-derive/tests/ui/pass/40_enum_discriminant_order.rs b/program-libs/zero-copy-derive/tests/ui/pass/40_enum_discriminant_order.rs index 2e281e0f58..2b0905252d 100644 --- a/program-libs/zero-copy-derive/tests/ui/pass/40_enum_discriminant_order.rs +++ b/program-libs/zero-copy-derive/tests/ui/pass/40_enum_discriminant_order.rs @@ -1,10 +1,10 @@ // Edge case: Many enum variants (testing discriminant handling) #![cfg(feature = "mut")] use borsh::{BorshDeserialize, BorshSerialize}; -use light_zero_copy::traits::ZeroCopyAt; -use light_zero_copy_derive::ZeroCopy; +use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut, ZeroCopyNew}; +use light_zero_copy_derive::{ZeroCopy, ZeroCopyMut}; -#[derive(Debug, ZeroCopy, BorshSerialize, BorshDeserialize)] +#[derive(Debug, ZeroCopy, ZeroCopyMut, BorshSerialize, BorshDeserialize)] // Note: ZeroCopyEq not supported for enums #[repr(C)] pub enum ManyVariants { @@ -29,4 +29,18 @@ fn main() { // Note: Can't use assert_eq! due to ZeroCopyEq limitation for enums assert!(remaining.is_empty()); + // Test ZeroCopyMut - mutable deserialization + let mut bytes_mut = bytes.clone(); + let (_enum_mut, remaining) = ManyVariants::zero_copy_at_mut(&mut bytes_mut).unwrap(); + assert!(remaining.is_empty()); + + // Test ZeroCopyNew - initialization with config + let config = ManyVariantsConfig::V5; + let byte_len = ManyVariants::byte_len(&config).unwrap(); + assert_eq!(bytes.len(), byte_len); + + let mut new_bytes = vec![0u8; byte_len]; + let (_enum_new, remaining) = ManyVariants::new_zero_copy(&mut new_bytes, config).unwrap(); + assert!(remaining.is_empty()); + assert_eq!(new_bytes, bytes); } diff --git a/program-libs/zero-copy-derive/tests/ui/pass/68_enum_containing_option.rs b/program-libs/zero-copy-derive/tests/ui/pass/68_enum_containing_option.rs index 34ff60a8c7..656b75318d 100644 --- a/program-libs/zero-copy-derive/tests/ui/pass/68_enum_containing_option.rs +++ b/program-libs/zero-copy-derive/tests/ui/pass/68_enum_containing_option.rs @@ -1,10 +1,10 @@ // Edge case: Enum containing Option #![cfg(feature = "mut")] use borsh::{BorshDeserialize, BorshSerialize}; -use light_zero_copy::traits::ZeroCopyAt; -use light_zero_copy_derive::ZeroCopy; +use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut, ZeroCopyNew}; +use light_zero_copy_derive::{ZeroCopy, ZeroCopyMut}; -#[derive(Debug, ZeroCopy, BorshSerialize, BorshDeserialize)] +#[derive(Debug, ZeroCopy, ZeroCopyMut, BorshSerialize, BorshDeserialize)] #[repr(C)] pub enum EnumWithOption { Empty, @@ -19,7 +19,21 @@ fn main() { let serialized = original.try_to_vec().unwrap(); // Test zero_copy_at (read-only) - let (zero_copy_read, _remaining) = EnumWithOption::zero_copy_at(&serialized).unwrap(); + let (_zero_copy_read, remaining) = EnumWithOption::zero_copy_at(&serialized).unwrap(); + assert!(remaining.is_empty()); + + // Test ZeroCopyMut - mutable deserialization + let mut bytes_mut = serialized.clone(); + let (_enum_mut, remaining) = EnumWithOption::zero_copy_at_mut(&mut bytes_mut).unwrap(); + assert!(remaining.is_empty()); + + // Test ZeroCopyNew - initialization with config + let config = EnumWithOptionConfig::MaybeData((true, ())); + let byte_len = EnumWithOption::byte_len(&config).unwrap(); + + let mut new_bytes = vec![0u8; byte_len]; + let (_enum_new, remaining) = EnumWithOption::new_zero_copy(&mut new_bytes, config).unwrap(); + assert!(remaining.is_empty()); // Note: Cannot use assert_eq! as enums don't implement ZeroCopyEq println!("Borsh compatibility test passed for EnumWithOption"); diff --git a/program-libs/zero-copy-derive/tests/ui/pass/basic_enum.rs b/program-libs/zero-copy-derive/tests/ui/pass/basic_enum.rs index a2fa471a52..c666331781 100644 --- a/program-libs/zero-copy-derive/tests/ui/pass/basic_enum.rs +++ b/program-libs/zero-copy-derive/tests/ui/pass/basic_enum.rs @@ -1,9 +1,9 @@ #![cfg(feature = "mut")] use borsh::{BorshDeserialize, BorshSerialize}; -use light_zero_copy::traits::ZeroCopyAt; -use light_zero_copy_derive::ZeroCopy; +use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut, ZeroCopyNew}; +use light_zero_copy_derive::{ZeroCopy, ZeroCopyMut}; -#[derive(Debug, ZeroCopy, BorshSerialize, BorshDeserialize)] +#[derive(Debug, ZeroCopy, ZeroCopyMut, BorshSerialize, BorshDeserialize)] // Note: ZeroCopyEq typically not used with enums #[repr(C)] pub enum BasicEnum { @@ -20,4 +20,32 @@ fn main() { let (_enum_copy, remaining) = BasicEnum::zero_copy_at(&bytes).unwrap(); assert!(remaining.is_empty()); + // Test ZeroCopyMut - mutable deserialization + let mut bytes_mut = bytes.clone(); + let (mut enum_mut, remaining) = BasicEnum::zero_copy_at_mut(&mut bytes_mut).unwrap(); + assert!(remaining.is_empty()); + + // Can mutate data within existing variant (discriminant remains immutable) + match &mut enum_mut { + ZBasicEnumMut::SingleField(ref mut data) => { + **data = 100u32.into(); // Modify the u32 value + } + _ => panic!("Expected SingleField variant"), + } + + // Test ZeroCopyNew - initialization with config + let config = BasicEnumConfig::SingleField(()); + let byte_len = BasicEnum::byte_len(&config).unwrap(); + + let mut new_bytes = vec![0u8; byte_len]; + let (mut enum_new, remaining) = BasicEnum::new_zero_copy(&mut new_bytes, config).unwrap(); + assert!(remaining.is_empty()); + + // Initialize the data field + match &mut enum_new { + ZBasicEnumMut::SingleField(ref mut data) => { + **data = 42u32.into(); + } + _ => panic!("Expected SingleField variant"), + } } \ No newline at end of file diff --git a/program-libs/zero-copy-derive/tests/ui/pass/complex_enum.rs b/program-libs/zero-copy-derive/tests/ui/pass/complex_enum.rs index 32920bb9f4..53c8a3d562 100644 --- a/program-libs/zero-copy-derive/tests/ui/pass/complex_enum.rs +++ b/program-libs/zero-copy-derive/tests/ui/pass/complex_enum.rs @@ -1,9 +1,9 @@ #![cfg(feature = "mut")] use borsh::{BorshDeserialize, BorshSerialize}; -use light_zero_copy::traits::ZeroCopyAt; -use light_zero_copy_derive::ZeroCopy; +use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut, ZeroCopyNew}; +use light_zero_copy_derive::{ZeroCopy, ZeroCopyMut}; -#[derive(Debug, ZeroCopy, BorshSerialize, BorshDeserialize)] +#[derive(Debug, ZeroCopy, ZeroCopyMut, BorshSerialize, BorshDeserialize)] // Note: ZeroCopyEq typically not used with enums #[repr(C)] pub enum ComplexEnum { @@ -21,4 +21,32 @@ fn main() { let (_enum_copy, remaining) = ComplexEnum::zero_copy_at(&bytes).unwrap(); assert!(remaining.is_empty()); + // Test ZeroCopyMut - mutable deserialization + let mut bytes_mut = bytes.clone(); + let (mut enum_mut, remaining) = ComplexEnum::zero_copy_at_mut(&mut bytes_mut).unwrap(); + assert!(remaining.is_empty()); + + // Can mutate data within existing variant (discriminant remains immutable) + match &mut enum_mut { + ZComplexEnumMut::U64Field(ref mut data) => { + **data = 99999u64.into(); // Modify the u64 value + } + _ => panic!("Expected U64Field variant"), + } + + // Test ZeroCopyNew - initialization with config + let config = ComplexEnumConfig::U64Field(()); + let byte_len = ComplexEnum::byte_len(&config).unwrap(); + + let mut new_bytes = vec![0u8; byte_len]; + let (mut enum_new, remaining) = ComplexEnum::new_zero_copy(&mut new_bytes, config).unwrap(); + assert!(remaining.is_empty()); + + // Initialize the data field + match &mut enum_new { + ZComplexEnumMut::U64Field(ref mut data) => { + **data = 12345u64.into(); + } + _ => panic!("Expected U64Field variant"), + } } \ No newline at end of file From c57550f77987ef97d74e542e0b9b28e45106d529 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 23 Aug 2025 20:52:50 +0100 Subject: [PATCH 2/2] stash --- .../tests/ui/pass/10_enum_mixed_variants.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/program-libs/zero-copy-derive/tests/ui/pass/10_enum_mixed_variants.rs b/program-libs/zero-copy-derive/tests/ui/pass/10_enum_mixed_variants.rs index db90c22373..7605ceabdd 100644 --- a/program-libs/zero-copy-derive/tests/ui/pass/10_enum_mixed_variants.rs +++ b/program-libs/zero-copy-derive/tests/ui/pass/10_enum_mixed_variants.rs @@ -1,8 +1,8 @@ // Edge case: Enum with mixed variant types #![cfg(feature = "mut")] -use light_zero_copy_derive::{ZeroCopy, ZeroCopyMut}; -use borsh::{BorshSerialize, BorshDeserialize}; +use borsh::{BorshDeserialize, BorshSerialize}; use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut, ZeroCopyNew}; +use light_zero_copy_derive::{ZeroCopy, ZeroCopyMut}; #[derive(Debug, ZeroCopy, ZeroCopyMut, BorshSerialize, BorshDeserialize)] #[repr(C)] @@ -20,12 +20,12 @@ fn main() { let (_enum_copy, remaining) = MixedEnum::zero_copy_at(&bytes).unwrap(); // Note: ZeroCopyEq not supported for enums assert!(remaining.is_empty()); - + // Test ZeroCopyMut - mutable deserialization let mut bytes_mut = bytes.clone(); let (mut enum_mut, remaining) = MixedEnum::zero_copy_at_mut(&mut bytes_mut).unwrap(); assert!(remaining.is_empty()); - + // Can mutate data within existing variant (discriminant remains immutable) match &mut enum_mut { ZMixedEnumMut::WithData(ref mut data) => { @@ -37,11 +37,11 @@ fn main() { // Test ZeroCopyNew - initialization with config let config = MixedEnumConfig::WithData(()); let byte_len = MixedEnum::byte_len(&config).unwrap(); - + let mut new_bytes = vec![0u8; byte_len]; let (mut enum_new, remaining) = MixedEnum::new_zero_copy(&mut new_bytes, config).unwrap(); assert!(remaining.is_empty()); - + // Initialize the data field match &mut enum_new { ZMixedEnumMut::WithData(ref mut data) => { @@ -49,4 +49,4 @@ fn main() { } _ => panic!("Expected WithData variant"), } -} \ No newline at end of file +}