tempo_precompiles_macros/lib.rs
1//! Procedural macros for generating type-safe EVM storage accessors.
2//!
3//! This crate provides:
4//! - `#[contract]` macro that transforms a storage schema into a fully-functional contract
5//! - `#[derive(Storable)]` macro for multi-slot storage structs
6//! - `storable_alloy_ints!` macro for generating alloy integer storage implementations
7//! - `storable_alloy_bytes!` macro for generating alloy FixedBytes storage implementations
8//! - `storable_rust_ints!` macro for generating standard Rust integer storage implementations
9
10mod layout;
11mod packing;
12mod storable;
13mod storable_primitives;
14mod storable_tests;
15mod utils;
16
17use alloy::primitives::U256;
18use proc_macro::TokenStream;
19use quote::quote;
20use syn::{
21 Data, DeriveInput, Expr, Fields, Ident, Token, Type, Visibility,
22 parse::{Parse, ParseStream, Parser},
23 parse_macro_input,
24 punctuated::Punctuated,
25};
26
27use crate::utils::extract_attributes;
28
29/// Configuration parsed from `#[contract(...)]` attribute arguments.
30struct ContractConfig {
31 /// Optional address expression for generating `Self::new()` and `Default`.
32 address: Option<Expr>,
33}
34
35impl Parse for ContractConfig {
36 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
37 if input.is_empty() {
38 return Ok(Self { address: None });
39 }
40
41 let ident: Ident = input.parse()?;
42 if ident != "addr" && ident != "address" {
43 return Err(syn::Error::new(
44 ident.span(),
45 "only `addr` attribute is supported",
46 ));
47 }
48
49 input.parse::<Token![=]>()?;
50 let address: Expr = input.parse()?;
51
52 Ok(Self {
53 address: Some(address),
54 })
55 }
56}
57
58const RESERVED: &[&str] = &["address", "storage", "msg_sender"];
59
60/// Transforms a struct that represents a storage layout into a contract with helper methods to
61/// easily interact with the EVM storage.
62/// Its packing and encoding schemes aim to be an exact representation of the storage model used by Solidity.
63///
64/// # Input: Storage Layout
65///
66/// ```ignore
67/// #[contract]
68/// pub struct TIP20Token {
69/// pub name: String,
70/// pub symbol: String,
71/// total_supply: U256,
72/// #[slot(10)]
73/// pub balances: Mapping<Address, U256>,
74/// #[slot(11)]
75/// pub allowances: Mapping<Address, Mapping<Address, U256>>,
76/// }
77/// ```
78///
79/// # Output: Contract with accessible storage via getter and setter methods.
80///
81/// The macro generates:
82/// 1. Transformed struct with generic parameters and runtime fields
83/// 2. Constructor: `__new(address, storage)`
84/// 3. Type-safe (private) getter and setter methods
85///
86/// # Requirements
87///
88/// - No duplicate slot assignments
89/// - Unique field names, excluding the reserved ones: `address`, `storage`, `msg_sender`.
90/// - All field types must implement `Storable`, and mapping keys must implement `StorageKey`.
91#[proc_macro_attribute]
92pub fn contract(attr: TokenStream, item: TokenStream) -> TokenStream {
93 let config = parse_macro_input!(attr as ContractConfig);
94 let input = parse_macro_input!(item as DeriveInput);
95
96 match gen_contract_output(input, config.address.as_ref()) {
97 Ok(tokens) => tokens.into(),
98 Err(err) => err.to_compile_error().into(),
99 }
100}
101
102/// Main code generation function with optional call trait generation
103fn gen_contract_output(
104 input: DeriveInput,
105 address: Option<&Expr>,
106) -> syn::Result<proc_macro2::TokenStream> {
107 let (ident, vis) = (input.ident.clone(), input.vis.clone());
108 let fields = parse_fields(input)?;
109
110 let storage_output = gen_contract_storage(&ident, &vis, &fields, address)?;
111 Ok(quote! { #storage_output })
112}
113
114/// Information extracted from a field in the storage schema
115#[derive(Debug)]
116struct FieldInfo {
117 name: Ident,
118 ty: Type,
119 slot: Option<U256>,
120 base_slot: Option<U256>,
121}
122
123/// Classification of a field based on its type
124#[derive(Debug, Clone, Copy)]
125enum FieldKind<'a> {
126 /// Fields with a direct slot allocation, either single or multi (`Slot<V>`).
127 Direct(&'a Type),
128 /// Mapping fields. Handles all nesting levels via recursive types.
129 Mapping { key: &'a Type, value: &'a Type },
130}
131
132fn parse_fields(input: DeriveInput) -> syn::Result<Vec<FieldInfo>> {
133 // Ensure no generic parameters on input
134 if !input.generics.params.is_empty() {
135 return Err(syn::Error::new_spanned(
136 &input.generics,
137 "Contract structs cannot have generic parameters",
138 ));
139 }
140
141 // Ensure struct with named fields
142 let named_fields = if let Data::Struct(data) = input.data
143 && let Fields::Named(fields) = data.fields
144 {
145 fields.named
146 } else {
147 return Err(syn::Error::new_spanned(
148 input.ident,
149 "Only structs with named fields are supported",
150 ));
151 };
152
153 // Parse extract attributes
154 named_fields
155 .into_iter()
156 .map(|field| {
157 let name = field
158 .ident
159 .as_ref()
160 .ok_or_else(|| syn::Error::new_spanned(&field, "Fields must have names"))?;
161
162 if RESERVED.contains(&name.to_string().as_str()) {
163 return Err(syn::Error::new_spanned(
164 name,
165 format!("Field name '{name}' is reserved"),
166 ));
167 }
168
169 let (slot, base_slot) = extract_attributes(&field.attrs)?;
170 Ok(FieldInfo {
171 name: name.to_owned(),
172 ty: field.ty,
173 slot,
174 base_slot,
175 })
176 })
177 .collect()
178}
179
180/// Main code generation function for storage accessors
181fn gen_contract_storage(
182 ident: &Ident,
183 vis: &Visibility,
184 fields: &[FieldInfo],
185 address: Option<&Expr>,
186) -> syn::Result<proc_macro2::TokenStream> {
187 // Generate the complete output
188 let allocated_fields = packing::allocate_slots(fields)?;
189 let transformed_struct = layout::gen_struct(ident, vis, &allocated_fields);
190 let storage_trait = layout::gen_contract_storage_impl(ident);
191 let constructor = layout::gen_constructor(ident, &allocated_fields, address);
192 let slots_module = layout::gen_slots_module(&allocated_fields);
193 let default_impl = if address.is_some() {
194 layout::gen_default_impl(ident)
195 } else {
196 proc_macro2::TokenStream::new()
197 };
198
199 let output = quote! {
200 #slots_module
201 #transformed_struct
202 #constructor
203 #storage_trait
204 #default_impl
205 };
206
207 Ok(output)
208}
209
210/// Derives the `Storable` trait for structs with named fields.
211///
212/// This macro generates implementations for loading and storing multi-slot
213/// struct layout in EVM storage.
214/// Its packing and encoding schemes aim to be an exact representation of
215/// the storage model used by Solidity.
216///
217/// # Requirements
218///
219/// - The struct must have named fields (not tuple structs or unit structs)
220/// - All fields must implement the `Storable` trait
221///
222/// # Generated Code
223///
224/// For each struct field, the macro generates sequential slot offsets.
225/// It implements the `Storable` trait methods:
226/// - `load` - Loads the struct from storage
227/// - `store` - Stores the struct to storage
228/// - `delete` - Uses default implementation (sets all slots to zero)
229///
230/// # Example
231///
232/// ```ignore
233/// use precompiles::storage::Storable;
234/// use alloy_primitives::{Address, U256};
235///
236/// #[derive(Storable)]
237/// pub struct RewardStream {
238/// pub funder: Address, // rel slot: 0 (20 bytes)
239/// pub start_time: u64, // rel slot: 0 (8 bytes)
240/// pub end_time: u64, // rel slot: 1 (8 bytes)
241/// pub rate_per_second_scaled: U256, // rel slot: 2 (32 bytes)
242/// pub amount_total: U256, // rel slot: 3 (32 bytes)
243/// }
244/// ```
245#[proc_macro_derive(Storable, attributes(storable_arrays))]
246pub fn derive_storage_block(input: TokenStream) -> TokenStream {
247 let input = parse_macro_input!(input as DeriveInput);
248
249 match storable::derive_impl(input) {
250 Ok(tokens) => tokens.into(),
251 Err(err) => err.to_compile_error().into(),
252 }
253}
254
255// -- STORAGE PRIMITIVES TRAIT IMPLEMENTATIONS -------------------------------------------
256
257/// Generate `StorableType` and `Storable` implementations for all standard integer types.
258///
259/// Generates implementations for all standard Rust integer types:
260/// u8/i8, u16/i16, u32/i32, u64/i64, u128/i128.
261///
262/// Each type gets:
263/// - `StorableType` impl with `BYTE_COUNT` constant
264/// - `Storable` impl with `load()`, `store()` methods
265/// - `StorageKey` impl for use as mapping keys
266/// - Auto-generated tests that verify round-trip conversions with random values
267#[proc_macro]
268pub fn storable_rust_ints(_input: TokenStream) -> TokenStream {
269 storable_primitives::gen_storable_rust_ints().into()
270}
271
272/// Generate `StorableType` and `Storable` implementations for alloy integer types.
273///
274/// Generates implementations for all alloy integer types (both signed and unsigned):
275/// U8/I8, U16/I16, U32/I32, U64/I64, U128/I128, U256/I256.
276///
277/// Each type gets:
278/// - `StorableType` impl with `BYTE_COUNT` constant
279/// - `Storable` impl with `load()`, `store()` methods
280/// - `StorageKey` impl for use as mapping keys
281/// - Auto-generated tests that verify round-trip conversions using alloy's `.random()` method
282#[proc_macro]
283pub fn storable_alloy_ints(_input: TokenStream) -> TokenStream {
284 storable_primitives::gen_storable_alloy_ints().into()
285}
286
287/// Generate `StorableType` and `Storable` implementations for alloy `FixedBytes<N>` types.
288///
289/// Generates implementations for all fixed-size byte arrays from `N = 1..32`
290/// All sizes fit within a single storage slot.
291///
292/// Each type gets:
293/// - `StorableType` impl with `BYTE_COUNT` constant
294/// - `Storable` impl with `load()`, `store()` methods
295/// - `StorageKey` impl for use as mapping keys
296/// - Auto-generated tests that verify round-trip conversions using alloy's `.random()` method
297///
298/// # Usage
299/// ```ignore
300/// storable_alloy_bytes!();
301/// ```
302#[proc_macro]
303pub fn storable_alloy_bytes(_input: TokenStream) -> TokenStream {
304 storable_primitives::gen_storable_alloy_bytes().into()
305}
306
307/// Generate comprehensive property tests for all storage types.
308///
309/// This macro generates:
310/// - Arbitrary function generators for all Rust and Alloy integer types
311/// - Arbitrary function generators for all `FixedBytes<N>` sizes `N = 1..32`
312/// - Property test invocations using the existing test body macros
313#[proc_macro]
314pub fn gen_storable_tests(_input: TokenStream) -> TokenStream {
315 storable_tests::gen_storable_tests().into()
316}
317
318/// Generate `Storable` implementations for fixed-size arrays of primitive types.
319///
320/// Generates implementations for arrays of sizes 1-32 for the following element types:
321/// - Rust integers: u8-u128, i8-i128
322/// - Alloy integers: U8-U256, I8-I256
323/// - Address
324/// - FixedBytes<20>, FixedBytes<32>
325///
326/// Each array gets:
327/// - `StorableType` impl with `LAYOUT = Layout::Slot`
328/// - `Storable`
329#[proc_macro]
330pub fn storable_arrays(_input: TokenStream) -> TokenStream {
331 storable_primitives::gen_storable_arrays().into()
332}
333
334/// Generate `Storable` implementations for nested arrays of small primitive types.
335///
336/// Generates implementations for nested arrays like `[[u8; 4]; 8]` where:
337/// - Inner arrays are small (2, 4, 8, 16 for u8; 2, 4, 8 for u16)
338/// - Total slot count ≤ 32
339#[proc_macro]
340pub fn storable_nested_arrays(_input: TokenStream) -> TokenStream {
341 storable_primitives::gen_nested_arrays().into()
342}
343
344// -- TEST HELPERS -------------------------------------------------------------
345
346/// Test helper macro for validating slots
347#[proc_macro]
348pub fn gen_test_fields_layout(input: TokenStream) -> TokenStream {
349 let input = proc_macro2::TokenStream::from(input);
350
351 // Parse comma-separated identifiers
352 let parser = syn::punctuated::Punctuated::<Ident, syn::Token![,]>::parse_terminated;
353 let idents = match parser.parse2(input) {
354 Ok(idents) => idents,
355 Err(err) => return err.to_compile_error().into(),
356 };
357
358 // Generate storage fields
359 let field_calls: Vec<_> = idents
360 .into_iter()
361 .map(|ident| {
362 let field_name = ident.to_string();
363 let const_name = field_name.to_uppercase();
364 let field_name = utils::to_camel_case(&field_name);
365 let slot_ident = Ident::new(&const_name, ident.span());
366 let offset_ident = Ident::new(&format!("{const_name}_OFFSET"), ident.span());
367 let bytes_ident = Ident::new(&format!("{const_name}_BYTES"), ident.span());
368
369 quote! {
370 RustStorageField::new(#field_name, slots::#slot_ident, slots::#offset_ident, slots::#bytes_ident)
371 }
372 })
373 .collect();
374
375 // Generate the final vec!
376 let output = quote! {
377 vec![#(#field_calls),*]
378 };
379
380 output.into()
381}
382
383/// Test helper macro for validating slots
384#[proc_macro]
385pub fn gen_test_fields_struct(input: TokenStream) -> TokenStream {
386 let input = proc_macro2::TokenStream::from(input);
387
388 // Parse comma-separated identifiers
389 let parser = |input: ParseStream<'_>| {
390 let base_slot: Expr = input.parse()?;
391 input.parse::<Token![,]>()?;
392 let fields = Punctuated::<Ident, Token![,]>::parse_terminated(input)?;
393 Ok((base_slot, fields))
394 };
395
396 let (base_slot, idents) = match Parser::parse2(parser, input) {
397 Ok(result) => result,
398 Err(err) => return err.to_compile_error().into(),
399 };
400
401 // Generate storage fields
402 let field_calls: Vec<_> = idents
403 .into_iter()
404 .map(|ident| {
405 let field_name = ident.to_string();
406 let const_name = field_name.to_uppercase();
407 let field_name = utils::to_camel_case(&field_name);
408 let slot_ident = Ident::new(&const_name, ident.span());
409 let offset_ident = Ident::new(&format!("{const_name}_OFFSET"), ident.span());
410 let loc_ident = Ident::new(&format!("{const_name}_LOC"), ident.span());
411 let bytes_ident = quote! {#loc_ident.size};
412
413 quote! {
414 RustStorageField::new(#field_name, #base_slot + #slot_ident, #offset_ident, #bytes_ident)
415 }
416 })
417 .collect();
418
419 // Generate the final vec!
420 let output = quote! {
421 vec![#(#field_calls),*]
422 };
423
424 output.into()
425}