Skip to main content

tempo_precompiles_macros/
storable.rs

1//! Implementation of the `#[derive(Storable)]` macro.
2
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote};
5use syn::{Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Ident, Type};
6
7use crate::{
8    FieldInfo,
9    layout::{gen_handler_field_decl, gen_handler_field_init},
10    packing::{self, LayoutField, PackingConstants},
11    storable_primitives::gen_struct_arrays,
12    utils::{extract_mapping_types, extract_storable_array_sizes, to_snake_case},
13};
14
15/// Implements the `Storable` derive macro for structs and `#[repr(u8)]` unit enums.
16///
17/// Packs fields into storage slots based on their byte sizes.
18/// Fields are placed sequentially in slots, moving to a new slot when
19/// the current slot cannot fit the next field (no spanning across slots).
20pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
21    match &input.data {
22        Data::Struct(data_struct) => derive_struct_impl(&input, data_struct),
23        Data::Enum(data_enum) => derive_unit_enum_impl(&input, data_enum),
24        _ => Err(syn::Error::new_spanned(
25            &input.ident,
26            "`Storable` can only be derived for structs with named fields or unit enums",
27        )),
28    }
29}
30
31fn derive_struct_impl(input: &DeriveInput, data_struct: &DataStruct) -> syn::Result<TokenStream> {
32    // Extract struct name, generics
33    let strukt = &input.ident;
34    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
35
36    // Parse struct fields
37    let fields = match &data_struct.fields {
38        Fields::Named(fields_named) => &fields_named.named,
39        _ => {
40            return Err(syn::Error::new_spanned(
41                &input.ident,
42                "`Storable` can only be derived for structs with named fields",
43            ));
44        }
45    };
46
47    if fields.is_empty() {
48        return Err(syn::Error::new_spanned(
49            &input.ident,
50            "`Storable` cannot be derived for empty structs",
51        ));
52    }
53
54    // Extract field names and types into `FieldInfo` structs
55    let field_infos: Vec<_> = fields
56        .iter()
57        .map(|f| FieldInfo {
58            name: f.ident.as_ref().unwrap().clone(),
59            ty: f.ty.clone(),
60            slot: None,
61            base_slot: None,
62        })
63        .collect();
64
65    // Build layout IR using the unified function
66    let layout_fields = packing::allocate_slots(&field_infos)?;
67
68    // Generate helper module with packing layout calculations
69    let mod_ident = format_ident!("__packing_{}", to_snake_case(&strukt.to_string()));
70    let packing_module = gen_packing_module_from_ir(&layout_fields, &mod_ident);
71
72    // Classify fields: direct (storable) vs indirect (mappings)
73    let len = fields.len();
74    let (direct_fields, direct_names, mapping_names) = field_infos.iter().fold(
75        (Vec::with_capacity(len), Vec::with_capacity(len), Vec::new()),
76        |mut out, field_info| {
77            if extract_mapping_types(&field_info.ty).is_none() {
78                // fields with direct slot allocation
79                out.0.push((&field_info.name, &field_info.ty));
80                out.1.push(&field_info.name);
81            } else {
82                // fields with indirect slot allocation (mappings)
83                out.2.push(&field_info.name);
84            }
85            out
86        },
87    );
88
89    // Extract just the types for IS_DYNAMIC calculation
90    let direct_tys: Vec<_> = direct_fields.iter().map(|(_, ty)| *ty).collect();
91
92    // Generate load/store/delete implementations for scalar fields only
93    let load_impl = gen_load_impl(&direct_fields, &mod_ident);
94    let store_impl = gen_store_impl(&direct_fields, &mod_ident);
95    let delete_impl = gen_delete_impl(&direct_fields, &mod_ident);
96
97    // Generate handler struct for field access
98    let handler_struct = gen_handler_struct(strukt, &layout_fields, &mod_ident);
99    let handler_name = format_ident!("{}Handler", strukt);
100
101    let expanded = quote! {
102        #packing_module
103        #handler_struct
104
105        // impl `StorableType` for layout information
106        impl #impl_generics crate::storage::StorableType for #strukt #ty_generics #where_clause {
107            // Structs cannot be packed, so they must take full slots
108            const LAYOUT: crate::storage::Layout = crate::storage::Layout::Slots(#mod_ident::SLOT_COUNT);
109
110            // A struct is dynamic if any of its fields is dynamic
111            const IS_DYNAMIC: bool = #(
112                <#direct_tys as crate::storage::StorableType>::IS_DYNAMIC
113            )||*;
114
115            type Handler = #handler_name;
116
117            fn handle(slot: ::alloy::primitives::U256, _ctx: crate::storage::LayoutCtx, address: ::alloy::primitives::Address) -> Self::Handler {
118                #handler_name::new(slot, address)
119            }
120        }
121
122        // `Storable` implementation: storage I/O with full logic
123        impl #impl_generics crate::storage::Storable for #strukt #ty_generics #where_clause {
124            fn load<S: crate::storage::StorageOps>(
125                storage: &S,
126                base_slot: ::alloy::primitives::U256,
127                ctx: crate::storage::LayoutCtx
128            ) -> crate::error::Result<Self> {
129                use crate::storage::Storable;
130                debug_assert_eq!(ctx, crate::storage::LayoutCtx::FULL, "Struct types can only be loaded with LayoutCtx::FULL");
131
132                #load_impl
133
134                Ok(Self {
135                    #(#direct_names),*,
136                    #(#mapping_names: Default::default()),*
137                })
138            }
139
140            fn store<S: crate::storage::StorageOps>(
141                &self,
142                storage: &mut S,
143                base_slot: ::alloy::primitives::U256,
144                ctx: crate::storage::LayoutCtx
145            ) -> crate::error::Result<()> {
146                use crate::storage::Storable;
147                debug_assert_eq!(ctx, crate::storage::LayoutCtx::FULL, "Struct types can only be stored with LayoutCtx::FULL");
148
149                #store_impl
150
151                Ok(())
152            }
153
154            fn delete<S: crate::storage::StorageOps>(
155                storage: &mut S,
156                base_slot: ::alloy::primitives::U256,
157                ctx: crate::storage::LayoutCtx
158            ) -> crate::error::Result<()> {
159                use crate::storage::Storable;
160                debug_assert_eq!(ctx, crate::storage::LayoutCtx::FULL, "Struct types can only be deleted with LayoutCtx::FULL");
161
162                #delete_impl
163
164                Ok(())
165            }
166        }
167    };
168
169    // Generate array implementations if requested
170    let array_impls = if let Some(sizes) = extract_storable_array_sizes(&input.attrs)? {
171        // Generate the struct type path for array generation
172        let struct_type = quote! { #strukt #ty_generics };
173        gen_struct_arrays(struct_type, &sizes)
174    } else {
175        quote! {}
176    };
177
178    // Combine struct implementation with array implementations
179    let combined = quote! {
180        #expanded
181        #array_impls
182    };
183
184    Ok(combined)
185}
186
187fn derive_unit_enum_impl(input: &DeriveInput, data_enum: &DataEnum) -> syn::Result<TokenStream> {
188    if extract_storable_array_sizes(&input.attrs)?.is_some() {
189        return Err(syn::Error::new_spanned(
190            &input.ident,
191            "`storable_arrays` is only supported for structs",
192        ));
193    }
194
195    if !has_repr_u8(&input.attrs)? {
196        return Err(syn::Error::new_spanned(
197            &input.ident,
198            "`Storable` unit enums must be annotated with `#[repr(u8)]`",
199        ));
200    }
201
202    if data_enum.variants.is_empty() {
203        return Err(syn::Error::new_spanned(
204            &input.ident,
205            "`Storable` cannot be derived for empty enums",
206        ));
207    }
208
209    for variant in &data_enum.variants {
210        if !matches!(variant.fields, Fields::Unit) {
211            return Err(syn::Error::new_spanned(
212                variant,
213                "`Storable` enums must use unit variants only",
214            ));
215        }
216    }
217
218    validate_sequential_discriminants(data_enum)?;
219
220    let enum_name = &input.ident;
221    let variant_names: Vec<_> = data_enum
222        .variants
223        .iter()
224        .map(|variant| &variant.ident)
225        .collect();
226    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
227
228    Ok(quote! {
229        impl #impl_generics crate::storage::StorableType for #enum_name #ty_generics #where_clause {
230            const LAYOUT: crate::storage::Layout = crate::storage::Layout::Bytes(1);
231
232            type Handler = crate::storage::Slot<Self>;
233
234            fn handle(slot: ::alloy::primitives::U256, ctx: crate::storage::LayoutCtx, address: ::alloy::primitives::Address) -> Self::Handler {
235                crate::storage::Slot::new_with_ctx(slot, ctx, address)
236            }
237        }
238
239        impl #impl_generics crate::storage::Storable for #enum_name #ty_generics #where_clause {
240            #[inline]
241            fn load<S: crate::storage::StorageOps>(
242                storage: &S,
243                slot: ::alloy::primitives::U256,
244                ctx: crate::storage::LayoutCtx
245            ) -> crate::error::Result<Self> {
246                let value = <u8 as crate::storage::Storable>::load(storage, slot, ctx)?;
247                match value {
248                    #(discriminant if discriminant == Self::#variant_names as u8 => Ok(Self::#variant_names),)*
249                    _ => Err(crate::error::TempoPrecompileError::enum_conversion_error()),
250                }
251            }
252
253            #[inline]
254            fn store<S: crate::storage::StorageOps>(
255                &self,
256                storage: &mut S,
257                slot: ::alloy::primitives::U256,
258                ctx: crate::storage::LayoutCtx
259            ) -> crate::error::Result<()> {
260                let value = match self {
261                    #(Self::#variant_names => Self::#variant_names as u8,)*
262                };
263
264                <u8 as crate::storage::Storable>::store(&value, storage, slot, ctx)
265            }
266        }
267    })
268}
269
270fn has_repr_u8(attrs: &[Attribute]) -> syn::Result<bool> {
271    let mut repr_u8 = false;
272
273    for attr in attrs {
274        if !attr.path().is_ident("repr") {
275            continue;
276        }
277
278        attr.parse_nested_meta(|meta| {
279            if meta.path.is_ident("u8") {
280                repr_u8 = true;
281            }
282            Ok(())
283        })?;
284    }
285
286    Ok(repr_u8)
287}
288
289fn validate_sequential_discriminants(data_enum: &DataEnum) -> syn::Result<()> {
290    if data_enum.variants.len() > usize::from(u8::MAX) + 1 {
291        return Err(syn::Error::new_spanned(
292            &data_enum.variants,
293            "`Storable` unit enums must have at most 256 variants to fit in `u8`",
294        ));
295    }
296
297    for variant in &data_enum.variants {
298        if variant.discriminant.is_some() {
299            return Err(syn::Error::new_spanned(
300                variant,
301                "`Storable` unit enums must not use explicit discriminants; \
302                 variants are assigned sequential values starting from 0, matching Solidity enum semantics",
303            ));
304        }
305    }
306
307    Ok(())
308}
309
310/// Generate a compile-time module that calculates the packing layout from IR.
311fn gen_packing_module_from_ir(fields: &[LayoutField<'_>], mod_ident: &Ident) -> TokenStream {
312    // Generate constants using the unified IR-based function (generates <FIELD>: U256)
313    let last_field = &fields[fields.len() - 1];
314    let last_slot_const = PackingConstants::new(last_field.name).slot();
315    let packing_constants = packing::gen_constants_from_ir(fields, true);
316    let last_type = &last_field.ty;
317
318    quote! {
319        pub mod #mod_ident {
320            use super::*;
321
322            #packing_constants
323            pub const SLOT_COUNT: usize = (#last_slot_const.saturating_add(
324                ::alloy::primitives::U256::from_limbs([<#last_type as crate::storage::StorableType>::SLOTS as u64, 0, 0, 0])
325            )).as_limbs()[0] as usize;
326        }
327    }
328}
329
330/// Generate a handler struct for the storable type.
331///
332/// The handler provides type-safe access to both the full struct and individual fields.
333fn gen_handler_struct(
334    struct_name: &Ident,
335    fields: &[LayoutField<'_>],
336    mod_ident: &Ident,
337) -> TokenStream {
338    let handler_name = format_ident!("{}Handler", struct_name);
339
340    // Generate public handler fields
341    let handler_fields = fields.iter().map(gen_handler_field_decl);
342
343    // Generate field initializations for constructor using the shared helper
344    let field_inits = fields
345        .iter()
346        .enumerate()
347        .map(|(idx, field)| gen_handler_field_init(field, idx, fields, Some(mod_ident)));
348
349    quote! {
350        /// Type-safe handler for accessing `#struct_name` in storage.
351        ///
352        /// Provides individual field access via public fields and whole-struct operations.
353        #[derive(Debug, Clone)]
354        pub struct #handler_name {
355            address: ::alloy::primitives::Address,
356            base_slot: ::alloy::primitives::U256,
357            #(#handler_fields,)*
358        }
359
360        impl #handler_name {
361            /// Creates a new handler for the struct at the given base slot.
362            #[inline]
363            pub fn new(base_slot: ::alloy::primitives::U256, address: ::alloy::primitives::Address) -> Self {
364                Self {
365                    base_slot,
366                    #(#field_inits,)*
367                    address,
368                }
369            }
370
371            /// Returns the base storage slot where this struct's data is stored.
372            ///
373            /// Single-slot structs pack all fields into this slot.
374            /// Multi-slot structs use consecutive slots starting from this base.
375            #[inline]
376            pub fn base_slot(&self) -> ::alloy::primitives::U256 {
377                self.base_slot
378            }
379
380            /// Returns a `Slot<T>` for whole-struct storage operations.
381            #[inline]
382            fn as_slot(&self) -> crate::storage::Slot<#struct_name> {
383                crate::storage::Slot::<#struct_name>::new(
384                    self.base_slot,
385                    self.address
386                )
387            }
388        }
389
390        impl crate::storage::Handler<#struct_name> for #handler_name {
391            #[inline]
392            fn read(&self) -> crate::error::Result<#struct_name> {
393                self.as_slot().read()
394            }
395
396            #[inline]
397            fn write(&mut self, value: #struct_name) -> crate::error::Result<()> {
398                self.as_slot().write(value)
399            }
400
401            #[inline]
402            fn delete(&mut self) -> crate::error::Result<()> {
403                self.as_slot().delete()
404            }
405
406            /// Reads the struct from transient storage.
407            #[inline]
408            fn t_read(&self) -> crate::error::Result<#struct_name> {
409                self.as_slot().t_read()
410            }
411
412            /// Writes the struct to transient storage.
413            #[inline]
414            fn t_write(&mut self, value: #struct_name) -> crate::error::Result<()> {
415                self.as_slot().t_write(value)
416            }
417
418            /// Deletes the struct from transient storage.
419            #[inline]
420            fn t_delete(&mut self) -> crate::error::Result<()> {
421                self.as_slot().t_delete()
422            }
423        }
424    }
425}
426
427/// Generate `fn load()` implementation.
428///
429/// For consecutive packable fields sharing a slot, loads the slot once and extracts
430/// all fields via `PackedSlot`, avoiding redundant SLOADs.
431fn gen_load_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
432    if fields.is_empty() {
433        return quote! {};
434    }
435
436    let field_loads = fields.iter().enumerate().map(|(idx, (name, ty))| {
437        let loc_const = PackingConstants::new(name).location();
438
439        let (prev_slot_ref, _) =
440            packing::get_neighbor_slot_refs(idx, fields, packing, |(name, _)| name, false);
441
442        let slot_addr = quote! { base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots) };
443        let packed_ctx = quote! { crate::storage::LayoutCtx::packed(#packing::#loc_const.offset_bytes) };
444
445        if let Some(prev_slot_ref) = prev_slot_ref {
446            quote! {
447                let #name = {
448                    let curr_offset = #packing::#loc_const.offset_slots;
449                    let prev_offset = #prev_slot_ref;
450
451                    if <#ty as crate::storage::StorableType>::IS_PACKABLE && curr_offset == prev_offset {
452                        // Same slot as previous packable field - reuse cached value
453                        let packed = crate::storage::packing::PackedSlot(cached_slot);
454                        <#ty as crate::storage::Storable>::load(&packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?
455                    } else if <#ty as crate::storage::StorableType>::IS_PACKABLE {
456                        // New slot, but packable - load and cache for potential reuse
457                        cached_slot = storage.load(#slot_addr)?;
458                        let packed = crate::storage::packing::PackedSlot(cached_slot);
459                        <#ty as crate::storage::Storable>::load(&packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?
460                    } else {
461                        // Non-packable - direct load
462                        <#ty as crate::storage::Storable>::load(storage, #slot_addr, crate::storage::LayoutCtx::FULL)?
463                    }
464                };
465            }
466        } else {
467            // First field
468            quote! {
469                let #name = if <#ty as crate::storage::StorableType>::IS_PACKABLE {
470                    cached_slot = storage.load(#slot_addr)?;
471                    let packed = crate::storage::packing::PackedSlot(cached_slot);
472                    <#ty as crate::storage::Storable>::load(&packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?
473                } else {
474                    <#ty as crate::storage::Storable>::load(storage, #slot_addr, crate::storage::LayoutCtx::FULL)?
475                };
476            }
477        }
478    });
479
480    quote! {
481        let mut cached_slot = ::alloy::primitives::U256::ZERO;
482        #(#field_loads)*
483    }
484}
485
486/// Generate `fn store()` implementation.
487///
488/// For consecutive packable fields sharing a slot, accumulates changes in memory
489/// and writes once, avoiding redundant SLOAD + SSTORE pairs.
490fn gen_store_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
491    if fields.is_empty() {
492        return quote! {};
493    }
494
495    let field_stores = fields.iter().enumerate().map(|(idx, (name, ty))| {
496        let loc_const = PackingConstants::new(name).location();
497        let next_ty = fields.get(idx + 1).map(|(_, ty)| *ty);
498
499        let (prev_slot_ref, next_slot_ref) =
500            packing::get_neighbor_slot_refs(idx, fields, packing, |(name, _)| name, false);
501
502        let slot_addr = quote! { base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots) };
503        let packed_ctx = quote! { crate::storage::LayoutCtx::packed(#packing::#loc_const.offset_bytes) };
504
505        // Determine if we need to store after this field
506        let should_store = match (&next_slot_ref, next_ty) {
507            (Some(next_slot), Some(next_ty)) => {
508                // Store if next field is in different slot OR next field is not packable
509                quote! {
510                    #packing::#loc_const.offset_slots != #next_slot
511                        || !<#next_ty as crate::storage::StorableType>::IS_PACKABLE
512                }
513            }
514            _ => quote! { true }, // Always store last field
515        };
516
517        if let Some(prev_slot_ref) = prev_slot_ref {
518            quote! {{
519                let curr_offset = #packing::#loc_const.offset_slots;
520                let prev_offset = #prev_slot_ref;
521
522                if <#ty as crate::storage::StorableType>::IS_PACKABLE && curr_offset == prev_offset {
523                    // Same slot as previous packable field - accumulate in pending slot
524                    let mut packed = crate::storage::packing::PackedSlot(pending_val);
525                    <#ty as crate::storage::Storable>::store(&self.#name, &mut packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?;
526                    pending_val = packed.0;
527                } else if <#ty as crate::storage::StorableType>::IS_PACKABLE {
528                    // New slot, but packable - commit previous and start new batch
529                    if let Some(offset) = pending_offset {
530                        storage.store(base_slot + ::alloy::primitives::U256::from(offset), pending_val)?;
531                    }
532                    pending_val = if crate::storage::StorageCtx.spec().is_t4() {
533                        // This slot group is exclusively owned by the struct and all
534                        // declared packed fields are written before commit, so previous
535                        // contents are irrelevant; zero-init only clears unowned padding.
536                        ::alloy::primitives::U256::ZERO
537                    } else {
538                        storage.load(#slot_addr)?
539                    };
540                    pending_offset = Some(curr_offset);
541                    let mut packed = crate::storage::packing::PackedSlot(pending_val);
542                    <#ty as crate::storage::Storable>::store(&self.#name, &mut packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?;
543                    pending_val = packed.0;
544                } else {
545                    // Non-packable - commit pending and do direct store
546                    if let Some(offset) = pending_offset {
547                        storage.store(base_slot + ::alloy::primitives::U256::from(offset), pending_val)?;
548                        pending_offset = None;
549                    }
550                    <#ty as crate::storage::Storable>::store(&self.#name, storage, #slot_addr, crate::storage::LayoutCtx::FULL)?;
551                }
552
553                // Store if this is the last field in the current slot group
554                if let Some(offset) = pending_offset && (#should_store) {
555                    storage.store(base_slot + ::alloy::primitives::U256::from(offset), pending_val)?;
556                    pending_offset = None;
557                }
558            }}
559        } else {
560            // First field
561            quote! {{
562                if <#ty as crate::storage::StorableType>::IS_PACKABLE {
563                    pending_val = if crate::storage::StorageCtx.spec().is_t4() {
564                        // This slot group is exclusively owned by the struct and all
565                        // declared packed fields are written before commit, so previous
566                        // contents are irrelevant; zero-init only clears unowned padding.
567                        ::alloy::primitives::U256::ZERO
568                    } else {
569                        storage.load(#slot_addr)?
570                    };
571                    pending_offset = Some(#packing::#loc_const.offset_slots);
572                    let mut packed = crate::storage::packing::PackedSlot(pending_val);
573                    <#ty as crate::storage::Storable>::store(&self.#name, &mut packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?;
574                    pending_val = packed.0;
575
576                    // Store if this is the last field in the current slot group
577                    if #should_store {
578                        storage.store(#slot_addr, pending_val)?;
579                        pending_offset = None;
580                    }
581                } else {
582                    <#ty as crate::storage::Storable>::store(&self.#name, storage, #slot_addr, crate::storage::LayoutCtx::FULL)?;
583                }
584            }}
585        }
586    });
587
588    quote! {
589        let mut pending_val = ::alloy::primitives::U256::ZERO;
590        let mut pending_offset: Option<usize> = None;
591        #(#field_stores)*
592    }
593}
594
595/// Generate `fn delete()` implementation.
596fn gen_delete_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
597    // Delete dynamic fields using their `Storable` impl so that they handle their own cleanup
598    let dynamic_deletes = fields.iter().map(|(name, ty)| {
599        let loc_const = PackingConstants::new(name).location();
600        quote! {
601            if <#ty as crate::storage::StorableType>::IS_DYNAMIC {
602                <#ty as crate::storage::Storable>::delete(
603                    storage,
604                    base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots),
605                    crate::storage::LayoutCtx::FULL
606                )?;
607            }
608        }
609    });
610
611    // Bulk clear static slots - only zero slots that contain non-dynamic fields
612    let is_static_slot = fields.iter().map(|(name, ty)| {
613        let loc_const = PackingConstants::new(name).location();
614        quote! {
615            ((#packing::#loc_const.offset_slots..#packing::#loc_const.offset_slots + <#ty as crate::storage::StorableType>::SLOTS)
616                .contains(&slot_offset) &&
617             !<#ty as crate::storage::StorableType>::IS_DYNAMIC)
618        }
619    });
620
621    quote! {
622        #(#dynamic_deletes)*
623
624        for slot_offset in 0..#packing::SLOT_COUNT {
625            // Only zero this slot if a static field occupies it
626            if #(#is_static_slot)||* {
627                storage.store(
628                    base_slot + ::alloy::primitives::U256::from(slot_offset),
629                    ::alloy::primitives::U256::ZERO
630                )?;
631            }
632        }
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639    use syn::parse_quote;
640
641    fn parse_enum(input: DeriveInput) -> DataEnum {
642        match input.data {
643            Data::Enum(data_enum) => data_enum,
644            _ => panic!("expected enum input"),
645        }
646    }
647
648    #[test]
649    fn validate_sequential_discriminants_accepts_implicit_variants() {
650        let data_enum = parse_enum(parse_quote! {
651            enum PackedStatus {
652                Pending,
653                Active,
654                Frozen,
655            }
656        });
657
658        validate_sequential_discriminants(&data_enum).unwrap();
659    }
660
661    #[test]
662    fn validate_sequential_discriminants_rejects_explicit_discriminants() {
663        let data_enum = parse_enum(parse_quote! {
664            enum PackedStatus {
665                Pending = 0,
666                Active = 1,
667                Frozen = 2,
668            }
669        });
670
671        let err = validate_sequential_discriminants(&data_enum).unwrap_err();
672        assert!(err.to_string().contains("explicit discriminants"));
673    }
674
675    #[test]
676    fn validate_sequential_discriminants_rejects_gaps() {
677        let data_enum = parse_enum(parse_quote! {
678            enum PackedStatus {
679                Pending = 0,
680                Active = 5,
681            }
682        });
683
684        let err = validate_sequential_discriminants(&data_enum).unwrap_err();
685        assert!(err.to_string().contains("explicit discriminants"));
686    }
687}