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::{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_impl(&direct_fields, &mod_ident);
91    let store_impl = gen_store_impl(&direct_fields, &mod_ident);
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    let last_type = &last_field.ty;
191
192    quote! {
193        pub mod #mod_ident {
194            use super::*;
195
196            #packing_constants
197            pub const SLOT_COUNT: usize = (#last_slot_const.saturating_add(
198                ::alloy::primitives::U256::from_limbs([<#last_type as crate::storage::StorableType>::SLOTS as u64, 0, 0, 0])
199            )).as_limbs()[0] as usize;
200        }
201    }
202}
203
204/// Generate a handler struct for the storable type.
205///
206/// The handler provides type-safe access to both the full struct and individual fields.
207fn gen_handler_struct(
208    struct_name: &Ident,
209    fields: &[LayoutField<'_>],
210    mod_ident: &Ident,
211) -> TokenStream {
212    let handler_name = format_ident!("{}Handler", struct_name);
213
214    // Generate public handler fields
215    let handler_fields = fields.iter().map(gen_handler_field_decl);
216
217    // Generate field initializations for constructor using the shared helper
218    let field_inits = fields
219        .iter()
220        .enumerate()
221        .map(|(idx, field)| gen_handler_field_init(field, idx, fields, Some(mod_ident)));
222
223    quote! {
224        /// Type-safe handler for accessing `#struct_name` in storage.
225        ///
226        /// Provides individual field access via public fields and whole-struct operations.
227        #[derive(Debug, Clone)]
228        pub struct #handler_name {
229            address: ::alloy::primitives::Address,
230            base_slot: ::alloy::primitives::U256,
231            #(#handler_fields,)*
232        }
233
234        impl #handler_name {
235            /// Creates a new handler for the struct at the given base slot.
236            #[inline]
237            pub fn new(base_slot: ::alloy::primitives::U256, address: ::alloy::primitives::Address) -> Self {
238                Self {
239                    base_slot,
240                    #(#field_inits,)*
241                    address,
242                }
243            }
244
245            /// Returns the base storage slot where this struct's data is stored.
246            ///
247            /// Single-slot structs pack all fields into this slot.
248            /// Multi-slot structs use consecutive slots starting from this base.
249            #[inline]
250            pub fn base_slot(&self) -> ::alloy::primitives::U256 {
251                self.base_slot
252            }
253
254            /// Returns a `Slot<T>` for whole-struct storage operations.
255            #[inline]
256            fn as_slot(&self) -> crate::storage::Slot<#struct_name> {
257                crate::storage::Slot::<#struct_name>::new(
258                    self.base_slot,
259                    self.address
260                )
261            }
262        }
263
264        impl crate::storage::Handler<#struct_name> for #handler_name {
265            #[inline]
266            fn read(&self) -> crate::error::Result<#struct_name> {
267                self.as_slot().read()
268            }
269
270            #[inline]
271            fn write(&mut self, value: #struct_name) -> crate::error::Result<()> {
272                self.as_slot().write(value)
273            }
274
275            #[inline]
276            fn delete(&mut self) -> crate::error::Result<()> {
277                self.as_slot().delete()
278            }
279
280            /// Reads the struct from transient storage.
281            #[inline]
282            fn t_read(&self) -> crate::error::Result<#struct_name> {
283                self.as_slot().t_read()
284            }
285
286            /// Writes the struct to transient storage.
287            #[inline]
288            fn t_write(&mut self, value: #struct_name) -> crate::error::Result<()> {
289                self.as_slot().t_write(value)
290            }
291
292            /// Deletes the struct from transient storage.
293            #[inline]
294            fn t_delete(&mut self) -> crate::error::Result<()> {
295                self.as_slot().t_delete()
296            }
297        }
298    }
299}
300
301/// Generate `fn load()` implementation.
302///
303/// For consecutive packable fields sharing a slot, loads the slot once and extracts
304/// all fields via `PackedSlot`, avoiding redundant SLOADs.
305fn gen_load_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
306    if fields.is_empty() {
307        return quote! {};
308    }
309
310    let field_loads = fields.iter().enumerate().map(|(idx, (name, ty))| {
311        let loc_const = PackingConstants::new(name).location();
312
313        let (prev_slot_ref, _) =
314            packing::get_neighbor_slot_refs(idx, fields, packing, |(name, _)| name, false);
315
316        let slot_addr = quote! { base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots) };
317        let packed_ctx = quote! { crate::storage::LayoutCtx::packed(#packing::#loc_const.offset_bytes) };
318
319        if let Some(prev_slot_ref) = prev_slot_ref {
320            quote! {
321                let #name = {
322                    let curr_offset = #packing::#loc_const.offset_slots;
323                    let prev_offset = #prev_slot_ref;
324
325                    if <#ty as crate::storage::StorableType>::IS_PACKABLE && curr_offset == prev_offset {
326                        // Same slot as previous packable field - reuse cached value
327                        let packed = crate::storage::packing::PackedSlot(cached_slot);
328                        <#ty as crate::storage::Storable>::load(&packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?
329                    } else if <#ty as crate::storage::StorableType>::IS_PACKABLE {
330                        // New slot, but packable - load and cache for potential reuse
331                        cached_slot = storage.load(#slot_addr)?;
332                        let packed = crate::storage::packing::PackedSlot(cached_slot);
333                        <#ty as crate::storage::Storable>::load(&packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?
334                    } else {
335                        // Non-packable - direct load
336                        <#ty as crate::storage::Storable>::load(storage, #slot_addr, crate::storage::LayoutCtx::FULL)?
337                    }
338                };
339            }
340        } else {
341            // First field
342            quote! {
343                let #name = if <#ty as crate::storage::StorableType>::IS_PACKABLE {
344                    cached_slot = storage.load(#slot_addr)?;
345                    let packed = crate::storage::packing::PackedSlot(cached_slot);
346                    <#ty as crate::storage::Storable>::load(&packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?
347                } else {
348                    <#ty as crate::storage::Storable>::load(storage, #slot_addr, crate::storage::LayoutCtx::FULL)?
349                };
350            }
351        }
352    });
353
354    quote! {
355        let mut cached_slot = ::alloy::primitives::U256::ZERO;
356        #(#field_loads)*
357    }
358}
359
360/// Generate `fn store()` implementation.
361///
362/// For consecutive packable fields sharing a slot, accumulates changes in memory
363/// and writes once, avoiding redundant SLOAD + SSTORE pairs.
364fn gen_store_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
365    if fields.is_empty() {
366        return quote! {};
367    }
368
369    let field_stores = fields.iter().enumerate().map(|(idx, (name, ty))| {
370        let loc_const = PackingConstants::new(name).location();
371        let next_ty = fields.get(idx + 1).map(|(_, ty)| *ty);
372
373        let (prev_slot_ref, next_slot_ref) =
374            packing::get_neighbor_slot_refs(idx, fields, packing, |(name, _)| name, false);
375
376        let slot_addr = quote! { base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots) };
377        let packed_ctx = quote! { crate::storage::LayoutCtx::packed(#packing::#loc_const.offset_bytes) };
378
379        // Determine if we need to store after this field
380        let should_store = match (&next_slot_ref, next_ty) {
381            (Some(next_slot), Some(next_ty)) => {
382                // Store if next field is in different slot OR next field is not packable
383                quote! {
384                    #packing::#loc_const.offset_slots != #next_slot
385                        || !<#next_ty as crate::storage::StorableType>::IS_PACKABLE
386                }
387            }
388            _ => quote! { true }, // Always store last field
389        };
390
391        if let Some(prev_slot_ref) = prev_slot_ref {
392            quote! {{
393                let curr_offset = #packing::#loc_const.offset_slots;
394                let prev_offset = #prev_slot_ref;
395
396                if <#ty as crate::storage::StorableType>::IS_PACKABLE && curr_offset == prev_offset {
397                    // Same slot as previous packable field - accumulate in pending slot
398                    let mut packed = crate::storage::packing::PackedSlot(pending_val);
399                    <#ty as crate::storage::Storable>::store(&self.#name, &mut packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?;
400                    pending_val = packed.0;
401                } else if <#ty as crate::storage::StorableType>::IS_PACKABLE {
402                    // New slot, but packable - commit previous and start new batch
403                    if let Some(offset) = pending_offset {
404                        storage.store(base_slot + ::alloy::primitives::U256::from(offset), pending_val)?;
405                    }
406                    pending_val = storage.load(#slot_addr)?;
407                    pending_offset = Some(curr_offset);
408                    let mut packed = crate::storage::packing::PackedSlot(pending_val);
409                    <#ty as crate::storage::Storable>::store(&self.#name, &mut packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?;
410                    pending_val = packed.0;
411                } else {
412                    // Non-packable - commit pending and do direct store
413                    if let Some(offset) = pending_offset {
414                        storage.store(base_slot + ::alloy::primitives::U256::from(offset), pending_val)?;
415                        pending_offset = None;
416                    }
417                    <#ty as crate::storage::Storable>::store(&self.#name, storage, #slot_addr, crate::storage::LayoutCtx::FULL)?;
418                }
419
420                // Store if this is the last field in the current slot group
421                if let Some(offset) = pending_offset && (#should_store) {
422                    storage.store(base_slot + ::alloy::primitives::U256::from(offset), pending_val)?;
423                    pending_offset = None;
424                }
425            }}
426        } else {
427            // First field
428            quote! {{
429                if <#ty as crate::storage::StorableType>::IS_PACKABLE {
430                    pending_val = storage.load(#slot_addr)?;
431                    pending_offset = Some(#packing::#loc_const.offset_slots);
432                    let mut packed = crate::storage::packing::PackedSlot(pending_val);
433                    <#ty as crate::storage::Storable>::store(&self.#name, &mut packed, ::alloy::primitives::U256::ZERO, #packed_ctx)?;
434                    pending_val = packed.0;
435
436                    // Store if this is the last field in the current slot group
437                    if #should_store {
438                        storage.store(#slot_addr, pending_val)?;
439                        pending_offset = None;
440                    }
441                } else {
442                    <#ty as crate::storage::Storable>::store(&self.#name, storage, #slot_addr, crate::storage::LayoutCtx::FULL)?;
443                }
444            }}
445        }
446    });
447
448    quote! {
449        let mut pending_val = ::alloy::primitives::U256::ZERO;
450        let mut pending_offset: Option<usize> = None;
451        #(#field_stores)*
452    }
453}
454
455/// Generate `fn delete()` implementation.
456fn gen_delete_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
457    // Delete dynamic fields using their `Storable` impl so that they handle their own cleanup
458    let dynamic_deletes = fields.iter().map(|(name, ty)| {
459        let loc_const = PackingConstants::new(name).location();
460        quote! {
461            if <#ty as crate::storage::StorableType>::IS_DYNAMIC {
462                <#ty as crate::storage::Storable>::delete(
463                    storage,
464                    base_slot + ::alloy::primitives::U256::from(#packing::#loc_const.offset_slots),
465                    crate::storage::LayoutCtx::FULL
466                )?;
467            }
468        }
469    });
470
471    // Bulk clear static slots - only zero slots that contain non-dynamic fields
472    let is_static_slot = fields.iter().map(|(name, ty)| {
473        let loc_const = PackingConstants::new(name).location();
474        quote! {
475            ((#packing::#loc_const.offset_slots..#packing::#loc_const.offset_slots + <#ty as crate::storage::StorableType>::SLOTS)
476                .contains(&slot_offset) &&
477             !<#ty as crate::storage::StorableType>::IS_DYNAMIC)
478        }
479    });
480
481    quote! {
482        #(#dynamic_deletes)*
483
484        for slot_offset in 0..#packing::SLOT_COUNT {
485            // Only zero this slot if a static field occupies it
486            if #(#is_static_slot)||* {
487                storage.store(
488                    base_slot + ::alloy::primitives::U256::from(slot_offset),
489                    ::alloy::primitives::U256::ZERO
490                )?;
491            }
492        }
493    }
494}