Skip to main content

tempo_precompiles_macros/
packing.rs

1//! Shared code generation utilities for storage slot packing.
2//!
3//! This module provides common logic for computing slot and offset assignments
4//! used by both the `#[derive(Storable)]` and `#[contract]` macros.
5
6use alloy::primitives::U256;
7use proc_macro2::TokenStream;
8use quote::{format_ident, quote};
9use syn::{Ident, Type};
10
11use crate::{FieldInfo, FieldKind};
12
13/// Helper for generating packing constant identifiers
14pub(crate) struct PackingConstants(String);
15
16impl PackingConstants {
17    /// Create packing constant helper struct
18    pub(crate) fn new(name: &Ident) -> Self {
19        Self(const_name(name))
20    }
21
22    /// The bare field name constant (U256 slot, used by `#[contract]` macro)
23    pub(crate) fn slot(&self) -> Ident {
24        format_ident!("{}", &self.0)
25    }
26
27    /// The `_LOC` suffixed constant
28    pub(crate) fn location(&self) -> Ident {
29        let span = proc_macro2::Span::call_site();
30        Ident::new(&format!("{}_LOC", self.0), span)
31    }
32
33    /// The `_OFFSET` constant identifier
34    pub(crate) fn offset(&self) -> Ident {
35        let span = proc_macro2::Span::call_site();
36        Ident::new(&format!("{}_OFFSET", self.0), span)
37    }
38
39    /// Returns the constant identifiers required by both macros (slot, offset)
40    pub(crate) fn into_tuple(self) -> (Ident, Ident) {
41        (self.slot(), self.offset())
42    }
43}
44
45/// Convert a field name to a constant name (SCREAMING_SNAKE_CASE)
46pub(crate) fn const_name(name: &Ident) -> String {
47    name.to_string().to_uppercase()
48}
49
50/// Represents how a slot is assigned
51#[derive(Debug, Clone)]
52pub(crate) enum SlotAssignment {
53    /// Manual slot value: `#[slot(N)]` or `#[base_slot(N)]`
54    Manual(U256),
55    /// Auto-assigned: stores after the latest auto-assigned field
56    Auto {
57        /// Base slot for packing decisions.
58        base_slot: U256,
59    },
60}
61
62impl SlotAssignment {
63    pub(crate) fn ref_slot(&self) -> &U256 {
64        match self {
65            Self::Manual(slot) => slot,
66            Self::Auto { base_slot } => base_slot,
67        }
68    }
69}
70
71/// A single field in the storage layout with computed slot information.
72#[derive(Debug)]
73pub(crate) struct LayoutField<'a> {
74    /// Field name
75    pub name: &'a Ident,
76    /// Field type
77    pub ty: &'a Type,
78    /// Field kind (Direct or Mapping)
79    pub kind: FieldKind<'a>,
80    /// The assigned storage slot for this field (or base for const-eval chain)
81    pub assigned_slot: SlotAssignment,
82}
83
84/// Build layout IR from field information.
85///
86/// This function performs slot allocation and packing decisions, returning
87/// a complete layout that can be used for code generation. The actual byte-level
88/// packing calculations (offsets, whether fields actually pack) are computed
89/// at compile-time via const expressions in the generated code.
90///
91/// The IR captures the *structure* of the layout (which fields share base slots,
92/// which are manually assigned, etc.) using the `SlotAssignment` enum.
93pub(crate) fn allocate_slots(fields: &[FieldInfo]) -> syn::Result<Vec<LayoutField<'_>>> {
94    let mut result = Vec::with_capacity(fields.len());
95    let mut current_base_slot = U256::ZERO;
96
97    for field in fields.iter() {
98        let kind = classify_field_type(&field.ty)?;
99
100        // Explicit fixed slot, doesn't affect auto-assignment chain
101        let assigned_slot = if let Some(explicit) = field.slot {
102            SlotAssignment::Manual(explicit)
103        } else if let Some(new_base) = field.base_slot {
104            // Explicit base slot, resets auto-assignment chain
105            current_base_slot = new_base;
106            SlotAssignment::Auto {
107                base_slot: new_base,
108            }
109        } else {
110            SlotAssignment::Auto {
111                base_slot: current_base_slot,
112            }
113        };
114
115        result.push(LayoutField {
116            name: &field.name,
117            ty: &field.ty,
118            kind,
119            assigned_slot,
120        });
121    }
122
123    Ok(result)
124}
125
126/// Generate packing constants from layout IR.
127///
128/// This function generates compile-time constants (`<FIELD>`, `<FIELD>_OFFSET`, `<FIELD>_BYTES`)
129/// for slot assignments, offsets, and byte sizes based on the layout IR using field-name-based naming.
130/// Slot constants (`<FIELD>`) are generated as `U256` types, while offset and bytes constants use `usize`.
131pub(crate) fn gen_constants_from_ir(fields: &[LayoutField<'_>], gen_location: bool) -> TokenStream {
132    let mut constants = TokenStream::new();
133    let mut current_base_slot: Option<&LayoutField<'_>> = None;
134
135    for field in fields {
136        let ty = field.ty;
137        let consts = PackingConstants::new(field.name);
138        let (loc_const, (slot_const, offset_const)) = (consts.location(), consts.into_tuple());
139        let slots_to_end = quote! {
140            ::alloy::primitives::U256::from_limbs([<#ty as crate::storage::StorableType>::SLOTS as u64, 0, 0, 0])
141                .saturating_sub(::alloy::primitives::U256::ONE)
142        };
143
144        // Generate byte count constants for each field
145        let bytes_expr = quote! { <#ty as crate::storage::StorableType>::BYTES };
146
147        // Generate slot and offset constants for each field
148        let (slot_expr, offset_expr) = match &field.assigned_slot {
149            // Manual slot assignment always has offset 0
150            SlotAssignment::Manual(manual_slot) => {
151                let hex_value = format!("{manual_slot}_U256");
152                let slot_lit = syn::LitInt::new(&hex_value, proc_macro2::Span::call_site());
153                // HACK: we leverage compiler evaluation checks to ensure that the full type can fit
154                // by computing the slot as: `SLOT = SLOT + (TYPE_LEN - 1)  - (TYPE_LEN - 1)`
155                let slot_expr = quote! {
156                    ::alloy::primitives::uint!(#slot_lit)
157                        .checked_add(#slots_to_end).expect("slot overflow")
158                        .saturating_sub(#slots_to_end)
159                };
160                (slot_expr, quote! { 0 })
161            }
162            // Auto-assignment computes slot/offset using const expressions
163            SlotAssignment::Auto { base_slot, .. } => {
164                let output = if let Some(current_base) = current_base_slot
165                    && current_base.assigned_slot.ref_slot() == field.assigned_slot.ref_slot()
166                {
167                    // Fields that share the same base compute their slots based on the previous field
168                    let (prev_slot, prev_offset) =
169                        PackingConstants::new(current_base.name).into_tuple();
170                    gen_slot_packing_logic(
171                        current_base.ty,
172                        field.ty,
173                        quote! { #prev_slot },
174                        quote! { #prev_offset },
175                    )
176                } else {
177                    // If a new base is adopted, start from the base slot and offset 0
178                    let limbs = *base_slot.as_limbs();
179
180                    // HACK: we leverage compiler evaluation checks to ensure that the full type can fit
181                    // by computing the slot as: `SLOT = SLOT + (TYPE_LEN - 1)  - (TYPE_LEN - 1)`
182                    let slot_expr = quote! {
183                        ::alloy::primitives::U256::from_limbs([#(#limbs),*])
184                            .checked_add(#slots_to_end).expect("slot overflow")
185                            .saturating_sub(#slots_to_end)
186                    };
187                    (slot_expr, quote! { 0 })
188                };
189                // update cache
190                current_base_slot = Some(field);
191                output
192            }
193        };
194
195        // Generate slot constant without suffix (U256) and offset constant (usize)
196        constants.extend(quote! {
197            pub const #slot_const: ::alloy::primitives::U256 = #slot_expr;
198            pub const #offset_const: usize = #offset_expr;
199        });
200
201        // For the `Storable` macro, also generate the location constant
202        // NOTE: `slot_const` refers to the slot offset of the struct field relative to the struct's base slot.
203        // Because of that it is safe to use the usize -> U256 conversion (a struct will never have 2**64 fields).
204        if gen_location {
205            constants.extend(quote! {
206                pub const #loc_const: crate::storage::packing::FieldLocation =
207                    crate::storage::packing::FieldLocation::new(#slot_const.as_limbs()[0] as usize, #offset_const, #bytes_expr);
208            });
209        }
210
211        // generate constants used in tests for solidity layout compatibility assertions
212        #[cfg(debug_assertions)]
213        {
214            let bytes_const = format_ident!("{slot_const}_BYTES");
215            constants.extend(quote! { pub const #bytes_const: usize = #bytes_expr; });
216        }
217    }
218
219    constants
220}
221
222/// Classify a field based on its type.
223///
224/// Determines if a field is a direct value or a mapping.
225/// Nested mappings like `Mapping<K, Mapping<K2, V>>` are handled automatically
226/// since the value type includes the full nested type.
227pub(crate) fn classify_field_type(ty: &Type) -> syn::Result<FieldKind<'_>> {
228    use crate::utils::extract_mapping_types;
229
230    // Check if it's a mapping (mappings have fundamentally different API)
231    if let Some((key_ty, value_ty)) = extract_mapping_types(ty) {
232        return Ok(FieldKind::Mapping {
233            key: key_ty,
234            value: value_ty,
235        });
236    }
237
238    // All non-mapping fields use the same accessor pattern
239    Ok(FieldKind::Direct(ty))
240}
241
242/// Helper to compute prev and next slot constant references for a field at a given index.
243///
244/// Generic over the field type - uses a closure to extract the field name.
245///
246/// - `use_full_slot=true`: returns `*_SLOT` (U256) for contracts
247/// - `use_full_slot=false`: returns `*_LOC.offset_slots` (usize) for storable structs
248pub(crate) fn get_neighbor_slot_refs<T, F>(
249    idx: usize,
250    fields: &[T],
251    packing: &Ident,
252    get_name: F,
253    use_full_slot: bool,
254) -> (Option<TokenStream>, Option<TokenStream>)
255where
256    F: Fn(&T) -> &Ident,
257{
258    let prev_slot_ref = if idx > 0 {
259        let prev_name = get_name(&fields[idx - 1]);
260        if use_full_slot {
261            let prev_slot = PackingConstants::new(prev_name).slot();
262            Some(quote! { #packing::#prev_slot })
263        } else {
264            let prev_loc = PackingConstants::new(prev_name).location();
265            Some(quote! { #packing::#prev_loc.offset_slots })
266        }
267    } else {
268        None
269    };
270
271    let next_slot_ref = if idx + 1 < fields.len() {
272        let next_name = get_name(&fields[idx + 1]);
273        if use_full_slot {
274            let next_slot = PackingConstants::new(next_name).slot();
275            Some(quote! { #packing::#next_slot })
276        } else {
277            let next_loc = PackingConstants::new(next_name).location();
278            Some(quote! { #packing::#next_loc.offset_slots })
279        }
280    } else {
281        None
282    };
283
284    (prev_slot_ref, next_slot_ref)
285}
286
287/// Generate slot packing decision logic.
288///
289/// This function generates const expressions that determine whether two consecutive
290/// fields can be packed into the same storage slot, and if so, calculates the
291/// appropriate slot index and offset. Slot expressions use U256 arithmetic,
292/// while offset expressions use usize.
293pub(crate) fn gen_slot_packing_logic(
294    prev_ty: &Type,
295    curr_ty: &Type,
296    prev_slot_expr: TokenStream,
297    prev_offset_expr: TokenStream,
298) -> (TokenStream, TokenStream) {
299    // Helper for converting SLOTS to U256
300    let prev_layout_slots = quote! {
301        ::alloy::primitives::U256::from_limbs([<#prev_ty as crate::storage::StorableType>::SLOTS as u64, 0, 0, 0])
302    };
303    let curr_slots_to_end = quote! {
304        ::alloy::primitives::U256::from_limbs([<#curr_ty as crate::storage::StorableType>::SLOTS as u64, 0, 0, 0])
305            .saturating_sub(::alloy::primitives::U256::ONE)
306    };
307
308    // Compute packing decision at compile-time
309    let can_pack_expr = quote! {
310        #prev_offset_expr
311            + <#prev_ty as crate::storage::StorableType>::BYTES
312            + <#curr_ty as crate::storage::StorableType>::BYTES <= 32
313    };
314
315    let slot_expr = quote! {{
316        if #can_pack_expr {
317            #prev_slot_expr
318        } else {
319            // HACK: we leverage compiler evaluation checks to ensure that the full type can fit
320            // by computing the slot as: `CURR_SLOT = PREV_SLOT + PREV_LEN + (CURR_LEN - 1) - (CURR_LEN - 1)`
321            #prev_slot_expr
322                .checked_add(#prev_layout_slots).expect("slot overflow")
323                .checked_add(#curr_slots_to_end).expect("slot overflow")
324                .saturating_sub(#curr_slots_to_end)
325        }
326    }};
327
328    let offset_expr = quote! {{
329        if #can_pack_expr { #prev_offset_expr + <#prev_ty as crate::storage::StorableType>::BYTES } else { 0 }
330    }};
331
332    (slot_expr, offset_expr)
333}
334
335/// Generate a `LayoutCtx` expression for accessing a field.
336///
337/// This helper unifies the logic for choosing between `LayoutCtx::FULL` and
338/// `LayoutCtx::packed` based on compile-time slot comparison with neighboring fields.
339///
340/// A field uses `Packed` if it shares a slot with any neighboring field.
341pub(crate) fn gen_layout_ctx_expr(
342    ty: &Type,
343    is_manual_slot: bool,
344    slot_const_ref: TokenStream,
345    offset_const_ref: TokenStream,
346    prev_slot_const_ref: Option<TokenStream>,
347    next_slot_const_ref: Option<TokenStream>,
348) -> TokenStream {
349    if !is_manual_slot && (prev_slot_const_ref.is_some() || next_slot_const_ref.is_some()) {
350        // Check if this field shares a slot with prev or next field
351        let prev_check = prev_slot_const_ref.map(|prev| quote! { #slot_const_ref == #prev });
352        let next_check = next_slot_const_ref.map(|next| quote! { #slot_const_ref == #next });
353
354        let shares_slot_check = match (prev_check, next_check) {
355            (Some(prev), Some(next)) => quote! { (#prev || #next) },
356            (Some(prev), None) => prev,
357            (None, Some(next)) => next,
358            (None, None) => unreachable!(),
359        };
360
361        quote! {
362            {
363                if #shares_slot_check && <#ty as crate::storage::StorableType>::IS_PACKABLE {
364                    crate::storage::LayoutCtx::packed(#offset_const_ref)
365                } else {
366                    crate::storage::LayoutCtx::FULL
367                }
368            }
369        }
370    } else {
371        quote! { crate::storage::LayoutCtx::FULL }
372    }
373}
374
375/// Generate collision detection debug assertions for a field against all other fields.
376///
377/// This function generates runtime checks that verify storage slots don't overlap.
378/// Checks are generated for all fields (both manual and auto-assigned) to ensure
379/// comprehensive collision detection.
380pub(crate) fn gen_collision_check_fn(
381    idx: usize,
382    field: &LayoutField<'_>,
383    all_fields: &[LayoutField<'_>],
384) -> (Ident, TokenStream) {
385    fn gen_slot_count_expr(ty: &Type) -> TokenStream {
386        quote! { ::alloy::primitives::U256::from_limbs([<#ty as crate::storage::StorableType>::SLOTS as u64, 0, 0, 0]) }
387    }
388
389    let check_fn_name = format_ident!("__check_collision_{}", field.name);
390    let consts = PackingConstants::new(field.name);
391    let (slot_const, offset_const) = consts.into_tuple();
392    let (field_name, field_ty) = (field.name, field.ty);
393
394    let mut checks = TokenStream::new();
395
396    // Check against all other fields
397    for (other_idx, other_field) in all_fields.iter().enumerate() {
398        if other_idx == idx {
399            continue;
400        }
401
402        let other_consts = PackingConstants::new(other_field.name);
403        let (other_slot_const, other_offset_const) = other_consts.into_tuple();
404        let other_name = other_field.name;
405        let other_ty = other_field.ty;
406
407        // Generate slot count expressions
408        let current_count_expr = gen_slot_count_expr(field.ty);
409        let other_count_expr = gen_slot_count_expr(other_field.ty);
410
411        // Generate runtime assertion that checks for overlap
412        // Two fields collide if their slot ranges overlap AND (if same slot) their byte ranges overlap
413        checks.extend(quote! {
414            {
415                let slot = #slot_const;
416                let slot_end = slot.checked_add(#current_count_expr).expect("slot range overflow");
417                let other_slot = #other_slot_const;
418                let other_slot_end = other_slot.checked_add(#other_count_expr).expect("slot range overflow");
419
420                // Determine if there's no overlap:
421                // - If starting in different slots: rely on slot range check
422                // - If starting in same slot (packed fields): check byte ranges
423                let no_overlap = if slot == other_slot {
424                    let byte_end = #offset_const + <#field_ty as crate::storage::StorableType>::BYTES;
425                    let other_byte_end = #other_offset_const + <#other_ty as crate::storage::StorableType>::BYTES;
426                    byte_end <= #other_offset_const || other_byte_end <= #offset_const
427                } else {
428                    slot_end.le(&other_slot) || other_slot_end.le(&slot)
429                };
430
431                debug_assert!(
432                    no_overlap,
433                    "Storage slot collision: field `{}` (slot {:?}, offset {}) overlaps with field `{}` (slot {:?}, offset {})",
434                    stringify!(#field_name),
435                    slot,
436                    #offset_const,
437                    stringify!(#other_name),
438                    other_slot,
439                    #other_offset_const
440                );
441            }
442        });
443    }
444
445    let check_fn = quote! {
446        #[cfg(debug_assertions)]
447        #[inline(always)]
448        #[allow(non_snake_case)]
449        fn #check_fn_name() {
450            #checks
451        }
452    };
453
454    (check_fn_name, check_fn)
455}