miden_base_macros/
lib.rs

1use std::{fs, str::FromStr};
2
3use account_component_metadata::AccountComponentMetadataBuilder;
4use miden_objects::{account::AccountType, utils::Serializable};
5use proc_macro::Span;
6use proc_macro2::Literal;
7use quote::quote;
8use semver::Version;
9use syn::{parse_macro_input, spanned::Spanned};
10use toml::Value;
11
12extern crate proc_macro;
13
14mod account_component_metadata;
15
16struct CargoMetadata {
17    name: String,
18    version: Version,
19    description: String,
20    /// Custom Miden field: list of supported account types
21    supported_types: Vec<String>,
22}
23
24struct StorageAttributeArgs {
25    slot: u8,
26    description: Option<String>,
27    type_attr: Option<String>,
28}
29
30/// Finds and parses Cargo.toml to extract package metadata.
31fn get_package_metadata(call_site_span: Span) -> Result<CargoMetadata, syn::Error> {
32    // Use CARGO_MANIFEST_DIR to find the Cargo.toml
33    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
34        // Fallback for rust-analyzer or other tools
35        ".".to_string()
36    });
37
38    let current_dir = std::path::Path::new(&manifest_dir);
39
40    let cargo_toml_path = current_dir.join("Cargo.toml");
41    if !cargo_toml_path.is_file() {
42        // Return default metadata for rust-analyzer or when Cargo.toml is not found
43        return Ok(CargoMetadata {
44            name: String::new(),
45            version: Version::new(0, 0, 1),
46            description: String::new(),
47            supported_types: vec![],
48        });
49    }
50
51    let cargo_toml_content = fs::read_to_string(&cargo_toml_path).map_err(|e| {
52        syn::Error::new(
53            call_site_span.into(),
54            format!("Failed to read {}: {}", cargo_toml_path.display(), e),
55        )
56    })?;
57    let cargo_toml: Value = cargo_toml_content.parse::<Value>().map_err(|e| {
58        syn::Error::new(
59            call_site_span.into(),
60            format!("Failed to parse {}: {}", cargo_toml_path.display(), e),
61        )
62    })?;
63
64    let package_table = cargo_toml.get("package").ok_or_else(|| {
65        syn::Error::new(
66            call_site_span.into(),
67            format!(
68                "Cargo.toml ({}) does not contain a [package] table",
69                cargo_toml_path.display()
70            ),
71        )
72    })?;
73
74    let name = package_table
75        .get("name")
76        .and_then(|n| n.as_str())
77        .map(String::from)
78        .ok_or_else(|| {
79            syn::Error::new(
80                call_site_span.into(),
81                format!("Missing 'name' field in [package] table of {}", cargo_toml_path.display()),
82            )
83        })?;
84
85    let version_str = package_table
86        .get("version")
87        .and_then(|v| v.as_str())
88        .or_else(|| {
89            let base = env!("CARGO_MANIFEST_DIR");
90            if base.ends_with(cargo_toml_path.parent().unwrap().to_str().unwrap()) {
91                // Special case for tests within this crate where version.workspace = true
92                Some("0.0.0")
93            } else {
94                None
95            }
96        })
97        .ok_or_else(|| {
98            syn::Error::new(
99                call_site_span.into(),
100                format!(
101                    "Missing 'version' field in [package] table of {} (version.workspace = true \
102                     is not yet supported for external crates)",
103                    cargo_toml_path.display()
104                ),
105            )
106        })?;
107
108    let version = Version::parse(version_str).map_err(|e| {
109        syn::Error::new(
110            call_site_span.into(),
111            format!(
112                "Failed to parse version '{}' from {}: {}",
113                version_str,
114                cargo_toml_path.display(),
115                e
116            ),
117        )
118    })?;
119
120    let description = package_table
121        .get("description")
122        .and_then(|d| d.as_str())
123        .map(String::from)
124        .unwrap_or_default();
125
126    let supported_types = cargo_toml
127        .get("package")
128        .and_then(|pkg| pkg.get("metadata"))
129        .and_then(|m| m.get("miden"))
130        .and_then(|m| m.get("supported-types"))
131        .and_then(|st| st.as_array())
132        .map(|arr| {
133            arr.iter()
134                .filter_map(|v| v.as_str().map(|s| s.to_string()))
135                .collect::<Vec<String>>()
136        })
137        .unwrap_or_default();
138
139    Ok(CargoMetadata {
140        name,
141        version,
142        description,
143        supported_types,
144    })
145}
146
147/// Parses the arguments inside a `#[storage(...)]` attribute.
148fn parse_storage_attribute(
149    attr: &syn::Attribute,
150) -> Result<Option<StorageAttributeArgs>, syn::Error> {
151    if !attr.path().is_ident("storage") {
152        return Ok(None);
153    }
154
155    let mut slot_value = None;
156    let mut description_value = None;
157    let mut type_value = None;
158
159    let list = match &attr.meta {
160        syn::Meta::List(list) => list,
161        _ => return Err(syn::Error::new(attr.span(), "Expected #[storage(...)]")),
162    };
163
164    // Use a custom parser with parse_args_with to handle mixed formats
165    let parser = syn::meta::parser(|meta| {
166        if meta.path.is_ident("slot") {
167            // Handle slot(N) format
168            // meta.input is the token stream *inside* the parentheses
169            let value_stream;
170            syn::parenthesized!(value_stream in meta.input);
171            let lit: syn::LitInt = value_stream.parse()?;
172            slot_value = Some(lit.base10_parse::<u8>()?);
173            Ok(())
174        } else if meta.path.is_ident("description") {
175            // Handle description = "..." format
176            let value = meta.value()?;
177            let lit: syn::LitStr = value.parse()?;
178            description_value = Some(lit.value());
179            Ok(())
180        } else if meta.path.is_ident("type") {
181            // Handle type = "..." format
182            let value = meta.value()?;
183            let lit: syn::LitStr = value.parse()?;
184            type_value = Some(lit.value());
185            Ok(())
186        } else {
187            Err(meta.error("unrecognized storage attribute argument"))
188        }
189    });
190
191    list.parse_args_with(parser)?;
192
193    let slot = slot_value.ok_or_else(|| {
194        syn::Error::new(attr.span(), "missing required `slot(N)` argument in `storage` attribute")
195    })?;
196
197    Ok(Some(StorageAttributeArgs {
198        slot,
199        description: description_value,
200        type_attr: type_value,
201    }))
202}
203
204/// Processes struct fields, extracts storage info, updates metadata, and generates Default impl parts.
205fn process_fields(
206    fields: &mut syn::FieldsNamed,
207    builder: &mut AccountComponentMetadataBuilder,
208) -> Result<Vec<proc_macro2::TokenStream>, syn::Error> {
209    let mut field_inits = Vec::new();
210    let mut errors = Vec::new();
211
212    for field in fields.named.iter_mut() {
213        let field_name = field.ident.as_ref().expect("Named field must have an identifier");
214        let field_type = &field.ty;
215        let mut storage_args = None;
216        let mut attr_indices_to_remove = Vec::new();
217
218        for (attr_idx, attr) in field.attrs.iter().enumerate() {
219            match parse_storage_attribute(attr) {
220                Ok(Some(args)) => {
221                    if storage_args.is_some() {
222                        errors.push(syn::Error::new(attr.span(), "duplicate `storage` attribute"));
223                    }
224                    storage_args = Some(args);
225                    attr_indices_to_remove.push(attr_idx);
226                }
227                Ok(None) => { /* Not a storage attribute */ }
228                Err(e) => errors.push(e),
229            }
230        }
231
232        // Remove storage attributes from the original struct definition
233        for (removed_count, idx_to_remove) in attr_indices_to_remove.into_iter().enumerate() {
234            field.attrs.remove(idx_to_remove - removed_count);
235        }
236
237        if let Some(args) = storage_args {
238            let slot = args.slot;
239            field_inits.push(quote! {
240                #field_name: #field_type { slot: #slot }
241            });
242
243            builder.add_storage_entry(
244                &field_name.to_string(),
245                args.description,
246                args.slot,
247                field_type,
248                args.type_attr,
249            );
250        } else {
251            // We require all fields to have `#[storage]`.
252            errors
253                .push(syn::Error::new(field.span(), "field is missing the `#[storage]` attribute"));
254        }
255    }
256
257    if let Some(first_error) = errors.into_iter().next() {
258        Err(first_error)
259    } else {
260        Ok(field_inits)
261    }
262}
263
264/// Generates the `impl Default for #struct_name { ... }` block.
265fn generate_default_impl(
266    struct_name: &syn::Ident,
267    field_inits: &[proc_macro2::TokenStream],
268) -> proc_macro2::TokenStream {
269    quote! {
270        impl Default for #struct_name {
271            fn default() -> Self {
272                Self {
273                    #(#field_inits),*
274                }
275            }
276        }
277    }
278}
279
280/// Generates the static byte array containing serialized metadata with the `#[link_section]` attribute.
281fn generate_link_section(metadata_bytes: &[u8]) -> proc_macro2::TokenStream {
282    let link_section_bytes_len = metadata_bytes.len();
283    let encoded_bytes_str = Literal::byte_string(metadata_bytes);
284
285    quote! {
286        #[unsafe(
287            // to test it in the integration(this crate) tests the section name needs to make mach-o section
288            // specifier happy and to have "segment and section separated by comma"
289            link_section = "rodata,miden_account"
290        )]
291        #[doc(hidden)]
292        #[allow(clippy::octal_escapes)]
293        pub static __MIDEN_ACCOUNT_COMPONENT_METADATA_BYTES: [u8; #link_section_bytes_len] = *#encoded_bytes_str;
294    }
295}
296
297/// Account component procedural macro.
298///
299/// Derives `Default` for the struct and generates AccountComponentTemplate, serializes it into a
300/// static byte array `__MIDEN_ACCOUNT_COMPONENT_METADATA_BYTES` containing serialized metadata
301/// placed in a specific link section.
302///
303/// ```ignore
304/// #[component]
305/// struct TestComponent {
306///    #[storage(
307///         slot(0),
308///         description = "test value",
309///         type = "auth::rpo_falcon512::pub_key"
310///     )]
311///     owner_public_key: Value,
312///
313///     #[storage(slot(1), description = "test map")]
314///     foo_map: StorageMap,
315///
316///     #[storage(slot(2))]
317///     without_description: Value,
318/// }
319/// ```
320#[proc_macro_attribute]
321pub fn component(
322    _attr: proc_macro::TokenStream,
323    item: proc_macro::TokenStream,
324) -> proc_macro::TokenStream {
325    let call_site_span = Span::call_site();
326
327    // Contains the struct with #[storage] attributes removed
328    let mut input_struct = parse_macro_input!(item as syn::ItemStruct);
329    let struct_name = &input_struct.ident;
330
331    let metadata = match get_package_metadata(call_site_span) {
332        Ok(m) => m,
333        Err(e) => return e.to_compile_error().into(),
334    };
335
336    let mut acc_builder =
337        AccountComponentMetadataBuilder::new(metadata.name, metadata.version, metadata.description);
338
339    // Populate supported account types from Cargo.toml
340    for st in &metadata.supported_types {
341        match AccountType::from_str(st) {
342            Ok(at) => acc_builder.add_supported_type(at),
343            Err(err) => {
344                return syn::Error::new(
345                    call_site_span.into(),
346                    format!("Invalid account type '{st}' in supported-types: {err}"),
347                )
348                .to_compile_error()
349                .into()
350            }
351        }
352    }
353
354    // Handle different field types
355    let default_impl = match &mut input_struct.fields {
356        syn::Fields::Named(fields) => {
357            // Process fields: extract storage info, generate Default parts, update builder
358            let field_inits = match process_fields(fields, &mut acc_builder) {
359                Ok(inits) => inits,
360                Err(e) => return e.to_compile_error().into(),
361            };
362            generate_default_impl(struct_name, &field_inits)
363        }
364        syn::Fields::Unit => {
365            // For unit structs, generate simple Default impl
366            quote! {
367                impl Default for #struct_name {
368                    fn default() -> Self {
369                        Self
370                    }
371                }
372            }
373        }
374        _ => {
375            return syn::Error::new(
376                input_struct.fields.span(),
377                "The `component` macro only supports unit structs or structs with named fields.",
378            )
379            .to_compile_error()
380            .into();
381        }
382    };
383
384    let acc_component_metadata_bytes = acc_builder.build().to_bytes();
385
386    let link_section = generate_link_section(&acc_component_metadata_bytes);
387
388    let output = quote! {
389        #input_struct
390        #default_impl
391        #link_section
392    };
393
394    proc_macro::TokenStream::from(output)
395}