1use 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
15pub(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 let strukt = &input.ident;
34 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
35
36 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 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 let layout_fields = packing::allocate_slots(&field_infos)?;
67
68 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 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 out.0.push((&field_info.name, &field_info.ty));
80 out.1.push(&field_info.name);
81 } else {
82 out.2.push(&field_info.name);
84 }
85 out
86 },
87 );
88
89 let direct_tys: Vec<_> = direct_fields.iter().map(|(_, ty)| *ty).collect();
91
92 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 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 #impl_generics crate::storage::StorableType for #strukt #ty_generics #where_clause {
107 const LAYOUT: crate::storage::Layout = crate::storage::Layout::Slots(#mod_ident::SLOT_COUNT);
109
110 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 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 let array_impls = if let Some(sizes) = extract_storable_array_sizes(&input.attrs)? {
171 let struct_type = quote! { #strukt #ty_generics };
173 gen_struct_arrays(struct_type, &sizes)
174 } else {
175 quote! {}
176 };
177
178 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
310fn gen_packing_module_from_ir(fields: &[LayoutField<'_>], mod_ident: &Ident) -> TokenStream {
312 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
330fn 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 let handler_fields = fields.iter().map(gen_handler_field_decl);
342
343 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 #[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 #[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 #[inline]
376 pub fn base_slot(&self) -> ::alloy::primitives::U256 {
377 self.base_slot
378 }
379
380 #[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 #[inline]
408 fn t_read(&self) -> crate::error::Result<#struct_name> {
409 self.as_slot().t_read()
410 }
411
412 #[inline]
414 fn t_write(&mut self, value: #struct_name) -> crate::error::Result<()> {
415 self.as_slot().t_write(value)
416 }
417
418 #[inline]
420 fn t_delete(&mut self) -> crate::error::Result<()> {
421 self.as_slot().t_delete()
422 }
423 }
424 }
425}
426
427fn 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 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 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 <#ty as crate::storage::Storable>::load(storage, #slot_addr, crate::storage::LayoutCtx::FULL)?
463 }
464 };
465 }
466 } else {
467 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
486fn 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 let should_store = match (&next_slot_ref, next_ty) {
507 (Some(next_slot), Some(next_ty)) => {
508 quote! {
510 #packing::#loc_const.offset_slots != #next_slot
511 || !<#next_ty as crate::storage::StorableType>::IS_PACKABLE
512 }
513 }
514 _ => quote! { true }, };
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 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 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 ::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 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 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 quote! {{
562 if <#ty as crate::storage::StorableType>::IS_PACKABLE {
563 pending_val = if crate::storage::StorageCtx.spec().is_t4() {
564 ::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 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
595fn gen_delete_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
597 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 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 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}