tempo_precompiles_macros/
storable.rs

1//! Implementation of the `#[derive(Storable)]` macro.
2
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote};
5use syn::{Data, 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.
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    // Extract struct name, generics
22    let strukt = &input.ident;
23    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
24
25    // Parse struct fields
26    let fields = match &input.data {
27        Data::Struct(data_struct) => match &data_struct.fields {
28            Fields::Named(fields_named) => &fields_named.named,
29            _ => {
30                return Err(syn::Error::new_spanned(
31                    &input.ident,
32                    "`Storable` can only be derived for structs with named fields",
33                ));
34            }
35        },
36        _ => {
37            return Err(syn::Error::new_spanned(
38                &input.ident,
39                "`Storable` can only be derived for structs",
40            ));
41        }
42    };
43
44    if fields.is_empty() {
45        return Err(syn::Error::new_spanned(
46            &input.ident,
47            "`Storable` cannot be derived for empty structs",
48        ));
49    }
50
51    // Extract field names and types into `FieldInfo` structs
52    let field_infos: Vec<_> = fields
53        .iter()
54        .map(|f| FieldInfo {
55            name: f.ident.as_ref().unwrap().clone(),
56            ty: f.ty.clone(),
57            slot: None,
58            base_slot: None,
59        })
60        .collect();
61
62    // Build layout IR using the unified function
63    let layout_fields = packing::allocate_slots(&field_infos)?;
64
65    // Generate helper module with packing layout calculations
66    let mod_ident = format_ident!("__packing_{}", to_snake_case(&strukt.to_string()));
67    let packing_module = gen_packing_module_from_ir(&layout_fields, &mod_ident);
68
69    // Classify fields: direct (storable) vs indirect (mappings)
70    let len = fields.len();
71    let (direct_fields, direct_names, mapping_names) = field_infos.iter().fold(
72        (Vec::with_capacity(len), Vec::with_capacity(len), Vec::new()),
73        |mut out, field_info| {
74            if extract_mapping_types(&field_info.ty).is_none() {
75                // fields with direct slot allocation
76                out.0.push((&field_info.name, &field_info.ty));
77                out.1.push(&field_info.name);
78            } else {
79                // fields with indirect slot allocation (mappings)
80                out.2.push(&field_info.name);
81            }
82            out
83        },
84    );
85
86    // Extract just the types for IS_DYNAMIC calculation
87    let direct_tys: Vec<_> = direct_fields.iter().map(|(_, ty)| *ty).collect();
88
89    // Generate load/store/delete implementations for scalar fields only
90    let load_impl = gen_load_store_impl(&direct_fields, &mod_ident, true);
91    let store_impl = gen_load_store_impl(&direct_fields, &mod_ident, false);
92    let delete_impl = gen_delete_impl(&direct_fields, &mod_ident);
93
94    // Generate handler struct for field access
95    let handler_struct = gen_handler_struct(strukt, &layout_fields, &mod_ident);
96    let handler_name = format_ident!("{}Handler", strukt);
97
98    let expanded = quote! {
99        #packing_module
100        #handler_struct
101
102        // impl `StorableType` for layout information
103        impl #impl_generics crate::storage::StorableType for #strukt #ty_generics #where_clause {
104            // Structs cannot be packed, so they must take full slots
105            const LAYOUT: crate::storage::Layout = crate::storage::Layout::Slots(#mod_ident::SLOT_COUNT);
106
107            // A struct is dynamic if any of its fields is dynamic
108            const IS_DYNAMIC: bool = #(
109                <#direct_tys as crate::storage::StorableType>::IS_DYNAMIC
110            )||*;
111
112            type Handler = #handler_name;
113
114            fn handle(slot: ::alloy::primitives::U256, _ctx: crate::storage::LayoutCtx, address: ::alloy::primitives::Address) -> Self::Handler {
115                #handler_name::new(slot, address)
116            }
117        }
118
119        // `Storable` implementation: storage I/O with full logic
120        impl #impl_generics crate::storage::Storable for #strukt #ty_generics #where_clause {
121            fn load<S: crate::storage::StorageOps>(
122                storage: &S,
123                base_slot: ::alloy::primitives::U256,
124                ctx: crate::storage::LayoutCtx
125            ) -> crate::error::Result<Self> {
126                use crate::storage::Storable;
127                debug_assert_eq!(ctx, crate::storage::LayoutCtx::FULL, "Struct types can only be loaded with LayoutCtx::FULL");
128
129                #load_impl
130
131                Ok(Self {
132                    #(#direct_names),*,
133                    #(#mapping_names: Default::default()),*
134                })
135            }
136
137            fn store<S: crate::storage::StorageOps>(
138                &self,
139                storage: &mut S,
140                base_slot: ::alloy::primitives::U256,
141                ctx: crate::storage::LayoutCtx
142            ) -> crate::error::Result<()> {
143                use crate::storage::Storable;
144                debug_assert_eq!(ctx, crate::storage::LayoutCtx::FULL, "Struct types can only be stored with LayoutCtx::FULL");
145
146                #store_impl
147
148                Ok(())
149            }
150
151            fn delete<S: crate::storage::StorageOps>(
152                storage: &mut S,
153                base_slot: ::alloy::primitives::U256,
154                ctx: crate::storage::LayoutCtx
155            ) -> crate::error::Result<()> {
156                use crate::storage::Storable;
157                debug_assert_eq!(ctx, crate::storage::LayoutCtx::FULL, "Struct types can only be deleted with LayoutCtx::FULL");
158
159                #delete_impl
160
161                Ok(())
162            }
163        }
164    };
165
166    // Generate array implementations if requested
167    let array_impls = if let Some(sizes) = extract_storable_array_sizes(&input.attrs)? {
168        // Generate the struct type path for array generation
169        let struct_type = quote! { #strukt #ty_generics };
170        gen_struct_arrays(struct_type, &sizes)
171    } else {
172        quote! {}
173    };
174
175    // Combine struct implementation with array implementations
176    let combined = quote! {
177        #expanded
178        #array_impls
179    };
180
181    Ok(combined)
182}
183
184/// Generate a compile-time module that calculates the packing layout from IR.
185fn gen_packing_module_from_ir(fields: &[LayoutField<'_>], mod_ident: &Ident) -> TokenStream {
186    // Generate constants using the unified IR-based function (generates <FIELD>: U256)
187    let last_field = &fields[fields.len() - 1];
188    let last_slot_const = PackingConstants::new(last_field.name).slot();
189    let packing_constants = packing::gen_constants_from_ir(fields, true);
190
191    quote! {
192        pub mod #mod_ident {
193            use super::*;
194
195            #packing_constants
196            pub const SLOT_COUNT: usize = (#last_slot_const.saturating_add(::alloy::primitives::U256::ONE)).as_limbs()[0] as usize;
197        }
198    }
199}
200
201/// Generate a handler struct for the storable type.
202///
203/// The handler provides type-safe access to both the full struct and individual fields.
204fn gen_handler_struct(
205    struct_name: &Ident,
206    fields: &[LayoutField<'_>],
207    mod_ident: &Ident,
208) -> TokenStream {
209    let handler_name = format_ident!("{}Handler", struct_name);
210
211    // Generate public handler fields
212    let handler_fields = fields.iter().map(gen_handler_field_decl);
213
214    // Generate field initializations for constructor using the shared helper
215    let field_inits = fields
216        .iter()
217        .enumerate()
218        .map(|(idx, field)| gen_handler_field_init(field, idx, fields, Some(mod_ident)));
219
220    quote! {
221        /// Type-safe handler for accessing `#struct_name` in storage.
222        ///
223        /// Provides individual field access via public fields and whole-struct operations.
224        #[derive(Debug, Clone)]
225        pub struct #handler_name {
226            address: ::alloy::primitives::Address,
227            base_slot: ::alloy::primitives::U256,
228            #(#handler_fields,)*
229        }
230
231        impl #handler_name {
232            /// Creates a new handler for the struct at the given base slot.
233            #[inline]
234            pub fn new(base_slot: ::alloy::primitives::U256, address: ::alloy::primitives::Address) -> Self {
235                Self {
236                    base_slot,
237                    #(#field_inits,)*
238                    address,
239                }
240            }
241
242            /// Returns the base storage slot where this struct's data is stored.
243            ///
244            /// Single-slot structs pack all fields into this slot.
245            /// Multi-slot structs use consecutive slots starting from this base.
246            #[inline]
247            pub fn base_slot(&self) -> ::alloy::primitives::U256 {
248                self.base_slot
249            }
250
251            /// Returns a `Slot<T>` for whole-struct storage operations.
252            #[inline]
253            fn as_slot(&self) -> crate::storage::Slot<#struct_name> {
254                crate::storage::Slot::<#struct_name>::new(
255                    self.base_slot,
256                    self.address
257                )
258            }
259        }
260
261        impl crate::storage::Handler<#struct_name> for #handler_name {
262            #[inline]
263            fn read(&self) -> crate::error::Result<#struct_name> {
264                self.as_slot().read()
265            }
266
267            #[inline]
268            fn write(&mut self, value: #struct_name) -> crate::error::Result<()> {
269                self.as_slot().write(value)
270            }
271
272            #[inline]
273            fn delete(&mut self) -> crate::error::Result<()> {
274                self.as_slot().delete()
275            }
276
277            /// Reads the struct from transient storage.
278            #[inline]
279            fn t_read(&self) -> crate::error::Result<#struct_name> {
280                self.as_slot().t_read()
281            }
282
283            /// Writes the struct to transient storage.
284            #[inline]
285            fn t_write(&mut self, value: #struct_name) -> crate::error::Result<()> {
286                self.as_slot().t_write(value)
287            }
288
289            /// Deletes the struct from transient storage.
290            #[inline]
291            fn t_delete(&mut self) -> crate::error::Result<()> {
292                self.as_slot().t_delete()
293            }
294        }
295    }
296}
297
298/// Generate `fn load()` or `fn store()` implementation.
299fn gen_load_store_impl(fields: &[(&Ident, &Type)], packing: &Ident, is_load: bool) -> TokenStream {
300    let field_ops = fields.iter().enumerate().map(|(idx, (name, ty))| {
301        let (prev_slot_const_ref, next_slot_const_ref) =
302            packing::get_neighbor_slot_refs(idx, fields, packing, |(name, _ty)| name);
303
304        // Generate `LayoutCtx` expression with compile-time packing detection
305        let loc_const = PackingConstants::new(name).location();
306        let layout_ctx = packing::gen_layout_ctx_expr(
307            ty,
308            false,
309            quote! { #packing::#loc_const.offset_slots },
310            quote! { #packing::#loc_const.offset_bytes },
311            prev_slot_const_ref,
312            next_slot_const_ref,
313        );
314
315        if is_load {
316            quote! {
317                let #name = <#ty as crate::storage::Storable>::load(
318                    storage,
319                    base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots),
320                    #layout_ctx
321                )?;
322            }
323        } else {
324            quote! {{
325                let target_slot = base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots);
326                <#ty as crate::storage::Storable>::store(&self.#name, storage, target_slot, #layout_ctx)?;
327            }}
328        }
329    });
330
331    quote! {
332        #(#field_ops)*
333    }
334}
335
336/// Generate `fn delete()` implementation.
337fn gen_delete_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
338    // Delete dynamic fields using their `Storable` impl so that they handle their own cleanup
339    let dynamic_deletes = fields.iter().map(|(name, ty)| {
340        let loc_const = PackingConstants::new(name).location();
341        quote! {
342            if <#ty as crate::storage::StorableType>::IS_DYNAMIC {
343                <#ty as crate::storage::Storable>::delete(
344                    storage,
345                    base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots),
346                    crate::storage::LayoutCtx::FULL
347                )?;
348            }
349        }
350    });
351
352    // Bulk clear static slots - only zero slots that contain non-dynamic fields
353    let is_static_slot = fields.iter().map(|(name, ty)| {
354        let loc_const = PackingConstants::new(name).location();
355        quote! {
356            ((#packing::#loc_const.offset_slots..#packing::#loc_const.offset_slots + <#ty as crate::storage::StorableType>::SLOTS)
357                .contains(&slot_offset) &&
358             !<#ty as crate::storage::StorableType>::IS_DYNAMIC)
359        }
360    });
361
362    quote! {
363        #(#dynamic_deletes)*
364
365        for slot_offset in 0..#packing::SLOT_COUNT {
366            // Only zero this slot if a static field occupies it
367            if #(#is_static_slot)||* {
368                storage.store(
369                    base_slot + ::alloy::primitives::U256::from(slot_offset),
370                    ::alloy::primitives::U256::ZERO
371                )?;
372            }
373        }
374    }
375}