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 storage structs and `#[repr(u8)]` unit enums
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 and `#[repr(u8)]` unit enums.
211///
212/// This macro generates implementations for loading and storing multi-slot
213/// struct layouts and single-byte unit enums 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/// - Structs must have named fields (not tuple structs or unit structs)
220/// - Unit enums must be annotated with `#[repr(u8)]`
221/// - Unit enums must include a zero-valued variant so fresh or deleted storage stays readable
222/// - All stored field types must implement the `Storable` trait
223///
224/// # Generated Code
225///
226/// For structs, the macro generates sequential slot offsets for each field.
227/// For `#[repr(u8)]` unit enums, it loads and stores the enum through `u8`.
228/// In both cases it implements the `Storable` trait methods:
229/// - `load` - Loads the struct from storage
230/// - `store` - Stores the struct to storage
231/// - `delete` - Removes the value from storage using the type-specific semantics
232///
233/// # Example
234///
235/// ```ignore
236/// use precompiles::storage::Storable;
237/// use alloy_primitives::{Address, U256};
238///
239/// #[derive(Storable)]
240/// pub struct RewardStream {
241/// pub funder: Address, // rel slot: 0 (20 bytes)
242/// pub start_time: u64, // rel slot: 0 (8 bytes)
243/// pub end_time: u64, // rel slot: 1 (8 bytes)
244/// pub rate_per_second_scaled: U256, // rel slot: 2 (32 bytes)
245/// pub amount_total: U256, // rel slot: 3 (32 bytes)
246/// }
247/// ```
248#[proc_macro_derive(Storable, attributes(storable_arrays))]
249pub fn derive_storage_block(input: TokenStream) -> TokenStream {
250 let input = parse_macro_input!(input as DeriveInput);
251
252 match storable::derive_impl(input) {
253 Ok(tokens) => tokens.into(),
254 Err(err) => err.to_compile_error().into(),
255 }
256}
257
258// -- STORAGE PRIMITIVES TRAIT IMPLEMENTATIONS -------------------------------------------
259
260/// Generate `StorableType` and `Storable` implementations for all standard integer types.
261///
262/// Generates implementations for all standard Rust integer types:
263/// u8/i8, u16/i16, u32/i32, u64/i64, u128/i128.
264///
265/// Each type gets:
266/// - `StorableType` impl with `BYTE_COUNT` constant
267/// - `Storable` impl with `load()`, `store()` methods
268/// - `StorageKey` impl for use as mapping keys
269/// - Auto-generated tests that verify round-trip conversions with random values
270#[proc_macro]
271pub fn storable_rust_ints(_input: TokenStream) -> TokenStream {
272 storable_primitives::gen_storable_rust_ints().into()
273}
274
275/// Generate `StorableType` and `Storable` implementations for alloy integer types.
276///
277/// Generates implementations for all alloy integer types (both signed and unsigned):
278/// U8/I8, U16/I16, U32/I32, U64/I64, U128/I128, U256/I256.
279///
280/// Each type gets:
281/// - `StorableType` impl with `BYTE_COUNT` constant
282/// - `Storable` impl with `load()`, `store()` methods
283/// - `StorageKey` impl for use as mapping keys
284/// - Auto-generated tests that verify round-trip conversions using alloy's `.random()` method
285#[proc_macro]
286pub fn storable_alloy_ints(_input: TokenStream) -> TokenStream {
287 storable_primitives::gen_storable_alloy_ints().into()
288}
289
290/// Generate `StorableType` and `Storable` implementations for alloy `FixedBytes<N>` types.
291///
292/// Generates implementations for all fixed-size byte arrays from `N = 1..32`
293/// All sizes fit within a single storage slot.
294///
295/// Each type gets:
296/// - `StorableType` impl with `BYTE_COUNT` constant
297/// - `Storable` impl with `load()`, `store()` methods
298/// - `StorageKey` impl for use as mapping keys
299/// - Auto-generated tests that verify round-trip conversions using alloy's `.random()` method
300///
301/// # Usage
302/// ```ignore
303/// storable_alloy_bytes!();
304/// ```
305#[proc_macro]
306pub fn storable_alloy_bytes(_input: TokenStream) -> TokenStream {
307 storable_primitives::gen_storable_alloy_bytes().into()
308}
309
310/// Generate comprehensive property tests for all storage types.
311///
312/// This macro generates:
313/// - Arbitrary function generators for all Rust and Alloy integer types
314/// - Arbitrary function generators for all `FixedBytes<N>` sizes `N = 1..32`
315/// - Property test invocations using the existing test body macros
316#[proc_macro]
317pub fn gen_storable_tests(_input: TokenStream) -> TokenStream {
318 storable_tests::gen_storable_tests().into()
319}
320
321/// Generate `Storable` implementations for fixed-size arrays of primitive types.
322///
323/// Generates implementations for arrays of sizes 1-32 for the following element types:
324/// - Rust integers: u8-u128, i8-i128
325/// - Alloy integers: U8-U256, I8-I256
326/// - Address
327/// - FixedBytes<20>, FixedBytes<32>
328///
329/// Each array gets:
330/// - `StorableType` impl with `LAYOUT = Layout::Slot`
331/// - `Storable`
332#[proc_macro]
333pub fn storable_arrays(_input: TokenStream) -> TokenStream {
334 storable_primitives::gen_storable_arrays().into()
335}
336
337/// Generate `Storable` implementations for nested arrays of small primitive types.
338///
339/// Generates implementations for nested arrays like `[[u8; 4]; 8]` where:
340/// - Inner arrays are small (2, 4, 8, 16 for u8; 2, 4, 8 for u16)
341/// - Total slot count ≤ 32
342#[proc_macro]
343pub fn storable_nested_arrays(_input: TokenStream) -> TokenStream {
344 storable_primitives::gen_nested_arrays().into()
345}
346
347// -- TEST HELPERS -------------------------------------------------------------
348
349/// Test helper macro for validating slots
350#[proc_macro]
351pub fn gen_test_fields_layout(input: TokenStream) -> TokenStream {
352 let input = proc_macro2::TokenStream::from(input);
353
354 // Parse comma-separated identifiers
355 let parser = syn::punctuated::Punctuated::<Ident, syn::Token![,]>::parse_terminated;
356 let idents = match parser.parse2(input) {
357 Ok(idents) => idents,
358 Err(err) => return err.to_compile_error().into(),
359 };
360
361 // Generate storage fields
362 let field_calls: Vec<_> = idents
363 .into_iter()
364 .map(|ident| {
365 let field_name = ident.to_string();
366 let const_name = field_name.to_uppercase();
367 let field_name = utils::to_camel_case(&field_name);
368 let slot_ident = Ident::new(&const_name, ident.span());
369 let offset_ident = Ident::new(&format!("{const_name}_OFFSET"), ident.span());
370 let bytes_ident = Ident::new(&format!("{const_name}_BYTES"), ident.span());
371
372 quote! {
373 RustStorageField::new(#field_name, slots::#slot_ident, slots::#offset_ident, slots::#bytes_ident)
374 }
375 })
376 .collect();
377
378 // Generate the final vec!
379 let output = quote! {
380 vec![#(#field_calls),*]
381 };
382
383 output.into()
384}
385
386/// Test helper macro for validating slots
387#[proc_macro]
388pub fn gen_test_fields_struct(input: TokenStream) -> TokenStream {
389 let input = proc_macro2::TokenStream::from(input);
390
391 // Parse comma-separated identifiers
392 let parser = |input: ParseStream<'_>| {
393 let base_slot: Expr = input.parse()?;
394 input.parse::<Token![,]>()?;
395 let fields = Punctuated::<Ident, Token![,]>::parse_terminated(input)?;
396 Ok((base_slot, fields))
397 };
398
399 let (base_slot, idents) = match Parser::parse2(parser, input) {
400 Ok(result) => result,
401 Err(err) => return err.to_compile_error().into(),
402 };
403
404 // Generate storage fields
405 let field_calls: Vec<_> = idents
406 .into_iter()
407 .map(|ident| {
408 let field_name = ident.to_string();
409 let const_name = field_name.to_uppercase();
410 let field_name = utils::to_camel_case(&field_name);
411 let slot_ident = Ident::new(&const_name, ident.span());
412 let offset_ident = Ident::new(&format!("{const_name}_OFFSET"), ident.span());
413 let loc_ident = Ident::new(&format!("{const_name}_LOC"), ident.span());
414 let bytes_ident = quote! {#loc_ident.size};
415
416 quote! {
417 RustStorageField::new(#field_name, #base_slot + #slot_ident, #offset_ident, #bytes_ident)
418 }
419 })
420 .collect();
421
422 // Generate the final vec!
423 let output = quote! {
424 vec![#(#field_calls),*]
425 };
426
427 output.into()
428}