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 supported_types: Vec<String>,
22}
23
24struct StorageAttributeArgs {
25 slot: u8,
26 description: Option<String>,
27 type_attr: Option<String>,
28}
29
30fn get_package_metadata(call_site_span: Span) -> Result<CargoMetadata, syn::Error> {
32 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
34 ".".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 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 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
147fn 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 let parser = syn::meta::parser(|meta| {
166 if meta.path.is_ident("slot") {
167 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 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 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
204fn 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) => { }
228 Err(e) => errors.push(e),
229 }
230 }
231
232 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 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
264fn 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
280fn 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 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#[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 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 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 let default_impl = match &mut input_struct.fields {
356 syn::Fields::Named(fields) => {
357 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 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}