use std::{
collections::{BTreeSet, HashMap},
env,
};
use heck::{ToKebabCase, ToSnakeCase};
use miden_project::TargetType;
use miden_protocol::utils::serde::Serializable;
use midenc_frontend_wasm_metadata::FrontendMetadata;
use proc_macro::Span;
use proc_macro2::{Ident, Literal, Span as Span2, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use syn::{
Attribute, FnArg, ImplItem, ImplItemFn, ItemImpl, ItemStruct, ItemTrait, PathArguments,
ReturnType, TraitItem, TraitItemFn, Type, spanned::Spanned,
};
pub(crate) use crate::component_macro::storage::typecheck_storage_field;
use crate::{
account_component_metadata::AccountComponentMetadataBuilder,
boilerplate::runtime_boilerplate,
component_macro::{
generate_wit::{ComponentWitSpec, build_component_wit, write_component_wit_file},
storage::process_storage_fields,
},
dependency_ref::{DependencyRef, DependencyRefArgs},
types::{
ExportedTypeDef, ExportedTypeKind, TypeRef, map_type_to_type_ref, registered_export_types,
},
util::generate_frontend_link_section,
};
mod generate_wit;
mod sibling;
mod storage;
const CORE_TYPES_PACKAGE: &str = "miden:base/core-types@1.0.0";
const AUTH_SCRIPT_ATTR: &str = "auth_script";
const AUTH_SCRIPT_MARKER_ATTR: &str = "miden_auth_script_requires_component";
const COMPONENT_TRAIT_MARKER_CONST: &str = "__MIDEN_COMPONENT_TRAIT_MARKER";
const COMPONENT_STORAGE_MARKER_CONST: &str = "__MIDEN_COMPONENT_STORAGE_MARKER";
#[derive(Clone, Copy)]
enum ReceiverKind {
Ref,
RefMut,
Value,
}
struct MethodParam {
ident: syn::Ident,
user_ty: syn::Type,
type_ref: TypeRef,
wit_param_name: String,
}
enum MethodReturn {
Unit,
Type {
user_ty: Box<syn::Type>,
type_ref: TypeRef,
},
}
struct ComponentMethod {
fn_ident: syn::Ident,
doc_attrs: Vec<Attribute>,
params: Vec<MethodParam>,
receiver_kind: ReceiverKind,
return_info: MethodReturn,
wit_name: String,
}
pub fn component(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let call_site_span = Span::call_site();
let attr_tokens: TokenStream2 = attr.into();
let item_tokens: TokenStream2 = item.into();
if let Ok(item_trait) = syn::parse2::<ItemTrait>(item_tokens.clone()) {
let sibling_refs = match syn::parse2::<DependencyRefArgs>(attr_tokens) {
Ok(args) => args.refs,
Err(err) => return err.to_compile_error().into(),
};
match expand_component_trait(call_site_span, item_trait, sibling_refs) {
Ok(expanded) => expanded.into(),
Err(err) => err.to_compile_error().into(),
}
} else if !attr_tokens.is_empty() {
syn::Error::new(
attr_tokens.span(),
"`#[component]` only accepts arguments on the component trait declaration; declare \
sibling component dependencies as `#[component(package::Interface, ...)]` on the \
trait",
)
.into_compile_error()
.into()
} else if let Ok(item_impl) = syn::parse2::<ItemImpl>(item_tokens.clone()) {
match expand_component_trait_impl(call_site_span, item_impl) {
Ok(expanded) => expanded.into(),
Err(err) => err.to_compile_error().into(),
}
} else if syn::parse2::<ItemStruct>(item_tokens).is_ok() {
syn::Error::new(
call_site_span.into(),
"`#[component]` no longer applies to structs; annotate the storage struct with \
`#[component_storage]` instead.",
)
.into_compile_error()
.into()
} else {
syn::Error::new(
call_site_span.into(),
"The `component` macro only supports a component trait or a trait implementation \
block.",
)
.into_compile_error()
.into()
}
}
pub fn component_storage(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
if !attr.is_empty() {
return syn::Error::new(
Span2::call_site(),
"#[component_storage] does not accept arguments",
)
.into_compile_error()
.into();
}
let call_site_span = Span::call_site();
let item_tokens: TokenStream2 = item.into();
match syn::parse2::<ItemStruct>(item_tokens) {
Ok(item_struct) => match expand_component_storage(call_site_span, item_struct) {
Ok(expanded) => expanded.into(),
Err(err) => err.to_compile_error().into(),
},
Err(_) => syn::Error::new(
call_site_span.into(),
"`#[component_storage]` only applies to a struct declaration.",
)
.into_compile_error()
.into(),
}
}
pub fn expand_auth_script(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
if !attr.is_empty() {
return syn::Error::new(Span2::call_site(), "#[auth_script] does not accept arguments")
.into_compile_error()
.into();
}
let item_tokens: TokenStream2 = item.clone().into();
let mut item_fn: TraitItemFn = match syn::parse2(item_tokens.clone()) {
Ok(item_fn) => item_fn,
Err(_) => {
if let Ok(item_fn) = syn::parse2::<ImplItemFn>(item_tokens.clone()) {
return syn::Error::new(
item_fn.sig.span(),
"`#[auth_script]` must be applied to a method inside a `#[component]` \
`trait`, not the implementation block",
)
.into_compile_error()
.into();
}
if let Ok(item_fn) = syn::parse2::<syn::ItemFn>(item_tokens.clone()) {
return syn::Error::new(
item_fn.sig.span(),
"`#[auth_script]` must be applied to a method inside a `#[component]` `trait`",
)
.into_compile_error()
.into();
}
return syn::Error::new(
Span2::call_site(),
"`#[auth_script]` must be applied to a method inside a `#[component]` `trait`",
)
.into_compile_error()
.into();
}
};
if item_fn.default.is_some() {
return syn::Error::new(
item_fn.sig.span(),
"`#[auth_script]` must be applied to a method inside a `#[component]` `trait`, not \
the implementation block",
)
.into_compile_error()
.into();
}
let marker_attr = format_ident!("{}", AUTH_SCRIPT_MARKER_ATTR);
item_fn.attrs.push(syn::parse_quote!(#[#marker_attr]));
quote!(#item_fn).into()
}
fn expand_component_storage(
call_site_span: Span,
mut input_struct: ItemStruct,
) -> Result<TokenStream2, syn::Error> {
let struct_name = &input_struct.ident;
reject_generics(&input_struct.generics, "component storage structs cannot be generic")?;
let metadata = crate::wit_world::ManifestPackage::load_or_default(call_site_span.into())?;
let mut acc_builder = AccountComponentMetadataBuilder::new(
metadata.package.name().to_string(),
metadata.package.version().into_inner().clone(),
metadata.description.clone(),
);
let default_impl = match &mut input_struct.fields {
syn::Fields::Named(fields) => {
let storage_namespace = metadata.package.name().into_inner();
let component_interface = namespace_interface_segment(&metadata).to_string();
let field_inits = process_storage_fields(
fields,
&mut acc_builder,
&storage_namespace,
&component_interface,
)?;
if !fields.named.is_empty() && !metadata.has_miden_project_toml {
return Err(syn::Error::new(
struct_name.span(),
"`#[component_storage]` with `#[storage]` fields requires a \
`miden-project.toml` next to the crate's `Cargo.toml`: storage slot names \
derive from the `[lib].namespace` interface segment",
));
}
generate_default_impl(struct_name, &field_inits)
}
syn::Fields::Unit => quote! {
impl Default for #struct_name {
fn default() -> Self {
Self
}
}
},
_ => {
return Err(syn::Error::new(
input_struct.fields.span(),
"`#[component_storage]` only supports unit structs or structs with named fields.",
));
}
};
let component_metadata = acc_builder.build(call_site_span.into())?;
let mut metadata_bytes = component_metadata.to_bytes();
let padded_len = metadata_bytes.len().div_ceil(16) * 16;
metadata_bytes.resize(padded_len, 0);
let link_section = generate_link_section(&metadata_bytes);
let runtime_boilerplate = runtime_boilerplate();
let marker_ident = format_ident!("{}", COMPONENT_STORAGE_MARKER_CONST);
Ok(quote! {
#runtime_boilerplate
#input_struct
#default_impl
impl #struct_name {
#[doc(hidden)]
pub const #marker_ident: () = ();
}
impl ::miden::native_account::NativeAccount for #struct_name {}
impl ::miden::active_account::ActiveAccount for #struct_name {}
#link_section
})
}
fn expand_component_trait(
call_site_span: Span,
mut input_trait: ItemTrait,
sibling_refs: Vec<DependencyRef>,
) -> Result<TokenStream2, syn::Error> {
let trait_ident = input_trait.ident.clone();
reject_generics(&input_trait.generics, "component traits cannot be generic")?;
let metadata = crate::wit_world::ManifestPackage::load_or_default(call_site_span.into())?;
if !metadata.has_miden_project_toml {
return Err(syn::Error::new(
trait_ident.span(),
"`#[component]` requires a `miden-project.toml` next to the crate's `Cargo.toml`, \
with `kind = \"account-component\"` and a `[lib].namespace` declaring the \
component's interface",
));
}
let package_name = format!("miden:{}", metadata.package.name().into_inner().to_kebab_case());
let interface_name = trait_ident.to_string().to_kebab_case();
validate_namespace_matches_interface(&metadata, &package_name, &interface_name, &trait_ident)?;
let mut auth_method_idents = Vec::new();
let mut method_count = 0usize;
for item in &mut input_trait.items {
let TraitItem::Fn(method) = item else {
return Err(syn::Error::new(
item.span(),
"component traits only support method declarations",
));
};
if method.default.is_some() {
return Err(syn::Error::new(
method.sig.ident.span(),
"component trait methods cannot have default bodies; exports are derived from the \
`impl` block, so a defaulted method that is not overridden there would silently \
disappear from the component's interface",
));
}
let is_auth_script = has_auth_script_marker_attr(&method.attrs);
method.attrs.retain(|attr| !is_auth_script_marker_attr(attr));
let (_, args) = validate_signature_shape(&method.sig)?;
if is_auth_script {
validate_auth_script_signature(&method.sig, &args)?;
auth_method_idents.push(method.sig.ident.clone());
}
method_count += 1;
}
if method_count == 0 {
return Err(syn::Error::new(
input_trait.span(),
"Component `trait` is missing methods. A component cannot have empty exports.",
));
}
validate_auth_script_count(
metadata.target.ty,
metadata.requires_auth_script(),
auth_method_idents.len(),
input_trait.span(),
)?;
let frontend_link_section = auth_method_idents.first().map_or_else(
|| quote! {},
|auth_ident| {
let metadata = auth_script_frontend_metadata(&trait_ident, auth_ident);
generate_frontend_link_section(&metadata)
},
);
let marker_ident = format_ident!("{}", COMPONENT_TRAIT_MARKER_CONST);
input_trait.items.push(syn::parse_quote! {
#[doc(hidden)]
const #marker_ident: () = ();
});
let sibling_traits = if sibling_refs.is_empty() {
quote! {}
} else {
sibling::expand_sibling_traits(&metadata, &trait_ident, &sibling_refs)?
};
Ok(quote! {
#input_trait
#frontend_link_section
#sibling_traits
})
}
fn expand_component_trait_impl(
call_site_span: Span,
mut impl_block: ItemImpl,
) -> Result<TokenStream2, syn::Error> {
let Some((_, trait_path, _)) = impl_block.trait_.clone() else {
return Err(syn::Error::new(
impl_block.span(),
"`#[component]` requires a trait implementation. Write `impl MyComponent for \
MyComponentStorage` and annotate the storage struct with `#[component_storage]`.",
));
};
reject_generics(&impl_block.generics, "component trait implementations cannot be generic")?;
let component_type = (*impl_block.self_ty).clone();
if extract_type_ident(&component_type).is_none() {
return Err(syn::Error::new(
impl_block.self_ty.span(),
"Failed to determine the storage type targeted by this implementation.",
));
}
let trait_segment = trait_path.segments.last().ok_or_else(|| {
syn::Error::new(trait_path.span(), "Failed to determine the component trait name.")
})?;
if !matches!(trait_segment.arguments, PathArguments::None) {
return Err(syn::Error::new(
trait_segment.arguments.span(),
"component trait paths cannot use generic arguments",
));
}
let trait_ident = trait_segment.ident.clone();
let metadata = crate::wit_world::ManifestPackage::load_or_default(call_site_span.into())?;
if !metadata.has_miden_project_toml {
return Err(syn::Error::new(
trait_ident.span(),
"`#[component]` requires a `miden-project.toml` next to the crate's `Cargo.toml`, \
with `kind = \"account-component\"` and a `[lib].namespace` declaring the \
component's interface",
));
}
let package_name = format!("miden:{}", metadata.package.name().into_inner().to_kebab_case());
let interface_name = trait_ident.to_string().to_kebab_case();
validate_namespace_matches_interface(&metadata, &package_name, &interface_name, &trait_ident)?;
let interface_module = interface_name.to_snake_case();
let world_name = format!("{interface_name}-world");
let mut exported_types = registered_export_types();
exported_types.sort_by(|a, b| a.wit_name.cmp(&b.wit_name));
let exported_types_by_rust: HashMap<_, _> =
exported_types.iter().map(|def| (def.rust_name.clone(), def.clone())).collect();
let mut methods = Vec::new();
let mut type_imports = BTreeSet::new();
for item in &mut impl_block.items {
let ImplItem::Fn(method) = item else {
continue;
};
if has_auth_script_marker_attr(&method.attrs) {
return Err(syn::Error::new(
method.sig.ident.span(),
"`#[auth_script]` must be applied to a method inside a `#[component]` `trait`, \
not the implementation block",
));
}
let (parsed_method, imports) =
parse_component_signature(&method.sig, &method.attrs, &exported_types_by_rust)?;
type_imports.extend(imports);
methods.push(parsed_method);
}
if methods.is_empty() {
return Err(syn::Error::new(
impl_block.span(),
"Component `impl` is missing methods. A component cannot have empty exports.",
));
}
let dependency_imports = metadata.collect_miden_dependency_imports(Span2::call_site())?;
let inline_wit_source = build_component_wit(ComponentWitSpec {
component_package: &package_name,
component_version: metadata.package.version().inner(),
interface_name: &interface_name,
world_name: &world_name,
dependency_imports: &dependency_imports,
type_imports: &type_imports,
methods: &methods,
exported_types: &exported_types,
})?;
let public_wit_source = build_component_wit(ComponentWitSpec {
component_package: &package_name,
component_version: metadata.package.version().inner(),
interface_name: &interface_name,
world_name: &world_name,
dependency_imports: &[],
type_imports: &type_imports,
methods: &methods,
exported_types: &exported_types,
})?;
write_component_wit_file(call_site_span, &public_wit_source, &package_name)?;
let inline_literal = Literal::string(&inline_wit_source);
let interface_path =
format!("{}/{}@{}", package_name, interface_name, metadata.package.version());
let custom_type_paths = collect_custom_type_paths(&exported_types, &methods, None);
let (custom_with_entries, debug_with_entries) =
build_custom_with_entries(&exported_types, &interface_path, None, &custom_type_paths);
if env::var_os("MIDEN_COMPONENT_DEBUG_WITH").is_some() {
eprintln!(
"[miden::component] with mappings for {package_name}: {}",
debug_with_entries.join(", ")
);
}
let guest_trait_path = build_guest_trait_path(&package_name, &interface_module)?;
let guest_methods: Vec<TokenStream2> = methods
.iter()
.map(|method| render_guest_method(method, &component_type, &trait_path))
.collect();
let marker_check = render_trait_marker_check(&component_type, &trait_path);
let storage_marker_check = render_storage_marker_check(&component_type);
Ok(quote! {
::miden::generate!(inline = #inline_literal, with = { #(#custom_with_entries)* });
#[allow(unused_imports)]
use ::miden::native_account::NativeAccount as _;
#[allow(unused_imports)]
use ::miden::active_account::ActiveAccount as _;
#impl_block
impl #guest_trait_path for #component_type {
#(#guest_methods)*
}
#marker_check
#storage_marker_check
self::bindings::export!(#component_type);
})
}
fn render_trait_marker_check(component_type: &Type, trait_path: &syn::Path) -> TokenStream2 {
let marker_ident = format_ident!("{}", COMPONENT_TRAIT_MARKER_CONST);
quote! {
const _: () = <#component_type as #trait_path>::#marker_ident;
}
}
fn render_storage_marker_check(component_type: &Type) -> TokenStream2 {
let marker_ident = format_ident!("{}", COMPONENT_STORAGE_MARKER_CONST);
quote! {
const _: () = <#component_type>::#marker_ident;
}
}
fn validate_namespace_matches_interface(
metadata: &crate::wit_world::ManifestPackage,
package_name: &str,
interface_name: &str,
trait_ident: &syn::Ident,
) -> Result<(), syn::Error> {
let namespace = declared_namespace(metadata);
let version = metadata.package.version();
let expected_namespace = format!("{package_name}/{interface_name}@{version}");
if namespace != expected_namespace {
return Err(syn::Error::new(
trait_ident.span(),
format!(
"component trait `{trait_ident}` produces WIT interface `{interface_name}` in \
package `{package_name}` version `{version}`, but `[lib].namespace` in \
`miden-project.toml` declares `{namespace}`. Update `[lib].namespace` to \
`{expected_namespace}`. WARNING: storage slot ids derive from the namespace's \
interface segment — changing it re-keys the storage of an already-deployed \
component."
),
));
}
Ok(())
}
fn declared_namespace(metadata: &crate::wit_world::ManifestPackage) -> &str {
metadata
.target
.namespace
.inner()
.as_str()
.trim_start_matches("::")
.trim_matches('"')
}
fn reject_generics(generics: &syn::Generics, message: &str) -> Result<(), syn::Error> {
if generics.lt_token.is_some() || !generics.params.is_empty() || generics.where_clause.is_some()
{
return Err(syn::Error::new(generics.span(), message));
}
Ok(())
}
fn namespace_interface_segment(metadata: &crate::wit_world::ManifestPackage) -> &str {
declared_namespace(metadata)
.rsplit('/')
.next()
.and_then(|segment| segment.split('@').next())
.unwrap_or_default()
}
fn validate_auth_script_count(
target_type: TargetType,
requires_auth_script: bool,
auth_method_count: usize,
span: Span2,
) -> Result<(), syn::Error> {
match (target_type, requires_auth_script, auth_method_count) {
(TargetType::AccountComponent, true, 1) => Ok(()),
(TargetType::AccountComponent, true, 0) => Err(syn::Error::new(
span,
"authentication components require exactly one `#[auth_script]` method",
)),
(TargetType::AccountComponent, _, count) if count > 1 => Err(syn::Error::new(
span,
"only one `#[auth_script]` method is allowed per `#[component]` trait",
)),
(TargetType::AccountComponent, ..) => Ok(()),
(_, _, count) if count > 0 => Err(syn::Error::new(
span,
"`#[auth_script]` method is only permitted on components of 'account-component' type",
)),
_ => Ok(()),
}
}
fn build_guest_trait_path(
package_name: &str,
interface_module: &str,
) -> Result<TokenStream2, syn::Error> {
let package_without_version = package_name.split('@').next().unwrap_or(package_name).trim();
let segments: Vec<_> = package_without_version
.split([':', '/'])
.filter(|segment| !segment.is_empty())
.map(to_snake_case)
.collect();
if segments.is_empty() {
return Err(syn::Error::new(
Span::call_site().into(),
"Invalid component package identifier provided in manifest metadata.",
));
}
let module_idents: Vec<_> =
segments.iter().map(|segment| format_ident!("{}", segment)).collect();
let interface_ident = format_ident!("{}", to_snake_case(interface_module));
Ok(quote! { self::bindings::exports #( :: #module_idents)* :: #interface_ident :: Guest })
}
fn render_guest_method(
method: &ComponentMethod,
component_type: &Type,
trait_path: &syn::Path,
) -> TokenStream2 {
let fn_ident = &method.fn_ident;
let doc_attrs = &method.doc_attrs;
let component_ident = format_ident!("__component_instance");
let mut param_tokens = Vec::new();
let mut call_args = Vec::new();
for param in &method.params {
let ident = ¶m.ident;
call_args.push(quote!(#ident));
let param_ty = ¶m.user_ty;
param_tokens.push(quote!(#ident: #param_ty));
}
let fn_inputs = if param_tokens.is_empty() {
quote!()
} else {
quote!(#(#param_tokens),*)
};
let component_init = match method.receiver_kind {
ReceiverKind::Ref | ReceiverKind::Value => {
quote! { let #component_ident = #component_type::default(); }
}
ReceiverKind::RefMut => quote! { let mut #component_ident = #component_type::default(); },
};
let receiver_arg = match method.receiver_kind {
ReceiverKind::Ref => quote!(&#component_ident),
ReceiverKind::RefMut => quote!(&mut #component_ident),
ReceiverKind::Value => quote!(#component_ident),
};
let call_expr = quote! {
<#component_type as #trait_path>::#fn_ident(#receiver_arg #(, #call_args)*)
};
let output = match &method.return_info {
MethodReturn::Unit => quote!(),
MethodReturn::Type { user_ty, .. } => {
let user_ty = user_ty.as_ref();
quote!(-> #user_ty)
}
};
let body = match &method.return_info {
MethodReturn::Unit => quote! {
#component_init
#call_expr;
},
MethodReturn::Type { .. } => {
quote! {
#component_init
#call_expr
}
}
};
quote! {
#(#doc_attrs)*
fn #fn_ident(#fn_inputs) #output {
#body
}
}
}
fn build_custom_with_entries(
exported_types: &[ExportedTypeDef],
interface_path: &str,
module_prefix: Option<&syn::Path>,
custom_type_paths: &HashMap<String, Vec<String>>,
) -> (Vec<TokenStream2>, Vec<String>) {
let mut tokens = Vec::new();
let mut debug = Vec::new();
for def in exported_types {
let wit_path_str = format!("{interface_path}/{}", def.wit_name);
let wit_path = Literal::string(&wit_path_str);
let type_ident = format_ident!("{}", def.rust_name);
let type_tokens = if let Some(segments) = custom_type_paths.get(&def.wit_name) {
build_path_tokens(segments, &type_ident)
} else if let Some(prefix) = module_prefix {
quote!(#prefix :: #type_ident)
} else {
quote!(crate :: #type_ident)
};
debug.push(format!("{wit_path_str} => {type_tokens}"));
tokens.push(quote! { #wit_path: #type_tokens, });
}
(tokens, debug)
}
fn record_type_path(
paths: &mut HashMap<String, Vec<String>>,
type_ref: &TypeRef,
module_prefix_segments: Option<&[String]>,
) {
for dependency in &type_ref.dependencies {
record_type_path(paths, dependency, module_prefix_segments);
}
if !type_ref.is_custom {
return;
}
let mut segments = type_ref.path.clone();
if let Some(first) = segments.first().cloned() {
match first.as_str() {
"self" => {
segments.remove(0);
if let Some(prefix) = module_prefix_segments {
let mut resolved = prefix.to_vec();
resolved.extend(segments);
segments = resolved;
}
}
"super" => {
let super_count = segments.iter().take_while(|segment| *segment == "super").count();
let mut resolved =
module_prefix_segments.map(|prefix| prefix.to_vec()).unwrap_or_default();
if super_count > resolved.len() {
resolved.clear();
} else {
for _ in 0..super_count {
let _ = resolved.pop();
}
}
segments =
resolved.into_iter().chain(segments.into_iter().skip(super_count)).collect();
}
"crate" => {}
_ => {}
}
}
if segments.len() <= 1
&& let Some(last) = segments.last().cloned()
&& let Some(prefix) = module_prefix_segments
{
let mut resolved = prefix.to_vec();
resolved.push(last);
segments = resolved;
}
paths.entry(type_ref.wit_name.clone()).or_insert(segments);
}
fn collect_custom_type_paths(
exported_types: &[ExportedTypeDef],
methods: &[ComponentMethod],
module_prefix_segments: Option<&[String]>,
) -> HashMap<String, Vec<String>> {
let mut paths = HashMap::new();
for def in exported_types {
match &def.kind {
ExportedTypeKind::Record { fields } => {
for field in fields {
record_type_path(&mut paths, &field.ty, module_prefix_segments);
}
}
ExportedTypeKind::Variant { variants } => {
for variant in variants {
if let Some(payload) = &variant.payload {
record_type_path(&mut paths, payload, module_prefix_segments);
}
}
}
}
}
for method in methods {
for param in &method.params {
record_type_path(&mut paths, ¶m.type_ref, module_prefix_segments);
}
if let MethodReturn::Type { type_ref, .. } = &method.return_info {
record_type_path(&mut paths, type_ref, module_prefix_segments);
}
}
paths
}
fn build_path_tokens(segments: &[String], type_ident: &Ident) -> TokenStream2 {
if segments.is_empty() {
return quote!(crate :: #type_ident);
}
let mut modules: Vec<String> = segments.to_vec();
let type_name = type_ident.to_string();
if modules.last().map(|seg| seg == &type_name).unwrap_or(false) {
modules.pop();
}
let mut iter = modules.iter();
let mut tokens: Option<TokenStream2> = None;
if let Some(first) = iter.next() {
tokens = Some(match first.as_str() {
"crate" => quote!(crate),
"self" => quote!(self),
"super" => quote!(super),
other => {
let ident = format_ident!("{}", other);
quote!(crate :: #ident)
}
});
}
for segment in iter {
let ident = format_ident!("{}", segment);
tokens = Some(match tokens {
Some(existing) => quote!(#existing :: #ident),
None => quote!(crate :: #ident),
});
}
let base = tokens.unwrap_or_else(|| quote!(crate));
quote!(#base :: #type_ident)
}
fn validate_signature_shape(
sig: &syn::Signature,
) -> Result<(ReceiverKind, Vec<(syn::Ident, syn::Type)>), syn::Error> {
if sig.constness.is_some() {
return Err(syn::Error::new(sig.ident.span(), "component methods cannot be `const`"));
}
if sig.asyncness.is_some() {
return Err(syn::Error::new(sig.ident.span(), "component methods cannot be `async`"));
}
if sig.unsafety.is_some() {
return Err(syn::Error::new(sig.ident.span(), "component methods cannot be `unsafe`"));
}
if sig.abi.is_some() {
return Err(syn::Error::new(
sig.ident.span(),
"component methods cannot specify an `extern` ABI",
));
}
if !sig.generics.params.is_empty() {
return Err(syn::Error::new(sig.generics.span(), "component methods cannot be generic"));
}
if sig.variadic.is_some() {
return Err(syn::Error::new(
sig.ident.span(),
"variadic component methods are unsupported",
));
}
let mut inputs_iter = sig.inputs.iter();
let receiver = inputs_iter.next().ok_or_else(|| {
syn::Error::new(
sig.span(),
"component methods must accept `self`, `&self`, or `&mut self` as the first argument",
)
})?;
let receiver_kind = match receiver {
FnArg::Receiver(recv) => match (&recv.reference, recv.mutability) {
(Some(_), Some(_)) => ReceiverKind::RefMut,
(Some(_), None) => ReceiverKind::Ref,
(None, _) => ReceiverKind::Value,
},
FnArg::Typed(other) => {
return Err(syn::Error::new(
other.span(),
"component methods must use an explicit receiver",
));
}
};
let mut args = Vec::new();
for arg in inputs_iter {
match arg {
FnArg::Typed(pat_type) => {
let ident = match pat_type.pat.as_ref() {
syn::Pat::Ident(pat_ident) => pat_ident.ident.clone(),
other => {
return Err(syn::Error::new(
other.span(),
"component method arguments must be simple identifiers",
));
}
};
args.push((ident, (*pat_type.ty).clone()));
}
FnArg::Receiver(other) => {
return Err(syn::Error::new(
other.span(),
"component methods support a single receiver argument",
));
}
}
}
Ok((receiver_kind, args))
}
fn parse_component_signature(
sig: &syn::Signature,
attrs: &[Attribute],
exported_types: &HashMap<String, ExportedTypeDef>,
) -> Result<(ComponentMethod, BTreeSet<String>), syn::Error> {
let (receiver_kind, args) = validate_signature_shape(sig)?;
let mut params = Vec::new();
let mut type_imports = BTreeSet::new();
for (ident, user_ty) in args {
let type_ref = map_type_to_type_ref(&user_ty, exported_types)?;
type_ref.add_required_core_type_imports(&mut type_imports);
params.push(MethodParam {
wit_param_name: to_kebab_case(&ident.to_string()),
ident,
user_ty,
type_ref,
});
}
let return_info = match &sig.output {
ReturnType::Default => MethodReturn::Unit,
ReturnType::Type(_, ty) if is_unit_type(ty) => MethodReturn::Unit,
ReturnType::Type(_, ty) => {
let type_ref = map_type_to_type_ref(ty, exported_types)?;
type_ref.add_required_core_type_imports(&mut type_imports);
MethodReturn::Type {
user_ty: ty.clone(),
type_ref,
}
}
};
let doc_attrs = attrs.iter().filter(|attr| attr.path().is_ident("doc")).cloned().collect();
let component_method = ComponentMethod {
fn_ident: sig.ident.clone(),
doc_attrs,
params,
receiver_kind,
return_info,
wit_name: to_kebab_case(&sig.ident.to_string()),
};
Ok((component_method, type_imports))
}
fn extract_type_ident(ty: &Type) -> Option<syn::Ident> {
match ty {
Type::Path(path) => path.path.segments.last().map(|segment| segment.ident.clone()),
Type::Group(group) => extract_type_ident(&group.elem),
Type::Paren(paren) => extract_type_ident(&paren.elem),
_ => None,
}
}
fn is_unit_type(ty: &Type) -> bool {
matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())
}
fn is_type_named(ty: &Type, name: &str) -> bool {
let Type::Path(type_path) = ty else {
return false;
};
if type_path.qself.is_some() {
return false;
}
type_path
.path
.segments
.last()
.is_some_and(|seg| seg.ident == name && matches!(seg.arguments, PathArguments::None))
}
fn to_kebab_case(name: &str) -> String {
name.to_kebab_case()
}
fn to_snake_case(name: &str) -> String {
name.to_snake_case()
}
fn generate_default_impl(
struct_name: &syn::Ident,
field_inits: &[proc_macro2::TokenStream],
) -> proc_macro2::TokenStream {
quote! {
impl Default for #struct_name {
fn default() -> Self {
Self {
#(#field_inits),*
}
}
}
}
}
fn validate_auth_script_signature(
sig: &syn::Signature,
args: &[(syn::Ident, syn::Type)],
) -> Result<(), syn::Error> {
if args.len() != 1 || !is_type_named(&args[0].1, "Word") {
return Err(syn::Error::new(
sig.span(),
"`#[auth_script]` methods must accept exactly one `Word` argument (excluding `self`)",
));
}
let returns_unit = match &sig.output {
ReturnType::Default => true,
ReturnType::Type(_, ty) => is_unit_type(ty),
};
if !returns_unit {
return Err(syn::Error::new(
sig.output.span(),
"`#[auth_script]` methods must return `()`",
));
}
Ok(())
}
fn auth_script_frontend_metadata(
trait_ident: &syn::Ident,
auth_method_ident: &syn::Ident,
) -> FrontendMetadata {
FrontendMetadata::AuthScript {
method_path: format!("{trait_ident}::{auth_method_ident}"),
export_name: to_kebab_case(&auth_method_ident.to_string()),
}
}
fn generate_link_section(metadata_bytes: &[u8]) -> proc_macro2::TokenStream {
let link_section_bytes_len = metadata_bytes.len();
let encoded_bytes_str = Literal::byte_string(metadata_bytes);
quote! {
#[unsafe(
// to test it in the integration(this crate) tests the section name needs to make mach-o section
// specifier happy and to have "segment and section separated by comma"
link_section = "rodata,miden_account"
)]
#[doc(hidden)]
#[allow(clippy::octal_escapes)]
pub static __MIDEN_ACCOUNT_COMPONENT_METADATA_BYTES: [u8; #link_section_bytes_len] = *#encoded_bytes_str;
}
}
fn has_auth_script_marker_attr(attrs: &[Attribute]) -> bool {
attrs.iter().any(is_auth_script_marker_attr)
}
fn is_attr_named(attr: &Attribute, name: &str) -> bool {
attr.path()
.segments
.last()
.is_some_and(|seg| seg.ident == name && matches!(seg.arguments, PathArguments::None))
}
fn is_auth_script_marker_attr(attr: &Attribute) -> bool {
is_attr_named(attr, AUTH_SCRIPT_ATTR)
|| is_attr_named(attr, AUTH_SCRIPT_MARKER_ATTR)
|| is_doc_marker_attr(attr, "__miden_auth_script_marker")
}
fn is_doc_marker_attr(attr: &Attribute, marker: &str) -> bool {
if !attr.path().is_ident("doc") {
return false;
}
let syn::Meta::NameValue(meta) = &attr.meta else {
return false;
};
let syn::Expr::Lit(expr) = &meta.value else {
return false;
};
let syn::Lit::Str(value) = &expr.lit else {
return false;
};
value.value() == marker
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use syn::parse_quote;
use super::*;
#[test]
fn record_type_path_defaults_to_crate_root() {
let mut paths = HashMap::new();
let type_ref = TypeRef {
wit_name: "struct-a".into(),
is_custom: true,
path: vec!["StructA".into()],
dependencies: Vec::new(),
};
record_type_path(&mut paths, &type_ref, None);
assert_eq!(paths.get("struct-a"), Some(&vec!["StructA".to_string()]));
}
#[test]
fn record_type_path_applies_module_prefix() {
let mut paths = HashMap::new();
let type_ref = TypeRef {
wit_name: "struct-a".into(),
is_custom: true,
path: vec!["StructA".into()],
dependencies: Vec::new(),
};
let prefix = vec!["foo".to_string(), "bar".to_string()];
record_type_path(&mut paths, &type_ref, Some(prefix.as_slice()));
assert_eq!(
paths.get("struct-a"),
Some(&vec!["foo".to_string(), "bar".to_string(), "StructA".to_string()])
);
}
#[test]
fn record_type_path_resolves_super_segments() {
let mut paths = HashMap::new();
let type_ref = TypeRef {
wit_name: "struct-a".into(),
is_custom: true,
path: vec!["super".into(), "StructA".into()],
dependencies: Vec::new(),
};
let prefix = vec!["foo".to_string(), "bar".to_string()];
record_type_path(&mut paths, &type_ref, Some(prefix.as_slice()));
assert_eq!(paths.get("struct-a"), Some(&vec!["foo".to_string(), "StructA".to_string()]));
}
#[test]
fn build_path_tokens_generates_absolute_path() {
let segments = vec!["foo".to_string(), "bar".to_string(), "StructA".to_string()];
let ident = format_ident!("StructA");
let tokens = build_path_tokens(&segments, &ident).to_string();
assert_eq!(tokens, "crate :: foo :: bar :: StructA");
}
#[test]
fn build_path_tokens_defaults_to_crate_root_for_single_segment() {
let segments = vec!["StructA".to_string()];
let ident = format_ident!("StructA");
let tokens = build_path_tokens(&segments, &ident).to_string();
assert_eq!(tokens, "crate :: StructA");
}
#[test]
fn build_custom_with_entries_prefers_custom_paths() {
let exported_types = vec![ExportedTypeDef {
rust_name: "StructA".into(),
wit_name: "struct-a".into(),
kind: ExportedTypeKind::Record { fields: Vec::new() },
}];
let interface_path = "miden:component/path";
let module_prefix: syn::Path = syn::parse_quote!(module::account);
let mut custom_paths = HashMap::new();
custom_paths.insert("struct-a".into(), vec!["types".into(), "StructA".into()]);
let (entries, _) = build_custom_with_entries(
&exported_types,
interface_path,
Some(&module_prefix),
&custom_paths,
);
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].to_string(),
"\"miden:component/path/struct-a\" : crate :: types :: StructA ,"
);
}
#[test]
fn auth_script_methods_preserve_user_defined_names() {
let method: TraitItemFn = parse_quote! {
fn whatever_name(&mut self, arg: Word);
};
let (_, args) = validate_signature_shape(&method.sig).unwrap();
validate_auth_script_signature(&method.sig, &args).unwrap();
let trait_ident = format_ident!("AuthComponent");
let metadata = auth_script_frontend_metadata(&trait_ident, &method.sig.ident);
assert!(matches!(
metadata,
FrontendMetadata::AuthScript { export_name, .. } if export_name == "whatever-name"
));
}
#[test]
fn auth_script_methods_require_word_argument() {
let method: TraitItemFn = parse_quote! {
fn auth_procedure(&mut self, arg: u32);
};
let (_, args) = validate_signature_shape(&method.sig).unwrap();
let err = match validate_auth_script_signature(&method.sig, &args) {
Ok(_) => panic!("expected `#[auth_script]` validation to reject non-`Word` arguments"),
Err(err) => err,
};
assert!(err.to_string().contains("exactly one `Word` argument"));
}
#[test]
fn auth_script_methods_require_unit_return() {
let method: TraitItemFn = parse_quote! {
fn auth_procedure(&mut self, arg: Word) -> Word;
};
let (_, args) = validate_signature_shape(&method.sig).unwrap();
let err = match validate_auth_script_signature(&method.sig, &args) {
Ok(_) => panic!("expected `#[auth_script]` validation to reject non-unit returns"),
Err(err) => err,
};
assert!(err.to_string().contains("must return `()`"));
}
#[test]
fn auth_script_frontend_metadata_emits_project_wide_uniqueness_guard() {
let trait_ident = format_ident!("AuthComponent");
let method_ident = format_ident!("whatever_name");
let metadata = auth_script_frontend_metadata(&trait_ident, &method_ident);
let tokens = generate_frontend_link_section(&metadata).to_string();
assert!(tokens.contains(crate::util::FRONTEND_METADATA_UNIQUENESS_GUARD_SYMBOL));
}
#[test]
fn auth_script_frontend_metadata_stores_method_path() {
let trait_ident = format_ident!("AuthComponent");
let method_ident = format_ident!("whatever_name");
let metadata = auth_script_frontend_metadata(&trait_ident, &method_ident);
assert_eq!(
metadata,
FrontendMetadata::AuthScript {
method_path: "AuthComponent::whatever_name".into(),
export_name: "whatever-name".into(),
}
);
}
#[test]
fn authentication_components_require_exactly_one_auth_script() {
let err =
validate_auth_script_count(TargetType::AccountComponent, true, 0, Span2::call_site())
.expect_err("expected authentication components to require an auth script");
assert!(
err.to_string()
.contains("authentication components require exactly one `#[auth_script]` method")
);
validate_auth_script_count(TargetType::AccountComponent, true, 1, Span2::call_site())
.expect("expected exactly one auth script to be accepted");
}
#[test]
fn ordinary_account_components_may_omit_auth_script() {
validate_auth_script_count(TargetType::AccountComponent, false, 0, Span2::call_site())
.expect("expected ordinary account components to allow no auth script");
}
#[test]
fn auth_script_marker_accepts_helper_attribute() {
let method: TraitItemFn = parse_quote! {
#[miden_auth_script_requires_component]
fn whatever_name(&mut self, arg: Word);
};
assert!(has_auth_script_marker_attr(&method.attrs));
}
}