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
140        // Generate byte count constants for each field
141        let bytes_expr = quote! { <#ty as crate::storage::StorableType>::BYTES };
142
143        // Generate slot and offset constants for each field
144        let (slot_expr, offset_expr) = match &field.assigned_slot {
145            // Manual slot assignment always has offset 0
146            SlotAssignment::Manual(manual_slot) => {
147                let hex_value = format!("{manual_slot}_U256");
148                let slot_lit = syn::LitInt::new(&hex_value, proc_macro2::Span::call_site());
149                let slot_expr = quote! { ::alloy::primitives::uint!(#slot_lit) };
150                (slot_expr, quote! { 0 })
151            }
152            // Auto-assignment computes slot/offset using const expressions
153            SlotAssignment::Auto { base_slot, .. } => {
154                let output = if let Some(current_base) = current_base_slot {
155                    // Fields that share the same base compute their slots based on the previous field
156                    if current_base.assigned_slot.ref_slot() == field.assigned_slot.ref_slot() {
157                        let (prev_slot, prev_offset) =
158                            PackingConstants::new(current_base.name).into_tuple();
159                        gen_slot_packing_logic(
160                            current_base.ty,
161                            field.ty,
162                            quote! { #prev_slot },
163                            quote! { #prev_offset },
164                        )
165                    }
166                    // If a new base is adopted, start from the base slot and offset 0
167                    else {
168                        let limbs = *base_slot.as_limbs();
169                        (
170                            quote! { ::alloy::primitives::U256::from_limbs([#(#limbs),*]) },
171                            quote! { 0 },
172                        )
173                    }
174                }
175                // First field always starts at slot 0 and offset 0
176                else {
177                    (quote! { ::alloy::primitives::U256::ZERO }, quote! { 0 })
178                };
179                // update cache
180                current_base_slot = Some(field);
181                output
182            }
183        };
184
185        // Generate slot constant without suffix (U256) and offset constant (usize)
186        constants.extend(quote! {
187            pub const #slot_const: ::alloy::primitives::U256 = #slot_expr;
188            pub const #offset_const: usize = #offset_expr;
189        });
190
191        // For the `Storable` macro, also generate the location constant
192        // NOTE: `slot_const` refers to the slot offset of the struct field relative to the struct's base slot.
193        // Because of that it is safe to use the usize -> U256 conversion (a struct will never have 2**64 fields).
194        if gen_location {
195            constants.extend(quote! {
196                pub const #loc_const: crate::storage::packing::FieldLocation =
197                    crate::storage::packing::FieldLocation::new(#slot_const.as_limbs()[0] as usize, #offset_const, #bytes_expr);
198            });
199        }
200
201        // generate constants used in tests for solidity layout compatibility assertions
202        #[cfg(debug_assertions)]
203        {
204            let bytes_const = format_ident!("{slot_const}_BYTES");
205            constants.extend(quote! { pub const #bytes_const: usize = #bytes_expr; });
206        }
207    }
208
209    constants
210}
211
212/// Classify a field based on its type.
213///
214/// Determines if a field is a direct value or a mapping.
215/// Nested mappings like `Mapping<K, Mapping<K2, V>>` are handled automatically
216/// since the value type includes the full nested type.
217pub(crate) fn classify_field_type(ty: &Type) -> syn::Result<FieldKind<'_>> {
218    use crate::utils::extract_mapping_types;
219
220    // Check if it's a mapping (mappings have fundamentally different API)
221    if let Some((key_ty, value_ty)) = extract_mapping_types(ty) {
222        return Ok(FieldKind::Mapping {
223            key: key_ty,
224            value: value_ty,
225        });
226    }
227
228    // All non-mapping fields use the same accessor pattern
229    Ok(FieldKind::Direct(ty))
230}
231
232/// Helper to compute prev and next slot constant references for a field at a given index.
233///
234/// Generic over the field type - uses a closure to extract the field name.
235pub(crate) fn get_neighbor_slot_refs<T, F>(
236    idx: usize,
237    fields: &[T],
238    packing: &Ident,
239    get_name: F,
240) -> (Option<TokenStream>, Option<TokenStream>)
241where
242    F: Fn(&T) -> &Ident,
243{
244    let prev_slot_ref = if idx > 0 {
245        let prev_name = get_name(&fields[idx - 1]);
246        let prev_slot = PackingConstants::new(prev_name).location();
247        Some(quote! { #packing::#prev_slot.offset_slots })
248    } else {
249        None
250    };
251
252    let next_slot_ref = if idx + 1 < fields.len() {
253        let next_name = get_name(&fields[idx + 1]);
254        let next_slot = PackingConstants::new(next_name).location();
255        Some(quote! { #packing::#next_slot.offset_slots })
256    } else {
257        None
258    };
259
260    (prev_slot_ref, next_slot_ref)
261}
262
263/// Generate slot packing decision logic.
264///
265/// This function generates const expressions that determine whether two consecutive
266/// fields can be packed into the same storage slot, and if so, calculates the
267/// appropriate slot index and offset. Slot expressions use U256 arithmetic,
268/// while offset expressions use usize.
269pub(crate) fn gen_slot_packing_logic(
270    prev_ty: &Type,
271    curr_ty: &Type,
272    prev_slot_expr: TokenStream,
273    prev_offset_expr: TokenStream,
274) -> (TokenStream, TokenStream) {
275    // Helper for converting SLOTS to U256
276    let prev_layout_slots = quote! {
277        ::alloy::primitives::U256::from_limbs([<#prev_ty as crate::storage::StorableType>::SLOTS as u64, 0, 0, 0])
278    };
279
280    // Compute packing decision at compile-time
281    let can_pack_expr = quote! {
282        #prev_offset_expr
283            + <#prev_ty as crate::storage::StorableType>::BYTES
284            + <#curr_ty as crate::storage::StorableType>::BYTES <= 32
285    };
286
287    let slot_expr = quote! {{
288        if #can_pack_expr { #prev_slot_expr } else { #prev_slot_expr.checked_add(#prev_layout_slots).expect("slot overflow") }
289    }};
290
291    let offset_expr = quote! {{
292        if #can_pack_expr { #prev_offset_expr + <#prev_ty as crate::storage::StorableType>::BYTES } else { 0 }
293    }};
294
295    (slot_expr, offset_expr)
296}
297
298/// Generate a `LayoutCtx` expression for accessing a field.
299///
300/// This helper unifies the logic for choosing between `LayoutCtx::FULL` and
301/// `LayoutCtx::packed` based on compile-time slot comparison with neighboring fields.
302///
303/// A field uses `Packed` if it shares a slot with any neighboring field.
304pub(crate) fn gen_layout_ctx_expr(
305    ty: &Type,
306    is_manual_slot: bool,
307    slot_const_ref: TokenStream,
308    offset_const_ref: TokenStream,
309    prev_slot_const_ref: Option<TokenStream>,
310    next_slot_const_ref: Option<TokenStream>,
311) -> TokenStream {
312    if !is_manual_slot && (prev_slot_const_ref.is_some() || next_slot_const_ref.is_some()) {
313        // Check if this field shares a slot with prev or next field
314        let prev_check = prev_slot_const_ref.map(|prev| quote! { #slot_const_ref == #prev });
315        let next_check = next_slot_const_ref.map(|next| quote! { #slot_const_ref == #next });
316
317        let shares_slot_check = match (prev_check, next_check) {
318            (Some(prev), Some(next)) => quote! { (#prev || #next) },
319            (Some(prev), None) => prev,
320            (None, Some(next)) => next,
321            (None, None) => unreachable!(),
322        };
323
324        quote! {
325            {
326                if #shares_slot_check && <#ty as crate::storage::StorableType>::IS_PACKABLE {
327                    crate::storage::LayoutCtx::packed(#offset_const_ref)
328                } else {
329                    crate::storage::LayoutCtx::FULL
330                }
331            }
332        }
333    } else {
334        quote! { crate::storage::LayoutCtx::FULL }
335    }
336}
337
338// TODO(rusowsky): fully embrace `fn gen_layout_ctx_expr` to reduce gas usage.
339// Note that this requires a hardfork and must be properly coordinated.
340
341/// Generate a `LayoutCtx` expression for accessing a field.
342///
343/// Despite we could deterministically know if a field shares its slot with a neighbour, we
344/// treat all primitive types as packable for backward-compatibility reasons.
345pub(crate) fn gen_layout_ctx_expr_inefficient(
346    ty: &Type,
347    is_manual_slot: bool,
348    _slot_const_ref: TokenStream,
349    offset_const_ref: TokenStream,
350    _prev_slot_const_ref: Option<TokenStream>,
351    _next_slot_const_ref: Option<TokenStream>,
352) -> TokenStream {
353    if !is_manual_slot {
354        quote! {
355            if <#ty as crate::storage::StorableType>::IS_PACKABLE {
356                crate::storage::LayoutCtx::packed(#offset_const_ref)
357            } else {
358                crate::storage::LayoutCtx::FULL
359            }
360        }
361    } else {
362        quote! { crate::storage::LayoutCtx::FULL }
363    }
364}
365
366/// Generate collision detection debug assertions for a field against all other fields.
367///
368/// This function generates runtime checks that verify storage slots don't overlap.
369/// Only manual slot assignments are checked, as auto-assigned slots are guaranteed
370/// not to collide by the allocation algorithm.
371pub(crate) fn gen_collision_check_fn(
372    idx: usize,
373    field: &LayoutField<'_>,
374    all_fields: &[LayoutField<'_>],
375) -> Option<(Ident, TokenStream)> {
376    fn gen_slot_count_expr(ty: &Type) -> TokenStream {
377        quote! { ::alloy::primitives::U256::from_limbs([<#ty as crate::storage::StorableType>::SLOTS as u64, 0, 0, 0]) }
378    }
379
380    // Only check explicit slot assignments against other fields
381    if let SlotAssignment::Manual(_) = field.assigned_slot {
382        let field_name = field.name;
383        let check_fn_name = format_ident!("__check_collision_{}", field_name);
384        let slot_const = PackingConstants::new(field.name).slot();
385
386        let mut checks = TokenStream::new();
387
388        // Check against all other fields
389        for (other_idx, other_field) in all_fields.iter().enumerate() {
390            if other_idx == idx {
391                continue;
392            }
393
394            let other_slot_const = PackingConstants::new(other_field.name).slot();
395            let other_name = other_field.name;
396
397            // Generate slot count expressions
398            let current_count_expr = gen_slot_count_expr(field.ty);
399            let other_count_expr = gen_slot_count_expr(other_field.ty);
400
401            // Generate runtime assertion that checks for overlap
402            checks.extend(quote! {
403                {
404                    let slot = #slot_const;
405                    let slot_end = slot + #current_count_expr;
406                    let other_slot = #other_slot_const;
407                    let other_end = other_slot + #other_count_expr;
408
409                    let no_overlap = slot_end.le(&other_slot) || other_end.le(&slot);
410                    debug_assert!(
411                        no_overlap,
412                        "Storage slot collision: field `{}` (slot {:?}) overlaps with field `{}` (slot {:?})",
413                        stringify!(#field_name),
414                        slot,
415                        stringify!(#other_name),
416                        other_slot
417                    );
418                }
419            });
420        }
421
422        let check_fn = quote! {
423            #[cfg(debug_assertions)]
424            #[inline(always)]
425            fn #check_fn_name() {
426                #checks
427            }
428        };
429
430        Some((check_fn_name, check_fn))
431    } else {
432        None
433    }
434}