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!(ctx.is_full(), "Struct types can only be loaded with a full-slot LayoutCtx (FULL or INIT)");
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!(ctx.is_full(), "Struct types can only be stored with a full-slot LayoutCtx (FULL or INIT)");
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!(ctx.is_full(), "Struct types can only be deleted with a full-slot LayoutCtx (FULL or INIT)");
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.
490///
491/// # Zero-init behaviour (T4+)
492///
493/// Each packed slot group starts from `U256::ZERO` instead of a previous SLOAD, so any byte not
494/// written by a declared packed field is zeroed on every store. If a struct later on removes a
495/// trailing field, those formerly-occupied bytes will be cleared on the next write.
496fn gen_store_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
497    if fields.is_empty() {
498        return quote! {};
499    }
500
501    let field_stores = fields.iter().enumerate().map(|(idx, (name, ty))| {
502        let loc_const = PackingConstants::new(name).location();
503        let next_ty = fields.get(idx + 1).map(|(_, ty)| *ty);
504
505        let (prev_slot_ref, next_slot_ref) =
506            packing::get_neighbor_slot_refs(idx, fields, packing, |(name, _)| name, false);
507
508        let slot_addr = quote! { base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots) };
509        let packed_ctx = quote! { crate::storage::LayoutCtx::packed(#packing::#loc_const.offset_bytes) };
510
511        // Determine if we need to store after this field
512        let should_store = match (&next_slot_ref, next_ty) {
513            (Some(next_slot), Some(next_ty)) => {
514                // Store if next field is in different slot OR next field is not packable
515                quote! {
516                    #packing::#loc_const.offset_slots != #next_slot
517                        || !<#next_ty as crate::storage::StorableType>::IS_PACKABLE
518                }
519            }
520            _ => quote! { true }, // Always store last field
521        };
522
523        if let Some(prev_slot_ref) = prev_slot_ref {
524            quote! {{
525                let curr_offset = #packing::#loc_const.offset_slots;
526                let prev_offset = #prev_slot_ref;
527
528                if <#ty as crate::storage::StorableType>::IS_PACKABLE && curr_offset == prev_offset {
529                    // Same slot as previous packable field - accumulate in pending slot
530                    let mut packed = crate::storage::packing::PackedSlot(pending_val);
531                    <#ty as crate::storage::Storable>::store(&self.#name, &mut packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?;
532                    pending_val = packed.0;
533                } else if <#ty as crate::storage::StorableType>::IS_PACKABLE {
534                    // New slot, but packable - commit previous and start new batch
535                    if let Some(offset) = pending_offset {
536                        storage.store(base_slot + ::alloy::primitives::U256::from(offset), pending_val)?;
537                    }
538                    pending_val = if crate::storage::StorageCtx.spec().is_t4() {
539                        // This slot group is exclusively owned by the struct and all
540                        // declared packed fields are written before commit, so previous
541                        // contents are irrelevant; zero-init only clears unowned padding.
542                        ::alloy::primitives::U256::ZERO
543                    } else {
544                        storage.load(#slot_addr)?
545                    };
546                    pending_offset = Some(curr_offset);
547                    let mut packed = crate::storage::packing::PackedSlot(pending_val);
548                    <#ty as crate::storage::Storable>::store(&self.#name, &mut packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?;
549                    pending_val = packed.0;
550                } else {
551                    // Non-packable - commit pending and do direct store
552                    if let Some(offset) = pending_offset {
553                        storage.store(base_slot + ::alloy::primitives::U256::from(offset), pending_val)?;
554                        pending_offset = None;
555                    }
556                    // Dynamic fields need INIT to skip stale-tail cleanup on virgin storage.
557                    // Static fields ignore INIT, so we always use FULL for them.
558                    let ctx = if <#ty as crate::storage::StorableType>::IS_DYNAMIC { ctx } else { crate::storage::LayoutCtx::FULL };
559                    debug_assert!(ctx.is_full(), "Struct types can only use full-slot LayoutCtx (FULL or INIT)");
560                    <#ty as crate::storage::Storable>::store(&self.#name, storage, #slot_addr, ctx)?;
561                }
562
563                // Store if this is the last field in the current slot group
564                if let Some(offset) = pending_offset && (#should_store) {
565                    storage.store(base_slot + ::alloy::primitives::U256::from(offset), pending_val)?;
566                    pending_offset = None;
567                }
568            }}
569        } else {
570            // First field
571            quote! {{
572                if <#ty as crate::storage::StorableType>::IS_PACKABLE {
573                    pending_val = if crate::storage::StorageCtx.spec().is_t4() {
574                        // This slot group is exclusively owned by the struct and all
575                        // declared packed fields are written before commit, so previous
576                        // contents are irrelevant; zero-init only clears unowned padding.
577                        ::alloy::primitives::U256::ZERO
578                    } else {
579                        storage.load(#slot_addr)?
580                    };
581                    pending_offset = Some(#packing::#loc_const.offset_slots);
582                    let mut packed = crate::storage::packing::PackedSlot(pending_val);
583                    <#ty as crate::storage::Storable>::store(&self.#name, &mut packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?;
584                    pending_val = packed.0;
585
586                    // Store if this is the last field in the current slot group
587                    if #should_store {
588                        storage.store(#slot_addr, pending_val)?;
589                        pending_offset = None;
590                    }
591                } else {
592                    // Dynamic fields need INIT to skip stale-tail cleanup on virgin storage.
593                    // Static fields ignore INIT, so we always use FULL for them.
594                    let ctx = if <#ty as crate::storage::StorableType>::IS_DYNAMIC { ctx } else { crate::storage::LayoutCtx::FULL };
595                    debug_assert!(ctx.is_full(), "Struct types can only use full-slot LayoutCtx (FULL or INIT)");
596                    <#ty as crate::storage::Storable>::store(&self.#name, storage, #slot_addr, ctx)?;
597                }
598            }}
599        }
600    });
601
602    quote! {
603        let mut pending_val = ::alloy::primitives::U256::ZERO;
604        let mut pending_offset: Option<usize> = None;
605        #(#field_stores)*
606    }
607}
608
609/// Generate `fn delete()` implementation.
610fn gen_delete_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
611    // Delete dynamic fields using their `Storable` impl so that they handle their own cleanup
612    let dynamic_deletes = fields.iter().map(|(name, ty)| {
613        let loc_const = PackingConstants::new(name).location();
614        quote! {
615            if <#ty as crate::storage::StorableType>::IS_DYNAMIC {
616                <#ty as crate::storage::Storable>::delete(
617                    storage,
618                    base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots),
619                    crate::storage::LayoutCtx::FULL
620                )?;
621            }
622        }
623    });
624
625    // Bulk clear static slots - only zero slots that contain non-dynamic fields
626    let is_static_slot = fields.iter().map(|(name, ty)| {
627        let loc_const = PackingConstants::new(name).location();
628        quote! {
629            ((#packing::#loc_const.offset_slots..#packing::#loc_const.offset_slots + <#ty as crate::storage::StorableType>::SLOTS)
630                .contains(&slot_offset) &&
631             !<#ty as crate::storage::StorableType>::IS_DYNAMIC)
632        }
633    });
634
635    quote! {
636        #(#dynamic_deletes)*
637
638        for slot_offset in 0..#packing::SLOT_COUNT {
639            // Only zero this slot if a static field occupies it
640            if #(#is_static_slot)||* {
641                storage.store(
642                    base_slot + ::alloy::primitives::U256::from(slot_offset),
643                    ::alloy::primitives::U256::ZERO
644                )?;
645            }
646        }
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653    use syn::parse_quote;
654
655    fn parse_enum(input: DeriveInput) -> DataEnum {
656        match input.data {
657            Data::Enum(data_enum) => data_enum,
658            _ => panic!("expected enum input"),
659        }
660    }
661
662    #[test]
663    fn validate_sequential_discriminants_accepts_implicit_variants() {
664        let data_enum = parse_enum(parse_quote! {
665            enum PackedStatus {
666                Pending,
667                Active,
668                Frozen,
669            }
670        });
671
672        validate_sequential_discriminants(&data_enum).unwrap();
673    }
674
675    #[test]
676    fn validate_sequential_discriminants_rejects_explicit_discriminants() {
677        let data_enum = parse_enum(parse_quote! {
678            enum PackedStatus {
679                Pending = 0,
680                Active = 1,
681                Frozen = 2,
682            }
683        });
684
685        let err = validate_sequential_discriminants(&data_enum).unwrap_err();
686        assert!(err.to_string().contains("explicit discriminants"));
687    }
688
689    #[test]
690    fn validate_sequential_discriminants_rejects_gaps() {
691        let data_enum = parse_enum(parse_quote! {
692            enum PackedStatus {
693                Pending = 0,
694                Active = 5,
695            }
696        });
697
698        let err = validate_sequential_discriminants(&data_enum).unwrap_err();
699        assert!(err.to_string().contains("explicit discriminants"));
700    }
701}